Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
be77d04
feat(rust): remove all symbol↔counter_id conversions from SDK
sunli829 Aug 10, 2026
13b2b16
refactor(all): replace RankCategoriesResponse raw JSON with typed str…
sunli829 Aug 10, 2026
4119c61
refactor(all): replace remaining serde_json::Value in public API with…
sunli829 Aug 10, 2026
2372dc6
refactor(all): rename remaining counter_id request params to symbol; …
sunli829 Aug 11, 2026
263f66f
fix(java): implement missing industry_rank / industry_peers JNI bindings
sunli829 Aug 11, 2026
c608d8d
fix(java): implement 4 more missing fundamental JNI bindings
sunli829 Aug 11, 2026
1731c66
fix(java): align AlertContext with other SDKs — expose update, drop e…
sunli829 Aug 11, 2026
f68969c
docs(rust): drop #![allow(missing_docs)] and document the surfaced items
sunli829 Aug 11, 2026
f9efb5c
docs(rust): document trade US-series types — crate is now missing_doc…
sunli829 Aug 11, 2026
7cbba33
refactor(all): type AlertItem.value_map and CorpActionLive.status
sunli829 Aug 11, 2026
e6c9db2
fix(cpp): mirror typed AlertItem.value_map (AlertValueMap) in the C++…
sunli829 Aug 11, 2026
1a042cd
refactor(rust): send DELETE params as query string, not request body
sunli829 Aug 11, 2026
4e62afc
fix(fundamental): tolerate explicit null in rating-detail and US divi…
hogan-yuan Aug 18, 2026
324ce28
chore(rust): drop imports left unused by the counter-id removal
hogan-yuan Aug 18, 2026
5961d35
fix(all): tolerate explicit JSON null on every optional response field
hogan-yuan Aug 18, 2026
d65e8fb
fix(all): disable ratings across all bindings — endpoint not yet open…
hogan-yuan Aug 24, 2026
36172a3
Merge branch 'main' into feat/remove-counter-id-conversion
hogan-yuan Aug 24, 2026
8cd7e46
fix(fundamental): send plural `symbols` param for executive
sunli829 Aug 25, 2026
70b8a96
Merge branch 'main' into feat/remove-counter-id-conversion
Copilot Aug 25, 2026
495cd76
fix(python): correct stub import syntax
Copilot Aug 25, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 28 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,32 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased]

### Fixed

- **All SDKs:** `FundamentalContext.executive` (`GET /v1/quote/company-professionals`) sent its security in a `symbol` query parameter, but the endpoint expects the plural `symbols` (it takes a comma-separated list). The server silently ignored the unknown parameter and answered with an empty group (`total: 0`, no `symbol`, a `forward_url` missing the security id), so the call appeared to succeed while returning nothing. Verified live against `700.HK` and `AAPL.US`
- **All SDKs:** every optional response field now tolerates an explicit JSON `null`. `#[serde(default)]` alone only covers a *missing* key, so a server that sent `null` for any of ~700 optional fields aborted the whole response with `deserialize response body error: invalid type: null`. All `#[serde(default)]` response fields across every module (quote, trade, fundamental, market, dca, alert, sharelist, portfolio, calendar, content, screener, agent) now map `null` to the field's default value. First observed live as `institution_rating_detail` `"target"/"evaluate": null` (symbols without analyst coverage) and `us_company_dividends` `"recent_dividends": null` (no trailing dividends). No field types changed, so the language bindings are unaffected

### Breaking changes

- **All SDKs:** the write methods on `AlertContext` (`add`, `update`, `delete`) and `SharelistContext` (`delete`, `add_securities`, `remove_securities`, `sort_securities`) no longer return the raw server JSON. They now return `()` / `void` / `Promise<void>` — the server response body carried no useful information
- **All SDKs:** `FundamentalContext.ratings` (`GET /v1/quote/ratings`) is temporarily unavailable — the endpoint has been designated 暂不开放 (not yet open) on the server side, so the method has been commented out across every language binding (Rust incl. blocking, Python sync/async + `openapi.pyi` stub, Node.js, Java incl. JNI, C, C++) pending release. The `StockRatings` types remain in place for when it reopens

