Skip to content

Commit 249869e

Browse files
authored
fix(trade): page history_executions through all has_more results (#591)
## Problem `/v1/trade/execution/history` caps each response at 1000 records. `history_executions` deserialized only the first page's `trades` and returned it, so any window with >1000 fills was **silently truncated** (reported in longbridge/longbridge-terminal#320). ## Change `history_executions` now walks every page via the `page` query parameter (1-based) until `has_more` is false, returning the complete set. Deduped by `trade_id` as a guard; bounded to 1000 pages. **Verified against production** (`openapi.longbridge.com`): on an account with 122 fills, `page=1` returns all 122 while **`page=2` returns empty** — confirming the gateway honors `page` (a page-ignoring gateway would return the same 122 for page=2). Response shape is `data: { has_more, trades[] }`. Public signature unchanged (`GetHistoryExecutionsOptions` → `Vec<Execution>`). The blocking wrapper and all language bindings delegate to this method, so they get complete results for free — no per-binding change. ## Testing `cargo build` + `cargo clippy` clean. Live-verified `page` semantics against production. Refs longbridge/longbridge-terminal#320
1 parent 0621e3f commit 249869e

3 files changed

Lines changed: 54 additions & 11 deletions

File tree

‎CHANGELOG.md‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
1212

1313
### Fixed
1414

15+
- **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.
1516
- **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
1617
- **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`)
1718
- **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

‎rust/src/trade/context.rs‎

Lines changed: 39 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -214,22 +214,50 @@ impl TradeContext {
214214
&self,
215215
options: impl Into<Option<GetHistoryExecutionsOptions>>,
216216
) -> Result<Vec<Execution>> {
217+
use std::collections::HashSet;
218+
217219
#[derive(Deserialize)]
218220
struct Response {
221+
#[serde(default)]
222+
has_more: bool,
219223
trades: Vec<Execution>,
220224
}
221225

222-
Ok(self
223-
.0
224-
.http_cli
225-
.request(Method::GET, "/v1/trade/execution/history")
226-
.query_params(options.into().unwrap_or_default())
227-
.response::<Json<Response>>()
228-
.send()
229-
.with_subscriber(self.0.log_subscriber.clone())
230-
.await?
231-
.0
232-
.trades)
226+
// The endpoint caps each response at 1000 records; walk the `page`
227+
// param (1-based) until `has_more` is false. Dedupe by
228+
// `trade_id` and stop if a page adds nothing new, guarding
229+
// against the gateway ignoring `page`. Bounded to 1000 pages as
230+
// a runaway guard.
231+
let mut options = options.into().unwrap_or_default();
232+
let mut all: Vec<Execution> = Vec::new();
233+
let mut seen: HashSet<String> = HashSet::new();
234+
for page in 1..=1000u32 {
235+
options = options.with_page(page);
236+
let resp = self
237+
.0
238+
.http_cli
239+
.request(Method::GET, "/v1/trade/execution/history")
240+
.query_params(&options)
241+
.response::<Json<Response>>()
242+
.send()
243+
.with_subscriber(self.0.log_subscriber.clone())
244+
.await?
245+
.0;
246+
if resp.trades.is_empty() {
247+
break;
248+
}
249+
let mut added = 0usize;
250+
for t in resp.trades {
251+
if seen.insert(t.trade_id.clone()) {
252+
all.push(t);
253+
added += 1;
254+
}
255+
}
256+
if !resp.has_more || added == 0 {
257+
break;
258+
}
259+
}
260+
Ok(all)
233261
}
234262

235263
/// Get today executions

‎rust/src/trade/requests/get_history_executions.rs‎

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,10 @@ pub struct GetHistoryExecutionsOptions {
1818
with = "serde_utils::timestamp_opt"
1919
)]
2020
end_at: Option<OffsetDateTime>,
21+
// Pagination cursor (1-based), set internally by `history_executions` while
22+
// walking pages. Not a public builder — callers always get every page.
23+
#[serde(skip_serializing_if = "Option::is_none")]
24+
page: Option<u32>,
2125
}
2226

2327
impl GetHistoryExecutionsOptions {
@@ -56,4 +60,14 @@ impl GetHistoryExecutionsOptions {
5660
..self
5761
}
5862
}
63+
64+
/// Internal 1-based pagination cursor used by `history_executions` to walk
65+
/// all pages. Not a public builder — the SDK sets this while paginating.
66+
#[inline]
67+
pub(crate) fn with_page(self, page: u32) -> Self {
68+
Self {
69+
page: Some(page),
70+
..self
71+
}
72+
}
5973
}

0 commit comments

Comments
 (0)