From eea924d22580811b7fdc733f2a24691e218c10dc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=A2=81=E7=AB=A0=E6=B4=AA?= Date: Fri, 11 Sep 2026 10:27:54 +0800 Subject: [PATCH] fix(trade): page history_executions through all results The endpoint caps each response at 1000 records. `history_executions` deserialized just the first page's `trades` and returned it, silently truncating windows with >1000 fills. Walk the `page` parameter (1-based, verified against production) until `has_more` is false, deduping by `trade_id` as a guard. Bounded to 1000 pages as a runaway guard. The blocking wrapper and all language bindings delegate to this and get it for free. --- CHANGELOG.md | 1 + rust/src/trade/context.rs | 50 +++++++++++++++---- .../trade/requests/get_history_executions.rs | 14 ++++++ 3 files changed, 54 insertions(+), 11 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 23ede6a267..5b56e1c98d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,6 +12,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Fixed +- **Rust SDK:** `TradeContext::history_executions` now pages through all results internally via the `page` parameter (the endpoint caps each response at 1000 records) until `has_more` is false, so callers get the complete execution set instead of a silently-truncated first page. Applies to the blocking wrapper and every binding that delegates to it. - **Python SDK:** reconciled the hand-maintained type stub `python/pysrc/longbridge/openapi.pyi` with the actual PyO3 implementation. Removed phantom methods that did not exist and would raise `AttributeError` when called (`AlertContext.enable`/`disable`, `AsyncQuoteContext.option_volume`/`option_volume_daily`); fixed wrong signatures/return types (`FundamentalContext.macroeconomic_indicators` had dropped its `country`/`keyword` params and had the wrong return type, `macroeconomic` was missing `offset`, `DCAContext.create`/`update` return `DcaCreateResult` not `DcaList`, `DCAContext.pause`/`resume`/`stop` and `SharelistContext.create` return `None`); added missing methods (`QuoteContext.filings` + async, `AlertContext.update`, `DCAContext.update`, `TradeContext.set_on_grid_order_changed` + async); added the three entirely-missing async context classes (`AsyncMarketContext`, `AsyncCalendarContext`, `AsyncPortfolioContext`) plus the missing method surfaces of `AsyncFundamentalContext` and `AsyncContentContext`; and added the missing referenced types (`FilingItem`, `DcaCreateResult`, `MacroeconomicCountry`, `MacroeconomicIndicatorListResponse`, `PushGridOrderChanged`). Type hints only — no runtime/behaviour change to the native module - **Java SDK:** fixed six JNI methods whose Rust `extern "system"` signature no longer matched the Java `native` declaration, so every call aborted with `java.lang.RuntimeException: JNI call failed` (or read misaligned stack arguments). The Java layer had been migrated to options objects / trimmed argument lists but the Rust JNI side was left in the old positional form. `MarketContext.getRankList` (`RankListOptions`), `QuoteContext.getShortTrades` (`ShortTradesOptions`), `ScreenerContext.getStrategy` (`ScreenerStrategyOptions`), `FundamentalContext.shareholderDetail` (`ShareholderDetailOptions`) and `FundamentalContext.valuationComparison` (`ValuationComparisonOptions`) now read their fields off the options object. Separately, `QuoteContext.getShortPositions` was missing the `count` parameter that the Rust core, Node.js and Python bindings all require — the Rust JNI still expected it, so the call crashed — so `getShortPositions(String symbol)` becomes `getShortPositions(String symbol, int count)`. Reported as longbridge/developers#1249 (`getRankList`) - **C/C++ SDKs:** every list argument that crosses the FFI boundary now tolerates a null pointer with a zero length. `std::vector::data()` is allowed to return `nullptr` for an empty vector, which is exactly what the C++ binding passes for an omitted list argument, but the C layer fed it straight to `std::slice::from_raw_parts` — undefined behaviour that **aborts the process** under the debug UB checks. Hit live by `QuoteContext::warrant_list` with no filters (`c/src/quote_context/context.rs:784`); all 17 call sites across `quote_context`, `trade_context`, `agent_context`, `alert_context`, and `types` now go through a null-tolerant `slice_from_raw_parts` helper diff --git a/rust/src/trade/context.rs b/rust/src/trade/context.rs index 05a78b5a8a..aa4402bfd4 100644 --- a/rust/src/trade/context.rs +++ b/rust/src/trade/context.rs @@ -214,22 +214,50 @@ impl TradeContext { &self, options: impl Into>, ) -> Result> { + use std::collections::HashSet; + #[derive(Deserialize)] struct Response { + #[serde(default)] + has_more: bool, trades: Vec, } - Ok(self - .0 - .http_cli - .request(Method::GET, "/v1/trade/execution/history") - .query_params(options.into().unwrap_or_default()) - .response::>() - .send() - .with_subscriber(self.0.log_subscriber.clone()) - .await? - .0 - .trades) + // The endpoint caps each response at 1000 records; walk the `page` + // param (1-based) until `has_more` is false. Dedupe by + // `trade_id` and stop if a page adds nothing new, guarding + // against the gateway ignoring `page`. Bounded to 1000 pages as + // a runaway guard. + let mut options = options.into().unwrap_or_default(); + let mut all: Vec = Vec::new(); + let mut seen: HashSet = HashSet::new(); + for page in 1..=1000u32 { + options = options.with_page(page); + let resp = self + .0 + .http_cli + .request(Method::GET, "/v1/trade/execution/history") + .query_params(&options) + .response::>() + .send() + .with_subscriber(self.0.log_subscriber.clone()) + .await? + .0; + if resp.trades.is_empty() { + break; + } + let mut added = 0usize; + for t in resp.trades { + if seen.insert(t.trade_id.clone()) { + all.push(t); + added += 1; + } + } + if !resp.has_more || added == 0 { + break; + } + } + Ok(all) } /// Get today executions diff --git a/rust/src/trade/requests/get_history_executions.rs b/rust/src/trade/requests/get_history_executions.rs index b9844acd3c..5d0d4f5b83 100644 --- a/rust/src/trade/requests/get_history_executions.rs +++ b/rust/src/trade/requests/get_history_executions.rs @@ -18,6 +18,10 @@ pub struct GetHistoryExecutionsOptions { with = "serde_utils::timestamp_opt" )] end_at: Option, + // Pagination cursor (1-based), set internally by `history_executions` while + // walking pages. Not a public builder — callers always get every page. + #[serde(skip_serializing_if = "Option::is_none")] + page: Option, } impl GetHistoryExecutionsOptions { @@ -56,4 +60,14 @@ impl GetHistoryExecutionsOptions { ..self } } + + /// Internal 1-based pagination cursor used by `history_executions` to walk + /// all pages. Not a public builder — the SDK sets this while paginating. + #[inline] + pub(crate) fn with_page(self, page: u32) -> Self { + Self { + page: Some(page), + ..self + } + } }