### Changed

- **Rust SDK:** `AlertContext.delete` and `SharelistContext.delete` / `remove_securities` no longer send a request body with their `DELETE` requests. Parameters that previously went in the JSON body now travel as query-string parameters (`AlertContext.delete` → `ids`, `SharelistContext.remove_securities` → `symbols`), and `SharelistContext.delete` (id in the path) sends no body at all — matching the existing `QuoteContext.delete_watchlist_group` convention
- **All SDKs:** `AlertItem.value_map` (`AlertContext.list` / `update`) is now a typed `AlertValueMap` struct instead of a raw JSON value. Exactly one field is populated depending on the alert condition: `price` (an absolute-price threshold) for price alerts, `chg` (a percentage-change threshold) for percentage alerts. `price` is a decimal (Rust `Option<Decimal>`, Python `Optional[Decimal]`, Java nullable `BigDecimal`, Node.js/C/C++ decimal string) and `chg` is a float (Rust `Option<f64>`, Python `Optional[float]`, Java nullable `Double`, Node.js `number`, C `const double*` NULL-when-absent, C++ `std::optional<double>`). Java exposes a new `AlertValueMap` class (`AlertItem.valueMap` is now `AlertValueMap` instead of a JSON string), C a nested `lb_alert_value_map_t` struct, and C++ a nested `AlertValueMap` struct. Previously exposed as an untyped JSON value / JSON string
- **All SDKs:** `CorpActionLive.status` (`FundamentalContext.corp_action`) is now a plain string instead of a raw JSON value. The server may send it as either an integer or a string; it is now normalized to a string (e.g. `"2"`). Previously the JSON-value form produced an inconsistent, quoted string (e.g. `"\"2\""`) in the Python/Node.js/Java/C bindings
- **All SDKs:** rename the `industry_peers` request parameter from `counter_id` to `symbol` for naming consistency (Rust blocking wrapper, Java `IndustryPeersOptions.counterId` → `symbol`, C/C++ parameter name) and the US-series fundamental request parameters likewise. The value has always been the user-facing symbol; only the name changes. Also refreshed doc comments that still said "converted from counter_id" — the SDK no longer performs any symbol↔counter_id conversion
- **All SDKs:** rename the `counter_id` field to `symbol` on `IndustryRankItem` (`FundamentalContext.industry_rank`) and `IndustryPeerNode` (`FundamentalContext.industry_peers`). These now expose the server's `symbol` value directly, matching every other security identifier in the SDK (Java `IndustryRankItem.symbol` / `IndustryPeerNode.symbol`, C `lb_industry_rank_item_t.symbol` / `lb_industry_peer_node_t.symbol`, C++ struct fields)
- **All SDKs:** `IndustryPeersResponse.top` (`FundamentalContext.industry_peers`) is now optional — the server may return `null` when there is no data (Rust `Option<IndustryPeersTop>`, Java nullable `IndustryPeersTop`, C `lb_industry_peers_top_t*` NULL-when-absent, C++ `std::optional<IndustryPeersTop>`). Previously a `null` here caused a deserialization error
- **All SDKs:** `ExecutiveGroup.symbol` (`FundamentalContext.executive`) is now optional — the server may return an empty/absent symbol at the group level (Rust/Python/Node.js `Option<String>` / `Optional[str]`, Java nullable `String`, C empty-string-when-absent, C++ `std::string`)
- **All SDKs:** `TopMoversResponse.next_params` is now a plain pagination-cursor string instead of a raw JSON value/object (Rust `String`, Python `str`, Node.js/Java `string`/`String`, C `const char*`, C++ `std::string`). An empty string means there are no more pages
- **All SDKs:** `FlowItem.executed_timestamp` (portfolio profit-analysis flows) is now a nullable Unix-seconds string instead of a raw JSON value (Rust/Python/Node.js `Option<String>` / `Optional[str]`, Java nullable `String`, C empty-string-when-absent). Previously it was exposed as an untyped JSON value that could be either an integer or a string
- **All SDKs:** `MarketContext.rank_categories` / `lb_market_context_rank_categories` now returns a typed response instead of a raw JSON string. The response contains a list of `RankCategory` (top-level, with `key`/`name`/`sub_categories`) where each `RankSubCategory` carries `key`, `name`, and `market`. The `key` values have the `ib_` prefix stripped so they can be passed directly to `rank_list`. Affects Rust (`RankCategoriesResponse { categories: Vec<RankCategory> }`), Python (`RankCategoriesResponse.categories: List[RankCategory]`), Node.js (`RankCategoriesResponse.categories: RankCategory[]`), Java (`RankCategoriesResponse.categories: RankCategory[]`), C (`lb_rank_categories_response_t` with `lb_rank_category_t*`/`lb_rank_sub_category_t*`), and C++ (`RankCategoriesResponse` struct)
- **Rust SDK:** removed internal `symbol_to_counter_id` / `index_symbol_to_counter_id` conversions from request parameters across `FundamentalContext` (financial\_report, institution\_rating, dividend, forecast\_eps, consensus, valuation, company, shareholder, fund\_holder, corp\_action, invest\_relation, operating, buyback, ratings, business\_segments, etf\_asset\_allocation, and US-series endpoints), `MarketContext` (broker\_holding, broker\_holding\_detail, broker\_holding\_daily, ah\_premium, ah\_premium\_intraday, trade\_stats, constituent), `DCAContext` (list, create, stats, calc\_date), and `AlertContext` (add). These endpoints now send the user-supplied symbol (e.g. `AAPL.US`, `HSI.HK`) directly instead of converting to internal counter-id format (e.g. `ST/US/AAPL`, `IX/HK/HSI`)
- **Rust SDK:** removed remaining `symbol_to_counter_id` conversions from `QuoteContext` (`short_positions`, `option_volume`, `option_volume_daily`, `short_trades`, `us_crypto_overview`), `TradeContext` (`us_query_orders`), `FundamentalContext` (`executive`, `industry_peers`, `valuation_comparison`), `DCAContext` (`check_support`), `SharelistContext` (`add_securities`, `remove_securities`, `sort_securities`), and `PortfolioContext` (`profit_analysis_detail`, `profit_analysis_flows`). All outbound query/body parameters now use the user-supplied symbol string directly
- **Rust SDK:** removed all `counter_id → symbol` response-deserialization conversions. All affected structs (`ExecutiveGroup`, `ShareholderStock`, `FundHolder`, `OperatingFinancial`, `EtfAllocationItem`, `CryptoStaticInfo`, `USOrder`, `USOrderDetail`, `USCryptoEntry`, `USStockEntry`) now read the `symbol` field directly from the server response instead of converting from `counter_id`. The `deserialize_counter_id_as_symbol` helper has been removed from `utils/counter.rs`

