Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
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
22 changes: 22 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,34 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased]

### 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

### 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 optional `parent_message_id` parameter to the AI Agent `conversation` and `conversation_streamed` methods — pass the `message_id` from a previous response to attach a follow-up message after the specified one, keeping the message stream in order. Only valid together with `chat_uid`; must not be set for a new conversation

### 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
- **Java SDK:** invalid arguments passed across the JNI boundary no longer crash the JVM (process abort / core dump). Previously a Rust `panic!`/`unwrap`/`expect` in the value conversions unwound across the `extern "system"` boundary and aborted the whole process. These cases now throw a catchable `java.lang.IllegalArgumentException` instead:
- a `null` or unrecognized constant passed for any enum argument (e.g. `Language`, `OrderSide`, `OrderType`, `Period`, `Market`, `AdjustType`, …)
- a `null` `BigDecimal` / `LocalDate` / `LocalTime` / `OffsetDateTime` / `String` argument, or an out-of-range date/time, an out-of-range/non-representable decimal, or a non-UTF-8 string
Expand Down
4 changes: 3 additions & 1 deletion c/cbindgen.toml
Original file line number Diff line number Diff line change
Expand Up @@ -322,6 +322,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 @@ -499,7 +501,7 @@ include = [
"CValuationHistoryPoint", "CValuationComparisonItem", "CValuationComparisonResponse",
# MarketContext new types
"CTopMoversStock", "CTopMoversEvent", "CTopMoversResponse",
"CRankCategoriesResponse",
"CRankSubCategory", "CRankCategory", "CRankCategoriesResponse",
"CRankListItem", "CRankListResponse",
# ScreenerContext
"CScreenerContext",
Expand Down
85 changes: 72 additions & 13 deletions c/csrc/include/longbridge.h
Original file line number Diff line number Diff line change
Expand Up @@ -2904,6 +2904,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 @@ -2941,9 +2955,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 @@ -8221,9 +8235,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 @@ -8301,9 +8315,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 @@ -8327,9 +8341,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 @@ -10142,19 +10156,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 @@ -11230,7 +11289,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
6 changes: 3 additions & 3 deletions c/src/fundamental_context/context.rs
Original file line number Diff line number Diff line change
Expand Up @@ -569,14 +569,14 @@ pub unsafe extern "C" fn lb_fundamental_context_industry_rank(
#[unsafe(no_mangle)]
pub unsafe extern "C" fn lb_fundamental_context_industry_peers(
ctx: *const CFundamentalContext,
counter_id: *const c_char,
symbol: *const c_char,
market: *const c_char,
industry_id: *const c_char,
callback: CAsyncCallback,
userdata: *mut c_void,
) {
let ctx_inner = (*ctx).ctx.clone();
let counter_id = cstr_to_rust(counter_id);
let symbol = cstr_to_rust(symbol);
let market = cstr_to_rust(market);
let industry_id: Option<String> = if industry_id.is_null() {
None
Expand All @@ -586,7 +586,7 @@ pub unsafe extern "C" fn lb_fundamental_context_industry_peers(
execute_async(callback, ctx, userdata, async move {
let resp: CCow<CIndustryPeersResponseOwned> = CCow::new(CIndustryPeersResponseOwned::from(
ctx_inner
.industry_peers(counter_id, market, industry_id)
.industry_peers(symbol, market, industry_id)
.await?,
));
Ok(resp)
Expand Down
Loading
Loading