### Added

- **All languages:** add `TradeContext.submit_multileg` (`POST /v1/trade/order/multileg`) — submit a multi-leg option combination order (vertical spreads, straddles, strangles, collars, etc.) whose legs are placed together as a single strategy order. Takes `side`, `order_type`, `submitted_quantity`, `strategy` (`MultiLegStrategy`), a list of legs (`symbol` + `ratio_quantity`), and optional `submitted_price` / `remark` / `client_request_id`; returns the existing `SubmitOrderResponse`
Expand All @@ -15,6 +41,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

### Fixed

- **Java SDK:** `AlertContext` now exposes `update(AlertItem)` — matching the other language SDKs — instead of the never-implemented `enable(String)` / `disable(String)` convenience methods (whose native functions had no JNI implementation and threw `UnsatisfiedLinkError`). Enable/disable an alert by fetching it from `list()`, setting `item.enabled`, and calling `update(item)`. The orphaned `alertContextUpdate` native binding is now wired up
- **Java SDK:** six `FundamentalContext` methods now work — `getIndustryRank`, `getIndustryPeers`, `getBusinessSegments`, `getBusinessSegmentsHistory`, `getFinancialReportSnapshot`, and `getInstitutionRatingViews`. Their native methods were declared and called but had no JNI implementation (calling them threw `UnsatisfiedLinkError`), and their result types were not registered for class-ref init. Added the missing JNI functions and registered the corresponding result classes
- **All languages:** the AI Agent streamed conversation no longer errors mid-run when the server sends an explicit `"outputs": null`. `WorkflowFinishedPayload.outputs`, `NodeToolUseFinishedPayload.outputs`, and `SubagentFinishedPayload.outputs` were annotated `#[serde(default)]`, which only covers a *missing* key, not an explicit `null` — so a `workflow_finished` / `node_tool_use_finished` / `subagent_finished` event carrying `null` outputs failed to deserialize and aborted the whole event stream (`invalid type: null, expected struct WorkflowOutputs`). These fields now map `null` to the type's default
- **All languages:** likewise, list-typed fields on the streamed AI Agent event payloads no longer error on an explicit `null` (`invalid type: null, expected a sequence`). `tip_chips` (on the node / subagent / agent-tool `*_started` payloads), `WorkflowFinishedPayload.process_data`, and `SubagentStartedPayload.tools` were `#[serde(default)]`, which does not accept an explicit `null`; they now deserialize `null` to an empty list
- **All languages:** clarified that `CompanyOverview.employees` is typed as a **string** (not an integer) across all SDK languages — the Longbridge API returns this field as a JSON string (e.g. `"10000"`). Doc comments have been updated to make this explicit and prevent downstream tools from incorrectly treating the value as an integer
Expand Down
4 changes: 3 additions & 1 deletion c/cbindgen.toml
Original file line number Diff line number Diff line change
Expand Up @@ -342,6 +342,8 @@ cpp_compat = true
"CTopMoversStock" = "lb_top_movers_stock_t"
"CTopMoversEvent" = "lb_top_movers_event_t"
"CTopMoversResponse" = "lb_top_movers_response_t"
"CRankSubCategory" = "lb_rank_sub_category_t"
"CRankCategory" = "lb_rank_category_t"
"CRankCategoriesResponse" = "lb_rank_categories_response_t"
"CRankListItem" = "lb_rank_list_item_t"
"CRankListResponse" = "lb_rank_list_response_t"
Expand Down Expand Up @@ -527,7 +529,7 @@ include = [
"CValuationHistoryPoint", "CValuationComparisonItem", "CValuationComparisonResponse",
# MarketContext new types
"CTopMoversStock", "CTopMoversEvent", "CTopMoversResponse",
"CRankCategoriesResponse",
"CRankSubCategory", "CRankCategory", "CRankCategoriesResponse",
"CRankListItem", "CRankListResponse",
# ScreenerContext
"CScreenerContext",
Expand Down
93 changes: 72 additions & 21 deletions c/csrc/include/longbridge.h
Original file line number Diff line number Diff line change
Expand Up @@ -3062,6 +3062,20 @@ typedef void (*lb_conversation_event_callback_t)(const struct lb_agent_context_t

typedef void (*lb_free_userdata_func_t)(void*);

/**
* Trigger value of a price alert (exactly one field is populated).
*/
typedef struct CAlertValueMap {
/**
* Absolute price threshold as a decimal string (empty string if not set).
*/
const char *price;
/**
* Percentage-change threshold (NULL if not set).
*/
const double *chg;
} CAlertValueMap;

/**
* A single alert indicator configuration for a symbol.
*/
Expand Down Expand Up @@ -3099,9 +3113,9 @@ typedef struct lb_alert_item_t {
*/
uintptr_t num_state;
/**
* JSON-serialized map of additional indicator parameter values.
* Trigger value of the alert.
*/
const char *value_map;
struct CAlertValueMap value_map;
} lb_alert_item_t;

/**
Expand Down Expand Up @@ -9503,9 +9517,9 @@ typedef struct lb_industry_rank_item_t {
*/
const char *name;
/**
* Counter ID of the industry.
* Industry symbol.
*/
const char *counter_id;
const char *symbol;
/**
* Change percentage.
*/
Expand Down Expand Up @@ -9583,9 +9597,9 @@ typedef struct lb_industry_peer_node_t {
*/
const char *name;
/**
* Counter ID.
* Node symbol.
*/
const char *counter_id;
const char *symbol;
/**
* Number of stocks in this node.
*/
Expand All @@ -9609,9 +9623,9 @@ typedef struct lb_industry_peer_node_t {
*/
typedef struct lb_industry_peers_response_t {
/**
* Top-level industry node info.
* Top-level industry node info (NULL if absent).
*/
struct lb_industry_peers_top_t top;
const struct lb_industry_peers_top_t *top;
/**
* Root peer chain node (NULL if absent).
*/
Expand Down Expand Up @@ -11424,19 +11438,64 @@ typedef struct lb_top_movers_response_t {
*/
uintptr_t num_events;
/**
* Pagination cursor as a JSON string
* Pagination cursor (empty string means no more pages)
*/
const char *next_params;
} lb_top_movers_response_t;

/**
* Rank categories response. `data` is a NUL-terminated JSON string.
* One leaf rank sub-category.
*/
typedef struct lb_rank_sub_category_t {
/**
* Sub-category key (e.g. `"hot_all-us"`). Pass to
* `lb_market_context_rank_list`.
*/
const char *key;
/**
* Display name
*/
const char *name;
/**
* Market code (e.g. `"US"`, `"HK"`)
*/
const char *market;
} lb_rank_sub_category_t;

/**
* A top-level rank category.
*/
typedef struct lb_rank_category_t {
/**
* Top-level key (e.g. `"hot"`)
*/
const char *key;
/**
* Display name
*/
const char *name;
/**
* Sub-categories pointer
*/
const struct lb_rank_sub_category_t *sub_categories;
/**
* Number of sub-categories
*/
uintptr_t num_sub_categories;
} lb_rank_category_t;

/**
* Rank categories response.
*/
typedef struct lb_rank_categories_response_t {
/**
* Raw rank categories data as a JSON string
* Top-level categories pointer
*/
const char *data;
const struct lb_rank_category_t *categories;
/**
* Number of categories
*/
uintptr_t num_categories;
} lb_rank_categories_response_t;

/**
Expand Down Expand Up @@ -12449,14 +12508,6 @@ void lb_fundamental_context_buyback(const struct lb_fundamental_context_t *ctx,
lb_async_callback_t callback,
void *userdata);

/**
* Get stock ratings. Returns `CStockRatings`.
*/
void lb_fundamental_context_ratings(const struct lb_fundamental_context_t *ctx,
const char *symbol,
lb_async_callback_t callback,
void *userdata);

/**
* Get ranked list of top shareholders. Returns `CShareholderTopResponse`.
*/
Expand Down Expand Up @@ -12523,7 +12574,7 @@ void lb_fundamental_context_industry_rank(const struct lb_fundamental_context_t
* Pass NULL for `industry_id` to omit it.
*/
void lb_fundamental_context_industry_peers(const struct lb_fundamental_context_t *ctx,
const char *counter_id,
const char *symbol,
const char *market,
const char *industry_id,
lb_async_callback_t callback,
Expand Down
67 changes: 56 additions & 11 deletions c/src/alert_context/types.rs
Original file line number Diff line number Diff line change
@@ -1,8 +1,57 @@
use std::os::raw::c_char;

use longbridge::alert::{AlertItem, AlertList, AlertSymbolGroup};
use longbridge::alert::{AlertItem, AlertList, AlertSymbolGroup, AlertValueMap};

use crate::types::{CString, CVec, ToFFI};
use crate::types::{COption, CString, CVec, ToFFI};

/// Trigger value of a price alert (exactly one field is populated).
#[repr(C)]
pub struct CAlertValueMap {
/// Absolute price threshold as a decimal string (empty string if not set).
pub price: *const c_char,
/// Percentage-change threshold (NULL if not set).
pub chg: *const f64,
}

pub(crate) struct CAlertValueMapOwned {
price: CString,
chg: COption<f64>,
}

impl From<AlertValueMap> for CAlertValueMapOwned {
fn from(v: AlertValueMap) -> Self {
Self {
price: v.price.map(|d| d.to_string()).unwrap_or_default().into(),
chg: v.chg.into(),
}
}
}

impl ToFFI for CAlertValueMapOwned {
type FFIType = CAlertValueMap;
fn to_ffi_type(&self) -> Self::FFIType {
CAlertValueMap {
price: self.price.to_ffi_type(),
chg: self.chg.to_ffi_type(),
}
}
}

impl CAlertValueMap {
/// Reconstruct a [`longbridge::alert::AlertValueMap`] from this C struct.
///
/// # Safety
/// `price` must be a valid null-terminated C string (or NULL) and `chg`
/// must be NULL or point to a valid `f64`.
unsafe fn to_value_map(&self) -> AlertValueMap {
use crate::types::cstr_to_rust;
let price = cstr_to_rust(self.price);
AlertValueMap {
price: (!price.is_empty()).then(|| price.parse().ok()).flatten(),
chg: (!self.chg.is_null()).then(|| *self.chg),
}
}
}

/// A single alert indicator configuration for a symbol.
#[repr(C)]
Expand All @@ -23,8 +72,8 @@ pub struct CAlertItem {
pub state: *const i32,
/// Number of elements in the `state` array.
pub num_state: usize,
/// JSON-serialized map of additional indicator parameter values.
pub value_map: *const c_char,
/// Trigger value of the alert.
pub value_map: CAlertValueMap,
}

pub(crate) struct CAlertItemOwned {
Expand All @@ -35,7 +84,7 @@ pub(crate) struct CAlertItemOwned {
scope: i32,
text: CString,
state: CVec<i32>,
value_map: CString,
value_map: CAlertValueMapOwned,
}

impl From<AlertItem> for CAlertItemOwned {
Expand All @@ -48,9 +97,7 @@ impl From<AlertItem> for CAlertItemOwned {
scope: v.scope,
text: v.text.into(),
state: v.state.into(),
value_map: serde_json::to_string(&v.value_map)
.unwrap_or_default()
.into(),
value_map: v.value_map.into(),
}
}
}
Expand All @@ -64,8 +111,6 @@ impl CAlertItem {
pub unsafe fn to_alert_item(&self) -> longbridge::alert::AlertItem {
use crate::types::cstr_to_rust;
let state = std::slice::from_raw_parts(self.state, self.num_state).to_vec();
let value_map_str = cstr_to_rust(self.value_map);
let value_map = serde_json::from_str(&value_map_str).unwrap_or(serde_json::Value::Null);
longbridge::alert::AlertItem {
id: cstr_to_rust(self.id),
indicator_id: cstr_to_rust(self.indicator_id),
Expand All @@ -74,7 +119,7 @@ impl CAlertItem {
scope: self.scope,
text: cstr_to_rust(self.text),
state,
value_map,
value_map: self.value_map.to_value_map(),
}
}
}
Expand Down
Loading
Loading