From d35f63416550729113aedd6d281aaf4492f578cc Mon Sep 17 00:00:00 2001 From: Jason Lee Date: Tue, 30 Jun 2026 11:39:40 +0800 Subject: [PATCH 01/44] feat(config): add x-dc-region data-center routing from credentials MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Longbridge credentials are prefixed with their data center: `us_…` for the US data center and `ap_…` for Asia-Pacific. This applies to the OAuth access token and, in legacy API-key mode, to the `app_key`, `app_secret`, and `access_token`. The API gateway routes a request to the matching data center via the `x-dc-region` header, defaulting to `ap` when absent. - `longbridge-geo`: add `DcRegion` (`from_credential` / `from_credentials` / `as_str`) and the `DC_REGION_HEADER` constant, re-exported through `longbridge-httpcli` and the crate root. - The HTTP client and the quote/trade WebSocket upgrade now auto-inject `x-dc-region` derived from the auth credentials, so US-region tokens reach the US data center with no caller changes. An explicitly-set header (e.g. via a custom header or `Config::dc_region`) is preserved. - `Config::dc_region()` remains as an explicit override. Co-Authored-By: Claude Opus 4.8 (1M context) --- rust/crates/geo/src/lib.rs | 98 +++++++++++++++++++++++++++ rust/crates/httpclient/src/lib.rs | 2 +- rust/crates/httpclient/src/request.rs | 20 +++++- rust/src/config.rs | 61 +++++++++++++++-- rust/src/lib.rs | 1 + 5 files changed, 174 insertions(+), 8 deletions(-) diff --git a/rust/crates/geo/src/lib.rs b/rust/crates/geo/src/lib.rs index 930f45ec63..839fc5dff9 100644 --- a/rust/crates/geo/src/lib.rs +++ b/rust/crates/geo/src/lib.rs @@ -62,3 +62,101 @@ pub async fn is_cn() -> bool { IS_CN_DONE.get().copied().unwrap_or(false) } } + +/// HTTP and WebSocket header that selects the data center serving a request. +/// +/// An absent header is treated as [`DcRegion::Ap`] by the API gateway. +pub const DC_REGION_HEADER: &str = "x-dc-region"; + +/// Data center region used for API gateway routing. +/// +/// Independent of [`is_cn`]: that picks the `*.longbridge.cn` vs +/// `*.longbridge.com` host (mainland acceleration), while this selects which +/// data center (`us`/`ap`) the gateway sources data from. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum DcRegion { + /// Asia-Pacific data center (`ap`). The gateway default. + Ap, + /// US data center (`us`). + Us, +} + +impl DcRegion { + /// Derive the region from a single credential's prefix. + /// + /// Longbridge credentials — the OAuth access token, and the legacy API-key + /// `app_key` / `app_secret` / `access_token` — are prefixed with their data + /// center: `us_…` for the US data center, `ap_…` for Asia-Pacific. A `us_` + /// prefix maps to [`DcRegion::Us`]; everything else — including + /// `ap_`-prefixed and unprefixed credentials — maps to + /// [`DcRegion::Ap`], matching the gateway default. A leading `Bearer ` + /// is tolerated so an `Authorization` value can be passed directly. + pub fn from_credential(credential: &str) -> Self { + let credential = credential.strip_prefix("Bearer ").unwrap_or(credential); + if credential.starts_with("us_") { + DcRegion::Us + } else { + DcRegion::Ap + } + } + + /// Derive the region from a set of credentials, returning [`DcRegion::Us`] + /// if any of them carries the `us_` prefix. + /// + /// Used for legacy API-key auth, where the `app_key`, `app_secret`, and + /// `access_token` all carry the region prefix. + pub fn from_credentials(credentials: &[&str]) -> Self { + if credentials + .iter() + .any(|c| DcRegion::from_credential(c) == DcRegion::Us) + { + DcRegion::Us + } else { + DcRegion::Ap + } + } + + /// The [`DC_REGION_HEADER`] value for this region (`"us"` or `"ap"`). + pub fn as_str(self) -> &'static str { + match self { + DcRegion::Us => "us", + DcRegion::Ap => "ap", + } + } +} + +#[cfg(test)] +mod dc_region_tests { + use super::*; + + #[test] + fn from_credential_detects_region() { + assert_eq!(DcRegion::from_credential("us_abc"), DcRegion::Us); + assert_eq!(DcRegion::from_credential("ap_abc"), DcRegion::Ap); + // Unprefixed and unknown prefixes fall back to the AP default. + assert_eq!(DcRegion::from_credential("abc"), DcRegion::Ap); + assert_eq!(DcRegion::from_credential(""), DcRegion::Ap); + // A `Bearer ` prefix is tolerated. + assert_eq!(DcRegion::from_credential("Bearer us_x"), DcRegion::Us); + assert_eq!(DcRegion::from_credential("Bearer ap_x"), DcRegion::Ap); + } + + #[test] + fn from_credentials_is_us_if_any_is_us() { + assert_eq!( + DcRegion::from_credentials(&["ap_key", "us_secret", "ap_token"]), + DcRegion::Us + ); + assert_eq!( + DcRegion::from_credentials(&["ap_key", "ap_secret", "ap_token"]), + DcRegion::Ap + ); + assert_eq!(DcRegion::from_credentials(&[]), DcRegion::Ap); + } + + #[test] + fn as_str_matches_header_value() { + assert_eq!(DcRegion::Us.as_str(), "us"); + assert_eq!(DcRegion::Ap.as_str(), "ap"); + } +} diff --git a/rust/crates/httpclient/src/lib.rs b/rust/crates/httpclient/src/lib.rs index 9a495ebe87..544c3050bf 100644 --- a/rust/crates/httpclient/src/lib.rs +++ b/rust/crates/httpclient/src/lib.rs @@ -16,7 +16,7 @@ mod timestamp; pub use client::HttpClient; pub use config::{AuthConfig, HttpClientConfig}; pub use error::{HttpClientError, HttpClientResult, HttpError}; -pub use longbridge_geo::is_cn; +pub use longbridge_geo::{DC_REGION_HEADER, DcRegion, is_cn}; pub use qs::QsError; pub use request::{FromPayload, Json, RequestBuilder, ToPayload}; pub use reqwest::Method; diff --git a/rust/crates/httpclient/src/request.rs b/rust/crates/httpclient/src/request.rs index aee7182578..8d7c942189 100644 --- a/rust/crates/httpclient/src/request.rs +++ b/rust/crates/httpclient/src/request.rs @@ -6,7 +6,7 @@ use std::{ time::{Duration, Instant}, }; -use longbridge_geo::is_cn; +use longbridge_geo::{DC_REGION_HEADER, DcRegion, is_cn}; use reqwest::{ Method, StatusCode, header::{HeaderMap, HeaderName, HeaderValue}, @@ -277,6 +277,24 @@ where .header("X-Timestamp", timestamp.to_string()) .header("Content-Type", "application/json; charset=utf-8"); + // Route to the data center matching the credential's region (us/ap), + // derived from the `us_`/`ap_` prefix on the OAuth token or — in legacy + // API-key mode — the app_key/app_secret/access_token. Skip when the + // caller already set the header explicitly (e.g. via custom headers). + let region_already_set = default_headers.contains_key(DC_REGION_HEADER) + || self.headers.contains_key(DC_REGION_HEADER); + if !region_already_set { + let dc_region = match &config.auth { + AuthConfig::ApiKey { + app_key, + app_secret, + access_token, + } => DcRegion::from_credentials(&[app_key, access_token, app_secret]), + AuthConfig::OAuth(_) => DcRegion::from_credential(&access_token), + }; + request_builder = request_builder.header(DC_REGION_HEADER, dc_region.as_str()); + } + // set the request body if let Some(body) = &self.body { let body = body diff --git a/rust/src/config.rs b/rust/src/config.rs index c2f0c49c56..94c839a1eb 100644 --- a/rust/src/config.rs +++ b/rust/src/config.rs @@ -7,7 +7,9 @@ use std::{ }; pub(crate) use http::{HeaderName, HeaderValue, Request, header}; -use longbridge_httpcli::{HttpClient, HttpClientConfig, Json, Method, is_cn}; +use longbridge_httpcli::{ + DC_REGION_HEADER, DcRegion, HttpClient, HttpClientConfig, Json, Method, is_cn, +}; use longbridge_oauth::OAuth; use num_enum::IntoPrimitive; use serde::{Deserialize, Serialize}; @@ -498,7 +500,29 @@ impl Config { .block_on(self.refresh_access_token(expired_at)) } - fn create_ws_request(&self, url: &str) -> tokio_tungstenite::tungstenite::Result> { + /// Resolve the data-center region from the auth credentials. For OAuth the + /// access token is resolved (and refreshed if needed); for legacy API-key + /// mode the `app_key`/`app_secret`/`access_token` prefixes are inspected. + async fn auth_dc_region(&self) -> DcRegion { + match &self.auth { + AuthMode::ApiKey { + app_key, + app_secret, + access_token, + } => DcRegion::from_credentials(&[app_key, access_token, app_secret]), + AuthMode::OAuth(oauth) => oauth + .access_token() + .await + .map(|token| DcRegion::from_credential(&token)) + .unwrap_or(DcRegion::Ap), + } + } + + fn create_ws_request( + &self, + url: &str, + dc_region: DcRegion, + ) -> tokio_tungstenite::tungstenite::Result> { let mut request = url.into_client_request()?; request.headers_mut().append( header::ACCEPT_LANGUAGE, @@ -512,21 +536,34 @@ impl Config { request.headers_mut().append(name, val); } } + // Route the upgrade to the data center matching the credential's region, + // unless the caller already set the header via a custom header. + if !self + .custom_headers + .keys() + .any(|key| key.eq_ignore_ascii_case(DC_REGION_HEADER)) + { + request.headers_mut().append( + HeaderName::from_static(DC_REGION_HEADER), + HeaderValue::from_static(dc_region.as_str()), + ); + } Ok(request) } pub(crate) async fn create_quote_ws_request( &self, ) -> (&str, tokio_tungstenite::tungstenite::Result>) { + let dc_region = self.auth_dc_region().await; match self.quote_ws_url.as_deref() { - Some(url) => (url, self.create_ws_request(url)), + Some(url) => (url, self.create_ws_request(url, dc_region)), None => { let url = if is_cn().await { DEFAULT_QUOTE_WS_URL_CN } else { DEFAULT_QUOTE_WS_URL }; - (url, self.create_ws_request(url)) + (url, self.create_ws_request(url, dc_region)) } } } @@ -534,15 +571,16 @@ impl Config { pub(crate) async fn create_trade_ws_request( &self, ) -> (&str, tokio_tungstenite::tungstenite::Result>) { + let dc_region = self.auth_dc_region().await; match self.trade_ws_url.as_deref() { - Some(url) => (url, self.create_ws_request(url)), + Some(url) => (url, self.create_ws_request(url, dc_region)), None => { let url = if is_cn().await { DEFAULT_TRADE_WS_URL_CN } else { DEFAULT_TRADE_WS_URL }; - (url, self.create_ws_request(url)) + (url, self.create_ws_request(url, dc_region)) } } } @@ -562,6 +600,17 @@ impl Config { self } + /// Override the data-center region for every HTTP and WebSocket request by + /// setting the [`DC_REGION_HEADER`](crate::DC_REGION_HEADER) explicitly. + /// + /// This is rarely needed: the region is otherwise derived automatically + /// from the auth credentials' prefix (`us_`/`ap_`). Use this only to + /// force a specific data center regardless of the credential. + #[must_use] + pub fn dc_region(self, region: DcRegion) -> Self { + self.header(DC_REGION_HEADER, region.as_str()) + } + /// Set the HTTP endpoint URL in place. pub fn set_http_url(&mut self, url: impl Into) { self.http_url = Some(url.into()); diff --git a/rust/src/lib.rs b/rust/src/lib.rs index df996bbd33..e2fb096b47 100644 --- a/rust/src/lib.rs +++ b/rust/src/lib.rs @@ -45,6 +45,7 @@ pub use dca::DCAContext; pub use error::{Error, Result, SimpleError, SimpleErrorKind}; pub use fundamental::FundamentalContext; pub use longbridge_httpcli as httpclient; +pub use longbridge_httpcli::{DC_REGION_HEADER, DcRegion}; pub use longbridge_wscli as wsclient; pub use market::MarketContext; pub use portfolio::PortfolioContext; From 4e60df83ab4893415546bf6df1b20cb249cb9ef1 Mon Sep 17 00:00:00 2001 From: Jason Lee Date: Tue, 30 Jun 2026 20:43:16 +0800 Subject: [PATCH 02/44] fix(httpcli): strip us_/ap_ region prefix from OAuth bearer token MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The `us_`/`ap_` prefix on an access token is region metadata used to derive the `x-dc-region` routing header — it is not part of the verifiable bearer credential. Sending the full `us_…` token in `Authorization: Bearer` makes the gateway reject it (401102 token verification failed). Strip the region prefix before building the `Authorization` header (deriving the region from the prefix first), so the gateway verifies the bare token and routes by `x-dc-region`. WebSocket auth is unaffected: it uses an OTP fetched over this same HTTP path. Add `DcRegion::strip_region_prefix`. Co-Authored-By: Claude Opus 4.8 (1M context) --- rust/crates/geo/src/lib.rs | 26 +++++++++++++++++++++++++ rust/crates/httpclient/src/request.rs | 28 +++++++++++++-------------- 2 files changed, 40 insertions(+), 14 deletions(-) diff --git a/rust/crates/geo/src/lib.rs b/rust/crates/geo/src/lib.rs index 839fc5dff9..dfb3687143 100644 --- a/rust/crates/geo/src/lib.rs +++ b/rust/crates/geo/src/lib.rs @@ -123,6 +123,22 @@ impl DcRegion { DcRegion::Ap => "ap", } } + + /// Strip the region prefix (`us_` / `ap_`), and any leading `Bearer `, from + /// a credential, returning the bare token to transmit. + /// + /// The prefix is region metadata for [`from_credential`] / the + /// [`DC_REGION_HEADER`] — it is **not** part of the verifiable credential. + /// The gateway verifies the bare token and routes by the region header, so + /// the prefix must be removed before the token is sent (e.g. in an + /// `Authorization: Bearer …` header or a WebSocket auth handshake). + pub fn strip_region_prefix(credential: &str) -> &str { + let credential = credential.strip_prefix("Bearer ").unwrap_or(credential); + credential + .strip_prefix("us_") + .or_else(|| credential.strip_prefix("ap_")) + .unwrap_or(credential) + } } #[cfg(test)] @@ -159,4 +175,14 @@ mod dc_region_tests { assert_eq!(DcRegion::Us.as_str(), "us"); assert_eq!(DcRegion::Ap.as_str(), "ap"); } + + #[test] + fn strip_region_prefix_removes_region_and_bearer() { + assert_eq!(DcRegion::strip_region_prefix("us_eyJabc"), "eyJabc"); + assert_eq!(DcRegion::strip_region_prefix("ap_eyJabc"), "eyJabc"); + assert_eq!(DcRegion::strip_region_prefix("Bearer us_eyJabc"), "eyJabc"); + // Unprefixed tokens pass through unchanged. + assert_eq!(DcRegion::strip_region_prefix("eyJabc"), "eyJabc"); + assert_eq!(DcRegion::strip_region_prefix("Bearer eyJabc"), "eyJabc"); + } } diff --git a/rust/crates/httpclient/src/request.rs b/rust/crates/httpclient/src/request.rs index 8d7c942189..862bb40521 100644 --- a/rust/crates/httpclient/src/request.rs +++ b/rust/crates/httpclient/src/request.rs @@ -237,8 +237,9 @@ where .and_then(|value| value.parse().ok()) .unwrap_or_else(Timestamp::now); - // Resolve app_key, access_token, and optional app_secret from auth config - let (app_key, access_token, app_secret) = match &config.auth { + // Resolve app_key, access_token, optional app_secret, and the data-center + // region from the auth config. + let (app_key, access_token, app_secret, dc_region) = match &config.auth { AuthConfig::ApiKey { app_key, app_secret, @@ -247,16 +248,24 @@ where app_key.clone(), access_token.clone(), Some(app_secret.clone()), + DcRegion::from_credentials(&[app_key, access_token, app_secret]), ), AuthConfig::OAuth(oauth) => { let token = oauth .access_token() .await .map_err(|e| HttpClientError::OAuth(e.to_string()))?; + // The `us_`/`ap_` prefix is region metadata, not part of the + // verifiable bearer credential: derive the region from it, then + // strip it so the gateway verifies the bare token and routes by + // the `x-dc-region` header. + let region = DcRegion::from_credential(&token); + let bare = DcRegion::strip_region_prefix(&token); ( oauth.client_id().to_string(), - format!("Bearer {token}"), + format!("Bearer {bare}"), None, + region, ) } }; @@ -278,20 +287,11 @@ where .header("Content-Type", "application/json; charset=utf-8"); // Route to the data center matching the credential's region (us/ap), - // derived from the `us_`/`ap_` prefix on the OAuth token or — in legacy - // API-key mode — the app_key/app_secret/access_token. Skip when the - // caller already set the header explicitly (e.g. via custom headers). + // unless the caller already set the header explicitly (e.g. via custom + // headers). let region_already_set = default_headers.contains_key(DC_REGION_HEADER) || self.headers.contains_key(DC_REGION_HEADER); if !region_already_set { - let dc_region = match &config.auth { - AuthConfig::ApiKey { - app_key, - app_secret, - access_token, - } => DcRegion::from_credentials(&[app_key, access_token, app_secret]), - AuthConfig::OAuth(_) => DcRegion::from_credential(&access_token), - }; request_builder = request_builder.header(DC_REGION_HEADER, dc_region.as_str()); } From b4495539e72d5cec8c937407b092735128217d20 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=A2=81=E7=AB=A0=E6=B4=AA?= Date: Wed, 1 Jul 2026 22:36:18 +0800 Subject: [PATCH 03/44] fix(dc-region): don't strip region prefix from tokens; add staging WS URLs MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Based on live testing: - Tokens with region prefixes (hk_m_, us_m_, ap_m_) are sent as-is to the gateway. The server accepts the full prefixed token and routes via the x-dc-region header — no prefix stripping is needed. - strip_region_prefix now only removes a leading "Bearer " prefix. - Add http_url_staging, quote_ws_url_staging, trade_ws_url_staging helpers: US: openapi-global.longbridge.xyz openapi-global-quote.longbridge.xyz openapi-global-trade.longbridge.xyz AP: openapi.longbridge.xyz / openapi-quote.longbridge.xyz / ... - Update strip_region_prefix test to reflect new behavior. Co-Authored-By: Claude Sonnet 4.6 (1M context) --- rust/crates/geo/src/lib.rs | 54 ++++++++++++++++++--------- rust/crates/httpclient/src/request.rs | 10 ++--- 2 files changed, 40 insertions(+), 24 deletions(-) diff --git a/rust/crates/geo/src/lib.rs b/rust/crates/geo/src/lib.rs index dfb3687143..2a20912e86 100644 --- a/rust/crates/geo/src/lib.rs +++ b/rust/crates/geo/src/lib.rs @@ -124,20 +124,38 @@ impl DcRegion { } } - /// Strip the region prefix (`us_` / `ap_`), and any leading `Bearer `, from - /// a credential, returning the bare token to transmit. + /// Strip any leading `Bearer ` from a credential. /// - /// The prefix is region metadata for [`from_credential`] / the - /// [`DC_REGION_HEADER`] — it is **not** part of the verifiable credential. - /// The gateway verifies the bare token and routes by the region header, so - /// the prefix must be removed before the token is sent (e.g. in an - /// `Authorization: Bearer …` header or a WebSocket auth handshake). + /// Region prefixes (`hk_m_`, `us_m_`, `ap_m_`, …) are routing metadata + /// consumed by [`from_credential`] to derive the [`DC_REGION_HEADER`]. + /// The gateway accepts the full prefixed token and routes via the header, + /// so **no region prefix is stripped** — only `Bearer ` is removed. pub fn strip_region_prefix(credential: &str) -> &str { - let credential = credential.strip_prefix("Bearer ").unwrap_or(credential); - credential - .strip_prefix("us_") - .or_else(|| credential.strip_prefix("ap_")) - .unwrap_or(credential) + credential.strip_prefix("Bearer ").unwrap_or(credential) + } + + /// HTTP base URL for this region (staging environment). + pub fn http_url_staging(self) -> &'static str { + match self { + DcRegion::Us => "https://openapi-global.longbridge.xyz", + DcRegion::Ap => "https://openapi.longbridge.xyz", + } + } + + /// Quote WebSocket URL for this region (staging environment). + pub fn quote_ws_url_staging(self) -> &'static str { + match self { + DcRegion::Us => "wss://openapi-global-quote.longbridge.xyz", + DcRegion::Ap => "wss://openapi-quote.longbridge.xyz", + } + } + + /// Trade WebSocket URL for this region (staging environment). + pub fn trade_ws_url_staging(self) -> &'static str { + match self { + DcRegion::Us => "wss://openapi-global-trade.longbridge.xyz", + DcRegion::Ap => "wss://openapi-trade.longbridge.xyz", + } } } @@ -177,12 +195,12 @@ mod dc_region_tests { } #[test] - fn strip_region_prefix_removes_region_and_bearer() { - assert_eq!(DcRegion::strip_region_prefix("us_eyJabc"), "eyJabc"); - assert_eq!(DcRegion::strip_region_prefix("ap_eyJabc"), "eyJabc"); - assert_eq!(DcRegion::strip_region_prefix("Bearer us_eyJabc"), "eyJabc"); - // Unprefixed tokens pass through unchanged. - assert_eq!(DcRegion::strip_region_prefix("eyJabc"), "eyJabc"); + fn strip_region_prefix_only_removes_bearer() { + // Region prefixes are kept as-is; only "Bearer " is stripped. + assert_eq!(DcRegion::strip_region_prefix("us_m_eyJabc"), "us_m_eyJabc"); + assert_eq!(DcRegion::strip_region_prefix("hk_m_eyJabc"), "hk_m_eyJabc"); + assert_eq!(DcRegion::strip_region_prefix("Bearer us_m_eyJabc"), "us_m_eyJabc"); assert_eq!(DcRegion::strip_region_prefix("Bearer eyJabc"), "eyJabc"); + assert_eq!(DcRegion::strip_region_prefix("eyJabc"), "eyJabc"); } } diff --git a/rust/crates/httpclient/src/request.rs b/rust/crates/httpclient/src/request.rs index 862bb40521..c656656098 100644 --- a/rust/crates/httpclient/src/request.rs +++ b/rust/crates/httpclient/src/request.rs @@ -255,15 +255,13 @@ where .access_token() .await .map_err(|e| HttpClientError::OAuth(e.to_string()))?; - // The `us_`/`ap_` prefix is region metadata, not part of the - // verifiable bearer credential: derive the region from it, then - // strip it so the gateway verifies the bare token and routes by - // the `x-dc-region` header. + // Derive DC region from the token prefix (us_→US, others→AP). + // The token is sent as-is (including any prefix); the gateway + // accepts the full token and routes via the x-dc-region header. let region = DcRegion::from_credential(&token); - let bare = DcRegion::strip_region_prefix(&token); ( oauth.client_id().to_string(), - format!("Bearer {bare}"), + format!("Bearer {token}"), None, region, ) From 005e0c6b1a37b7988a3c8bb428a0387c9858769d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=A2=81=E7=AB=A0=E6=B4=AA?= Date: Wed, 1 Jul 2026 22:50:55 +0800 Subject: [PATCH 04/44] chore: apply cargo fmt --- rust/crates/geo/src/lib.rs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/rust/crates/geo/src/lib.rs b/rust/crates/geo/src/lib.rs index 2a20912e86..dcaad469a4 100644 --- a/rust/crates/geo/src/lib.rs +++ b/rust/crates/geo/src/lib.rs @@ -199,7 +199,10 @@ mod dc_region_tests { // Region prefixes are kept as-is; only "Bearer " is stripped. assert_eq!(DcRegion::strip_region_prefix("us_m_eyJabc"), "us_m_eyJabc"); assert_eq!(DcRegion::strip_region_prefix("hk_m_eyJabc"), "hk_m_eyJabc"); - assert_eq!(DcRegion::strip_region_prefix("Bearer us_m_eyJabc"), "us_m_eyJabc"); + assert_eq!( + DcRegion::strip_region_prefix("Bearer us_m_eyJabc"), + "us_m_eyJabc" + ); assert_eq!(DcRegion::strip_region_prefix("Bearer eyJabc"), "eyJabc"); assert_eq!(DcRegion::strip_region_prefix("eyJabc"), "eyJabc"); } From 7ad89e7d52ee209f785a0ea1a850821d4bb5249c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=A2=81=E7=AB=A0=E6=B4=AA?= Date: Wed, 1 Jul 2026 22:54:51 +0800 Subject: [PATCH 05/44] fix(dc-region): staging always uses openapi-global endpoints; dc routing via x-dc-region header --- rust/crates/geo/src/lib.rs | 30 ++++++++++++------------------ 1 file changed, 12 insertions(+), 18 deletions(-) diff --git a/rust/crates/geo/src/lib.rs b/rust/crates/geo/src/lib.rs index dcaad469a4..effee01d13 100644 --- a/rust/crates/geo/src/lib.rs +++ b/rust/crates/geo/src/lib.rs @@ -134,28 +134,22 @@ impl DcRegion { credential.strip_prefix("Bearer ").unwrap_or(credential) } - /// HTTP base URL for this region (staging environment). - pub fn http_url_staging(self) -> &'static str { - match self { - DcRegion::Us => "https://openapi-global.longbridge.xyz", - DcRegion::Ap => "https://openapi.longbridge.xyz", - } + /// HTTP base URL for the staging environment. + /// + /// Staging always uses the global endpoint; the data-center is selected + /// by the [`DC_REGION_HEADER`] (`ap` or `us`), not the hostname. + pub fn http_url_staging(_self: DcRegion) -> &'static str { + "https://openapi-global.longbridge.xyz" } - /// Quote WebSocket URL for this region (staging environment). - pub fn quote_ws_url_staging(self) -> &'static str { - match self { - DcRegion::Us => "wss://openapi-global-quote.longbridge.xyz", - DcRegion::Ap => "wss://openapi-quote.longbridge.xyz", - } + /// Quote WebSocket URL for the staging environment. + pub fn quote_ws_url_staging(_self: DcRegion) -> &'static str { + "wss://openapi-global-quote.longbridge.xyz" } - /// Trade WebSocket URL for this region (staging environment). - pub fn trade_ws_url_staging(self) -> &'static str { - match self { - DcRegion::Us => "wss://openapi-global-trade.longbridge.xyz", - DcRegion::Ap => "wss://openapi-trade.longbridge.xyz", - } + /// Trade WebSocket URL for the staging environment. + pub fn trade_ws_url_staging(_self: DcRegion) -> &'static str { + "wss://openapi-global-trade.longbridge.xyz" } } From b1194278fd5b7f43ae2a2abf9ec6a962b2937c01 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=A2=81=E7=AB=A0=E6=B4=AA?= Date: Wed, 1 Jul 2026 22:56:34 +0800 Subject: [PATCH 06/44] chore: remove http_url_staging/quote_ws_url_staging/trade_ws_url_staging methods --- rust/crates/geo/src/lib.rs | 18 ------------------ 1 file changed, 18 deletions(-) diff --git a/rust/crates/geo/src/lib.rs b/rust/crates/geo/src/lib.rs index effee01d13..f6e319569a 100644 --- a/rust/crates/geo/src/lib.rs +++ b/rust/crates/geo/src/lib.rs @@ -133,24 +133,6 @@ impl DcRegion { pub fn strip_region_prefix(credential: &str) -> &str { credential.strip_prefix("Bearer ").unwrap_or(credential) } - - /// HTTP base URL for the staging environment. - /// - /// Staging always uses the global endpoint; the data-center is selected - /// by the [`DC_REGION_HEADER`] (`ap` or `us`), not the hostname. - pub fn http_url_staging(_self: DcRegion) -> &'static str { - "https://openapi-global.longbridge.xyz" - } - - /// Quote WebSocket URL for the staging environment. - pub fn quote_ws_url_staging(_self: DcRegion) -> &'static str { - "wss://openapi-global-quote.longbridge.xyz" - } - - /// Trade WebSocket URL for the staging environment. - pub fn trade_ws_url_staging(_self: DcRegion) -> &'static str { - "wss://openapi-global-trade.longbridge.xyz" - } } #[cfg(test)] From 99321466ad051803de52896d66c19a1fb69b866c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=A2=81=E7=AB=A0=E6=B4=AA?= Date: Wed, 1 Jul 2026 23:00:45 +0800 Subject: [PATCH 07/44] fix(oauth): update staging URL to use openapi-global.longbridge.xyz Co-Authored-By: Claude Sonnet 4.6 (1M context) --- rust/crates/oauth/src/client.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/rust/crates/oauth/src/client.rs b/rust/crates/oauth/src/client.rs index 2b4682d834..1872ecf6a8 100644 --- a/rust/crates/oauth/src/client.rs +++ b/rust/crates/oauth/src/client.rs @@ -16,7 +16,7 @@ use crate::{ const OAUTH_BASE_URL: &str = "https://openapi.longbridge.com/oauth2"; const OAUTH_BASE_URL_CN: &str = "https://openapi.longbridge.cn/oauth2"; -const OAUTH_BASE_URL_TEST: &str = "https://openapi.longbridge.xyz/oauth2"; +const OAUTH_BASE_URL_TEST: &str = "https://openapi-global.longbridge.xyz/oauth2"; async fn oauth_base_url() -> &'static str { if std::env::var("LONGBRIDGE_ENV") From bc9d3badcd380097d1797d21a976d6064a8a3dac Mon Sep 17 00:00:00 2001 From: Jason Lee Date: Thu, 2 Jul 2026 14:44:30 +0800 Subject: [PATCH 08/44] feat: Add per-request dc_restrict API for region-limited endpoints MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Introduce a declarative `RequestBuilder::dc_restrict(region)` API so a call site can restrict a request to a single data center. When the session's region differs, `do_send` short-circuits with a unified `HttpClientError::DcRegionRestricted { path, required, current }` instead of forwarding a request the target data center cannot serve. - geo: `DcRegion::allows()` + uppercase `Display` (AP/US) for messages, while `as_str()` keeps the lowercase `x-dc-region` header value (us/ap). - httpcli: `RequestBuilder::dc_restrict()`, `HttpClient::dc_region()`, and the `DcRegionRestricted` error variant. - Declare AP-only endpoints via the API: recurring investment (DCA, whole module), operating reviews, broker holdings, and the broker-queue WebSocket command. Supports future US-only endpoints by declaring `DcRegion::Us` at their call sites — no path-prefix heuristics. Co-Authored-By: Claude Opus 4.8 (1M context) --- rust/crates/geo/src/lib.rs | 36 +++++++++++++++++++++++++++ rust/crates/httpclient/src/client.rs | 23 ++++++++++++++++- rust/crates/httpclient/src/error.rs | 15 +++++++++++ rust/crates/httpclient/src/request.rs | 30 ++++++++++++++++++++++ rust/src/dca/context.rs | 6 ++++- rust/src/fundamental/context.rs | 25 +++++++++++++++++-- rust/src/market/context.rs | 31 ++++++++++++++++++++--- rust/src/quote/context.rs | 11 ++++++++ 8 files changed, 169 insertions(+), 8 deletions(-) diff --git a/rust/crates/geo/src/lib.rs b/rust/crates/geo/src/lib.rs index f6e319569a..dad4436056 100644 --- a/rust/crates/geo/src/lib.rs +++ b/rust/crates/geo/src/lib.rs @@ -124,6 +124,14 @@ impl DcRegion { } } + /// Whether this session may reach an API limited to `required`. + /// + /// `true` when the session's region matches the API's required region; + /// callers short-circuit with a unified error when it is `false`. + pub fn allows(self, required: DcRegion) -> bool { + self == required + } + /// Strip any leading `Bearer ` from a credential. /// /// Region prefixes (`hk_m_`, `us_m_`, `ap_m_`, …) are routing metadata @@ -135,6 +143,17 @@ impl DcRegion { } } +impl std::fmt::Display for DcRegion { + /// Human-facing uppercase name (`AP`/`US`), for error messages and display. + /// The lowercase [`DC_REGION_HEADER`] value is [`as_str`](Self::as_str). + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.write_str(match self { + DcRegion::Us => "US", + DcRegion::Ap => "AP", + }) + } +} + #[cfg(test)] mod dc_region_tests { use super::*; @@ -170,6 +189,23 @@ mod dc_region_tests { assert_eq!(DcRegion::Ap.as_str(), "ap"); } + #[test] + fn allows_matches_same_region() { + assert!(DcRegion::Ap.allows(DcRegion::Ap)); + assert!(DcRegion::Us.allows(DcRegion::Us)); + assert!(!DcRegion::Us.allows(DcRegion::Ap)); + assert!(!DcRegion::Ap.allows(DcRegion::Us)); + } + + #[test] + fn display_is_uppercase() { + // Human-facing display is uppercase; the header value stays lowercase. + assert_eq!(DcRegion::Us.to_string(), "US"); + assert_eq!(DcRegion::Ap.to_string(), "AP"); + assert_eq!(DcRegion::Us.as_str(), "us"); + assert_eq!(DcRegion::Ap.as_str(), "ap"); + } + #[test] fn strip_region_prefix_only_removes_bearer() { // Region prefixes are kept as-is; only "Bearer " is stripped. diff --git a/rust/crates/httpclient/src/client.rs b/rust/crates/httpclient/src/client.rs index 30f59f6ea9..57ca30b39e 100644 --- a/rust/crates/httpclient/src/client.rs +++ b/rust/crates/httpclient/src/client.rs @@ -1,12 +1,15 @@ use std::sync::Arc; +use longbridge_geo::DcRegion; use reqwest::{ Client, Method, header::{HeaderMap, HeaderName, HeaderValue}, }; use serde::Deserialize; -use crate::{HttpClientConfig, HttpClientError, HttpClientResult, Json, RequestBuilder}; +use crate::{ + AuthConfig, HttpClientConfig, HttpClientError, HttpClientResult, Json, RequestBuilder, +}; /// Longbridge HTTP client #[derive(Clone)] @@ -40,6 +43,24 @@ impl HttpClient { self } + /// The data-center region (`us`/`ap`) derived from this client's auth + /// credentials. Used by non-HTTP call sites (e.g. WebSocket quote commands) + /// to apply the same AP-only routing guard the HTTP path enforces. + pub async fn dc_region(&self) -> DcRegion { + match &self.config.auth { + AuthConfig::ApiKey { + app_key, + app_secret, + access_token, + } => DcRegion::from_credentials(&[app_key, access_token, app_secret]), + AuthConfig::OAuth(oauth) => oauth + .access_token() + .await + .map(|token| DcRegion::from_credential(&token)) + .unwrap_or(DcRegion::Ap), + } + } + /// Create a new request builder #[inline] pub fn request( diff --git a/rust/crates/httpclient/src/error.rs b/rust/crates/httpclient/src/error.rs index 9eb87c72ad..ddf57047a6 100644 --- a/rust/crates/httpclient/src/error.rs +++ b/rust/crates/httpclient/src/error.rs @@ -1,5 +1,6 @@ use std::error::Error; +use longbridge_geo::DcRegion; use reqwest::StatusCode; use crate::qs::QsError; @@ -34,6 +35,20 @@ pub enum HttpClientError { #[error("request timeout")] RequestTimeout, + /// The requested API is restricted to one data center and cannot be reached + /// from a session in a different region. + #[error( + "this API ({path}) is only available in the {required} data center and is not supported for your {current}-region account" + )] + DcRegionRestricted { + /// The restricted API path (or WebSocket command) that was requested. + path: String, + /// The data center this API is limited to. + required: DcRegion, + /// The session's current data-center region. + current: DcRegion, + }, + /// OpenAPI error #[error("openapi error: code={code}: {message}")] OpenApi { diff --git a/rust/crates/httpclient/src/request.rs b/rust/crates/httpclient/src/request.rs index c656656098..8477a7fc3e 100644 --- a/rust/crates/httpclient/src/request.rs +++ b/rust/crates/httpclient/src/request.rs @@ -125,6 +125,7 @@ pub struct RequestBuilder<'a, T, Q, R> { headers: HeaderMap, body: Option, query_params: Option, + dc_restrict: Option, mark_resp: PhantomData, } @@ -137,6 +138,7 @@ impl<'a> RequestBuilder<'a, (), (), ()> { headers: Default::default(), body: None, query_params: None, + dc_restrict: None, mark_resp: PhantomData, } } @@ -156,6 +158,7 @@ impl<'a, T, Q, R> RequestBuilder<'a, T, Q, R> { headers: self.headers, body: Some(body), query_params: self.query_params, + dc_restrict: self.dc_restrict, mark_resp: self.mark_resp, } } @@ -175,6 +178,19 @@ impl<'a, T, Q, R> RequestBuilder<'a, T, Q, R> { self } + /// Restrict this request to a single data center. + /// + /// When set, [`do_send`](Self::do_send) short-circuits with + /// [`HttpClientError::DcRegionRestricted`] if the session's region differs, + /// instead of forwarding a request the target data center cannot serve. + /// Call sites for region-limited endpoints declare their region here — + /// `Ap` for AP-only APIs, `Us` for US-only ones. + #[must_use] + pub fn dc_restrict(mut self, region: DcRegion) -> Self { + self.dc_restrict = Some(region); + self + } + /// Set the query string #[must_use] pub fn query_params(self, params: Q2) -> RequestBuilder<'a, T, Q2, R> @@ -188,6 +204,7 @@ impl<'a, T, Q, R> RequestBuilder<'a, T, Q, R> { headers: self.headers, body: self.body, query_params: Some(params), + dc_restrict: self.dc_restrict, mark_resp: self.mark_resp, } } @@ -205,6 +222,7 @@ impl<'a, T, Q, R> RequestBuilder<'a, T, Q, R> { headers: self.headers, body: self.body, query_params: self.query_params, + dc_restrict: self.dc_restrict, mark_resp: PhantomData, } } @@ -268,6 +286,18 @@ where } }; + // Short-circuit region-limited endpoints with a single unified error, + // instead of forwarding a request the target data center cannot serve. + if let Some(required) = self.dc_restrict + && !dc_region.allows(required) + { + return Err(HttpClientError::DcRegionRestricted { + path: self.path.clone(), + required, + current: dc_region, + }); + } + let app_key_value = HeaderValue::from_str(&app_key).map_err(|_| HttpClientError::InvalidApiKey)?; let access_token_value = HeaderValue::from_str(&access_token) diff --git a/rust/src/dca/context.rs b/rust/src/dca/context.rs index d9eaaf028e..8c96f16922 100644 --- a/rust/src/dca/context.rs +++ b/rust/src/dca/context.rs @@ -1,6 +1,6 @@ use std::sync::Arc; -use longbridge_httpcli::{HttpClient, Json, Method}; +use longbridge_httpcli::{DcRegion, HttpClient, Json, Method}; use serde::{Serialize, de::DeserializeOwned}; use tracing::{Subscriber, dispatcher, instrument::WithSubscriber}; @@ -55,6 +55,8 @@ impl DCAContext { .0 .http_cli .request(Method::GET, path) + // Recurring investment (DCA) is served only by the AP data center. + .dc_restrict(DcRegion::Ap) .query_params(query) .response::>() .send() @@ -72,6 +74,8 @@ impl DCAContext { .0 .http_cli .request(Method::POST, path) + // Recurring investment (DCA) is served only by the AP data center. + .dc_restrict(DcRegion::Ap) .body(Json(body)) .response::>() .send() diff --git a/rust/src/fundamental/context.rs b/rust/src/fundamental/context.rs index 450848147e..4987453db4 100644 --- a/rust/src/fundamental/context.rs +++ b/rust/src/fundamental/context.rs @@ -1,6 +1,6 @@ use std::sync::Arc; -use longbridge_httpcli::{HttpClient, Json, Method}; +use longbridge_httpcli::{DcRegion, HttpClient, Json, Method}; use serde::{Serialize, de::DeserializeOwned}; use tracing::{Subscriber, dispatcher, instrument::WithSubscriber}; @@ -80,6 +80,26 @@ impl FundamentalContext { .0) } + /// Like [`get`](Self::get), but restricted to a single data center. Used by + /// region-limited endpoints (e.g. AP-only fundamentals). + async fn get_dc(&self, path: &'static str, query: Q, dc_restrict: DcRegion) -> Result + where + R: DeserializeOwned + Send + Sync + 'static, + Q: Serialize + Send + Sync, + { + Ok(self + .0 + .http_cli + .request(Method::GET, path) + .dc_restrict(dc_restrict) + .query_params(query) + .response::>() + .send() + .with_subscriber(self.0.log_subscriber.clone()) + .await? + .0) + } + // ── financial_report ───────────────────────────────────────── /// Get financial reports for a security. @@ -466,11 +486,12 @@ impl FundamentalContext { struct Query { counter_id: String, } - self.get( + self.get_dc( "/v1/quote/operatings", Query { counter_id: symbol_to_counter_id(&symbol.into()), }, + DcRegion::Ap, ) .await } diff --git a/rust/src/market/context.rs b/rust/src/market/context.rs index e0a27b4701..c3512cb030 100644 --- a/rust/src/market/context.rs +++ b/rust/src/market/context.rs @@ -1,6 +1,6 @@ use std::sync::Arc; -use longbridge_httpcli::{HttpClient, Json, Method}; +use longbridge_httpcli::{DcRegion, HttpClient, Json, Method}; use serde::{Serialize, de::DeserializeOwned}; use tracing::{Subscriber, dispatcher, instrument::WithSubscriber}; @@ -85,6 +85,26 @@ impl MarketContext { .0) } + /// Like [`get`](Self::get), but restricted to a single data center. Used by + /// region-limited endpoints (e.g. AP-only broker holdings). + async fn get_dc(&self, path: &'static str, query: Q, dc_restrict: DcRegion) -> Result + where + R: DeserializeOwned + Send + Sync + 'static, + Q: Serialize + Send + Sync, + { + Ok(self + .0 + .http_cli + .request(Method::GET, path) + .dc_restrict(dc_restrict) + .query_params(query) + .response::>() + .send() + .with_subscriber(self.0.log_subscriber.clone()) + .await? + .0) + } + async fn post(&self, path: &'static str, body: B) -> Result where R: DeserializeOwned + Send + Sync + 'static, @@ -135,12 +155,13 @@ impl MarketContext { #[serde(rename = "type")] period: &'static str, } - self.get( + self.get_dc( "/v1/quote/broker-holding", Query { counter_id: symbol_to_counter_id(&symbol.into()), period: period_str, }, + DcRegion::Ap, ) .await } @@ -156,11 +177,12 @@ impl MarketContext { struct Query { counter_id: String, } - self.get( + self.get_dc( "/v1/quote/broker-holding/detail", Query { counter_id: symbol_to_counter_id(&symbol.into()), }, + DcRegion::Ap, ) .await } @@ -178,12 +200,13 @@ impl MarketContext { counter_id: String, parti_number: String, } - self.get( + self.get_dc( "/v1/quote/broker-holding/daily", Query { counter_id: symbol_to_counter_id(&symbol.into()), parti_number: broker_id.into(), }, + DcRegion::Ap, ) .await } diff --git a/rust/src/quote/context.rs b/rust/src/quote/context.rs index 663142c2fc..a3de649c0d 100644 --- a/rust/src/quote/context.rs +++ b/rust/src/quote/context.rs @@ -668,6 +668,17 @@ impl QuoteContext { /// # }); /// ``` pub async fn brokers(&self, symbol: impl Into) -> Result { + // Broker queue is served only by the AP data center; short-circuit a + // non-AP session with the same unified error the HTTP path returns. + let current = self.0.http_cli.dc_region().await; + if !current.allows(longbridge_httpcli::DcRegion::Ap) { + return Err(longbridge_httpcli::HttpClientError::DcRegionRestricted { + path: "quote/brokers (WebSocket)".to_string(), + required: longbridge_httpcli::DcRegion::Ap, + current, + } + .into()); + } let resp: quote::SecurityBrokersResponse = self .request( cmd_code::GET_SECURITY_BROKERS, From 019de31d98c7e96aeb76b096c69fcc96ff0a8e0d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=A2=81=E7=AB=A0=E6=B4=AA?= Date: Thu, 2 Jul 2026 16:00:58 +0800 Subject: [PATCH 09/44] feat(us): add US-market APIs across Rust, Python, Node.js, Java Implements 14 US-only endpoints behind dc_restrict(DcRegion::Us): FundamentalContext (9 methods): - us_company_overview GET /v1/stock-info/company-overview - us_valuation_overview GET /v1/stock-info/valuation-overview - us_financial_overview GET /v1/stock-info/finn-overview - us_financial_statement_v3 GET /v1/us/quote/financials/statements - us_key_financial_metrics GET /v1/stock-info/fin-keyfactor - us_analyst_consensus GET /v1/stock-info/fin-consensus - us_etf_dividend_info GET /v1/stock-info/etf-dividend-info - us_company_dividends GET /v1/stock-info/company-dividends - us_etf_files GET /v1/stock-info/etf-files QuoteContext (1 method): - us_crypto_overview GET /v1/gemini/crypto-overview TradeContext (4 methods): - us_query_orders POST /v1/orders/query - us_order_detail GET /v3/orders/{order_id} - us_asset_overview GET /v1/us/assets/overview - us_realized_pl GET /v1/us/assets/pl/realized Layers: - Rust core + blocking wrappers - Python (sync + async, serde_json::Value via pythonize) - Node.js (async, raw JSON string for flexible fields) - Java JNI (returns JSON strings for complex types) Co-Authored-By: Claude Sonnet 4.6 (1M context) --- .claude/worktrees/agent-a962fdff6e0c7fa74 | 1 + java/src/fundamental_context.rs | 103 ++++++++++ nodejs/src/fundamental/context.rs | 59 ++++++ nodejs/src/fundamental/types.rs | 196 +++++++++++++++++++ python/src/fundamental/context.rs | 68 +++++++ python/src/fundamental/context_async.rs | 93 +++++++++ python/src/fundamental/mod.rs | 11 ++ python/src/fundamental/types.rs | 210 +++++++++++++++++++++ rust/src/blocking/fundamental.rs | 84 +++++++++ rust/src/blocking/quote.rs | 14 +- rust/src/blocking/trade.rs | 37 +++- rust/src/fundamental/context.rs | 220 ++++++++++++++++++++++ rust/src/fundamental/types.rs | 181 ++++++++++++++++++ rust/src/lib.rs | 12 ++ rust/src/quote/context.rs | 33 +++- rust/src/quote/mod.rs | 1 + rust/src/quote/types.rs | 45 +++++ rust/src/trade/context.rs | 120 +++++++++++- rust/src/trade/mod.rs | 4 + rust/src/trade/types.rs | 165 ++++++++++++++++ 20 files changed, 1650 insertions(+), 7 deletions(-) create mode 160000 .claude/worktrees/agent-a962fdff6e0c7fa74 diff --git a/.claude/worktrees/agent-a962fdff6e0c7fa74 b/.claude/worktrees/agent-a962fdff6e0c7fa74 new file mode 160000 index 0000000000..4e7e056927 --- /dev/null +++ b/.claude/worktrees/agent-a962fdff6e0c7fa74 @@ -0,0 +1 @@ +Subproject commit 4e7e0569270443e306621e9a97d98059fa59a9ec diff --git a/java/src/fundamental_context.rs b/java/src/fundamental_context.rs index bfd2d8b373..162370d60d 100644 --- a/java/src/fundamental_context.rs +++ b/java/src/fundamental_context.rs @@ -330,3 +330,106 @@ pub unsafe extern "system" fn Java_com_longbridge_SdkNative_fundamentalContextMa Ok(()) }) } + +// ── US-market JNI stubs ─────────────────────────────────────────────────────── +// All US APIs return JSON strings; Java callers parse with Gson/Jackson. + +macro_rules! us_counter_id_method { + ($jni_name:ident, $method:ident) => { + #[unsafe(no_mangle)] + pub unsafe extern "system" fn $jni_name( + mut env: JNIEnv, + _class: JClass, + context: i64, + counter_id: JObject, + callback: JObject, + ) { + jni_result(&mut env, (), |env| { + let context = &*(context as *const ContextObj); + let counter_id: String = FromJValue::from_jvalue(env, counter_id.into())?; + async_util::execute(env, callback, async move { + let resp = context.ctx.$method(counter_id).await?; + Ok(serde_json::to_string(&resp).unwrap_or_default()) + })?; + Ok(()) + }) + } + }; +} + +macro_rules! us_counter_id_report_method { + ($jni_name:ident, $method:ident) => { + #[unsafe(no_mangle)] + pub unsafe extern "system" fn $jni_name( + mut env: JNIEnv, + _class: JClass, + context: i64, + counter_id: JObject, + report: JObject, + callback: JObject, + ) { + jni_result(&mut env, (), |env| { + let context = &*(context as *const ContextObj); + let counter_id: String = FromJValue::from_jvalue(env, counter_id.into())?; + let report: String = FromJValue::from_jvalue(env, report.into())?; + async_util::execute(env, callback, async move { + let resp = context.ctx.$method(counter_id, report).await?; + Ok(serde_json::to_string(&resp).unwrap_or_default()) + })?; + Ok(()) + }) + } + }; +} + +us_counter_id_method!(Java_com_longbridge_SdkNative_fundamentalContextUsCompanyOverview, us_company_overview); +us_counter_id_method!(Java_com_longbridge_SdkNative_fundamentalContextUsValuationOverview, us_valuation_overview); +us_counter_id_report_method!(Java_com_longbridge_SdkNative_fundamentalContextUsFinancialOverview, us_financial_overview); +us_counter_id_report_method!(Java_com_longbridge_SdkNative_fundamentalContextUsKeyFinancialMetrics, us_key_financial_metrics); +us_counter_id_report_method!(Java_com_longbridge_SdkNative_fundamentalContextUsAnalystConsensus, us_analyst_consensus); +us_counter_id_method!(Java_com_longbridge_SdkNative_fundamentalContextUsEtfDividendInfo, us_etf_dividend_info); +us_counter_id_method!(Java_com_longbridge_SdkNative_fundamentalContextUsCompanyDividends, us_company_dividends); + +#[unsafe(no_mangle)] +pub unsafe extern "system" fn Java_com_longbridge_SdkNative_fundamentalContextUsFinancialStatementV3( + mut env: JNIEnv, + _class: JClass, + context: i64, + counter_id: JObject, + kind: JObject, + report: JObject, + callback: JObject, +) { + jni_result(&mut env, (), |env| { + let context = &*(context as *const ContextObj); + let counter_id: String = FromJValue::from_jvalue(env, counter_id.into())?; + let kind: String = FromJValue::from_jvalue(env, kind.into())?; + let report: String = FromJValue::from_jvalue(env, report.into())?; + async_util::execute(env, callback, async move { + let resp = context.ctx.us_financial_statement_v3(counter_id, kind, report).await?; + Ok(serde_json::to_string(&resp).unwrap_or_default()) + })?; + Ok(()) + }) +} + +#[unsafe(no_mangle)] +pub unsafe extern "system" fn Java_com_longbridge_SdkNative_fundamentalContextUsEtfFiles( + mut env: JNIEnv, + _class: JClass, + context: i64, + counter_id: JObject, + size: JObject, + callback: JObject, +) { + jni_result(&mut env, (), |env| { + let context = &*(context as *const ContextObj); + let counter_id: String = FromJValue::from_jvalue(env, counter_id.into())?; + let size: Option = FromJValue::from_jvalue(env, size.into())?; + async_util::execute(env, callback, async move { + let resp = context.ctx.us_etf_files(counter_id, size.map(|s| s as u32)).await?; + Ok(serde_json::to_string(&resp).unwrap_or_default()) + })?; + Ok(()) + }) +} diff --git a/nodejs/src/fundamental/context.rs b/nodejs/src/fundamental/context.rs index 78ae9285f6..112eb0a828 100644 --- a/nodejs/src/fundamental/context.rs +++ b/nodejs/src/fundamental/context.rs @@ -322,4 +322,63 @@ impl FundamentalContext { .map_err(ErrorNewType)? .into()) } + + // ── US-market methods ───────────────────────────────────────────────────── + + /// Get US company overview. US token required. + #[napi] + pub async fn us_company_overview(&self, counter_id: String) -> Result { + Ok(self.ctx.us_company_overview(counter_id).await.map_err(ErrorNewType)?.into()) + } + + /// Get US valuation overview. US token required. + #[napi] + pub async fn us_valuation_overview(&self, counter_id: String) -> Result { + Ok(self.ctx.us_valuation_overview(counter_id).await.map_err(ErrorNewType)?.into()) + } + + /// Get US financial overview (JSON string). US token required. + #[napi] + pub async fn us_financial_overview(&self, counter_id: String, report: String) -> Result { + let v = self.ctx.us_financial_overview(counter_id, report).await.map_err(ErrorNewType)?; + serde_json::to_string(&v).map_err(|e| napi::Error::from_reason(e.to_string())) + } + + /// Get US financial statement v3. kind: "IS"/"BS"/"CF". US token required. + #[napi] + pub async fn us_financial_statement_v3(&self, counter_id: String, kind: String, report: String) -> Result { + Ok(self.ctx.us_financial_statement_v3(counter_id, kind, report).await.map_err(ErrorNewType)?.into()) + } + + /// Get US key financial metrics (JSON string). US token required. + #[napi] + pub async fn us_key_financial_metrics(&self, counter_id: String, report: String) -> Result { + let v = self.ctx.us_key_financial_metrics(counter_id, report).await.map_err(ErrorNewType)?; + serde_json::to_string(&v).map_err(|e| napi::Error::from_reason(e.to_string())) + } + + /// Get US analyst consensus (JSON string). US token required. + #[napi] + pub async fn us_analyst_consensus(&self, counter_id: String, report: String) -> Result { + let v = self.ctx.us_analyst_consensus(counter_id, report).await.map_err(ErrorNewType)?; + serde_json::to_string(&v).map_err(|e| napi::Error::from_reason(e.to_string())) + } + + /// Get US ETF dividend history. US token required. + #[napi] + pub async fn us_etf_dividend_info(&self, counter_id: String) -> Result { + Ok(self.ctx.us_etf_dividend_info(counter_id).await.map_err(ErrorNewType)?.into()) + } + + /// Get US company dividends. US token required. + #[napi] + pub async fn us_company_dividends(&self, counter_id: String) -> Result { + Ok(self.ctx.us_company_dividends(counter_id).await.map_err(ErrorNewType)?.into()) + } + + /// Get US ETF document list. size=None returns all. US token required. + #[napi] + pub async fn us_etf_files(&self, counter_id: String, size: Option) -> Result { + Ok(self.ctx.us_etf_files(counter_id, size).await.map_err(ErrorNewType)?.into()) + } } diff --git a/nodejs/src/fundamental/types.rs b/nodejs/src/fundamental/types.rs index 0e8526c87d..aa203792b3 100644 --- a/nodejs/src/fundamental/types.rs +++ b/nodejs/src/fundamental/types.rs @@ -2057,3 +2057,199 @@ impl From for MacroeconomicResponse { } } } + +// ── US-market types ─────────────────────────────────────────────────────────── + +use longbridge::fundamental::types as lb_us; + +/// Industry rank tag +#[napi_derive::napi(object)] +#[derive(Debug, Clone)] +pub struct USRankTag { + pub name: String, + pub chg: String, + pub rank_type: String, +} + +impl From for USRankTag { + fn from(v: lb_us::USRankTag) -> Self { + Self { name: v.name, chg: v.chg, rank_type: v.rank_type } + } +} + +/// US company overview +#[napi_derive::napi(object)] +#[derive(Debug, Clone)] +pub struct USCompanyOverview { + pub intro: String, + pub market_cap: String, + pub ccy_symbol: String, + pub top_rank_tags: Vec, + pub detail_url: String, +} + +impl From for USCompanyOverview { + fn from(v: lb_us::USCompanyOverview) -> Self { + Self { + intro: v.intro, + market_cap: v.market_cap, + ccy_symbol: v.ccy_symbol, + top_rank_tags: v.top_rank_tags.into_iter().map(Into::into).collect(), + detail_url: v.detail_url, + } + } +} + +/// US valuation indicator +#[napi_derive::napi(object)] +#[derive(Debug, Clone)] +pub struct USValuationIndicator { + pub circle: String, + pub part: String, + pub metric: String, + pub metric_type: String, + pub desc: String, + pub ccy_symbol: String, +} + +impl From for USValuationIndicator { + fn from(v: lb_us::USValuationIndicator) -> Self { + Self { + circle: v.circle, part: v.part, metric: v.metric, + metric_type: v.metric_type, desc: v.desc, ccy_symbol: v.ccy_symbol, + } + } +} + +/// US valuation overview +#[napi_derive::napi(object)] +#[derive(Debug, Clone)] +pub struct USValuationOverview { + pub indicator: String, + pub current_indicator: USValuationIndicator, + pub range: i32, + pub date: String, + pub ai_summary: String, +} + +impl From for USValuationOverview { + fn from(v: lb_us::USValuationOverview) -> Self { + Self { + indicator: v.indicator, + current_indicator: v.current_indicator.into(), + range: v.range, + date: v.date, + ai_summary: v.ai_summary, + } + } +} + +/// US financial statement +#[napi_derive::napi(object)] +#[derive(Debug, Clone)] +pub struct USFinancialStatement { + pub revenue: String, + pub net_income: String, + pub net_margin: String, + pub periods: Vec, + pub currency: String, +} + +impl From for USFinancialStatement { + fn from(v: lb_us::USFinancialStatement) -> Self { + Self { + revenue: v.revenue, net_income: v.net_income, + net_margin: v.net_margin, periods: v.periods, currency: v.currency, + } + } +} + +/// US ETF dividend info +#[napi_derive::napi(object)] +#[derive(Debug, Clone)] +pub struct USETFDividendInfo { + pub dividend_ttm: String, + pub dividend_yield_ttm: String, + pub dividend_frequency: String, + pub currency: String, + pub fiscal_year_info: Vec, +} + +impl From for USETFDividendInfo { + fn from(v: lb_us::USETFDividendInfo) -> Self { + Self { + dividend_ttm: v.dividend_ttm, dividend_yield_ttm: v.dividend_yield_ttm, + dividend_frequency: v.dividend_frequency, currency: v.currency, + fiscal_year_info: v.fiscal_year_info, + } + } +} + +/// US dividend item +#[napi_derive::napi(object)] +#[derive(Debug, Clone)] +pub struct USDividendItem { + pub dividend: String, + pub dividend_type: String, + pub ex_date: String, + pub payment_date: String, + pub record_date: String, +} + +impl From for USDividendItem { + fn from(v: lb_us::USDividendItem) -> Self { + Self { + dividend: v.dividend, dividend_type: v.dividend_type, + ex_date: v.ex_date, payment_date: v.payment_date, record_date: v.record_date, + } + } +} + +/// US company dividends +#[napi_derive::napi(object)] +#[derive(Debug, Clone)] +pub struct USCompanyDividends { + pub dividend_ttm: String, + pub dividend_yield_ttm: String, + pub payouts: String, + pub currency: String, + pub items: Vec, +} + +impl From for USCompanyDividends { + fn from(v: lb_us::USCompanyDividends) -> Self { + Self { + dividend_ttm: v.dividend_ttm, dividend_yield_ttm: v.dividend_yield_ttm, + payouts: v.payouts, currency: v.currency, + items: v.items.into_iter().map(Into::into).collect(), + } + } +} + +/// US ETF file +#[napi_derive::napi(object)] +#[derive(Debug, Clone)] +pub struct USETFFile { + pub name: String, + pub file_type: String, + pub url: String, +} + +impl From for USETFFile { + fn from(v: lb_us::USETFFile) -> Self { + Self { name: v.name, file_type: v.file_type, url: v.url } + } +} + +/// US ETF files response +#[napi_derive::napi(object)] +#[derive(Debug, Clone)] +pub struct USETFFilesResponse { + pub files: Vec, +} + +impl From for USETFFilesResponse { + fn from(v: lb_us::USETFFilesResponse) -> Self { + Self { files: v.files.into_iter().map(Into::into).collect() } + } +} diff --git a/python/src/fundamental/context.rs b/python/src/fundamental/context.rs index 1804b19193..b4fc55b17b 100644 --- a/python/src/fundamental/context.rs +++ b/python/src/fundamental/context.rs @@ -5,6 +5,9 @@ use pyo3::prelude::*; use crate::{config::Config, error::ErrorNewType, fundamental::types::*}; +#[allow(unused_imports)] +use pythonize; + /// Fundamental data context (synchronous). #[pyclass] pub(crate) struct FundamentalContext { @@ -239,4 +242,69 @@ impl FundamentalContext { .map_err(ErrorNewType)? .into()) } + + // ── US-market methods ───────────────────────────────────────────────────── + + /// Get US company overview. US token required. + fn us_company_overview(&self, counter_id: String) -> PyResult { + Ok(self.ctx.us_company_overview(counter_id).map_err(ErrorNewType)?.into()) + } + + /// Get US valuation overview. US token required. + fn us_valuation_overview(&self, counter_id: String) -> PyResult { + Ok(self.ctx.us_valuation_overview(counter_id).map_err(ErrorNewType)?.into()) + } + + /// Get US financial overview. `report`: "annual" or "quarterly". US token required. + fn us_financial_overview(&self, py: Python<'_>, counter_id: String, report: String) -> PyResult> { + let v = self.ctx.us_financial_overview(counter_id, report).map_err(ErrorNewType)?; + pythonize::pythonize(py, &v) + .map(|b| b.unbind()) + .map_err(|e| pyo3::exceptions::PyRuntimeError::new_err(e.to_string())) + } + + /// Get US financial statement v3. `kind`: "IS"/"BS"/"CF". US token required. + fn us_financial_statement_v3( + &self, + counter_id: String, + kind: String, + report: String, + ) -> PyResult { + Ok(self + .ctx + .us_financial_statement_v3(counter_id, kind, report) + .map_err(ErrorNewType)? + .into()) + } + + /// Get US key financial metrics. US token required. + fn us_key_financial_metrics(&self, py: Python<'_>, counter_id: String, report: String) -> PyResult> { + let v = self.ctx.us_key_financial_metrics(counter_id, report).map_err(ErrorNewType)?; + pythonize::pythonize(py, &v) + .map(|b| b.unbind()) + .map_err(|e| pyo3::exceptions::PyRuntimeError::new_err(e.to_string())) + } + + /// Get US analyst consensus estimates. US token required. + fn us_analyst_consensus(&self, py: Python<'_>, counter_id: String, report: String) -> PyResult> { + let v = self.ctx.us_analyst_consensus(counter_id, report).map_err(ErrorNewType)?; + pythonize::pythonize(py, &v) + .map(|b| b.unbind()) + .map_err(|e| pyo3::exceptions::PyRuntimeError::new_err(e.to_string())) + } + + /// Get US ETF dividend history. US token required. + fn us_etf_dividend_info(&self, counter_id: String) -> PyResult { + Ok(self.ctx.us_etf_dividend_info(counter_id).map_err(ErrorNewType)?.into()) + } + + /// Get US company historical dividends. US token required. + fn us_company_dividends(&self, counter_id: String) -> PyResult { + Ok(self.ctx.us_company_dividends(counter_id).map_err(ErrorNewType)?.into()) + } + + /// Get US ETF document list. `size`: None = all. US token required. + fn us_etf_files(&self, counter_id: String, size: Option) -> PyResult { + Ok(self.ctx.us_etf_files(counter_id, size).map_err(ErrorNewType)?.into()) + } } diff --git a/python/src/fundamental/context_async.rs b/python/src/fundamental/context_async.rs index d0ef8151a6..55819f7c80 100644 --- a/python/src/fundamental/context_async.rs +++ b/python/src/fundamental/context_async.rs @@ -5,6 +5,10 @@ use pyo3::{prelude::*, types::PyType}; use crate::{config::Config, error::ErrorNewType, fundamental::types::*}; +// needed for us_financial_overview / us_key_financial_metrics / us_analyst_consensus +#[allow(unused_imports)] +use pythonize; + /// Fundamental data context (async). #[pyclass] pub(crate) struct AsyncFundamentalContext { @@ -358,4 +362,93 @@ impl AsyncFundamentalContext { }) .map(|b| b.unbind()) } + + // ── US-market async methods ─────────────────────────────────────────────── + + /// Get US company overview. US token required. Returns awaitable. + fn us_company_overview(&self, py: Python<'_>, counter_id: String) -> PyResult> { + let ctx = self.ctx.clone(); + pyo3_async_runtimes::tokio::future_into_py(py, async move { + Ok(USCompanyOverview::from(ctx.us_company_overview(counter_id).await.map_err(ErrorNewType)?)) + }).map(|b| b.unbind()) + } + + /// Get US valuation overview. US token required. Returns awaitable. + fn us_valuation_overview(&self, py: Python<'_>, counter_id: String) -> PyResult> { + let ctx = self.ctx.clone(); + pyo3_async_runtimes::tokio::future_into_py(py, async move { + Ok(USValuationOverview::from(ctx.us_valuation_overview(counter_id).await.map_err(ErrorNewType)?)) + }).map(|b| b.unbind()) + } + + /// Get US financial overview (raw dict). Returns awaitable. + fn us_financial_overview(&self, py: Python<'_>, counter_id: String, report: String) -> PyResult> { + let ctx = self.ctx.clone(); + pyo3_async_runtimes::tokio::future_into_py(py, async move { + let v = ctx.us_financial_overview(counter_id, report).await.map_err(ErrorNewType)?; + Python::attach(|py| { + pythonize::pythonize(py, &v) + .map(|b| b.unbind()) + .map_err(|e| pyo3::exceptions::PyRuntimeError::new_err(e.to_string())) + }) + }).map(|b| b.unbind()) + } + + /// Get US financial statement v3. Returns awaitable. + fn us_financial_statement_v3(&self, py: Python<'_>, counter_id: String, kind: String, report: String) -> PyResult> { + let ctx = self.ctx.clone(); + pyo3_async_runtimes::tokio::future_into_py(py, async move { + Ok(USFinancialStatement::from(ctx.us_financial_statement_v3(counter_id, kind, report).await.map_err(ErrorNewType)?)) + }).map(|b| b.unbind()) + } + + /// Get US key financial metrics (raw dict). Returns awaitable. + fn us_key_financial_metrics(&self, py: Python<'_>, counter_id: String, report: String) -> PyResult> { + let ctx = self.ctx.clone(); + pyo3_async_runtimes::tokio::future_into_py(py, async move { + let v = ctx.us_key_financial_metrics(counter_id, report).await.map_err(ErrorNewType)?; + Python::attach(|py| { + pythonize::pythonize(py, &v) + .map(|b| b.unbind()) + .map_err(|e| pyo3::exceptions::PyRuntimeError::new_err(e.to_string())) + }) + }).map(|b| b.unbind()) + } + + /// Get US analyst consensus (raw dict). Returns awaitable. + fn us_analyst_consensus(&self, py: Python<'_>, counter_id: String, report: String) -> PyResult> { + let ctx = self.ctx.clone(); + pyo3_async_runtimes::tokio::future_into_py(py, async move { + let v = ctx.us_analyst_consensus(counter_id, report).await.map_err(ErrorNewType)?; + Python::attach(|py| { + pythonize::pythonize(py, &v) + .map(|b| b.unbind()) + .map_err(|e| pyo3::exceptions::PyRuntimeError::new_err(e.to_string())) + }) + }).map(|b| b.unbind()) + } + + /// Get US ETF dividend info. Returns awaitable. + fn us_etf_dividend_info(&self, py: Python<'_>, counter_id: String) -> PyResult> { + let ctx = self.ctx.clone(); + pyo3_async_runtimes::tokio::future_into_py(py, async move { + Ok(USETFDividendInfo::from(ctx.us_etf_dividend_info(counter_id).await.map_err(ErrorNewType)?)) + }).map(|b| b.unbind()) + } + + /// Get US company dividends. Returns awaitable. + fn us_company_dividends(&self, py: Python<'_>, counter_id: String) -> PyResult> { + let ctx = self.ctx.clone(); + pyo3_async_runtimes::tokio::future_into_py(py, async move { + Ok(USCompanyDividends::from(ctx.us_company_dividends(counter_id).await.map_err(ErrorNewType)?)) + }).map(|b| b.unbind()) + } + + /// Get US ETF files. Returns awaitable. + fn us_etf_files(&self, py: Python<'_>, counter_id: String, size: Option) -> PyResult> { + let ctx = self.ctx.clone(); + pyo3_async_runtimes::tokio::future_into_py(py, async move { + Ok(USETFFilesResponse::from(ctx.us_etf_files(counter_id, size).await.map_err(ErrorNewType)?)) + }).map(|b| b.unbind()) + } } diff --git a/python/src/fundamental/mod.rs b/python/src/fundamental/mod.rs index 8101017515..eb4fa08aa9 100644 --- a/python/src/fundamental/mod.rs +++ b/python/src/fundamental/mod.rs @@ -80,6 +80,17 @@ pub(crate) fn register_types(parent: &Bound) -> PyResult<()> { parent.add_class::()?; parent.add_class::()?; parent.add_class::()?; + // US-market types + parent.add_class::()?; + parent.add_class::()?; + parent.add_class::()?; + parent.add_class::()?; + parent.add_class::()?; + parent.add_class::()?; + parent.add_class::()?; + parent.add_class::()?; + parent.add_class::()?; + parent.add_class::()?; parent.add_class::()?; parent.add_class::()?; Ok(()) diff --git a/python/src/fundamental/types.rs b/python/src/fundamental/types.rs index ea842a54b0..8873bcf019 100644 --- a/python/src/fundamental/types.rs +++ b/python/src/fundamental/types.rs @@ -2111,3 +2111,213 @@ impl From for MacroeconomicResponse { } } } + +// ── US-market types ─────────────────────────────────────────────────────────── + +use longbridge::fundamental::types as lb_us; + +/// Industry rank tag +#[pyclass(get_all, skip_from_py_object)] +#[derive(Debug, Clone)] +pub(crate) struct USRankTag { + pub name: String, + pub chg: String, + pub rank_type: String, +} + +impl From for USRankTag { + fn from(v: lb_us::USRankTag) -> Self { + Self { name: v.name, chg: v.chg, rank_type: v.rank_type } + } +} + +/// US company overview +#[pyclass(get_all, skip_from_py_object)] +#[derive(Debug, Clone)] +pub(crate) struct USCompanyOverview { + pub intro: String, + pub market_cap: String, + pub ccy_symbol: String, + pub top_rank_tags: Vec, + pub detail_url: String, +} + +impl From for USCompanyOverview { + fn from(v: lb_us::USCompanyOverview) -> Self { + Self { + intro: v.intro, + market_cap: v.market_cap, + ccy_symbol: v.ccy_symbol, + top_rank_tags: v.top_rank_tags.into_iter().map(Into::into).collect(), + detail_url: v.detail_url, + } + } +} + +/// US valuation indicator +#[pyclass(get_all, skip_from_py_object)] +#[derive(Debug, Clone)] +pub(crate) struct USValuationIndicator { + pub circle: String, + pub part: String, + pub metric: String, + pub metric_type: String, + pub desc: String, + pub ccy_symbol: String, +} + +impl From for USValuationIndicator { + fn from(v: lb_us::USValuationIndicator) -> Self { + Self { + circle: v.circle, + part: v.part, + metric: v.metric, + metric_type: v.metric_type, + desc: v.desc, + ccy_symbol: v.ccy_symbol, + } + } +} + +/// US valuation overview +#[pyclass(get_all, skip_from_py_object)] +#[derive(Debug, Clone)] +pub(crate) struct USValuationOverview { + pub indicator: String, + pub current_indicator: USValuationIndicator, + pub range: i32, + pub date: String, + pub ai_summary: String, +} + +impl From for USValuationOverview { + fn from(v: lb_us::USValuationOverview) -> Self { + Self { + indicator: v.indicator, + current_indicator: v.current_indicator.into(), + range: v.range, + date: v.date, + ai_summary: v.ai_summary, + } + } +} + +/// US financial statement (IS/BS/CF) +#[pyclass(get_all, skip_from_py_object)] +#[derive(Debug, Clone)] +pub(crate) struct USFinancialStatement { + pub revenue: String, + pub net_income: String, + pub net_margin: String, + pub periods: Vec, + pub currency: String, +} + +impl From for USFinancialStatement { + fn from(v: lb_us::USFinancialStatement) -> Self { + Self { + revenue: v.revenue, + net_income: v.net_income, + net_margin: v.net_margin, + periods: v.periods.into_iter().map(JsonValue).collect(), + currency: v.currency, + } + } +} + +/// US ETF dividend info +#[pyclass(get_all, skip_from_py_object)] +#[derive(Debug, Clone)] +pub(crate) struct USETFDividendInfo { + pub dividend_ttm: String, + pub dividend_yield_ttm: String, + pub dividend_frequency: String, + pub currency: String, + pub fiscal_year_info: Vec, +} + +impl From for USETFDividendInfo { + fn from(v: lb_us::USETFDividendInfo) -> Self { + Self { + dividend_ttm: v.dividend_ttm, + dividend_yield_ttm: v.dividend_yield_ttm, + dividend_frequency: v.dividend_frequency, + currency: v.currency, + fiscal_year_info: v.fiscal_year_info.into_iter().map(JsonValue).collect(), + } + } +} + +/// A single US dividend payment record +#[pyclass(get_all, skip_from_py_object)] +#[derive(Debug, Clone)] +pub(crate) struct USDividendItem { + pub dividend: String, + pub dividend_type: String, + pub ex_date: String, + pub payment_date: String, + pub record_date: String, +} + +impl From for USDividendItem { + fn from(v: lb_us::USDividendItem) -> Self { + Self { + dividend: v.dividend, + dividend_type: v.dividend_type, + ex_date: v.ex_date, + payment_date: v.payment_date, + record_date: v.record_date, + } + } +} + +/// US company historical dividends +#[pyclass(get_all, skip_from_py_object)] +#[derive(Debug, Clone)] +pub(crate) struct USCompanyDividends { + pub dividend_ttm: String, + pub dividend_yield_ttm: String, + pub payouts: String, + pub currency: String, + pub items: Vec, +} + +impl From for USCompanyDividends { + fn from(v: lb_us::USCompanyDividends) -> Self { + Self { + dividend_ttm: v.dividend_ttm, + dividend_yield_ttm: v.dividend_yield_ttm, + payouts: v.payouts, + currency: v.currency, + items: v.items.into_iter().map(Into::into).collect(), + } + } +} + +/// A single ETF document +#[pyclass(get_all, skip_from_py_object)] +#[derive(Debug, Clone)] +pub(crate) struct USETFFile { + pub name: String, + pub file_type: String, + pub url: String, +} + +impl From for USETFFile { + fn from(v: lb_us::USETFFile) -> Self { + Self { name: v.name, file_type: v.file_type, url: v.url } + } +} + +/// US ETF files response +#[pyclass(get_all, skip_from_py_object)] +#[derive(Debug, Clone)] +pub(crate) struct USETFFilesResponse { + pub files: Vec, +} + +impl From for USETFFilesResponse { + fn from(v: lb_us::USETFFilesResponse) -> Self { + Self { files: v.files.into_iter().map(Into::into).collect() } + } +} diff --git a/rust/src/blocking/fundamental.rs b/rust/src/blocking/fundamental.rs index 532c09bc1d..fa15ea4307 100644 --- a/rust/src/blocking/fundamental.rs +++ b/rust/src/blocking/fundamental.rs @@ -349,4 +349,88 @@ impl FundamentalContextSync { .await }) } + + // ── US-market blocking wrappers ─────────────────────────────────────────── + + /// Get US company overview (blocking) + pub fn us_company_overview( + &self, + counter_id: impl Into + Send + 'static, + ) -> Result { + self.rt.call(move |ctx| async move { ctx.us_company_overview(counter_id).await }) + } + + /// Get US valuation overview snapshot (blocking) + pub fn us_valuation_overview( + &self, + counter_id: impl Into + Send + 'static, + ) -> Result { + self.rt.call(move |ctx| async move { ctx.us_valuation_overview(counter_id).await }) + } + + /// Get US financial overview (blocking) + pub fn us_financial_overview( + &self, + counter_id: impl Into + Send + 'static, + report: impl Into + Send + 'static, + ) -> Result { + self.rt.call(move |ctx| async move { ctx.us_financial_overview(counter_id, report).await }) + } + + /// Get US financial statement v3 (blocking) + pub fn us_financial_statement_v3( + &self, + counter_id: impl Into + Send + 'static, + kind: impl Into + Send + 'static, + report: impl Into + Send + 'static, + ) -> Result { + self.rt.call(move |ctx| async move { + ctx.us_financial_statement_v3(counter_id, kind, report).await + }) + } + + /// Get US key financial metrics (blocking) + pub fn us_key_financial_metrics( + &self, + counter_id: impl Into + Send + 'static, + report: impl Into + Send + 'static, + ) -> Result { + self.rt + .call(move |ctx| async move { ctx.us_key_financial_metrics(counter_id, report).await }) + } + + /// Get US analyst consensus estimates (blocking) + pub fn us_analyst_consensus( + &self, + counter_id: impl Into + Send + 'static, + report: impl Into + Send + 'static, + ) -> Result { + self.rt + .call(move |ctx| async move { ctx.us_analyst_consensus(counter_id, report).await }) + } + + /// Get US ETF dividend history (blocking) + pub fn us_etf_dividend_info( + &self, + counter_id: impl Into + Send + 'static, + ) -> Result { + self.rt.call(move |ctx| async move { ctx.us_etf_dividend_info(counter_id).await }) + } + + /// Get US company historical dividends (blocking) + pub fn us_company_dividends( + &self, + counter_id: impl Into + Send + 'static, + ) -> Result { + self.rt.call(move |ctx| async move { ctx.us_company_dividends(counter_id).await }) + } + + /// Get US ETF document list (blocking) + pub fn us_etf_files( + &self, + counter_id: impl Into + Send + 'static, + size: Option, + ) -> Result { + self.rt.call(move |ctx| async move { ctx.us_etf_files(counter_id, size).await }) + } } diff --git a/rust/src/blocking/quote.rs b/rust/src/blocking/quote.rs index a18cfd2174..d855549434 100644 --- a/rust/src/blocking/quote.rs +++ b/rust/src/blocking/quote.rs @@ -14,8 +14,8 @@ use crate::{ RequestCreateWatchlistGroup, RequestUpdateWatchlistGroup, Security, SecurityBrokers, SecurityCalcIndex, SecurityDepth, SecurityListCategory, SecurityQuote, SecurityStaticInfo, ShortPositionsResponse, ShortTradesResponse, SortOrderType, StrikePriceInfo, SubFlags, - Subscription, Trade, TradeSessions, WarrantInfo, WarrantQuote, WarrantSortBy, - WarrantStatus, WarrantType, WatchlistGroup, + Subscription, Trade, TradeSessions, USCryptoOverview, WarrantInfo, WarrantQuote, + WarrantSortBy, WarrantStatus, WarrantType, WatchlistGroup, }, }; @@ -1232,4 +1232,14 @@ impl QuoteContextSync { self.rt .call(move |ctx| async move { ctx.resolve_counter_ids(symbols).await }) } + + // ── US-market blocking wrappers ─────────────────────────────────────────── + + /// Get cryptocurrency market overview (blocking) + pub fn us_crypto_overview( + &self, + counter_id: impl Into + Send + 'static, + ) -> Result { + self.rt.call(move |ctx| async move { ctx.us_crypto_overview(counter_id).await }) + } } diff --git a/rust/src/blocking/trade.rs b/rust/src/blocking/trade.rs index 3573120353..c4fa017605 100644 --- a/rust/src/blocking/trade.rs +++ b/rust/src/blocking/trade.rs @@ -8,8 +8,9 @@ use crate::{ EstimateMaxPurchaseQuantityResponse, Execution, FundPositionsResponse, GetCashFlowOptions, GetFundPositionsOptions, GetHistoryExecutionsOptions, GetHistoryOrdersOptions, GetStockPositionsOptions, GetTodayExecutionsOptions, GetTodayOrdersOptions, MarginRatio, - Order, OrderDetail, PushEvent, ReplaceOrderOptions, StockPositionsResponse, - SubmitOrderOptions, SubmitOrderResponse, TopicType, TradeContext, + Order, OrderDetail, PushEvent, QueryUSOrdersOptions, QueryUSOrdersResponse, + ReplaceOrderOptions, StockPositionsResponse, SubmitOrderOptions, SubmitOrderResponse, + TopicType, TradeContext, USAssetOverview, USOrderDetailResponse, USRealizedPL, }, }; @@ -456,4 +457,36 @@ impl TradeContextSync { self.rt .call(move |ctx| async move { ctx.estimate_max_purchase_quantity(opts).await }) } + + // ── US-market blocking wrappers ─────────────────────────────────────────── + + /// Query the paginated US order list (blocking) + pub fn us_query_orders(&self, opts: QueryUSOrdersOptions) -> Result { + self.rt.call(move |ctx| async move { ctx.us_query_orders(opts).await }) + } + + /// Get US order detail (blocking) + pub fn us_order_detail( + &self, + order_id: impl Into + Send + 'static, + is_attached: bool, + ) -> Result { + self.rt + .call(move |ctx| async move { ctx.us_order_detail(order_id, is_attached).await }) + } + + /// Get the full US account asset overview (blocking) + pub fn us_asset_overview(&self) -> Result { + self.rt.call(move |ctx| async move { ctx.us_asset_overview().await }) + } + + /// Get realized P&L for the US account (blocking) + pub fn us_realized_pl( + &self, + currency: impl Into + Send + 'static, + category: Option + Send + 'static>, + ) -> Result { + self.rt + .call(move |ctx| async move { ctx.us_realized_pl(currency, category).await }) + } } diff --git a/rust/src/fundamental/context.rs b/rust/src/fundamental/context.rs index 4987453db4..59788cc10b 100644 --- a/rust/src/fundamental/context.rs +++ b/rust/src/fundamental/context.rs @@ -1050,4 +1050,224 @@ impl FundamentalContext { let count = if total > 0 { total } else { count }; Ok(MacroeconomicResponse { info, data, count }) } + + // ── US-market APIs (US token required) ──────────────────────────────────── + + /// Get US company overview. + /// + /// Path: `GET /v1/stock-info/company-overview` + /// + /// US token required — returns [`longbridge_httpcli::HttpClientError::DcRegionRestricted`] + /// for non-US credentials. + pub async fn us_company_overview( + &self, + counter_id: impl Into, + ) -> Result { + #[derive(Serialize)] + struct Query { + counter_id: String, + } + self.get_dc( + "/v1/stock-info/company-overview", + Query { counter_id: counter_id.into() }, + DcRegion::Us, + ) + .await + } + + /// Get US valuation overview snapshot. + /// + /// Path: `GET /v1/stock-info/valuation-overview` + /// + /// US token required. + pub async fn us_valuation_overview( + &self, + counter_id: impl Into, + ) -> Result { + #[derive(Serialize)] + struct Query { + counter_id: String, + } + self.get_dc( + "/v1/stock-info/valuation-overview", + Query { counter_id: counter_id.into() }, + DcRegion::Us, + ) + .await + } + + /// Get US financial overview (revenue, net income, EPS, cash flow). + /// + /// `report`: `"annual"` or `"quarterly"`. + /// + /// Path: `GET /v1/stock-info/finn-overview` + /// + /// US token required. Returns raw JSON for maximum flexibility. + pub async fn us_financial_overview( + &self, + counter_id: impl Into, + report: impl Into, + ) -> Result { + #[derive(Serialize)] + struct Query { + counter_id: String, + report: String, + } + self.get_dc( + "/v1/stock-info/finn-overview", + Query { counter_id: counter_id.into(), report: report.into() }, + DcRegion::Us, + ) + .await + } + + /// Get US financial statement detail (IS / BS / CF). + /// + /// `kind`: `"IS"` (income statement), `"BS"` (balance sheet), `"CF"` (cash flow). + /// `report`: `"annual"` or `"quarterly"`. + /// + /// Path: `GET /v1/us/quote/financials/statements` + /// + /// US token required. + pub async fn us_financial_statement_v3( + &self, + counter_id: impl Into, + kind: impl Into, + report: impl Into, + ) -> Result { + #[derive(Serialize)] + struct Query { + counter_id: String, + kind: String, + report: String, + } + self.get_dc( + "/v1/us/quote/financials/statements", + Query { + counter_id: counter_id.into(), + kind: kind.into(), + report: report.into(), + }, + DcRegion::Us, + ) + .await + } + + /// Get key financial metrics (ROE, margins, leverage ratios). + /// + /// `report`: `"annual"` or `"quarterly"`. + /// + /// Path: `GET /v1/stock-info/fin-keyfactor` + /// + /// US token required. Returns raw JSON. + pub async fn us_key_financial_metrics( + &self, + counter_id: impl Into, + report: impl Into, + ) -> Result { + #[derive(Serialize)] + struct Query { + counter_id: String, + report: String, + } + self.get_dc( + "/v1/stock-info/fin-keyfactor", + Query { counter_id: counter_id.into(), report: report.into() }, + DcRegion::Us, + ) + .await + } + + /// Get analyst consensus estimates (EPS and revenue forecasts). + /// + /// `report`: `"annual"` or `"quarterly"`. + /// + /// Path: `GET /v1/stock-info/fin-consensus` + /// + /// US token required. Returns raw JSON. + pub async fn us_analyst_consensus( + &self, + counter_id: impl Into, + report: impl Into, + ) -> Result { + #[derive(Serialize)] + struct Query { + counter_id: String, + report: String, + } + self.get_dc( + "/v1/stock-info/fin-consensus", + Query { counter_id: counter_id.into(), report: report.into() }, + DcRegion::Us, + ) + .await + } + + /// Get ETF dividend history. + /// + /// Path: `GET /v1/stock-info/etf-dividend-info` + /// + /// US token required. + pub async fn us_etf_dividend_info( + &self, + counter_id: impl Into, + ) -> Result { + #[derive(Serialize)] + struct Query { + counter_id: String, + } + self.get_dc( + "/v1/stock-info/etf-dividend-info", + Query { counter_id: counter_id.into() }, + DcRegion::Us, + ) + .await + } + + /// Get company historical dividend payments. + /// + /// Path: `GET /v1/stock-info/company-dividends` + /// + /// US token required. + pub async fn us_company_dividends( + &self, + counter_id: impl Into, + ) -> Result { + #[derive(Serialize)] + struct Query { + counter_id: String, + } + self.get_dc( + "/v1/stock-info/company-dividends", + Query { counter_id: counter_id.into() }, + DcRegion::Us, + ) + .await + } + + /// Get ETF document list (prospectus, annual reports, etc.). + /// + /// `size`: number of files to return; `None` returns all (server default 0 = all). + /// + /// Path: `GET /v1/stock-info/etf-files` + /// + /// US token required. + pub async fn us_etf_files( + &self, + counter_id: impl Into, + size: Option, + ) -> Result { + #[derive(Serialize)] + struct Query { + counter_id: String, + #[serde(skip_serializing_if = "Option::is_none")] + size: Option, + } + self.get_dc( + "/v1/stock-info/etf-files", + Query { counter_id: counter_id.into(), size }, + DcRegion::Us, + ) + .await + } } diff --git a/rust/src/fundamental/types.rs b/rust/src/fundamental/types.rs index fa127e9c43..9280f91b2f 100644 --- a/rust/src/fundamental/types.rs +++ b/rust/src/fundamental/types.rs @@ -1720,6 +1720,187 @@ pub struct MacroeconomicResponse { pub count: i32, } +// ── US-market types ─────────────────────────────────────────────────────────── + +/// Industry rank tag returned by [`crate::FundamentalContext::us_company_overview`]. +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct USRankTag { + /// Tag name + #[serde(default)] + pub name: String, + /// Change value + #[serde(default)] + pub chg: String, + /// Rank type identifier + #[serde(default)] + pub rank_type: String, +} + +/// Response for [`crate::FundamentalContext::us_company_overview`]. +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct USCompanyOverview { + /// Company description + #[serde(default)] + pub intro: String, + /// Market cap + #[serde(default)] + pub market_cap: String, + /// Currency symbol (e.g. `$`) + #[serde(default)] + pub ccy_symbol: String, + /// Industry rank tags + #[serde(default)] + pub top_rank_tags: Vec, + /// Deep-link URL to detail page + #[serde(default)] + pub detail_url: String, +} + +/// Current valuation indicator detail. +#[derive(Debug, Clone, Serialize, Deserialize, Default)] +pub struct USValuationIndicator { + /// Full-circle reference value + #[serde(default)] + pub circle: String, + /// Current partial value + #[serde(default)] + pub part: String, + /// Displayed metric value + #[serde(default)] + pub metric: String, + /// Metric type identifier + #[serde(default)] + pub metric_type: String, + /// Human-readable description + #[serde(default)] + pub desc: String, + /// Currency symbol + #[serde(default)] + pub ccy_symbol: String, +} + +/// Response for [`crate::FundamentalContext::us_valuation_overview`]. +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct USValuationOverview { + /// Recommended valuation indicator (e.g. `PE`) + #[serde(default)] + pub indicator: String, + /// Current indicator detail + #[serde(default)] + pub current_indicator: USValuationIndicator, + /// Historical percentile range in years + #[serde(default)] + pub range: i32, + /// Data date string + #[serde(default)] + pub date: String, + /// AI summary text + #[serde(default)] + pub ai_summary: String, +} + +/// Response for [`crate::FundamentalContext::us_financial_statement_v3`]. +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct USFinancialStatement { + /// Revenue + #[serde(default)] + pub revenue: String, + /// Net income + #[serde(default)] + pub net_income: String, + /// Net margin + #[serde(default)] + pub net_margin: String, + /// Per-period data + #[serde(default)] + pub periods: Vec, + /// Report currency + #[serde(default)] + pub currency: String, +} + +/// Response for [`crate::FundamentalContext::us_etf_dividend_info`]. +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct USETFDividendInfo { + /// Total dividend over trailing 12 months + #[serde(default)] + pub dividend_ttm: String, + /// Dividend yield over trailing 12 months + #[serde(default)] + pub dividend_yield_ttm: String, + /// Dividend frequency + #[serde(default)] + pub dividend_frequency: String, + /// Currency + #[serde(default)] + pub currency: String, + /// Per-fiscal-year dividend records + #[serde(default)] + pub fiscal_year_info: Vec, +} + +/// A single historical dividend payment. +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct USDividendItem { + /// Per-share dividend amount + #[serde(default)] + pub dividend: String, + /// Dividend type (e.g. cash dividend) + #[serde(default)] + pub dividend_type: String, + /// Ex-dividend date + #[serde(default)] + pub ex_date: String, + /// Payment date + #[serde(default)] + pub payment_date: String, + /// Record date + #[serde(default)] + pub record_date: String, +} + +/// Response for [`crate::FundamentalContext::us_company_dividends`]. +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct USCompanyDividends { + /// Total dividend over trailing 12 months + #[serde(default)] + pub dividend_ttm: String, + /// Dividend yield over trailing 12 months + #[serde(default)] + pub dividend_yield_ttm: String, + /// Number of dividend payments + #[serde(default)] + pub payouts: String, + /// Currency + #[serde(default)] + pub currency: String, + /// Individual payment records + #[serde(default)] + pub items: Vec, +} + +/// A single file in an ETF document list. +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct USETFFile { + /// File name + #[serde(default)] + pub name: String, + /// File type (e.g. prospectus, annual report) + #[serde(default)] + pub file_type: String, + /// Download URL + #[serde(default)] + pub url: String, +} + +/// Response for [`crate::FundamentalContext::us_etf_files`]. +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct USETFFilesResponse { + /// List of ETF documents + #[serde(default)] + pub files: Vec, +} + // ── v2 wire types (internal, used for mapping to existing public types) ────── /// v2 wire: one indicator from GET /v2/quote/macrodata diff --git a/rust/src/lib.rs b/rust/src/lib.rs index e2fb096b47..4bb622053d 100644 --- a/rust/src/lib.rs +++ b/rust/src/lib.rs @@ -55,3 +55,15 @@ pub use screener::ScreenerContext; pub use sharelist::SharelistContext; pub use trade::TradeContext; pub use types::Market; + +// ── US-market type re-exports ───────────────────────────────────────────────── +pub use fundamental::types::{ + USCompanyDividends, USCompanyOverview, USDividendItem, USETFDividendInfo, USETFFile, + USETFFilesResponse, USFinancialStatement, USRankTag, USValuationIndicator, USValuationOverview, +}; +pub use quote::USCryptoOverview; +pub use trade::{ + QueryUSOrdersOptions, QueryUSOrdersResponse, USAssetOverview, USAttachedOrder, USBuyPower, + USCryptoPosition, USOptionPosition, USOrderDetailResponse, USRealizedPL, USRealizedPLItem, + USStockPosition, +}; diff --git a/rust/src/quote/context.rs b/rust/src/quote/context.rs index a3de649c0d..3a5e231acb 100644 --- a/rust/src/quote/context.rs +++ b/rust/src/quote/context.rs @@ -4,7 +4,7 @@ use std::{ time::Duration, }; -use longbridge_httpcli::{HttpClient, Json, Method}; +use longbridge_httpcli::{DcRegion, HttpClient, Json, Method}; use longbridge_proto::quote; use longbridge_wscli::WsClientError; use serde::{Deserialize, Serialize}; @@ -2285,6 +2285,37 @@ impl QuoteContext { } Ok(result) } + + // ── US-market APIs ──────────────────────────────────────────────────────── + + /// Get cryptocurrency market overview. + /// + /// `counter_id`: crypto counter_id, e.g. `"CY/US/BTC"`. + /// + /// Path: `GET /v1/gemini/crypto-overview` + /// + /// US token required — returns + /// [`longbridge_httpcli::HttpClientError::DcRegionRestricted`] for non-US credentials. + pub async fn us_crypto_overview( + &self, + counter_id: impl Into, + ) -> Result { + #[derive(Serialize)] + struct Query { + counter_id: String, + } + Ok(self + .0 + .http_cli + .request(Method::GET, "/v1/gemini/crypto-overview") + .dc_restrict(DcRegion::Us) + .query_params(Query { counter_id: counter_id.into() }) + .response::>() + .send() + .with_subscriber(self.0.log_subscriber.clone()) + .await? + .0) + } } fn normalize_symbol(symbol: &str) -> &str { diff --git a/rust/src/quote/mod.rs b/rust/src/quote/mod.rs index 78dcba0cba..c9931d92c7 100644 --- a/rust/src/quote/mod.rs +++ b/rust/src/quote/mod.rs @@ -75,6 +75,7 @@ pub use types::{ WarrantSortBy, WarrantStatus, WarrantType, + USCryptoOverview, WatchlistGroup, WatchlistSecurity, }; diff --git a/rust/src/quote/types.rs b/rust/src/quote/types.rs index 08df9b0074..5c146d4f9d 100644 --- a/rust/src/quote/types.rs +++ b/rust/src/quote/types.rs @@ -2164,6 +2164,51 @@ pub enum PinnedMode { Remove, } +// ── US-market types ─────────────────────────────────────────────────────────── + +/// Market overview for a single cryptocurrency. +/// +/// Returned by [`crate::QuoteContext::us_crypto_overview`]. +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct USCryptoOverview { + /// Full name (e.g. `"Bitcoin"`) + #[serde(default)] + pub name: String, + /// Ticker symbol (e.g. `"BTC"`) + #[serde(default)] + pub ticker: String, + /// Pricing currency + #[serde(default)] + pub currency: String, + /// All-time high price + #[serde(default)] + pub all_time_high: String, + /// All-time high date + #[serde(default)] + pub all_time_high_date: String, + /// All-time low price + #[serde(default)] + pub all_time_low: String, + /// All-time low date + #[serde(default)] + pub all_time_low_date: String, + /// Listing date + #[serde(default)] + pub ipo_date: String, + /// Issue price + #[serde(default)] + pub issue_price: String, + /// Circulating supply + #[serde(default)] + pub shares: String, + /// Official website URL + #[serde(default)] + pub official_web_address: String, + /// Multi-language profile / description + #[serde(default)] + pub profile: serde_json::Value, +} + #[cfg(test)] mod tests { use serde::Deserialize; diff --git a/rust/src/trade/context.rs b/rust/src/trade/context.rs index 4e4c1c0dbe..b3d7618942 100644 --- a/rust/src/trade/context.rs +++ b/rust/src/trade/context.rs @@ -1,6 +1,6 @@ use std::sync::Arc; -use longbridge_httpcli::{HttpClient, Json, Method}; +use longbridge_httpcli::{DcRegion, HttpClient, Json, Method}; use longbridge_wscli::WsClientError; use rust_decimal::Decimal; use serde::{Deserialize, Serialize}; @@ -14,7 +14,9 @@ use crate::{ FundPositionsResponse, GetCashFlowOptions, GetFundPositionsOptions, GetHistoryExecutionsOptions, GetHistoryOrdersOptions, GetStockPositionsOptions, GetTodayExecutionsOptions, GetTodayOrdersOptions, MarginRatio, Order, OrderDetail, - PushEvent, ReplaceOrderOptions, StockPositionsResponse, SubmitOrderOptions, TopicType, + PushEvent, QueryUSOrdersOptions, QueryUSOrdersResponse, ReplaceOrderOptions, + StockPositionsResponse, SubmitOrderOptions, TopicType, USAssetOverview, + USOrderDetailResponse, USRealizedPL, core::{Command, Core}, }, }; @@ -834,4 +836,118 @@ impl TradeContext { .await? .0) } + + // ── US-market APIs ──────────────────────────────────────────────────────── + + /// Query the paginated US order list. + /// + /// Path: `POST /v1/orders/query` + /// + /// US token required. + pub async fn us_query_orders( + &self, + opts: QueryUSOrdersOptions, + ) -> Result { + Ok(self + .0 + .http_cli + .request(Method::POST, "/v1/orders/query") + .dc_restrict(DcRegion::Us) + .body(Json(opts)) + .response::>() + .send() + .with_subscriber(self.0.log_subscriber.clone()) + .await? + .0) + } + + /// Get US order detail, optionally including attached stop-loss/take-profit sub-orders. + /// + /// Path: `GET /v3/orders/{order_id}` + /// + /// US token required. + pub async fn us_order_detail( + &self, + order_id: impl Into, + is_attached: bool, + ) -> Result { + let order_id = order_id.into(); + let path = format!("/v3/orders/{order_id}"); + + #[derive(Serialize)] + struct Query { + order_id_str: String, + #[serde(skip_serializing_if = "Option::is_none")] + is_attached: Option, + } + + Ok(self + .0 + .http_cli + .request(Method::GET, path.as_str()) + .dc_restrict(DcRegion::Us) + .query_params(Query { + order_id_str: order_id, + is_attached: if is_attached { Some(true) } else { None }, + }) + .response::>() + .send() + .with_subscriber(self.0.log_subscriber.clone()) + .await? + .0) + } + + /// Get the full US account asset snapshot (stocks, options, crypto, buying power). + /// + /// Path: `GET /v1/us/assets/overview` + /// + /// US token required. + pub async fn us_asset_overview(&self) -> Result { + Ok(self + .0 + .http_cli + .request(Method::GET, "/v1/us/assets/overview") + .dc_restrict(DcRegion::Us) + .response::>() + .send() + .with_subscriber(self.0.log_subscriber.clone()) + .await? + .0) + } + + /// Get realized profit-and-loss for the US account. + /// + /// `currency`: required, e.g. `"USD"`. + /// `category`: optional filter — `"ALL"`, `"STOCK"`, `"OPTION"`, or `"CRYPTO"`. + /// + /// Path: `GET /v1/us/assets/pl/realized` + /// + /// US token required. + pub async fn us_realized_pl( + &self, + currency: impl Into, + category: Option>, + ) -> Result { + #[derive(Serialize)] + struct Query { + currency: String, + #[serde(skip_serializing_if = "Option::is_none")] + category: Option, + } + + Ok(self + .0 + .http_cli + .request(Method::GET, "/v1/us/assets/pl/realized") + .dc_restrict(DcRegion::Us) + .query_params(Query { + currency: currency.into(), + category: category.map(Into::into), + }) + .response::>() + .send() + .with_subscriber(self.0.log_subscriber.clone()) + .await? + .0) + } } diff --git a/rust/src/trade/mod.rs b/rust/src/trade/mod.rs index 21df810dcc..0cc9c10d96 100644 --- a/rust/src/trade/mod.rs +++ b/rust/src/trade/mod.rs @@ -21,4 +21,8 @@ pub use types::{ OrderChargeFee, OrderChargeItem, OrderDetail, OrderHistoryDetail, OrderSide, OrderStatus, OrderTag, OrderType, OutsideRTH, StockPosition, StockPositionChannel, StockPositionsResponse, TimeInForceType, TriggerPriceType, TriggerStatus, + // US-market types + QueryUSOrdersOptions, QueryUSOrdersResponse, USAssetOverview, USAttachedOrder, USBuyPower, + USCryptoPosition, USOptionPosition, USOrderDetailResponse, USRealizedPL, USRealizedPLItem, + USStockPosition, }; diff --git a/rust/src/trade/types.rs b/rust/src/trade/types.rs index d78082f73b..8d1fc2ef82 100644 --- a/rust/src/trade/types.rs +++ b/rust/src/trade/types.rs @@ -804,6 +804,171 @@ impl_default_for_enum_string!( ChargeCategoryCode ); +// ── US-market types ─────────────────────────────────────────────────────────── + +/// Request body for [`crate::TradeContext::us_query_orders`]. +#[derive(Debug, Clone, Serialize)] +pub struct QueryUSOrdersOptions { + /// Account channel (injected by the framework) + pub account_channel: String, + /// Direction filter: 0 = all, 1 = buy, 2 = sell + pub action: i32, + /// Start timestamp (seconds) + pub start_at: f64, + /// End timestamp (seconds) + pub end_at: f64, + /// Counter-ID filter (empty = all) + pub counter_ids: Vec, + /// Security-type filter (empty = all) + pub security_types: Vec, + /// Status: 0 = all, 1 = pending, 2 = history + pub query_type: i32, + /// Page number (1-based) + pub page: i32, + /// Page size + pub limit: i32, + /// Epoch-seconds timestamp from the first request in a paging series + pub query_version: f64, +} + +/// Response for [`crate::TradeContext::us_query_orders`]. +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct QueryUSOrdersResponse { + /// Order list (raw JSON for forward compatibility) + #[serde(default)] + pub orders: Vec, +} + +/// An attached take-profit or stop-loss sub-order. +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct USAttachedOrder { + /// Sub-order ID + #[serde(default)] + pub order_id: String, + /// Type: `TAKE_PROFIT` or `STOP_LOSS` + #[serde(default, rename = "type")] + pub order_type: String, + /// Direction: `BUY` or `SELL` + #[serde(default)] + pub side: String, + /// Limit price + #[serde(default)] + pub price: String, + /// Trailing stop amount + #[serde(default)] + pub trail_amount: String, + /// Trailing stop percentage + #[serde(default)] + pub trail_percent: String, + /// Order status + #[serde(default)] + pub status: String, +} + +/// Response for [`crate::TradeContext::us_order_detail`]. +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct USOrderDetailResponse { + /// Raw order detail fields (pass-through) + #[serde(flatten)] + pub detail: serde_json::Value, + /// Attached stop-loss / take-profit orders (populated when `is_attached = true`) + #[serde(default)] + pub attached_orders: Vec, +} + +/// A stock position in a US account. +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct USStockPosition { + #[serde(default)] pub symbol: String, + #[serde(default)] pub name: String, + #[serde(default)] pub quantity: String, + #[serde(default)] pub available_quantity: String, + #[serde(default)] pub currency: String, + #[serde(default)] pub cost_price: String, + #[serde(default)] pub market_value: String, + #[serde(default)] pub unrealized_pl: String, + #[serde(default)] pub unrealized_pl_ratio: String, + #[serde(default)] pub last_done: String, + #[serde(default)] pub prev_close: String, + #[serde(default)] pub change_rate: String, + /// Overnight/night-session last price (US-specific) + #[serde(default)] pub night_last_done: String, + /// Pre-market close price (US-specific) + #[serde(default)] pub pretrade_close: String, + /// Trading session status (US-specific) + #[serde(default)] pub trade_status: String, + /// Individual quantity after multi-leg exclusion (US-specific) + #[serde(default)] pub individual_quantity: String, +} + +/// An option position in a US account. +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct USOptionPosition { + #[serde(default)] pub symbol: String, + #[serde(default)] pub strike_price: String, + #[serde(default)] pub due_date: String, + #[serde(default)] pub contract_multiplier: i32, + #[serde(default, rename = "type")] pub option_type: String, + #[serde(default)] pub quantity: String, + #[serde(default)] pub market_value: String, + #[serde(default)] pub unrealized_pl: String, +} + +/// A cryptocurrency position in a US account. +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct USCryptoPosition { + #[serde(default)] pub symbol: String, + #[serde(default)] pub quantity: String, + #[serde(default)] pub market_value: String, + #[serde(default)] pub unrealized_pl: String, + #[serde(default)] pub cost_price: String, +} + +/// Purchasing power breakdown for a US account. +#[derive(Debug, Clone, Serialize, Deserialize, Default)] +pub struct USBuyPower { + #[serde(default)] pub cash_buy_power: String, + #[serde(default)] pub overnight_buy_power: String, + /// Day-trade buying power (margin accounts only) + #[serde(default)] pub day_trade_buy_power: String, + #[serde(default)] pub option_buy_power: String, + #[serde(default)] pub crypto_buy_power: String, +} + +/// Response for [`crate::TradeContext::us_asset_overview`]. +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct USAssetOverview { + #[serde(default)] pub account_type: String, + #[serde(default)] pub net_assets: String, + #[serde(default)] pub total_cash: String, + #[serde(default)] pub unrealized_pl: String, + #[serde(default)] pub positions: Vec, + #[serde(default)] pub option_positions: Vec, + #[serde(default)] pub multi_legs: Vec, + #[serde(default)] pub crypto_positions: Vec, + #[serde(default)] pub buy_power: USBuyPower, +} + +/// A single realized P&L entry. +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct USRealizedPLItem { + #[serde(default)] pub symbol: String, + #[serde(default)] pub name: String, + #[serde(default)] pub category: String, + #[serde(default)] pub realized_pl: String, + #[serde(default)] pub quantity_sold: String, + #[serde(default)] pub avg_cost: String, + #[serde(default)] pub avg_sell_price: String, +} + +/// Response for [`crate::TradeContext::us_realized_pl`]. +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct USRealizedPL { + #[serde(default)] pub total_realized_pl: String, + #[serde(default)] pub currency: String, + #[serde(default)] pub items: Vec, +} + #[cfg(test)] mod tests { use time::macros::datetime; From 344ee86bb6f71d42d2988435f07fe3dcc09d4e8a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=A2=81=E7=AB=A0=E6=B4=AA?= Date: Thu, 2 Jul 2026 16:17:11 +0800 Subject: [PATCH 10/44] feat(us): add Python/Node.js/Java bindings for Quote and Trade US APIs MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Python: us_crypto_overview (sync+async QuoteContext), us_query_orders, us_order_detail, us_asset_overview, us_realized_pl (sync+async TradeContext) + all US types (USStockPosition, USOptionPosition, USCryptoPosition, USBuyPower, USAssetOverview, USRealizedPLItem, USRealizedPL, USCryptoOverview) - Node.js: us_crypto_overview (QuoteContext), us_query_orders, us_order_detail, us_asset_overview, us_realized_pl (TradeContext) + typed napi(object) structs for all US response types - Java: quoteContextUsCryptoOverview, tradeContextUsQueryOrders, tradeContextUsOrderDetail, tradeContextUsAssetOverview, tradeContextUsRealizedPl — all return JSON strings All four language bindings compile without errors. Co-Authored-By: Claude Sonnet 4.6 (1M context) --- java/src/quote_context.rs | 19 ++++ java/src/trade_context.rs | 110 +++++++++++++++++++++- nodejs/src/quote/context.rs | 15 ++- nodejs/src/quote/types.rs | 38 ++++++++ nodejs/src/trade/context.rs | 63 ++++++++++++- nodejs/src/trade/types.rs | 150 ++++++++++++++++++++++++++++++ python/src/quote/context.rs | 9 ++ python/src/quote/context_async.rs | 14 +++ python/src/quote/mod.rs | 1 + python/src/quote/types.rs | 38 ++++++++ python/src/trade/context.rs | 51 +++++++++- python/src/trade/context_async.rs | 64 ++++++++++++- python/src/trade/mod.rs | 8 ++ python/src/trade/types.rs | 149 +++++++++++++++++++++++++++++ 14 files changed, 720 insertions(+), 9 deletions(-) diff --git a/java/src/quote_context.rs b/java/src/quote_context.rs index 167942ab73..e12d99befd 100644 --- a/java/src/quote_context.rs +++ b/java/src/quote_context.rs @@ -1282,3 +1282,22 @@ pub unsafe extern "system" fn Java_com_longbridge_SdkNative_quoteContextOptionVo Ok(()) }) } + +#[unsafe(no_mangle)] +pub unsafe extern "system" fn Java_com_longbridge_SdkNative_quoteContextUsCryptoOverview( + mut env: JNIEnv, + _class: JClass, + context: i64, + counter_id: JObject, + callback: JObject, +) { + jni_result(&mut env, (), |env| { + let context = &*(context as *const ContextObj); + let counter_id: String = FromJValue::from_jvalue(env, counter_id.into())?; + async_util::execute(env, callback, async move { + let resp = context.ctx.us_crypto_overview(counter_id).await?; + Ok(serde_json::to_string(&resp).unwrap_or_default()) + })?; + Ok(()) + }) +} diff --git a/java/src/trade_context.rs b/java/src/trade_context.rs index 214812ffdc..c38b2e3b44 100644 --- a/java/src/trade_context.rs +++ b/java/src/trade_context.rs @@ -4,7 +4,7 @@ use jni::{ JNIEnv, JavaVM, errors::Result, objects::{GlobalRef, JClass, JObject, JString}, - sys::jobjectArray, + sys::{jboolean, jobjectArray}, }; use longbridge::{ Config, Decimal, Market, TradeContext, @@ -12,8 +12,8 @@ use longbridge::{ BalanceType, EstimateMaxPurchaseQuantityOptions, GetCashFlowOptions, GetFundPositionsOptions, GetHistoryExecutionsOptions, GetHistoryOrdersOptions, GetStockPositionsOptions, GetTodayExecutionsOptions, GetTodayOrdersOptions, OrderSide, - OrderStatus, OrderType, OutsideRTH, PushEvent, ReplaceOrderOptions, SubmitOrderOptions, - TimeInForceType, TopicType, + OrderStatus, OrderType, OutsideRTH, PushEvent, QueryUSOrdersOptions, ReplaceOrderOptions, + SubmitOrderOptions, TimeInForceType, TopicType, }, }; use parking_lot::Mutex; @@ -625,3 +625,107 @@ pub unsafe extern "system" fn Java_com_longbridge_SdkNative_tradeContextEstimate Ok(()) }) } + +// ── US-market JNI stubs ────────────────────────────────────────────────────── + +#[unsafe(no_mangle)] +pub unsafe extern "system" fn Java_com_longbridge_SdkNative_tradeContextUsQueryOrders( + mut env: JNIEnv, + _class: JClass, + context: i64, + account_channel: JObject, + action: i64, + start_at: i64, + end_at: i64, + counter_ids: JObject, + security_types: JObject, + query_type: i64, + page: i64, + limit: i64, + query_version: i64, + callback: JObject, +) { + use crate::types::ObjectArray; + jni_result(&mut env, (), |env| { + let context = &*(context as *const ContextObj); + let account_channel: String = FromJValue::from_jvalue(env, account_channel.into())?; + let counter_ids: ObjectArray = FromJValue::from_jvalue(env, counter_ids.into())?; + let security_types: ObjectArray = FromJValue::from_jvalue(env, security_types.into())?; + let us_opts = QueryUSOrdersOptions { + account_channel, + action: action as i32, + start_at: start_at as f64, + end_at: end_at as f64, + counter_ids: counter_ids.0, + security_types: security_types.0, + query_type: query_type as i32, + page: page as i32, + limit: limit as i32, + query_version: query_version as f64, + }; + async_util::execute(env, callback, async move { + let resp = context.ctx.us_query_orders(us_opts).await?; + Ok(serde_json::to_string(&resp).unwrap_or_default()) + })?; + Ok(()) + }) +} + +#[unsafe(no_mangle)] +pub unsafe extern "system" fn Java_com_longbridge_SdkNative_tradeContextUsOrderDetail( + mut env: JNIEnv, + _class: JClass, + context: i64, + order_id: JObject, + is_attached: jboolean, + callback: JObject, +) { + jni_result(&mut env, (), |env| { + let context = &*(context as *const ContextObj); + let order_id: String = FromJValue::from_jvalue(env, order_id.into())?; + let is_attached = is_attached != 0; + async_util::execute(env, callback, async move { + let resp = context.ctx.us_order_detail(order_id, is_attached).await?; + Ok(serde_json::to_string(&resp).unwrap_or_default()) + })?; + Ok(()) + }) +} + +#[unsafe(no_mangle)] +pub unsafe extern "system" fn Java_com_longbridge_SdkNative_tradeContextUsAssetOverview( + mut env: JNIEnv, + _class: JClass, + context: i64, + callback: JObject, +) { + jni_result(&mut env, (), |env| { + let context = &*(context as *const ContextObj); + async_util::execute(env, callback, async move { + let resp = context.ctx.us_asset_overview().await?; + Ok(serde_json::to_string(&resp).unwrap_or_default()) + })?; + Ok(()) + }) +} + +#[unsafe(no_mangle)] +pub unsafe extern "system" fn Java_com_longbridge_SdkNative_tradeContextUsRealizedPl( + mut env: JNIEnv, + _class: JClass, + context: i64, + currency: JObject, + category: JObject, + callback: JObject, +) { + jni_result(&mut env, (), |env| { + let context = &*(context as *const ContextObj); + let currency: String = FromJValue::from_jvalue(env, currency.into())?; + let category: Option = FromJValue::from_jvalue(env, category.into()).ok(); + async_util::execute(env, callback, async move { + let resp = context.ctx.us_realized_pl(currency, category).await?; + Ok(serde_json::to_string(&resp).unwrap_or_default()) + })?; + Ok(()) + }) +} diff --git a/nodejs/src/quote/context.rs b/nodejs/src/quote/context.rs index 4ee04a1e25..41ac25bbf3 100644 --- a/nodejs/src/quote/context.rs +++ b/nodejs/src/quote/context.rs @@ -21,8 +21,8 @@ use crate::{ RealtimeQuote, Security, SecurityBrokers, SecurityCalcIndex, SecurityDepth, SecurityListCategory, SecurityQuote, SecurityStaticInfo, ShortPositionsResponse, ShortTradesResponse, SortOrderType, StrikePriceInfo, SubType, SubTypes, Subscription, - Trade, TradeSessions, WarrantInfo, WarrantQuote, WarrantSortBy, WarrantStatus, - WarrantType, WatchlistGroup, + Trade, TradeSessions, USCryptoOverview, WarrantInfo, WarrantQuote, WarrantSortBy, + WarrantStatus, WarrantType, WatchlistGroup, }, }, time::{NaiveDate, NaiveDatetime}, @@ -1290,4 +1290,15 @@ impl QuoteContext { .map_err(ErrorNewType)? .into()) } + + /// Get US cryptocurrency market overview. US token required. + #[napi] + pub async fn us_crypto_overview(&self, counter_id: String) -> Result { + Ok(self + .ctx + .us_crypto_overview(counter_id) + .await + .map_err(ErrorNewType)? + .into()) + } } diff --git a/nodejs/src/quote/types.rs b/nodejs/src/quote/types.rs index acd0cfdd46..13ab083a2d 100644 --- a/nodejs/src/quote/types.rs +++ b/nodejs/src/quote/types.rs @@ -1663,3 +1663,41 @@ impl From for OptionVolumeDailyStat { } } } + +/// US cryptocurrency market overview +#[napi_derive::napi(object)] +#[derive(Debug, Clone)] +pub struct USCryptoOverview { + pub name: String, + pub ticker: String, + pub currency: String, + pub all_time_high: String, + pub all_time_high_date: String, + pub all_time_low: String, + pub all_time_low_date: String, + pub ipo_date: String, + pub issue_price: String, + pub shares: String, + pub official_web_address: String, + /// Profile serialized as JSON string + pub profile: String, +} + +impl From for USCryptoOverview { + fn from(v: longbridge::quote::USCryptoOverview) -> Self { + Self { + name: v.name, + ticker: v.ticker, + currency: v.currency, + all_time_high: v.all_time_high, + all_time_high_date: v.all_time_high_date, + all_time_low: v.all_time_low, + all_time_low_date: v.all_time_low_date, + ipo_date: v.ipo_date, + issue_price: v.issue_price, + shares: v.shares, + official_web_address: v.official_web_address, + profile: serde_json::to_string(&v.profile).unwrap_or_default(), + } + } +} diff --git a/nodejs/src/trade/context.rs b/nodejs/src/trade/context.rs index eca83725b5..960d224892 100644 --- a/nodejs/src/trade/context.rs +++ b/nodejs/src/trade/context.rs @@ -1,6 +1,6 @@ use std::sync::Arc; -use longbridge::trade::{GetFundPositionsOptions, GetStockPositionsOptions, PushEvent}; +use longbridge::trade::{GetFundPositionsOptions, GetStockPositionsOptions, PushEvent, QueryUSOrdersOptions}; use napi::{Result, bindgen_prelude::*, threadsafe_function::ThreadsafeFunctionCallMode}; use parking_lot::Mutex; @@ -502,6 +502,67 @@ impl TradeContext { .try_into() } + // ── US-market methods ───────────────────────────────────────────────── + + /// Query US order list. Returns JSON string. US token required. + #[napi] + pub fn us_query_orders<'env>( + &self, + env: &'env Env, + account_channel: String, action: i32, + start_at: f64, end_at: f64, + counter_ids: Vec, security_types: Vec, + query_type: i32, page: i32, limit: i32, query_version: f64, + ) -> Result> { + let ctx = self.ctx.clone(); + let opts = QueryUSOrdersOptions { + account_channel, action, start_at, end_at, + counter_ids, security_types, query_type, page, limit, query_version, + }; + env.spawn_future(async move { + let resp = ctx.us_query_orders(opts).await.map_err(ErrorNewType)?; + serde_json::to_string(&resp).map_err(|e| napi::Error::from_reason(e.to_string())) + }) + } + + /// Get US order detail. Returns JSON string. US token required. + #[napi] + pub fn us_order_detail<'env>( + &self, + env: &'env Env, + order_id: String, + is_attached: bool, + ) -> Result> { + let ctx = self.ctx.clone(); + env.spawn_future(async move { + let resp = ctx.us_order_detail(order_id, is_attached).await.map_err(ErrorNewType)?; + serde_json::to_string(&resp).map_err(|e| napi::Error::from_reason(e.to_string())) + }) + } + + /// Get US account asset overview. US token required. + #[napi] + pub fn us_asset_overview<'env>(&self, env: &'env Env) -> Result> { + let ctx = self.ctx.clone(); + env.spawn_future(async move { + Ok(ctx.us_asset_overview().await.map_err(ErrorNewType)?.into()) + }) + } + + /// Get US realized P&L. US token required. + #[napi] + pub fn us_realized_pl<'env>( + &self, + env: &'env Env, + currency: String, + category: Option, + ) -> Result> { + let ctx = self.ctx.clone(); + env.spawn_future(async move { + Ok(ctx.us_realized_pl(currency, category).await.map_err(ErrorNewType)?.into()) + }) + } + /// Estimating the maximum purchase quantity for Hong Kong and US stocks, /// warrants, and options /// diff --git a/nodejs/src/trade/types.rs b/nodejs/src/trade/types.rs index be4e0e257e..a4b9d56016 100644 --- a/nodejs/src/trade/types.rs +++ b/nodejs/src/trade/types.rs @@ -795,3 +795,153 @@ pub struct EstimateMaxPurchaseQuantityResponse { /// Margin available quantity margin_max_qty: Decimal, } + +// ── US-market types ────────────────────────────────────────────────────────── + +/// A stock position in a US account +#[napi_derive::napi(object)] +#[derive(Debug, Clone)] +pub struct USStockPosition { + pub symbol: String, pub name: String, pub quantity: String, + pub available_quantity: String, pub currency: String, + pub cost_price: String, pub market_value: String, + pub unrealized_pl: String, pub unrealized_pl_ratio: String, + pub last_done: String, pub prev_close: String, pub change_rate: String, + pub night_last_done: String, pub pretrade_close: String, + pub trade_status: String, pub individual_quantity: String, +} + +impl From for USStockPosition { + fn from(v: longbridge::trade::USStockPosition) -> Self { + Self { + symbol: v.symbol, name: v.name, quantity: v.quantity, + available_quantity: v.available_quantity, currency: v.currency, + cost_price: v.cost_price, market_value: v.market_value, + unrealized_pl: v.unrealized_pl, unrealized_pl_ratio: v.unrealized_pl_ratio, + last_done: v.last_done, prev_close: v.prev_close, change_rate: v.change_rate, + night_last_done: v.night_last_done, pretrade_close: v.pretrade_close, + trade_status: v.trade_status, individual_quantity: v.individual_quantity, + } + } +} + +/// An option position in a US account +#[napi_derive::napi(object)] +#[derive(Debug, Clone)] +pub struct USOptionPosition { + pub symbol: String, pub strike_price: String, pub due_date: String, + pub contract_multiplier: i32, pub option_type: String, + pub quantity: String, pub market_value: String, pub unrealized_pl: String, +} + +impl From for USOptionPosition { + fn from(v: longbridge::trade::USOptionPosition) -> Self { + Self { + symbol: v.symbol, strike_price: v.strike_price, due_date: v.due_date, + contract_multiplier: v.contract_multiplier, option_type: v.option_type, + quantity: v.quantity, market_value: v.market_value, unrealized_pl: v.unrealized_pl, + } + } +} + +/// A cryptocurrency position in a US account +#[napi_derive::napi(object)] +#[derive(Debug, Clone)] +pub struct USCryptoPosition { + pub symbol: String, pub quantity: String, pub market_value: String, + pub unrealized_pl: String, pub cost_price: String, +} + +impl From for USCryptoPosition { + fn from(v: longbridge::trade::USCryptoPosition) -> Self { + Self { + symbol: v.symbol, quantity: v.quantity, market_value: v.market_value, + unrealized_pl: v.unrealized_pl, cost_price: v.cost_price, + } + } +} + +/// Purchasing power breakdown for a US account +#[napi_derive::napi(object)] +#[derive(Debug, Clone, Default)] +pub struct USBuyPower { + pub cash_buy_power: String, pub overnight_buy_power: String, + pub day_trade_buy_power: String, pub option_buy_power: String, + pub crypto_buy_power: String, +} + +impl From for USBuyPower { + fn from(v: longbridge::trade::USBuyPower) -> Self { + Self { + cash_buy_power: v.cash_buy_power, overnight_buy_power: v.overnight_buy_power, + day_trade_buy_power: v.day_trade_buy_power, option_buy_power: v.option_buy_power, + crypto_buy_power: v.crypto_buy_power, + } + } +} + +/// Full US account asset snapshot +#[napi_derive::napi(object)] +#[derive(Debug, Clone)] +pub struct USAssetOverview { + pub account_type: String, pub net_assets: String, + pub total_cash: String, pub unrealized_pl: String, + pub positions: Vec, + pub option_positions: Vec, + /// Multi-leg strategies as JSON string + pub multi_legs: String, + pub crypto_positions: Vec, + pub buy_power: USBuyPower, +} + +impl From for USAssetOverview { + fn from(v: longbridge::trade::USAssetOverview) -> Self { + Self { + account_type: v.account_type, net_assets: v.net_assets, + total_cash: v.total_cash, unrealized_pl: v.unrealized_pl, + positions: v.positions.into_iter().map(Into::into).collect(), + option_positions: v.option_positions.into_iter().map(Into::into).collect(), + multi_legs: serde_json::to_string(&v.multi_legs).unwrap_or_default(), + crypto_positions: v.crypto_positions.into_iter().map(Into::into).collect(), + buy_power: v.buy_power.into(), + } + } +} + +/// A single realized P&L entry +#[napi_derive::napi(object)] +#[derive(Debug, Clone)] +pub struct USRealizedPLItem { + pub symbol: String, pub name: String, pub category: String, + pub realized_pl: String, pub quantity_sold: String, + pub avg_cost: String, pub avg_sell_price: String, +} + +impl From for USRealizedPLItem { + fn from(v: longbridge::trade::USRealizedPLItem) -> Self { + Self { + symbol: v.symbol, name: v.name, category: v.category, + realized_pl: v.realized_pl, quantity_sold: v.quantity_sold, + avg_cost: v.avg_cost, avg_sell_price: v.avg_sell_price, + } + } +} + +/// Realized P&L response for a US account +#[napi_derive::napi(object)] +#[derive(Debug, Clone)] +pub struct USRealizedPL { + pub total_realized_pl: String, + pub currency: String, + pub items: Vec, +} + +impl From for USRealizedPL { + fn from(v: longbridge::trade::USRealizedPL) -> Self { + Self { + total_realized_pl: v.total_realized_pl, + currency: v.currency, + items: v.items.into_iter().map(Into::into).collect(), + } + } +} diff --git a/python/src/quote/context.rs b/python/src/quote/context.rs index a2d6f9e1e1..028c1276f6 100644 --- a/python/src/quote/context.rs +++ b/python/src/quote/context.rs @@ -687,4 +687,13 @@ impl QuoteContext { .map_err(ErrorNewType)? .into()) } + + /// Get US cryptocurrency market overview. US token required. + fn us_crypto_overview(&self, counter_id: String) -> PyResult { + Ok(self + .ctx + .us_crypto_overview(counter_id) + .map_err(ErrorNewType)? + .into()) + } } diff --git a/python/src/quote/context_async.rs b/python/src/quote/context_async.rs index cd5ca44c92..823f78eaeb 100644 --- a/python/src/quote/context_async.rs +++ b/python/src/quote/context_async.rs @@ -895,4 +895,18 @@ impl AsyncQuoteContext { }) .map(|b| b.unbind()) } + + /// Get US cryptocurrency market overview. US token required. Returns awaitable. + fn us_crypto_overview(&self, py: Python<'_>, counter_id: String) -> PyResult> { + let ctx = self.ctx.clone(); + pyo3_async_runtimes::tokio::future_into_py(py, async move { + let r: crate::quote::types::USCryptoOverview = ctx + .us_crypto_overview(counter_id) + .await + .map_err(ErrorNewType)? + .into(); + Ok(r) + }) + .map(|b| b.unbind()) + } } diff --git a/python/src/quote/mod.rs b/python/src/quote/mod.rs index 749f0deb07..0c89ee2c29 100644 --- a/python/src/quote/mod.rs +++ b/python/src/quote/mod.rs @@ -71,6 +71,7 @@ pub(crate) fn register_types(parent: &Bound) -> PyResult<()> { parent.add_class::()?; parent.add_class::()?; parent.add_class::()?; + parent.add_class::()?; parent.add_class::()?; parent.add_class::()?; diff --git a/python/src/quote/types.rs b/python/src/quote/types.rs index d9849dac0d..e94d01e7a4 100644 --- a/python/src/quote/types.rs +++ b/python/src/quote/types.rs @@ -1621,3 +1621,41 @@ impl From for OptionVolumeDailyStat { } } } + +/// US cryptocurrency market overview +#[pyclass(get_all, skip_from_py_object)] +#[derive(Debug, Clone)] +pub(crate) struct USCryptoOverview { + pub name: String, + pub ticker: String, + pub currency: String, + pub all_time_high: String, + pub all_time_high_date: String, + pub all_time_low: String, + pub all_time_low_date: String, + pub ipo_date: String, + pub issue_price: String, + pub shares: String, + pub official_web_address: String, + /// Profile as JSON string + pub profile: String, +} + +impl From for USCryptoOverview { + fn from(v: longbridge::quote::USCryptoOverview) -> Self { + Self { + name: v.name, + ticker: v.ticker, + currency: v.currency, + all_time_high: v.all_time_high, + all_time_high_date: v.all_time_high_date, + all_time_low: v.all_time_low, + all_time_low_date: v.all_time_low_date, + ipo_date: v.ipo_date, + issue_price: v.issue_price, + shares: v.shares, + official_web_address: v.official_web_address, + profile: serde_json::to_string(&v.profile).unwrap_or_default(), + } + } +} diff --git a/python/src/trade/context.rs b/python/src/trade/context.rs index 2e7ff3a090..bcdb9a6366 100644 --- a/python/src/trade/context.rs +++ b/python/src/trade/context.rs @@ -5,7 +5,8 @@ use longbridge::{ trade::{ EstimateMaxPurchaseQuantityOptions, GetCashFlowOptions, GetFundPositionsOptions, GetHistoryExecutionsOptions, GetHistoryOrdersOptions, GetStockPositionsOptions, - GetTodayExecutionsOptions, GetTodayOrdersOptions, ReplaceOrderOptions, SubmitOrderOptions, + GetTodayExecutionsOptions, GetTodayOrdersOptions, QueryUSOrdersOptions, + ReplaceOrderOptions, SubmitOrderOptions, }, }; use parking_lot::Mutex; @@ -409,6 +410,54 @@ impl TradeContext { .try_into() } + // ── US-market methods ───────────────────────────────────────────────── + + /// Query US order list (returns JSON string). US token required. + #[pyo3(signature = (account_channel, action, start_at, end_at, counter_ids, security_types, query_type, page, limit, query_version))] + #[allow(clippy::too_many_arguments)] + fn us_query_orders( + &self, + account_channel: String, + action: i32, + start_at: f64, + end_at: f64, + counter_ids: Vec, + security_types: Vec, + query_type: i32, + page: i32, + limit: i32, + query_version: f64, + ) -> PyResult { + let opts = QueryUSOrdersOptions { + account_channel, action, start_at, end_at, + counter_ids, security_types, query_type, page, limit, query_version, + }; + let resp = self.ctx.us_query_orders(opts).map_err(ErrorNewType)?; + serde_json::to_string(&resp) + .map_err(|e| pyo3::exceptions::PyValueError::new_err(e.to_string())) + } + + /// Get US order detail (returns JSON string). US token required. + fn us_order_detail(&self, order_id: String, is_attached: bool) -> PyResult { + let resp = self.ctx.us_order_detail(order_id, is_attached).map_err(ErrorNewType)?; + serde_json::to_string(&resp) + .map_err(|e| pyo3::exceptions::PyValueError::new_err(e.to_string())) + } + + /// Get US account asset overview. US token required. + fn us_asset_overview(&self) -> PyResult { + Ok(self.ctx.us_asset_overview().map_err(ErrorNewType)?.into()) + } + + /// Get US realized P&L. US token required. + fn us_realized_pl( + &self, + currency: String, + category: Option, + ) -> PyResult { + Ok(self.ctx.us_realized_pl(currency, category).map_err(ErrorNewType)?.into()) + } + /// Estimating the maximum purchase quantity for Hong Kong and US stocks, /// warrants, and options #[allow(clippy::too_many_arguments)] diff --git a/python/src/trade/context_async.rs b/python/src/trade/context_async.rs index a0c02d3fe9..73c6968d13 100644 --- a/python/src/trade/context_async.rs +++ b/python/src/trade/context_async.rs @@ -5,8 +5,8 @@ use std::sync::Arc; use longbridge::trade::{ EstimateMaxPurchaseQuantityOptions, GetCashFlowOptions, GetFundPositionsOptions, GetHistoryExecutionsOptions, GetHistoryOrdersOptions, GetStockPositionsOptions, - GetTodayExecutionsOptions, GetTodayOrdersOptions, ReplaceOrderOptions, SubmitOrderOptions, - TradeContext, + GetTodayExecutionsOptions, GetTodayOrdersOptions, QueryUSOrdersOptions, + ReplaceOrderOptions, SubmitOrderOptions, TradeContext, }; use parking_lot::Mutex; use pyo3::{prelude::*, types::PyType}; @@ -487,6 +487,66 @@ impl AsyncTradeContext { .map(|b| b.unbind()) } + // ── US-market async methods ─────────────────────────────────────────── + + /// Query US order list (JSON string). US token required. Returns awaitable. + #[pyo3(signature = (account_channel, action, start_at, end_at, counter_ids, security_types, query_type, page, limit, query_version))] + #[allow(clippy::too_many_arguments)] + fn us_query_orders( + &self, py: Python<'_>, + account_channel: String, action: i32, + start_at: f64, end_at: f64, + counter_ids: Vec, security_types: Vec, + query_type: i32, page: i32, limit: i32, query_version: f64, + ) -> PyResult> { + let ctx = self.ctx.clone(); + let opts = QueryUSOrdersOptions { + account_channel, action, start_at, end_at, + counter_ids, security_types, query_type, page, limit, query_version, + }; + pyo3_async_runtimes::tokio::future_into_py(py, async move { + let resp = ctx.us_query_orders(opts).await.map_err(ErrorNewType)?; + let s = serde_json::to_string(&resp) + .map_err(|e| pyo3::exceptions::PyValueError::new_err(e.to_string()))?; + Ok(s) + }) + .map(|b| b.unbind()) + } + + /// Get US order detail (JSON string). US token required. Returns awaitable. + fn us_order_detail(&self, py: Python<'_>, order_id: String, is_attached: bool) -> PyResult> { + let ctx = self.ctx.clone(); + pyo3_async_runtimes::tokio::future_into_py(py, async move { + let resp = ctx.us_order_detail(order_id, is_attached).await.map_err(ErrorNewType)?; + let s = serde_json::to_string(&resp) + .map_err(|e| pyo3::exceptions::PyValueError::new_err(e.to_string()))?; + Ok(s) + }) + .map(|b| b.unbind()) + } + + /// Get US account asset overview. US token required. Returns awaitable. + fn us_asset_overview(&self, py: Python<'_>) -> PyResult> { + let ctx = self.ctx.clone(); + pyo3_async_runtimes::tokio::future_into_py(py, async move { + let r: crate::trade::types::USAssetOverview = + ctx.us_asset_overview().await.map_err(ErrorNewType)?.into(); + Ok(r) + }) + .map(|b| b.unbind()) + } + + /// Get US realized P&L. US token required. Returns awaitable. + fn us_realized_pl(&self, py: Python<'_>, currency: String, category: Option) -> PyResult> { + let ctx = self.ctx.clone(); + pyo3_async_runtimes::tokio::future_into_py(py, async move { + let r: crate::trade::types::USRealizedPL = + ctx.us_realized_pl(currency, category).await.map_err(ErrorNewType)?.into(); + Ok(r) + }) + .map(|b| b.unbind()) + } + /// Estimate max purchase quantity. Returns awaitable. #[allow(clippy::too_many_arguments)] #[pyo3(signature = (symbol, order_type, side, price = None, currency = None, order_id = None, fractional_shares = false))] diff --git a/python/src/trade/mod.rs b/python/src/trade/mod.rs index 01f249eef2..3065fa1bc0 100644 --- a/python/src/trade/mod.rs +++ b/python/src/trade/mod.rs @@ -41,6 +41,14 @@ pub(crate) fn register_types(parent: &Bound) -> PyResult<()> { parent.add_class::()?; parent.add_class::()?; + parent.add_class::()?; + parent.add_class::()?; + parent.add_class::()?; + parent.add_class::()?; + parent.add_class::()?; + parent.add_class::()?; + parent.add_class::()?; + parent.add_class::()?; parent.add_class::()?; Ok(()) diff --git a/python/src/trade/types.rs b/python/src/trade/types.rs index 49a4bd9179..6247e2df18 100644 --- a/python/src/trade/types.rs +++ b/python/src/trade/types.rs @@ -791,3 +791,152 @@ pub(crate) struct EstimateMaxPurchaseQuantityResponse { /// Margin available quantity pub margin_max_qty: PyDecimal, } + +// ── US-market types ────────────────────────────────────────────────────────── + +/// A stock position in a US account +#[pyclass(get_all, skip_from_py_object)] +#[derive(Debug, Clone)] +pub(crate) struct USStockPosition { + pub symbol: String, pub name: String, pub quantity: String, + pub available_quantity: String, pub currency: String, + pub cost_price: String, pub market_value: String, + pub unrealized_pl: String, pub unrealized_pl_ratio: String, + pub last_done: String, pub prev_close: String, pub change_rate: String, + pub night_last_done: String, pub pretrade_close: String, + pub trade_status: String, pub individual_quantity: String, +} + +impl From for USStockPosition { + fn from(v: longbridge::trade::USStockPosition) -> Self { + Self { + symbol: v.symbol, name: v.name, quantity: v.quantity, + available_quantity: v.available_quantity, currency: v.currency, + cost_price: v.cost_price, market_value: v.market_value, + unrealized_pl: v.unrealized_pl, unrealized_pl_ratio: v.unrealized_pl_ratio, + last_done: v.last_done, prev_close: v.prev_close, change_rate: v.change_rate, + night_last_done: v.night_last_done, pretrade_close: v.pretrade_close, + trade_status: v.trade_status, individual_quantity: v.individual_quantity, + } + } +} + +/// An option position in a US account +#[pyclass(get_all, skip_from_py_object)] +#[derive(Debug, Clone)] +pub(crate) struct USOptionPosition { + pub symbol: String, pub strike_price: String, pub due_date: String, + pub contract_multiplier: i32, pub option_type: String, + pub quantity: String, pub market_value: String, pub unrealized_pl: String, +} + +impl From for USOptionPosition { + fn from(v: longbridge::trade::USOptionPosition) -> Self { + Self { + symbol: v.symbol, strike_price: v.strike_price, due_date: v.due_date, + contract_multiplier: v.contract_multiplier, option_type: v.option_type, + quantity: v.quantity, market_value: v.market_value, unrealized_pl: v.unrealized_pl, + } + } +} + +/// A cryptocurrency position in a US account +#[pyclass(get_all, skip_from_py_object)] +#[derive(Debug, Clone)] +pub(crate) struct USCryptoPosition { + pub symbol: String, pub quantity: String, pub market_value: String, + pub unrealized_pl: String, pub cost_price: String, +} + +impl From for USCryptoPosition { + fn from(v: longbridge::trade::USCryptoPosition) -> Self { + Self { + symbol: v.symbol, quantity: v.quantity, market_value: v.market_value, + unrealized_pl: v.unrealized_pl, cost_price: v.cost_price, + } + } +} + +/// Purchasing power breakdown for a US account +#[pyclass(get_all, skip_from_py_object)] +#[derive(Debug, Clone, Default)] +pub(crate) struct USBuyPower { + pub cash_buy_power: String, pub overnight_buy_power: String, + pub day_trade_buy_power: String, pub option_buy_power: String, + pub crypto_buy_power: String, +} + +impl From for USBuyPower { + fn from(v: longbridge::trade::USBuyPower) -> Self { + Self { + cash_buy_power: v.cash_buy_power, overnight_buy_power: v.overnight_buy_power, + day_trade_buy_power: v.day_trade_buy_power, option_buy_power: v.option_buy_power, + crypto_buy_power: v.crypto_buy_power, + } + } +} + +/// Full US account asset snapshot +#[pyclass(get_all, skip_from_py_object)] +#[derive(Debug, Clone)] +pub(crate) struct USAssetOverview { + pub account_type: String, pub net_assets: String, + pub total_cash: String, pub unrealized_pl: String, + pub positions: Vec, + pub option_positions: Vec, + pub multi_legs: String, + pub crypto_positions: Vec, + pub buy_power: USBuyPower, +} + +impl From for USAssetOverview { + fn from(v: longbridge::trade::USAssetOverview) -> Self { + Self { + account_type: v.account_type, net_assets: v.net_assets, + total_cash: v.total_cash, unrealized_pl: v.unrealized_pl, + positions: v.positions.into_iter().map(Into::into).collect(), + option_positions: v.option_positions.into_iter().map(Into::into).collect(), + multi_legs: serde_json::to_string(&v.multi_legs).unwrap_or_default(), + crypto_positions: v.crypto_positions.into_iter().map(Into::into).collect(), + buy_power: v.buy_power.into(), + } + } +} + +/// A single realized P&L entry +#[pyclass(get_all, skip_from_py_object)] +#[derive(Debug, Clone)] +pub(crate) struct USRealizedPLItem { + pub symbol: String, pub name: String, pub category: String, + pub realized_pl: String, pub quantity_sold: String, + pub avg_cost: String, pub avg_sell_price: String, +} + +impl From for USRealizedPLItem { + fn from(v: longbridge::trade::USRealizedPLItem) -> Self { + Self { + symbol: v.symbol, name: v.name, category: v.category, + realized_pl: v.realized_pl, quantity_sold: v.quantity_sold, + avg_cost: v.avg_cost, avg_sell_price: v.avg_sell_price, + } + } +} + +/// Realized P&L response for a US account +#[pyclass(get_all, skip_from_py_object)] +#[derive(Debug, Clone)] +pub(crate) struct USRealizedPL { + pub total_realized_pl: String, + pub currency: String, + pub items: Vec, +} + +impl From for USRealizedPL { + fn from(v: longbridge::trade::USRealizedPL) -> Self { + Self { + total_realized_pl: v.total_realized_pl, + currency: v.currency, + items: v.items.into_iter().map(Into::into).collect(), + } + } +} From b7fd5d7fcde3711991bdb14c76290b4c82f5f9f2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=A2=81=E7=AB=A0=E6=B4=AA?= Date: Thu, 2 Jul 2026 16:26:48 +0800 Subject: [PATCH 11/44] chore(python): add US-market API type stubs to openapi.pyi Add type stubs for all 14 US-only endpoints: - QuoteContext/AsyncQuoteContext: us_crypto_overview - TradeContext/AsyncTradeContext: us_query_orders, us_order_detail, us_asset_overview, us_realized_pl - FundamentalContext: us_company_overview, us_valuation_overview, us_financial_overview, us_financial_statement_v3, us_key_financial_metrics, us_analyst_consensus, us_etf_dividend_info, us_company_dividends, us_etf_files Add new type classes: USCryptoOverview, USStockPosition, USOptionPosition, USCryptoPosition, USBuyPower, USAssetOverview, USRealizedPLItem, USRealizedPL, USRankTag, USCompanyOverview, USValuationIndicator, USValuationOverview, USFinancialStatement, USETFDividendInfo, USDividendItem, USCompanyDividends, USETFFile, USETFFilesResponse Co-Authored-By: Claude Sonnet 4.6 (1M context) --- python/pysrc/longbridge/openapi.pyi | 572 ++++++++++++++++++++++++++++ 1 file changed, 572 insertions(+) diff --git a/python/pysrc/longbridge/openapi.pyi b/python/pysrc/longbridge/openapi.pyi index b5a518f60f..d065efff0e 100644 --- a/python/pysrc/longbridge/openapi.pyi +++ b/python/pysrc/longbridge/openapi.pyi @@ -4020,6 +4020,17 @@ class QuoteContext: :class:`ShortTradesResponse` with raw JSON data """ + def us_crypto_overview(self, counter_id: str) -> "USCryptoOverview": + """Get US cryptocurrency market overview. US token required. + + Args: + counter_id: Crypto counter_id, e.g. ``"CY/US/BTC"`` + + Returns: + :class:`USCryptoOverview` + """ + ... + class AsyncQuoteContext: """ Async quote context for use with asyncio. Create via `AsyncQuoteContext.create(config)` and await inside asyncio. @@ -5372,6 +5383,17 @@ class AsyncQuoteContext: """ ... + def us_crypto_overview(self, counter_id: str) -> "Awaitable[USCryptoOverview]": + """Get US cryptocurrency market overview. US token required. Returns awaitable. + + Args: + counter_id: Crypto counter_id, e.g. ``"CY/US/BTC"`` + + Returns: + Awaitable resolving to :class:`USCryptoOverview` + """ + ... + class OrderSide: """ Order side @@ -7319,6 +7341,75 @@ class TradeContext: print(resp) """ + def us_query_orders( + self, + account_channel: str, + action: int, + start_at: float, + end_at: float, + counter_ids: list, + security_types: list, + query_type: int, + page: int, + limit: int, + query_version: float, + ) -> str: + """Query US order list (paginated). Returns JSON string. US token required. + + Args: + account_channel: Account channel (e.g. ``"regular"``) + action: Order action filter + start_at: Start timestamp (unix seconds) + end_at: End timestamp (unix seconds) + counter_ids: Filter by counter IDs + security_types: Filter by security types (``"stock"``, ``"option"``, ``"crypto"``) + query_type: Query type code + page: Page number (1-based) + limit: Page size + query_version: Query version (pass ``0`` for latest) + + Returns: + JSON string with order list + """ + ... + + def us_order_detail(self, order_id: str, is_attached: bool) -> str: + """Get US order detail. Returns JSON string. US token required. + + When ``is_attached`` is ``True``, attached take-profit/stop-loss + sub-orders are included in the response. + + Args: + order_id: Order ID + is_attached: Whether to include attached sub-orders + + Returns: + JSON string with order detail + """ + ... + + def us_asset_overview(self) -> "USAssetOverview": + """Get US account asset overview. US token required. + + Returns: + :class:`USAssetOverview` with stock, option, and crypto positions + plus purchasing power + """ + ... + + def us_realized_pl(self, currency: str, category: Optional[str] = None) -> "USRealizedPL": + """Get realized P&L for the US account. US token required. + + Args: + currency: Currency (e.g. ``"USD"``) + category: Asset category filter: ``"ALL"``, ``"STOCK"``, + ``"OPTION"``, or ``"CRYPTO"``; ``None`` for all + + Returns: + :class:`USRealizedPL` + """ + ... + class AsyncTradeContext: """ Async trade context for use with asyncio. Create via `AsyncTradeContext.create(config)` and await inside asyncio. @@ -7985,6 +8076,74 @@ class AsyncTradeContext: """ ... + def us_query_orders( + self, + account_channel: str, + action: int, + start_at: float, + end_at: float, + counter_ids: list, + security_types: list, + query_type: int, + page: int, + limit: int, + query_version: float, + ) -> "Awaitable[str]": + """Query US order list (paginated). Returns awaitable JSON string. US token required. + + Args: + account_channel: Account channel (e.g. ``"regular"``) + action: Order action filter + start_at: Start timestamp (unix seconds) + end_at: End timestamp (unix seconds) + counter_ids: Filter by counter IDs + security_types: Filter by security types (``"stock"``, ``"option"``, ``"crypto"``) + query_type: Query type code + page: Page number (1-based) + limit: Page size + query_version: Query version (pass ``0`` for latest) + + Returns: + Awaitable resolving to JSON string with order list + """ + ... + + def us_order_detail(self, order_id: str, is_attached: bool) -> "Awaitable[str]": + """Get US order detail. Returns awaitable JSON string. US token required. + + When ``is_attached`` is ``True``, attached take-profit/stop-loss + sub-orders are included in the response. + + Args: + order_id: Order ID + is_attached: Whether to include attached sub-orders + + Returns: + Awaitable resolving to JSON string with order detail + """ + ... + + def us_asset_overview(self) -> "Awaitable[USAssetOverview]": + """Get US account asset overview. US token required. Returns awaitable. + + Returns: + Awaitable resolving to :class:`USAssetOverview` + """ + ... + + def us_realized_pl(self, currency: str, category: Optional[str] = None) -> "Awaitable[USRealizedPL]": + """Get realized P&L for the US account. US token required. Returns awaitable. + + Args: + currency: Currency (e.g. ``"USD"``) + category: Asset category filter: ``"ALL"``, ``"STOCK"``, + ``"OPTION"``, or ``"CRYPTO"``; ``None`` for all + + Returns: + Awaitable resolving to :class:`USRealizedPL` + """ + ... + class StatementType: """ Statement type @@ -9960,6 +10119,111 @@ class FundamentalContext: """ ... + def us_company_overview(self, counter_id: str) -> "USCompanyOverview": + """Get US company overview. US token required. + + Args: + counter_id: Counter ID, e.g. ``"ST/US/AAPL"`` + + Returns: + :class:`USCompanyOverview` + """ + ... + + def us_valuation_overview(self, counter_id: str) -> "USValuationOverview": + """Get US valuation overview. US token required. + + Args: + counter_id: Counter ID, e.g. ``"ST/US/AAPL"`` + + Returns: + :class:`USValuationOverview` + """ + ... + + def us_financial_overview(self, counter_id: str, report: str) -> Any: + """Get US financial overview (revenue, net income, EPS, cash flow). US token required. + + Args: + counter_id: Counter ID, e.g. ``"ST/US/AAPL"`` + report: ``"annual"`` or ``"quarterly"`` + + Returns: + Dict with financial overview data + """ + ... + + def us_financial_statement_v3(self, counter_id: str, kind: str, report: str) -> "USFinancialStatement": + """Get US financial statement detail (IS/BS/CF). US token required. + + Args: + counter_id: Counter ID, e.g. ``"ST/US/AAPL"`` + kind: Statement kind: ``"IS"`` (income), ``"BS"`` (balance sheet), ``"CF"`` (cash flow) + report: ``"annual"`` or ``"quarterly"`` + + Returns: + :class:`USFinancialStatement` + """ + ... + + def us_key_financial_metrics(self, counter_id: str, report: str) -> Any: + """Get US key financial metrics (ROE, margins, debt ratio). US token required. + + Args: + counter_id: Counter ID, e.g. ``"ST/US/AAPL"`` + report: ``"annual"`` or ``"quarterly"`` + + Returns: + Dict with key metrics + """ + ... + + def us_analyst_consensus(self, counter_id: str, report: str) -> Any: + """Get US analyst consensus estimates (EPS, revenue forecasts). US token required. + + Args: + counter_id: Counter ID, e.g. ``"ST/US/AAPL"`` + report: ``"annual"`` or ``"quarterly"`` + + Returns: + Dict with consensus estimates + """ + ... + + def us_etf_dividend_info(self, counter_id: str) -> "USETFDividendInfo": + """Get US ETF dividend history. US token required. + + Args: + counter_id: ETF counter ID, e.g. ``"ST/US/SPY"`` + + Returns: + :class:`USETFDividendInfo` + """ + ... + + def us_company_dividends(self, counter_id: str) -> "USCompanyDividends": + """Get US company historical dividends. US token required. + + Args: + counter_id: Counter ID, e.g. ``"ST/US/AAPL"`` + + Returns: + :class:`USCompanyDividends` + """ + ... + + def us_etf_files(self, counter_id: str, size: Optional[int] = None) -> "USETFFilesResponse": + """Get US ETF document list. US token required. + + Args: + counter_id: ETF counter ID, e.g. ``"ST/US/SPY"`` + size: Number of files to return; ``None`` returns all + + Returns: + :class:`USETFFilesResponse` + """ + ... + # ── FundamentalContext new response types ───────────────────────── @@ -12106,3 +12370,311 @@ class OptionVolumeDaily: stats: list[OptionVolumeDailyStat] """Daily option volume statistics""" + + +# ── US-market API types ──────────────────────────────────────────── + +class USCryptoOverview: + """US cryptocurrency market overview. Returned by QuoteContext.us_crypto_overview.""" + + name: str + """Cryptocurrency full name""" + ticker: str + """Ticker symbol""" + currency: str + """Quote currency""" + all_time_high: str + """All-time high price""" + all_time_high_date: str + """All-time high date""" + all_time_low: str + """All-time low price""" + all_time_low_date: str + """All-time low date""" + ipo_date: str + """IPO / genesis date""" + issue_price: str + """Issue price""" + shares: str + """Total supply""" + official_web_address: str + """Official website URL""" + profile: str + """Extended profile as JSON string""" + + +class USStockPosition: + """A stock holding in the US account.""" + + symbol: str + """Security symbol""" + name: str + """Security name""" + quantity: str + """Total quantity held""" + available_quantity: str + """Available (tradable) quantity""" + currency: str + """Settlement currency""" + cost_price: str + """Average cost price""" + market_value: str + """Current market value""" + unrealized_pl: str + """Unrealized profit/loss""" + unrealized_pl_ratio: str + """Unrealized P&L ratio""" + last_done: str + """Last trade price""" + prev_close: str + """Previous close price""" + change_rate: str + """Price change rate""" + night_last_done: str + """After-hours last trade price""" + pretrade_close: str + """Pre-trade close price""" + trade_status: str + """Trade status code""" + individual_quantity: str + """Individual account quantity""" + + +class USOptionPosition: + """An option holding in the US account.""" + + symbol: str + """Option contract symbol""" + strike_price: str + """Strike price""" + due_date: str + """Expiry date""" + contract_multiplier: int + """Contract multiplier (typically 100)""" + option_type: str + """Option type: ``"call"`` or ``"put"``""" + quantity: str + """Quantity held""" + market_value: str + """Current market value""" + unrealized_pl: str + """Unrealized profit/loss""" + + +class USCryptoPosition: + """A cryptocurrency holding in the US account.""" + + symbol: str + """Cryptocurrency symbol""" + quantity: str + """Quantity held""" + market_value: str + """Current market value""" + unrealized_pl: str + """Unrealized profit/loss""" + cost_price: str + """Average cost price""" + + +class USBuyPower: + """Purchasing power breakdown for a US account.""" + + cash_buy_power: str + """Cash buying power""" + overnight_buy_power: str + """Overnight buying power""" + day_trade_buy_power: str + """Day-trade buying power""" + option_buy_power: str + """Option buying power""" + crypto_buy_power: str + """Crypto buying power""" + + +class USAssetOverview: + """Full US account asset snapshot. Returned by TradeContext.us_asset_overview.""" + + account_type: str + """Account type string""" + net_assets: str + """Total net assets""" + total_cash: str + """Total cash balance""" + unrealized_pl: str + """Total unrealized profit/loss""" + positions: List[USStockPosition] + """Stock positions""" + option_positions: List[USOptionPosition] + """Option positions""" + multi_legs: str + """Multi-leg positions as JSON string""" + crypto_positions: List[USCryptoPosition] + """Cryptocurrency positions""" + buy_power: USBuyPower + """Purchasing power breakdown""" + + +class USRealizedPLItem: + """A single realized P&L entry by symbol.""" + + symbol: str + """Security symbol""" + name: str + """Security name""" + category: str + """Asset category""" + realized_pl: str + """Realized profit/loss""" + quantity_sold: str + """Total quantity sold""" + avg_cost: str + """Average cost price""" + avg_sell_price: str + """Average sell price""" + + +class USRealizedPL: + """Realized P&L response for a US account. Returned by TradeContext.us_realized_pl.""" + + total_realized_pl: str + """Total realized profit/loss""" + currency: str + """Currency""" + items: List[USRealizedPLItem] + """Per-symbol breakdown""" + + +class USRankTag: + """A rank tag entry for a US company overview.""" + + name: str + """Tag name""" + chg: str + """Change value""" + rank_type: str + """Rank type""" + + +class USCompanyOverview: + """US company overview. Returned by FundamentalContext.us_company_overview.""" + + intro: str + """Company introduction""" + market_cap: str + """Market capitalisation""" + ccy_symbol: str + """Currency symbol""" + top_rank_tags: List[USRankTag] + """Top rank tags""" + detail_url: str + """Detail URL""" + + +class USValuationIndicator: + """A single US valuation indicator entry.""" + + circle: str + """Gauge circle value""" + part: str + """Gauge part label""" + metric: str + """Metric value""" + metric_type: str + """Metric type label""" + desc: str + """Description""" + ccy_symbol: str + """Currency symbol""" + + +class USValuationOverview: + """US valuation overview. Returned by FundamentalContext.us_valuation_overview.""" + + indicator: str + """Overall valuation indicator""" + current_indicator: USValuationIndicator + """Current indicator detail""" + range: int + """Valuation range score""" + date: str + """Date of snapshot""" + ai_summary: str + """AI-generated valuation summary""" + + +class USFinancialStatement: + """US financial statement (IS/BS/CF). Returned by FundamentalContext.us_financial_statement_v3.""" + + revenue: str + """Revenue""" + net_income: str + """Net income""" + net_margin: str + """Net margin""" + periods: list + """Per-period data as a list of dicts""" + currency: str + """Currency""" + + +class USETFDividendInfo: + """US ETF dividend history. Returned by FundamentalContext.us_etf_dividend_info.""" + + dividend_ttm: str + """TTM dividend per share""" + dividend_yield_ttm: str + """TTM dividend yield""" + dividend_frequency: str + """Distribution frequency""" + currency: str + """Currency""" + fiscal_year_info: list + """Fiscal year dividend data as a list of dicts""" + + +class USDividendItem: + """A single US dividend payment record.""" + + dividend: str + """Dividend per share""" + dividend_type: str + """Dividend type""" + ex_date: str + """Ex-dividend date""" + payment_date: str + """Payment date""" + record_date: str + """Record date""" + + +class USCompanyDividends: + """US company historical dividends. Returned by FundamentalContext.us_company_dividends.""" + + dividend_ttm: str + """TTM dividend per share""" + dividend_yield_ttm: str + """TTM dividend yield""" + payouts: str + """Number of payouts""" + currency: str + """Currency""" + items: List[USDividendItem] + """Historical dividend records""" + + +class USETFFile: + """A single ETF document entry.""" + + name: str + """Document name""" + file_type: str + """File type (e.g. ``"pdf"``)""" + url: str + """Download URL""" + + +class USETFFilesResponse: + """US ETF document list. Returned by FundamentalContext.us_etf_files.""" + + files: List[USETFFile] + """Document entries""" From 4f6d4fe0a86af1c151629a33cc2d5845d28e7499 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=A2=81=E7=AB=A0=E6=B4=AA?= Date: Thu, 2 Jul 2026 17:56:28 +0800 Subject: [PATCH 12/44] fix(ci): remove stray .claude worktree, add to .gitignore; apply rustfmt; add Node.js TypeScript types - Remove .claude/worktrees/agent-a962fdff6e0c7fa74 from git index (submodule reference was causing CI checkout failure) - Add .claude/ to .gitignore to prevent recurrence - Run cargo +nightly fmt across all crates (rust/python/nodejs/java) - Add US type definitions and method signatures to nodejs/index.d.ts: USCompanyOverview, USValuationOverview, USFinancialStatement, USETFDividendInfo, USCompanyDividends, USETFFilesResponse, USCryptoOverview, USAssetOverview, USRealizedPL, USBuyPower, USStockPosition, USOptionPosition, USCryptoPosition, USDividendItem + usCompanyOverview/usValuationOverview/usFinancialOverview/ usFinancialStatementV3/usKeyFinancialMetrics/usAnalystConsensus/ usEtfDividendInfo/usCompanyDividends/usEtfFiles on FundamentalContext + usCryptoOverview on QuoteContext + usQueryOrders/usOrderDetail/usAssetOverview/usRealizedPl on TradeContext Co-Authored-By: Claude Sonnet 4.6 (1M context) --- .claude/worktrees/agent-a962fdff6e0c7fa74 | 1 - .gitignore | 1 + java/src/fundamental_context.rs | 50 ++++-- java/src/trade_context.rs | 3 +- nodejs/index.d.ts | 192 ++++++++++++++++++++++ nodejs/src/fundamental/context.rs | 85 ++++++++-- nodejs/src/fundamental/types.rs | 53 ++++-- nodejs/src/trade/context.rs | 52 ++++-- nodejs/src/trade/types.rs | 126 +++++++++----- python/src/fundamental/context.rs | 77 +++++++-- python/src/fundamental/context_async.rs | 119 +++++++++++--- python/src/fundamental/types.rs | 19 ++- python/src/quote/context.rs | 5 +- python/src/quote/context_async.rs | 3 +- python/src/trade/context.rs | 23 ++- python/src/trade/context_async.rs | 59 +++++-- python/src/trade/types.rs | 126 +++++++++----- rust/src/blocking/fundamental.rs | 21 ++- rust/src/blocking/quote.rs | 3 +- rust/src/blocking/trade.rs | 6 +- rust/src/fundamental/context.rs | 46 ++++-- rust/src/fundamental/types.rs | 6 +- rust/src/lib.rs | 23 ++- rust/src/quote/context.rs | 7 +- rust/src/quote/mod.rs | 2 +- rust/src/quote/types.rs | 3 +- rust/src/trade/context.rs | 9 +- rust/src/trade/mod.rs | 49 +++++- rust/src/trade/types.rs | 165 ++++++++++++------- 29 files changed, 1032 insertions(+), 302 deletions(-) delete mode 160000 .claude/worktrees/agent-a962fdff6e0c7fa74 diff --git a/.claude/worktrees/agent-a962fdff6e0c7fa74 b/.claude/worktrees/agent-a962fdff6e0c7fa74 deleted file mode 160000 index 4e7e056927..0000000000 --- a/.claude/worktrees/agent-a962fdff6e0c7fa74 +++ /dev/null @@ -1 +0,0 @@ -Subproject commit 4e7e0569270443e306621e9a97d98059fa59a9ec diff --git a/.gitignore b/.gitignore index 6d0f7e95d5..ca1b0defd8 100644 --- a/.gitignore +++ b/.gitignore @@ -3,6 +3,7 @@ .vscode *.log cmake.build +.claude/ build # Eclipse IDE diff --git a/java/src/fundamental_context.rs b/java/src/fundamental_context.rs index 162370d60d..7215d411f3 100644 --- a/java/src/fundamental_context.rs +++ b/java/src/fundamental_context.rs @@ -331,8 +331,9 @@ pub unsafe extern "system" fn Java_com_longbridge_SdkNative_fundamentalContextMa }) } -// ── US-market JNI stubs ─────────────────────────────────────────────────────── -// All US APIs return JSON strings; Java callers parse with Gson/Jackson. +// ── US-market JNI stubs +// ─────────────────────────────────────────────────────── All US APIs return +// JSON strings; Java callers parse with Gson/Jackson. macro_rules! us_counter_id_method { ($jni_name:ident, $method:ident) => { @@ -382,13 +383,34 @@ macro_rules! us_counter_id_report_method { }; } -us_counter_id_method!(Java_com_longbridge_SdkNative_fundamentalContextUsCompanyOverview, us_company_overview); -us_counter_id_method!(Java_com_longbridge_SdkNative_fundamentalContextUsValuationOverview, us_valuation_overview); -us_counter_id_report_method!(Java_com_longbridge_SdkNative_fundamentalContextUsFinancialOverview, us_financial_overview); -us_counter_id_report_method!(Java_com_longbridge_SdkNative_fundamentalContextUsKeyFinancialMetrics, us_key_financial_metrics); -us_counter_id_report_method!(Java_com_longbridge_SdkNative_fundamentalContextUsAnalystConsensus, us_analyst_consensus); -us_counter_id_method!(Java_com_longbridge_SdkNative_fundamentalContextUsEtfDividendInfo, us_etf_dividend_info); -us_counter_id_method!(Java_com_longbridge_SdkNative_fundamentalContextUsCompanyDividends, us_company_dividends); +us_counter_id_method!( + Java_com_longbridge_SdkNative_fundamentalContextUsCompanyOverview, + us_company_overview +); +us_counter_id_method!( + Java_com_longbridge_SdkNative_fundamentalContextUsValuationOverview, + us_valuation_overview +); +us_counter_id_report_method!( + Java_com_longbridge_SdkNative_fundamentalContextUsFinancialOverview, + us_financial_overview +); +us_counter_id_report_method!( + Java_com_longbridge_SdkNative_fundamentalContextUsKeyFinancialMetrics, + us_key_financial_metrics +); +us_counter_id_report_method!( + Java_com_longbridge_SdkNative_fundamentalContextUsAnalystConsensus, + us_analyst_consensus +); +us_counter_id_method!( + Java_com_longbridge_SdkNative_fundamentalContextUsEtfDividendInfo, + us_etf_dividend_info +); +us_counter_id_method!( + Java_com_longbridge_SdkNative_fundamentalContextUsCompanyDividends, + us_company_dividends +); #[unsafe(no_mangle)] pub unsafe extern "system" fn Java_com_longbridge_SdkNative_fundamentalContextUsFinancialStatementV3( @@ -406,7 +428,10 @@ pub unsafe extern "system" fn Java_com_longbridge_SdkNative_fundamentalContextUs let kind: String = FromJValue::from_jvalue(env, kind.into())?; let report: String = FromJValue::from_jvalue(env, report.into())?; async_util::execute(env, callback, async move { - let resp = context.ctx.us_financial_statement_v3(counter_id, kind, report).await?; + let resp = context + .ctx + .us_financial_statement_v3(counter_id, kind, report) + .await?; Ok(serde_json::to_string(&resp).unwrap_or_default()) })?; Ok(()) @@ -427,7 +452,10 @@ pub unsafe extern "system" fn Java_com_longbridge_SdkNative_fundamentalContextUs let counter_id: String = FromJValue::from_jvalue(env, counter_id.into())?; let size: Option = FromJValue::from_jvalue(env, size.into())?; async_util::execute(env, callback, async move { - let resp = context.ctx.us_etf_files(counter_id, size.map(|s| s as u32)).await?; + let resp = context + .ctx + .us_etf_files(counter_id, size.map(|s| s as u32)) + .await?; Ok(serde_json::to_string(&resp).unwrap_or_default()) })?; Ok(()) diff --git a/java/src/trade_context.rs b/java/src/trade_context.rs index c38b2e3b44..a9c483c7f8 100644 --- a/java/src/trade_context.rs +++ b/java/src/trade_context.rs @@ -650,7 +650,8 @@ pub unsafe extern "system" fn Java_com_longbridge_SdkNative_tradeContextUsQueryO let context = &*(context as *const ContextObj); let account_channel: String = FromJValue::from_jvalue(env, account_channel.into())?; let counter_ids: ObjectArray = FromJValue::from_jvalue(env, counter_ids.into())?; - let security_types: ObjectArray = FromJValue::from_jvalue(env, security_types.into())?; + let security_types: ObjectArray = + FromJValue::from_jvalue(env, security_types.into())?; let us_opts = QueryUSOrdersOptions { account_channel, action: action as i32, diff --git a/nodejs/index.d.ts b/nodejs/index.d.ts index 4fcaab997c..af14edb458 100644 --- a/nodejs/index.d.ts +++ b/nodejs/index.d.ts @@ -622,6 +622,24 @@ export declare class FundamentalContext { macroeconomicIndicators(country?: MacroeconomicCountry | undefined | null, keyword?: string | undefined | null, offset?: number | undefined | null, limit?: number | undefined | null): Promise /** Get historical data for a macroeconomic indicator */ macroeconomic(indicatorCode: string, startDate?: string | undefined | null, endDate?: string | undefined | null, offset?: number | undefined | null, limit?: number | undefined | null): Promise + /** Get US company overview. US token required. counterID format: "ST/US/AAPL" */ + usCompanyOverview(counterId: string): Promise + /** Get US valuation snapshot (PE/PB/PS). US token required. */ + usValuationOverview(counterId: string): Promise + /** Get US financial overview (revenue/net income/EPS). Returns JSON string. US token required. */ + usFinancialOverview(counterId: string, report: string): Promise + /** Get US financial statement (IS/BS/CF). kind: "IS" | "BS" | "CF". US token required. */ + usFinancialStatementV3(counterId: string, kind: string, report: string): Promise + /** Get US key financial metrics (ROE/margins). Returns JSON string. US token required. */ + usKeyFinancialMetrics(counterId: string, report: string): Promise + /** Get US analyst consensus estimates. Returns JSON string. US token required. */ + usAnalystConsensus(counterId: string, report: string): Promise + /** Get US ETF dividend history. US token required. */ + usEtfDividendInfo(counterId: string): Promise + /** Get US company historical dividends. US token required. */ + usCompanyDividends(counterId: string): Promise + /** Get US ETF document list. size=null returns all. US token required. */ + usEtfFiles(counterId: string, size?: number | undefined | null): Promise } /** Fund position */ @@ -2030,6 +2048,8 @@ export declare class QuoteContext { optionVolume(symbol: string): Promise /** Get daily historical option volume */ optionVolumeDaily(symbol: string, timestamp: number, count: number): Promise + /** Get US cryptocurrency market overview. counterID format: "CY/US/BTC". US token required. */ + usCryptoOverview(counterId: string): Promise } export declare class QuotePackageDetail { @@ -2800,6 +2820,14 @@ export declare class TradeContext { * ``` */ estimateMaxPurchaseQuantity(opts: EstimateMaxPurchaseQuantityOptions): Promise + /** Query US order list (paginated). Returns JSON string. US token required. */ + usQueryOrders(accountChannel: string, action: number, startAt: number, endAt: number, counterIds: string[], securityTypes: string[], queryType: number, page: number, limit: number, queryVersion: number): Promise + /** Get US order detail. isAttached=true includes take-profit/stop-loss sub-orders. Returns JSON string. US token required. */ + usOrderDetail(orderId: string, isAttached: boolean): Promise + /** Get US account asset overview (stocks/options/crypto/buy power). US token required. */ + usAssetOverview(): Promise + /** Get US realized P&L. category: "ALL"|"STOCK"|"OPTION"|"CRYPTO". US token required. */ + usRealizedPl(currency: string, category?: string | undefined | null): Promise } /** The information of trading session */ @@ -6059,3 +6087,167 @@ export declare const enum WarrantType { /** Inline */ Inline = 5 } + +// ── US-market types ──────────────────────────────────────────────────────── + +export interface USRankTag { + name: string + chg: string + rankType: string +} + +export interface USCompanyOverview { + intro: string + marketCap: string + ccySymbol: string + topRankTags: Array + detailUrl: string +} + +export interface USValuationIndicator { + circle: string + part: string + metric: string + metricType: string + desc: string + ccySymbol: string +} + +export interface USValuationOverview { + indicator: string + currentIndicator: USValuationIndicator + range: number + date: string + aiSummary: string +} + +export interface USFinancialStatement { + revenue: string + netIncome: string + netMargin: string + periods: Array + currency: string +} + +export interface USETFDividendInfo { + dividendTtm: string + dividendYieldTtm: string + dividendFrequency: string + currency: string + fiscalYearInfo: Array +} + +export interface USDividendItem { + dividend: string + dividendType: string + exDate: string + paymentDate: string + recordDate: string +} + +export interface USCompanyDividends { + dividendTtm: string + dividendYieldTtm: string + payouts: string + currency: string + items: Array +} + +export interface USETFFile { + name: string + fileType: string + url: string +} + +export interface USETFFilesResponse { + files: Array +} + +export interface USCryptoOverview { + name: string + ticker: string + currency: string + allTimeHigh: string + allTimeHighDate: string + allTimeLow: string + allTimeLowDate: string + ipoDate: string + issuePrice: string + shares: string + officialWebAddress: string + profile: unknown +} + +export interface USStockPosition { + symbol: string + name: string + quantity: string + availableQuantity: string + currency: string + costPrice: string + marketValue: string + unrealizedPl: string + unrealizedPlRatio: string + lastDone: string + prevClose: string + changeRate: string + nightLastDone: string + pretradeClose: string + tradeStatus: string + individualQuantity: string +} + +export interface USOptionPosition { + symbol: string + strikePrice: string + dueDate: string + contractMultiplier: number + optionType: string + quantity: string + marketValue: string + unrealizedPl: string +} + +export interface USCryptoPosition { + symbol: string + quantity: string + marketValue: string + unrealizedPl: string + costPrice: string +} + +export interface USBuyPower { + cashBuyPower: string + overnightBuyPower: string + dayTradeBuyPower: string + optionBuyPower: string + cryptoBuyPower: string +} + +export interface USAssetOverview { + accountType: string + netAssets: string + totalCash: string + unrealizedPl: string + positions: Array + optionPositions: Array + multiLegs: Array + cryptoPositions: Array + buyPower: USBuyPower +} + +export interface USRealizedPLItem { + symbol: string + name: string + category: string + realizedPl: string + quantitySold: string + avgCost: string + avgSellPrice: string +} + +export interface USRealizedPL { + totalRealizedPl: string + currency: string + items: Array +} diff --git a/nodejs/src/fundamental/context.rs b/nodejs/src/fundamental/context.rs index 112eb0a828..abf42ec218 100644 --- a/nodejs/src/fundamental/context.rs +++ b/nodejs/src/fundamental/context.rs @@ -328,57 +328,116 @@ impl FundamentalContext { /// Get US company overview. US token required. #[napi] pub async fn us_company_overview(&self, counter_id: String) -> Result { - Ok(self.ctx.us_company_overview(counter_id).await.map_err(ErrorNewType)?.into()) + Ok(self + .ctx + .us_company_overview(counter_id) + .await + .map_err(ErrorNewType)? + .into()) } /// Get US valuation overview. US token required. #[napi] pub async fn us_valuation_overview(&self, counter_id: String) -> Result { - Ok(self.ctx.us_valuation_overview(counter_id).await.map_err(ErrorNewType)?.into()) + Ok(self + .ctx + .us_valuation_overview(counter_id) + .await + .map_err(ErrorNewType)? + .into()) } /// Get US financial overview (JSON string). US token required. #[napi] - pub async fn us_financial_overview(&self, counter_id: String, report: String) -> Result { - let v = self.ctx.us_financial_overview(counter_id, report).await.map_err(ErrorNewType)?; + pub async fn us_financial_overview( + &self, + counter_id: String, + report: String, + ) -> Result { + let v = self + .ctx + .us_financial_overview(counter_id, report) + .await + .map_err(ErrorNewType)?; serde_json::to_string(&v).map_err(|e| napi::Error::from_reason(e.to_string())) } /// Get US financial statement v3. kind: "IS"/"BS"/"CF". US token required. #[napi] - pub async fn us_financial_statement_v3(&self, counter_id: String, kind: String, report: String) -> Result { - Ok(self.ctx.us_financial_statement_v3(counter_id, kind, report).await.map_err(ErrorNewType)?.into()) + pub async fn us_financial_statement_v3( + &self, + counter_id: String, + kind: String, + report: String, + ) -> Result { + Ok(self + .ctx + .us_financial_statement_v3(counter_id, kind, report) + .await + .map_err(ErrorNewType)? + .into()) } /// Get US key financial metrics (JSON string). US token required. #[napi] - pub async fn us_key_financial_metrics(&self, counter_id: String, report: String) -> Result { - let v = self.ctx.us_key_financial_metrics(counter_id, report).await.map_err(ErrorNewType)?; + pub async fn us_key_financial_metrics( + &self, + counter_id: String, + report: String, + ) -> Result { + let v = self + .ctx + .us_key_financial_metrics(counter_id, report) + .await + .map_err(ErrorNewType)?; serde_json::to_string(&v).map_err(|e| napi::Error::from_reason(e.to_string())) } /// Get US analyst consensus (JSON string). US token required. #[napi] pub async fn us_analyst_consensus(&self, counter_id: String, report: String) -> Result { - let v = self.ctx.us_analyst_consensus(counter_id, report).await.map_err(ErrorNewType)?; + let v = self + .ctx + .us_analyst_consensus(counter_id, report) + .await + .map_err(ErrorNewType)?; serde_json::to_string(&v).map_err(|e| napi::Error::from_reason(e.to_string())) } /// Get US ETF dividend history. US token required. #[napi] pub async fn us_etf_dividend_info(&self, counter_id: String) -> Result { - Ok(self.ctx.us_etf_dividend_info(counter_id).await.map_err(ErrorNewType)?.into()) + Ok(self + .ctx + .us_etf_dividend_info(counter_id) + .await + .map_err(ErrorNewType)? + .into()) } /// Get US company dividends. US token required. #[napi] pub async fn us_company_dividends(&self, counter_id: String) -> Result { - Ok(self.ctx.us_company_dividends(counter_id).await.map_err(ErrorNewType)?.into()) + Ok(self + .ctx + .us_company_dividends(counter_id) + .await + .map_err(ErrorNewType)? + .into()) } /// Get US ETF document list. size=None returns all. US token required. #[napi] - pub async fn us_etf_files(&self, counter_id: String, size: Option) -> Result { - Ok(self.ctx.us_etf_files(counter_id, size).await.map_err(ErrorNewType)?.into()) + pub async fn us_etf_files( + &self, + counter_id: String, + size: Option, + ) -> Result { + Ok(self + .ctx + .us_etf_files(counter_id, size) + .await + .map_err(ErrorNewType)? + .into()) } } diff --git a/nodejs/src/fundamental/types.rs b/nodejs/src/fundamental/types.rs index aa203792b3..9a61e71a3d 100644 --- a/nodejs/src/fundamental/types.rs +++ b/nodejs/src/fundamental/types.rs @@ -2058,7 +2058,8 @@ impl From for MacroeconomicResponse { } } -// ── US-market types ─────────────────────────────────────────────────────────── +// ── US-market types +// ─────────────────────────────────────────────────────────── use longbridge::fundamental::types as lb_us; @@ -2073,7 +2074,11 @@ pub struct USRankTag { impl From for USRankTag { fn from(v: lb_us::USRankTag) -> Self { - Self { name: v.name, chg: v.chg, rank_type: v.rank_type } + Self { + name: v.name, + chg: v.chg, + rank_type: v.rank_type, + } } } @@ -2115,8 +2120,12 @@ pub struct USValuationIndicator { impl From for USValuationIndicator { fn from(v: lb_us::USValuationIndicator) -> Self { Self { - circle: v.circle, part: v.part, metric: v.metric, - metric_type: v.metric_type, desc: v.desc, ccy_symbol: v.ccy_symbol, + circle: v.circle, + part: v.part, + metric: v.metric, + metric_type: v.metric_type, + desc: v.desc, + ccy_symbol: v.ccy_symbol, } } } @@ -2158,8 +2167,11 @@ pub struct USFinancialStatement { impl From for USFinancialStatement { fn from(v: lb_us::USFinancialStatement) -> Self { Self { - revenue: v.revenue, net_income: v.net_income, - net_margin: v.net_margin, periods: v.periods, currency: v.currency, + revenue: v.revenue, + net_income: v.net_income, + net_margin: v.net_margin, + periods: v.periods, + currency: v.currency, } } } @@ -2178,8 +2190,10 @@ pub struct USETFDividendInfo { impl From for USETFDividendInfo { fn from(v: lb_us::USETFDividendInfo) -> Self { Self { - dividend_ttm: v.dividend_ttm, dividend_yield_ttm: v.dividend_yield_ttm, - dividend_frequency: v.dividend_frequency, currency: v.currency, + dividend_ttm: v.dividend_ttm, + dividend_yield_ttm: v.dividend_yield_ttm, + dividend_frequency: v.dividend_frequency, + currency: v.currency, fiscal_year_info: v.fiscal_year_info, } } @@ -2199,8 +2213,11 @@ pub struct USDividendItem { impl From for USDividendItem { fn from(v: lb_us::USDividendItem) -> Self { Self { - dividend: v.dividend, dividend_type: v.dividend_type, - ex_date: v.ex_date, payment_date: v.payment_date, record_date: v.record_date, + dividend: v.dividend, + dividend_type: v.dividend_type, + ex_date: v.ex_date, + payment_date: v.payment_date, + record_date: v.record_date, } } } @@ -2219,8 +2236,10 @@ pub struct USCompanyDividends { impl From for USCompanyDividends { fn from(v: lb_us::USCompanyDividends) -> Self { Self { - dividend_ttm: v.dividend_ttm, dividend_yield_ttm: v.dividend_yield_ttm, - payouts: v.payouts, currency: v.currency, + dividend_ttm: v.dividend_ttm, + dividend_yield_ttm: v.dividend_yield_ttm, + payouts: v.payouts, + currency: v.currency, items: v.items.into_iter().map(Into::into).collect(), } } @@ -2237,7 +2256,11 @@ pub struct USETFFile { impl From for USETFFile { fn from(v: lb_us::USETFFile) -> Self { - Self { name: v.name, file_type: v.file_type, url: v.url } + Self { + name: v.name, + file_type: v.file_type, + url: v.url, + } } } @@ -2250,6 +2273,8 @@ pub struct USETFFilesResponse { impl From for USETFFilesResponse { fn from(v: lb_us::USETFFilesResponse) -> Self { - Self { files: v.files.into_iter().map(Into::into).collect() } + Self { + files: v.files.into_iter().map(Into::into).collect(), + } } } diff --git a/nodejs/src/trade/context.rs b/nodejs/src/trade/context.rs index 960d224892..2171200c34 100644 --- a/nodejs/src/trade/context.rs +++ b/nodejs/src/trade/context.rs @@ -1,6 +1,8 @@ use std::sync::Arc; -use longbridge::trade::{GetFundPositionsOptions, GetStockPositionsOptions, PushEvent, QueryUSOrdersOptions}; +use longbridge::trade::{ + GetFundPositionsOptions, GetStockPositionsOptions, PushEvent, QueryUSOrdersOptions, +}; use napi::{Result, bindgen_prelude::*, threadsafe_function::ThreadsafeFunctionCallMode}; use parking_lot::Mutex; @@ -509,15 +511,29 @@ impl TradeContext { pub fn us_query_orders<'env>( &self, env: &'env Env, - account_channel: String, action: i32, - start_at: f64, end_at: f64, - counter_ids: Vec, security_types: Vec, - query_type: i32, page: i32, limit: i32, query_version: f64, + account_channel: String, + action: i32, + start_at: f64, + end_at: f64, + counter_ids: Vec, + security_types: Vec, + query_type: i32, + page: i32, + limit: i32, + query_version: f64, ) -> Result> { let ctx = self.ctx.clone(); let opts = QueryUSOrdersOptions { - account_channel, action, start_at, end_at, - counter_ids, security_types, query_type, page, limit, query_version, + account_channel, + action, + start_at, + end_at, + counter_ids, + security_types, + query_type, + page, + limit, + query_version, }; env.spawn_future(async move { let resp = ctx.us_query_orders(opts).await.map_err(ErrorNewType)?; @@ -535,18 +551,24 @@ impl TradeContext { ) -> Result> { let ctx = self.ctx.clone(); env.spawn_future(async move { - let resp = ctx.us_order_detail(order_id, is_attached).await.map_err(ErrorNewType)?; + let resp = ctx + .us_order_detail(order_id, is_attached) + .await + .map_err(ErrorNewType)?; serde_json::to_string(&resp).map_err(|e| napi::Error::from_reason(e.to_string())) }) } /// Get US account asset overview. US token required. #[napi] - pub fn us_asset_overview<'env>(&self, env: &'env Env) -> Result> { + pub fn us_asset_overview<'env>( + &self, + env: &'env Env, + ) -> Result> { let ctx = self.ctx.clone(); - env.spawn_future(async move { - Ok(ctx.us_asset_overview().await.map_err(ErrorNewType)?.into()) - }) + env.spawn_future( + async move { Ok(ctx.us_asset_overview().await.map_err(ErrorNewType)?.into()) }, + ) } /// Get US realized P&L. US token required. @@ -559,7 +581,11 @@ impl TradeContext { ) -> Result> { let ctx = self.ctx.clone(); env.spawn_future(async move { - Ok(ctx.us_realized_pl(currency, category).await.map_err(ErrorNewType)?.into()) + Ok(ctx + .us_realized_pl(currency, category) + .await + .map_err(ErrorNewType)? + .into()) }) } diff --git a/nodejs/src/trade/types.rs b/nodejs/src/trade/types.rs index a4b9d56016..0d8b6deb8a 100644 --- a/nodejs/src/trade/types.rs +++ b/nodejs/src/trade/types.rs @@ -802,25 +802,43 @@ pub struct EstimateMaxPurchaseQuantityResponse { #[napi_derive::napi(object)] #[derive(Debug, Clone)] pub struct USStockPosition { - pub symbol: String, pub name: String, pub quantity: String, - pub available_quantity: String, pub currency: String, - pub cost_price: String, pub market_value: String, - pub unrealized_pl: String, pub unrealized_pl_ratio: String, - pub last_done: String, pub prev_close: String, pub change_rate: String, - pub night_last_done: String, pub pretrade_close: String, - pub trade_status: String, pub individual_quantity: String, + pub symbol: String, + pub name: String, + pub quantity: String, + pub available_quantity: String, + pub currency: String, + pub cost_price: String, + pub market_value: String, + pub unrealized_pl: String, + pub unrealized_pl_ratio: String, + pub last_done: String, + pub prev_close: String, + pub change_rate: String, + pub night_last_done: String, + pub pretrade_close: String, + pub trade_status: String, + pub individual_quantity: String, } impl From for USStockPosition { fn from(v: longbridge::trade::USStockPosition) -> Self { Self { - symbol: v.symbol, name: v.name, quantity: v.quantity, - available_quantity: v.available_quantity, currency: v.currency, - cost_price: v.cost_price, market_value: v.market_value, - unrealized_pl: v.unrealized_pl, unrealized_pl_ratio: v.unrealized_pl_ratio, - last_done: v.last_done, prev_close: v.prev_close, change_rate: v.change_rate, - night_last_done: v.night_last_done, pretrade_close: v.pretrade_close, - trade_status: v.trade_status, individual_quantity: v.individual_quantity, + symbol: v.symbol, + name: v.name, + quantity: v.quantity, + available_quantity: v.available_quantity, + currency: v.currency, + cost_price: v.cost_price, + market_value: v.market_value, + unrealized_pl: v.unrealized_pl, + unrealized_pl_ratio: v.unrealized_pl_ratio, + last_done: v.last_done, + prev_close: v.prev_close, + change_rate: v.change_rate, + night_last_done: v.night_last_done, + pretrade_close: v.pretrade_close, + trade_status: v.trade_status, + individual_quantity: v.individual_quantity, } } } @@ -829,17 +847,27 @@ impl From for USStockPosition { #[napi_derive::napi(object)] #[derive(Debug, Clone)] pub struct USOptionPosition { - pub symbol: String, pub strike_price: String, pub due_date: String, - pub contract_multiplier: i32, pub option_type: String, - pub quantity: String, pub market_value: String, pub unrealized_pl: String, + pub symbol: String, + pub strike_price: String, + pub due_date: String, + pub contract_multiplier: i32, + pub option_type: String, + pub quantity: String, + pub market_value: String, + pub unrealized_pl: String, } impl From for USOptionPosition { fn from(v: longbridge::trade::USOptionPosition) -> Self { Self { - symbol: v.symbol, strike_price: v.strike_price, due_date: v.due_date, - contract_multiplier: v.contract_multiplier, option_type: v.option_type, - quantity: v.quantity, market_value: v.market_value, unrealized_pl: v.unrealized_pl, + symbol: v.symbol, + strike_price: v.strike_price, + due_date: v.due_date, + contract_multiplier: v.contract_multiplier, + option_type: v.option_type, + quantity: v.quantity, + market_value: v.market_value, + unrealized_pl: v.unrealized_pl, } } } @@ -848,15 +876,21 @@ impl From for USOptionPosition { #[napi_derive::napi(object)] #[derive(Debug, Clone)] pub struct USCryptoPosition { - pub symbol: String, pub quantity: String, pub market_value: String, - pub unrealized_pl: String, pub cost_price: String, + pub symbol: String, + pub quantity: String, + pub market_value: String, + pub unrealized_pl: String, + pub cost_price: String, } impl From for USCryptoPosition { fn from(v: longbridge::trade::USCryptoPosition) -> Self { Self { - symbol: v.symbol, quantity: v.quantity, market_value: v.market_value, - unrealized_pl: v.unrealized_pl, cost_price: v.cost_price, + symbol: v.symbol, + quantity: v.quantity, + market_value: v.market_value, + unrealized_pl: v.unrealized_pl, + cost_price: v.cost_price, } } } @@ -865,16 +899,20 @@ impl From for USCryptoPosition { #[napi_derive::napi(object)] #[derive(Debug, Clone, Default)] pub struct USBuyPower { - pub cash_buy_power: String, pub overnight_buy_power: String, - pub day_trade_buy_power: String, pub option_buy_power: String, + pub cash_buy_power: String, + pub overnight_buy_power: String, + pub day_trade_buy_power: String, + pub option_buy_power: String, pub crypto_buy_power: String, } impl From for USBuyPower { fn from(v: longbridge::trade::USBuyPower) -> Self { Self { - cash_buy_power: v.cash_buy_power, overnight_buy_power: v.overnight_buy_power, - day_trade_buy_power: v.day_trade_buy_power, option_buy_power: v.option_buy_power, + cash_buy_power: v.cash_buy_power, + overnight_buy_power: v.overnight_buy_power, + day_trade_buy_power: v.day_trade_buy_power, + option_buy_power: v.option_buy_power, crypto_buy_power: v.crypto_buy_power, } } @@ -884,8 +922,10 @@ impl From for USBuyPower { #[napi_derive::napi(object)] #[derive(Debug, Clone)] pub struct USAssetOverview { - pub account_type: String, pub net_assets: String, - pub total_cash: String, pub unrealized_pl: String, + pub account_type: String, + pub net_assets: String, + pub total_cash: String, + pub unrealized_pl: String, pub positions: Vec, pub option_positions: Vec, /// Multi-leg strategies as JSON string @@ -897,8 +937,10 @@ pub struct USAssetOverview { impl From for USAssetOverview { fn from(v: longbridge::trade::USAssetOverview) -> Self { Self { - account_type: v.account_type, net_assets: v.net_assets, - total_cash: v.total_cash, unrealized_pl: v.unrealized_pl, + account_type: v.account_type, + net_assets: v.net_assets, + total_cash: v.total_cash, + unrealized_pl: v.unrealized_pl, positions: v.positions.into_iter().map(Into::into).collect(), option_positions: v.option_positions.into_iter().map(Into::into).collect(), multi_legs: serde_json::to_string(&v.multi_legs).unwrap_or_default(), @@ -912,17 +954,25 @@ impl From for USAssetOverview { #[napi_derive::napi(object)] #[derive(Debug, Clone)] pub struct USRealizedPLItem { - pub symbol: String, pub name: String, pub category: String, - pub realized_pl: String, pub quantity_sold: String, - pub avg_cost: String, pub avg_sell_price: String, + pub symbol: String, + pub name: String, + pub category: String, + pub realized_pl: String, + pub quantity_sold: String, + pub avg_cost: String, + pub avg_sell_price: String, } impl From for USRealizedPLItem { fn from(v: longbridge::trade::USRealizedPLItem) -> Self { Self { - symbol: v.symbol, name: v.name, category: v.category, - realized_pl: v.realized_pl, quantity_sold: v.quantity_sold, - avg_cost: v.avg_cost, avg_sell_price: v.avg_sell_price, + symbol: v.symbol, + name: v.name, + category: v.category, + realized_pl: v.realized_pl, + quantity_sold: v.quantity_sold, + avg_cost: v.avg_cost, + avg_sell_price: v.avg_sell_price, } } } diff --git a/python/src/fundamental/context.rs b/python/src/fundamental/context.rs index b4fc55b17b..66bc19776f 100644 --- a/python/src/fundamental/context.rs +++ b/python/src/fundamental/context.rs @@ -2,12 +2,11 @@ use std::sync::Arc; use longbridge::blocking::FundamentalContextSync; use pyo3::prelude::*; - -use crate::{config::Config, error::ErrorNewType, fundamental::types::*}; - #[allow(unused_imports)] use pythonize; +use crate::{config::Config, error::ErrorNewType, fundamental::types::*}; + /// Fundamental data context (synchronous). #[pyclass] pub(crate) struct FundamentalContext { @@ -247,23 +246,41 @@ impl FundamentalContext { /// Get US company overview. US token required. fn us_company_overview(&self, counter_id: String) -> PyResult { - Ok(self.ctx.us_company_overview(counter_id).map_err(ErrorNewType)?.into()) + Ok(self + .ctx + .us_company_overview(counter_id) + .map_err(ErrorNewType)? + .into()) } /// Get US valuation overview. US token required. fn us_valuation_overview(&self, counter_id: String) -> PyResult { - Ok(self.ctx.us_valuation_overview(counter_id).map_err(ErrorNewType)?.into()) + Ok(self + .ctx + .us_valuation_overview(counter_id) + .map_err(ErrorNewType)? + .into()) } - /// Get US financial overview. `report`: "annual" or "quarterly". US token required. - fn us_financial_overview(&self, py: Python<'_>, counter_id: String, report: String) -> PyResult> { - let v = self.ctx.us_financial_overview(counter_id, report).map_err(ErrorNewType)?; + /// Get US financial overview. `report`: "annual" or "quarterly". US token + /// required. + fn us_financial_overview( + &self, + py: Python<'_>, + counter_id: String, + report: String, + ) -> PyResult> { + let v = self + .ctx + .us_financial_overview(counter_id, report) + .map_err(ErrorNewType)?; pythonize::pythonize(py, &v) .map(|b| b.unbind()) .map_err(|e| pyo3::exceptions::PyRuntimeError::new_err(e.to_string())) } - /// Get US financial statement v3. `kind`: "IS"/"BS"/"CF". US token required. + /// Get US financial statement v3. `kind`: "IS"/"BS"/"CF". US token + /// required. fn us_financial_statement_v3( &self, counter_id: String, @@ -278,16 +295,32 @@ impl FundamentalContext { } /// Get US key financial metrics. US token required. - fn us_key_financial_metrics(&self, py: Python<'_>, counter_id: String, report: String) -> PyResult> { - let v = self.ctx.us_key_financial_metrics(counter_id, report).map_err(ErrorNewType)?; + fn us_key_financial_metrics( + &self, + py: Python<'_>, + counter_id: String, + report: String, + ) -> PyResult> { + let v = self + .ctx + .us_key_financial_metrics(counter_id, report) + .map_err(ErrorNewType)?; pythonize::pythonize(py, &v) .map(|b| b.unbind()) .map_err(|e| pyo3::exceptions::PyRuntimeError::new_err(e.to_string())) } /// Get US analyst consensus estimates. US token required. - fn us_analyst_consensus(&self, py: Python<'_>, counter_id: String, report: String) -> PyResult> { - let v = self.ctx.us_analyst_consensus(counter_id, report).map_err(ErrorNewType)?; + fn us_analyst_consensus( + &self, + py: Python<'_>, + counter_id: String, + report: String, + ) -> PyResult> { + let v = self + .ctx + .us_analyst_consensus(counter_id, report) + .map_err(ErrorNewType)?; pythonize::pythonize(py, &v) .map(|b| b.unbind()) .map_err(|e| pyo3::exceptions::PyRuntimeError::new_err(e.to_string())) @@ -295,16 +328,28 @@ impl FundamentalContext { /// Get US ETF dividend history. US token required. fn us_etf_dividend_info(&self, counter_id: String) -> PyResult { - Ok(self.ctx.us_etf_dividend_info(counter_id).map_err(ErrorNewType)?.into()) + Ok(self + .ctx + .us_etf_dividend_info(counter_id) + .map_err(ErrorNewType)? + .into()) } /// Get US company historical dividends. US token required. fn us_company_dividends(&self, counter_id: String) -> PyResult { - Ok(self.ctx.us_company_dividends(counter_id).map_err(ErrorNewType)?.into()) + Ok(self + .ctx + .us_company_dividends(counter_id) + .map_err(ErrorNewType)? + .into()) } /// Get US ETF document list. `size`: None = all. US token required. fn us_etf_files(&self, counter_id: String, size: Option) -> PyResult { - Ok(self.ctx.us_etf_files(counter_id, size).map_err(ErrorNewType)?.into()) + Ok(self + .ctx + .us_etf_files(counter_id, size) + .map_err(ErrorNewType)? + .into()) } } diff --git a/python/src/fundamental/context_async.rs b/python/src/fundamental/context_async.rs index 55819f7c80..5d0367c37b 100644 --- a/python/src/fundamental/context_async.rs +++ b/python/src/fundamental/context_async.rs @@ -2,13 +2,12 @@ use std::sync::Arc; use longbridge::FundamentalContext; use pyo3::{prelude::*, types::PyType}; - -use crate::{config::Config, error::ErrorNewType, fundamental::types::*}; - // needed for us_financial_overview / us_key_financial_metrics / us_analyst_consensus #[allow(unused_imports)] use pythonize; +use crate::{config::Config, error::ErrorNewType, fundamental::types::*}; + /// Fundamental data context (async). #[pyclass] pub(crate) struct AsyncFundamentalContext { @@ -369,86 +368,154 @@ impl AsyncFundamentalContext { fn us_company_overview(&self, py: Python<'_>, counter_id: String) -> PyResult> { let ctx = self.ctx.clone(); pyo3_async_runtimes::tokio::future_into_py(py, async move { - Ok(USCompanyOverview::from(ctx.us_company_overview(counter_id).await.map_err(ErrorNewType)?)) - }).map(|b| b.unbind()) + Ok(USCompanyOverview::from( + ctx.us_company_overview(counter_id) + .await + .map_err(ErrorNewType)?, + )) + }) + .map(|b| b.unbind()) } /// Get US valuation overview. US token required. Returns awaitable. fn us_valuation_overview(&self, py: Python<'_>, counter_id: String) -> PyResult> { let ctx = self.ctx.clone(); pyo3_async_runtimes::tokio::future_into_py(py, async move { - Ok(USValuationOverview::from(ctx.us_valuation_overview(counter_id).await.map_err(ErrorNewType)?)) - }).map(|b| b.unbind()) + Ok(USValuationOverview::from( + ctx.us_valuation_overview(counter_id) + .await + .map_err(ErrorNewType)?, + )) + }) + .map(|b| b.unbind()) } /// Get US financial overview (raw dict). Returns awaitable. - fn us_financial_overview(&self, py: Python<'_>, counter_id: String, report: String) -> PyResult> { + fn us_financial_overview( + &self, + py: Python<'_>, + counter_id: String, + report: String, + ) -> PyResult> { let ctx = self.ctx.clone(); pyo3_async_runtimes::tokio::future_into_py(py, async move { - let v = ctx.us_financial_overview(counter_id, report).await.map_err(ErrorNewType)?; + let v = ctx + .us_financial_overview(counter_id, report) + .await + .map_err(ErrorNewType)?; Python::attach(|py| { pythonize::pythonize(py, &v) .map(|b| b.unbind()) .map_err(|e| pyo3::exceptions::PyRuntimeError::new_err(e.to_string())) }) - }).map(|b| b.unbind()) + }) + .map(|b| b.unbind()) } /// Get US financial statement v3. Returns awaitable. - fn us_financial_statement_v3(&self, py: Python<'_>, counter_id: String, kind: String, report: String) -> PyResult> { + fn us_financial_statement_v3( + &self, + py: Python<'_>, + counter_id: String, + kind: String, + report: String, + ) -> PyResult> { let ctx = self.ctx.clone(); pyo3_async_runtimes::tokio::future_into_py(py, async move { - Ok(USFinancialStatement::from(ctx.us_financial_statement_v3(counter_id, kind, report).await.map_err(ErrorNewType)?)) - }).map(|b| b.unbind()) + Ok(USFinancialStatement::from( + ctx.us_financial_statement_v3(counter_id, kind, report) + .await + .map_err(ErrorNewType)?, + )) + }) + .map(|b| b.unbind()) } /// Get US key financial metrics (raw dict). Returns awaitable. - fn us_key_financial_metrics(&self, py: Python<'_>, counter_id: String, report: String) -> PyResult> { + fn us_key_financial_metrics( + &self, + py: Python<'_>, + counter_id: String, + report: String, + ) -> PyResult> { let ctx = self.ctx.clone(); pyo3_async_runtimes::tokio::future_into_py(py, async move { - let v = ctx.us_key_financial_metrics(counter_id, report).await.map_err(ErrorNewType)?; + let v = ctx + .us_key_financial_metrics(counter_id, report) + .await + .map_err(ErrorNewType)?; Python::attach(|py| { pythonize::pythonize(py, &v) .map(|b| b.unbind()) .map_err(|e| pyo3::exceptions::PyRuntimeError::new_err(e.to_string())) }) - }).map(|b| b.unbind()) + }) + .map(|b| b.unbind()) } /// Get US analyst consensus (raw dict). Returns awaitable. - fn us_analyst_consensus(&self, py: Python<'_>, counter_id: String, report: String) -> PyResult> { + fn us_analyst_consensus( + &self, + py: Python<'_>, + counter_id: String, + report: String, + ) -> PyResult> { let ctx = self.ctx.clone(); pyo3_async_runtimes::tokio::future_into_py(py, async move { - let v = ctx.us_analyst_consensus(counter_id, report).await.map_err(ErrorNewType)?; + let v = ctx + .us_analyst_consensus(counter_id, report) + .await + .map_err(ErrorNewType)?; Python::attach(|py| { pythonize::pythonize(py, &v) .map(|b| b.unbind()) .map_err(|e| pyo3::exceptions::PyRuntimeError::new_err(e.to_string())) }) - }).map(|b| b.unbind()) + }) + .map(|b| b.unbind()) } /// Get US ETF dividend info. Returns awaitable. fn us_etf_dividend_info(&self, py: Python<'_>, counter_id: String) -> PyResult> { let ctx = self.ctx.clone(); pyo3_async_runtimes::tokio::future_into_py(py, async move { - Ok(USETFDividendInfo::from(ctx.us_etf_dividend_info(counter_id).await.map_err(ErrorNewType)?)) - }).map(|b| b.unbind()) + Ok(USETFDividendInfo::from( + ctx.us_etf_dividend_info(counter_id) + .await + .map_err(ErrorNewType)?, + )) + }) + .map(|b| b.unbind()) } /// Get US company dividends. Returns awaitable. fn us_company_dividends(&self, py: Python<'_>, counter_id: String) -> PyResult> { let ctx = self.ctx.clone(); pyo3_async_runtimes::tokio::future_into_py(py, async move { - Ok(USCompanyDividends::from(ctx.us_company_dividends(counter_id).await.map_err(ErrorNewType)?)) - }).map(|b| b.unbind()) + Ok(USCompanyDividends::from( + ctx.us_company_dividends(counter_id) + .await + .map_err(ErrorNewType)?, + )) + }) + .map(|b| b.unbind()) } /// Get US ETF files. Returns awaitable. - fn us_etf_files(&self, py: Python<'_>, counter_id: String, size: Option) -> PyResult> { + fn us_etf_files( + &self, + py: Python<'_>, + counter_id: String, + size: Option, + ) -> PyResult> { let ctx = self.ctx.clone(); pyo3_async_runtimes::tokio::future_into_py(py, async move { - Ok(USETFFilesResponse::from(ctx.us_etf_files(counter_id, size).await.map_err(ErrorNewType)?)) - }).map(|b| b.unbind()) + Ok(USETFFilesResponse::from( + ctx.us_etf_files(counter_id, size) + .await + .map_err(ErrorNewType)?, + )) + }) + .map(|b| b.unbind()) } } diff --git a/python/src/fundamental/types.rs b/python/src/fundamental/types.rs index 8873bcf019..acb1ecacb8 100644 --- a/python/src/fundamental/types.rs +++ b/python/src/fundamental/types.rs @@ -2112,7 +2112,8 @@ impl From for MacroeconomicResponse { } } -// ── US-market types ─────────────────────────────────────────────────────────── +// ── US-market types +// ─────────────────────────────────────────────────────────── use longbridge::fundamental::types as lb_us; @@ -2127,7 +2128,11 @@ pub(crate) struct USRankTag { impl From for USRankTag { fn from(v: lb_us::USRankTag) -> Self { - Self { name: v.name, chg: v.chg, rank_type: v.rank_type } + Self { + name: v.name, + chg: v.chg, + rank_type: v.rank_type, + } } } @@ -2305,7 +2310,11 @@ pub(crate) struct USETFFile { impl From for USETFFile { fn from(v: lb_us::USETFFile) -> Self { - Self { name: v.name, file_type: v.file_type, url: v.url } + Self { + name: v.name, + file_type: v.file_type, + url: v.url, + } } } @@ -2318,6 +2327,8 @@ pub(crate) struct USETFFilesResponse { impl From for USETFFilesResponse { fn from(v: lb_us::USETFFilesResponse) -> Self { - Self { files: v.files.into_iter().map(Into::into).collect() } + Self { + files: v.files.into_iter().map(Into::into).collect(), + } } } diff --git a/python/src/quote/context.rs b/python/src/quote/context.rs index 028c1276f6..bf785a8a08 100644 --- a/python/src/quote/context.rs +++ b/python/src/quote/context.rs @@ -689,7 +689,10 @@ impl QuoteContext { } /// Get US cryptocurrency market overview. US token required. - fn us_crypto_overview(&self, counter_id: String) -> PyResult { + fn us_crypto_overview( + &self, + counter_id: String, + ) -> PyResult { Ok(self .ctx .us_crypto_overview(counter_id) diff --git a/python/src/quote/context_async.rs b/python/src/quote/context_async.rs index 823f78eaeb..48ad340870 100644 --- a/python/src/quote/context_async.rs +++ b/python/src/quote/context_async.rs @@ -896,7 +896,8 @@ impl AsyncQuoteContext { .map(|b| b.unbind()) } - /// Get US cryptocurrency market overview. US token required. Returns awaitable. + /// Get US cryptocurrency market overview. US token required. Returns + /// awaitable. fn us_crypto_overview(&self, py: Python<'_>, counter_id: String) -> PyResult> { let ctx = self.ctx.clone(); pyo3_async_runtimes::tokio::future_into_py(py, async move { diff --git a/python/src/trade/context.rs b/python/src/trade/context.rs index bcdb9a6366..96ef040dba 100644 --- a/python/src/trade/context.rs +++ b/python/src/trade/context.rs @@ -429,8 +429,16 @@ impl TradeContext { query_version: f64, ) -> PyResult { let opts = QueryUSOrdersOptions { - account_channel, action, start_at, end_at, - counter_ids, security_types, query_type, page, limit, query_version, + account_channel, + action, + start_at, + end_at, + counter_ids, + security_types, + query_type, + page, + limit, + query_version, }; let resp = self.ctx.us_query_orders(opts).map_err(ErrorNewType)?; serde_json::to_string(&resp) @@ -439,7 +447,10 @@ impl TradeContext { /// Get US order detail (returns JSON string). US token required. fn us_order_detail(&self, order_id: String, is_attached: bool) -> PyResult { - let resp = self.ctx.us_order_detail(order_id, is_attached).map_err(ErrorNewType)?; + let resp = self + .ctx + .us_order_detail(order_id, is_attached) + .map_err(ErrorNewType)?; serde_json::to_string(&resp) .map_err(|e| pyo3::exceptions::PyValueError::new_err(e.to_string())) } @@ -455,7 +466,11 @@ impl TradeContext { currency: String, category: Option, ) -> PyResult { - Ok(self.ctx.us_realized_pl(currency, category).map_err(ErrorNewType)?.into()) + Ok(self + .ctx + .us_realized_pl(currency, category) + .map_err(ErrorNewType)? + .into()) } /// Estimating the maximum purchase quantity for Hong Kong and US stocks, diff --git a/python/src/trade/context_async.rs b/python/src/trade/context_async.rs index 73c6968d13..4ba1a79d77 100644 --- a/python/src/trade/context_async.rs +++ b/python/src/trade/context_async.rs @@ -5,8 +5,8 @@ use std::sync::Arc; use longbridge::trade::{ EstimateMaxPurchaseQuantityOptions, GetCashFlowOptions, GetFundPositionsOptions, GetHistoryExecutionsOptions, GetHistoryOrdersOptions, GetStockPositionsOptions, - GetTodayExecutionsOptions, GetTodayOrdersOptions, QueryUSOrdersOptions, - ReplaceOrderOptions, SubmitOrderOptions, TradeContext, + GetTodayExecutionsOptions, GetTodayOrdersOptions, QueryUSOrdersOptions, ReplaceOrderOptions, + SubmitOrderOptions, TradeContext, }; use parking_lot::Mutex; use pyo3::{prelude::*, types::PyType}; @@ -493,16 +493,31 @@ impl AsyncTradeContext { #[pyo3(signature = (account_channel, action, start_at, end_at, counter_ids, security_types, query_type, page, limit, query_version))] #[allow(clippy::too_many_arguments)] fn us_query_orders( - &self, py: Python<'_>, - account_channel: String, action: i32, - start_at: f64, end_at: f64, - counter_ids: Vec, security_types: Vec, - query_type: i32, page: i32, limit: i32, query_version: f64, + &self, + py: Python<'_>, + account_channel: String, + action: i32, + start_at: f64, + end_at: f64, + counter_ids: Vec, + security_types: Vec, + query_type: i32, + page: i32, + limit: i32, + query_version: f64, ) -> PyResult> { let ctx = self.ctx.clone(); let opts = QueryUSOrdersOptions { - account_channel, action, start_at, end_at, - counter_ids, security_types, query_type, page, limit, query_version, + account_channel, + action, + start_at, + end_at, + counter_ids, + security_types, + query_type, + page, + limit, + query_version, }; pyo3_async_runtimes::tokio::future_into_py(py, async move { let resp = ctx.us_query_orders(opts).await.map_err(ErrorNewType)?; @@ -514,10 +529,18 @@ impl AsyncTradeContext { } /// Get US order detail (JSON string). US token required. Returns awaitable. - fn us_order_detail(&self, py: Python<'_>, order_id: String, is_attached: bool) -> PyResult> { + fn us_order_detail( + &self, + py: Python<'_>, + order_id: String, + is_attached: bool, + ) -> PyResult> { let ctx = self.ctx.clone(); pyo3_async_runtimes::tokio::future_into_py(py, async move { - let resp = ctx.us_order_detail(order_id, is_attached).await.map_err(ErrorNewType)?; + let resp = ctx + .us_order_detail(order_id, is_attached) + .await + .map_err(ErrorNewType)?; let s = serde_json::to_string(&resp) .map_err(|e| pyo3::exceptions::PyValueError::new_err(e.to_string()))?; Ok(s) @@ -537,11 +560,19 @@ impl AsyncTradeContext { } /// Get US realized P&L. US token required. Returns awaitable. - fn us_realized_pl(&self, py: Python<'_>, currency: String, category: Option) -> PyResult> { + fn us_realized_pl( + &self, + py: Python<'_>, + currency: String, + category: Option, + ) -> PyResult> { let ctx = self.ctx.clone(); pyo3_async_runtimes::tokio::future_into_py(py, async move { - let r: crate::trade::types::USRealizedPL = - ctx.us_realized_pl(currency, category).await.map_err(ErrorNewType)?.into(); + let r: crate::trade::types::USRealizedPL = ctx + .us_realized_pl(currency, category) + .await + .map_err(ErrorNewType)? + .into(); Ok(r) }) .map(|b| b.unbind()) diff --git a/python/src/trade/types.rs b/python/src/trade/types.rs index 6247e2df18..0395447d61 100644 --- a/python/src/trade/types.rs +++ b/python/src/trade/types.rs @@ -798,25 +798,43 @@ pub(crate) struct EstimateMaxPurchaseQuantityResponse { #[pyclass(get_all, skip_from_py_object)] #[derive(Debug, Clone)] pub(crate) struct USStockPosition { - pub symbol: String, pub name: String, pub quantity: String, - pub available_quantity: String, pub currency: String, - pub cost_price: String, pub market_value: String, - pub unrealized_pl: String, pub unrealized_pl_ratio: String, - pub last_done: String, pub prev_close: String, pub change_rate: String, - pub night_last_done: String, pub pretrade_close: String, - pub trade_status: String, pub individual_quantity: String, + pub symbol: String, + pub name: String, + pub quantity: String, + pub available_quantity: String, + pub currency: String, + pub cost_price: String, + pub market_value: String, + pub unrealized_pl: String, + pub unrealized_pl_ratio: String, + pub last_done: String, + pub prev_close: String, + pub change_rate: String, + pub night_last_done: String, + pub pretrade_close: String, + pub trade_status: String, + pub individual_quantity: String, } impl From for USStockPosition { fn from(v: longbridge::trade::USStockPosition) -> Self { Self { - symbol: v.symbol, name: v.name, quantity: v.quantity, - available_quantity: v.available_quantity, currency: v.currency, - cost_price: v.cost_price, market_value: v.market_value, - unrealized_pl: v.unrealized_pl, unrealized_pl_ratio: v.unrealized_pl_ratio, - last_done: v.last_done, prev_close: v.prev_close, change_rate: v.change_rate, - night_last_done: v.night_last_done, pretrade_close: v.pretrade_close, - trade_status: v.trade_status, individual_quantity: v.individual_quantity, + symbol: v.symbol, + name: v.name, + quantity: v.quantity, + available_quantity: v.available_quantity, + currency: v.currency, + cost_price: v.cost_price, + market_value: v.market_value, + unrealized_pl: v.unrealized_pl, + unrealized_pl_ratio: v.unrealized_pl_ratio, + last_done: v.last_done, + prev_close: v.prev_close, + change_rate: v.change_rate, + night_last_done: v.night_last_done, + pretrade_close: v.pretrade_close, + trade_status: v.trade_status, + individual_quantity: v.individual_quantity, } } } @@ -825,17 +843,27 @@ impl From for USStockPosition { #[pyclass(get_all, skip_from_py_object)] #[derive(Debug, Clone)] pub(crate) struct USOptionPosition { - pub symbol: String, pub strike_price: String, pub due_date: String, - pub contract_multiplier: i32, pub option_type: String, - pub quantity: String, pub market_value: String, pub unrealized_pl: String, + pub symbol: String, + pub strike_price: String, + pub due_date: String, + pub contract_multiplier: i32, + pub option_type: String, + pub quantity: String, + pub market_value: String, + pub unrealized_pl: String, } impl From for USOptionPosition { fn from(v: longbridge::trade::USOptionPosition) -> Self { Self { - symbol: v.symbol, strike_price: v.strike_price, due_date: v.due_date, - contract_multiplier: v.contract_multiplier, option_type: v.option_type, - quantity: v.quantity, market_value: v.market_value, unrealized_pl: v.unrealized_pl, + symbol: v.symbol, + strike_price: v.strike_price, + due_date: v.due_date, + contract_multiplier: v.contract_multiplier, + option_type: v.option_type, + quantity: v.quantity, + market_value: v.market_value, + unrealized_pl: v.unrealized_pl, } } } @@ -844,15 +872,21 @@ impl From for USOptionPosition { #[pyclass(get_all, skip_from_py_object)] #[derive(Debug, Clone)] pub(crate) struct USCryptoPosition { - pub symbol: String, pub quantity: String, pub market_value: String, - pub unrealized_pl: String, pub cost_price: String, + pub symbol: String, + pub quantity: String, + pub market_value: String, + pub unrealized_pl: String, + pub cost_price: String, } impl From for USCryptoPosition { fn from(v: longbridge::trade::USCryptoPosition) -> Self { Self { - symbol: v.symbol, quantity: v.quantity, market_value: v.market_value, - unrealized_pl: v.unrealized_pl, cost_price: v.cost_price, + symbol: v.symbol, + quantity: v.quantity, + market_value: v.market_value, + unrealized_pl: v.unrealized_pl, + cost_price: v.cost_price, } } } @@ -861,16 +895,20 @@ impl From for USCryptoPosition { #[pyclass(get_all, skip_from_py_object)] #[derive(Debug, Clone, Default)] pub(crate) struct USBuyPower { - pub cash_buy_power: String, pub overnight_buy_power: String, - pub day_trade_buy_power: String, pub option_buy_power: String, + pub cash_buy_power: String, + pub overnight_buy_power: String, + pub day_trade_buy_power: String, + pub option_buy_power: String, pub crypto_buy_power: String, } impl From for USBuyPower { fn from(v: longbridge::trade::USBuyPower) -> Self { Self { - cash_buy_power: v.cash_buy_power, overnight_buy_power: v.overnight_buy_power, - day_trade_buy_power: v.day_trade_buy_power, option_buy_power: v.option_buy_power, + cash_buy_power: v.cash_buy_power, + overnight_buy_power: v.overnight_buy_power, + day_trade_buy_power: v.day_trade_buy_power, + option_buy_power: v.option_buy_power, crypto_buy_power: v.crypto_buy_power, } } @@ -880,8 +918,10 @@ impl From for USBuyPower { #[pyclass(get_all, skip_from_py_object)] #[derive(Debug, Clone)] pub(crate) struct USAssetOverview { - pub account_type: String, pub net_assets: String, - pub total_cash: String, pub unrealized_pl: String, + pub account_type: String, + pub net_assets: String, + pub total_cash: String, + pub unrealized_pl: String, pub positions: Vec, pub option_positions: Vec, pub multi_legs: String, @@ -892,8 +932,10 @@ pub(crate) struct USAssetOverview { impl From for USAssetOverview { fn from(v: longbridge::trade::USAssetOverview) -> Self { Self { - account_type: v.account_type, net_assets: v.net_assets, - total_cash: v.total_cash, unrealized_pl: v.unrealized_pl, + account_type: v.account_type, + net_assets: v.net_assets, + total_cash: v.total_cash, + unrealized_pl: v.unrealized_pl, positions: v.positions.into_iter().map(Into::into).collect(), option_positions: v.option_positions.into_iter().map(Into::into).collect(), multi_legs: serde_json::to_string(&v.multi_legs).unwrap_or_default(), @@ -907,17 +949,25 @@ impl From for USAssetOverview { #[pyclass(get_all, skip_from_py_object)] #[derive(Debug, Clone)] pub(crate) struct USRealizedPLItem { - pub symbol: String, pub name: String, pub category: String, - pub realized_pl: String, pub quantity_sold: String, - pub avg_cost: String, pub avg_sell_price: String, + pub symbol: String, + pub name: String, + pub category: String, + pub realized_pl: String, + pub quantity_sold: String, + pub avg_cost: String, + pub avg_sell_price: String, } impl From for USRealizedPLItem { fn from(v: longbridge::trade::USRealizedPLItem) -> Self { Self { - symbol: v.symbol, name: v.name, category: v.category, - realized_pl: v.realized_pl, quantity_sold: v.quantity_sold, - avg_cost: v.avg_cost, avg_sell_price: v.avg_sell_price, + symbol: v.symbol, + name: v.name, + category: v.category, + realized_pl: v.realized_pl, + quantity_sold: v.quantity_sold, + avg_cost: v.avg_cost, + avg_sell_price: v.avg_sell_price, } } } diff --git a/rust/src/blocking/fundamental.rs b/rust/src/blocking/fundamental.rs index fa15ea4307..85c319f6d9 100644 --- a/rust/src/blocking/fundamental.rs +++ b/rust/src/blocking/fundamental.rs @@ -357,7 +357,8 @@ impl FundamentalContextSync { &self, counter_id: impl Into + Send + 'static, ) -> Result { - self.rt.call(move |ctx| async move { ctx.us_company_overview(counter_id).await }) + self.rt + .call(move |ctx| async move { ctx.us_company_overview(counter_id).await }) } /// Get US valuation overview snapshot (blocking) @@ -365,7 +366,8 @@ impl FundamentalContextSync { &self, counter_id: impl Into + Send + 'static, ) -> Result { - self.rt.call(move |ctx| async move { ctx.us_valuation_overview(counter_id).await }) + self.rt + .call(move |ctx| async move { ctx.us_valuation_overview(counter_id).await }) } /// Get US financial overview (blocking) @@ -374,7 +376,8 @@ impl FundamentalContextSync { counter_id: impl Into + Send + 'static, report: impl Into + Send + 'static, ) -> Result { - self.rt.call(move |ctx| async move { ctx.us_financial_overview(counter_id, report).await }) + self.rt + .call(move |ctx| async move { ctx.us_financial_overview(counter_id, report).await }) } /// Get US financial statement v3 (blocking) @@ -385,7 +388,8 @@ impl FundamentalContextSync { report: impl Into + Send + 'static, ) -> Result { self.rt.call(move |ctx| async move { - ctx.us_financial_statement_v3(counter_id, kind, report).await + ctx.us_financial_statement_v3(counter_id, kind, report) + .await }) } @@ -414,7 +418,8 @@ impl FundamentalContextSync { &self, counter_id: impl Into + Send + 'static, ) -> Result { - self.rt.call(move |ctx| async move { ctx.us_etf_dividend_info(counter_id).await }) + self.rt + .call(move |ctx| async move { ctx.us_etf_dividend_info(counter_id).await }) } /// Get US company historical dividends (blocking) @@ -422,7 +427,8 @@ impl FundamentalContextSync { &self, counter_id: impl Into + Send + 'static, ) -> Result { - self.rt.call(move |ctx| async move { ctx.us_company_dividends(counter_id).await }) + self.rt + .call(move |ctx| async move { ctx.us_company_dividends(counter_id).await }) } /// Get US ETF document list (blocking) @@ -431,6 +437,7 @@ impl FundamentalContextSync { counter_id: impl Into + Send + 'static, size: Option, ) -> Result { - self.rt.call(move |ctx| async move { ctx.us_etf_files(counter_id, size).await }) + self.rt + .call(move |ctx| async move { ctx.us_etf_files(counter_id, size).await }) } } diff --git a/rust/src/blocking/quote.rs b/rust/src/blocking/quote.rs index d855549434..19ba760660 100644 --- a/rust/src/blocking/quote.rs +++ b/rust/src/blocking/quote.rs @@ -1240,6 +1240,7 @@ impl QuoteContextSync { &self, counter_id: impl Into + Send + 'static, ) -> Result { - self.rt.call(move |ctx| async move { ctx.us_crypto_overview(counter_id).await }) + self.rt + .call(move |ctx| async move { ctx.us_crypto_overview(counter_id).await }) } } diff --git a/rust/src/blocking/trade.rs b/rust/src/blocking/trade.rs index c4fa017605..5347d7d933 100644 --- a/rust/src/blocking/trade.rs +++ b/rust/src/blocking/trade.rs @@ -462,7 +462,8 @@ impl TradeContextSync { /// Query the paginated US order list (blocking) pub fn us_query_orders(&self, opts: QueryUSOrdersOptions) -> Result { - self.rt.call(move |ctx| async move { ctx.us_query_orders(opts).await }) + self.rt + .call(move |ctx| async move { ctx.us_query_orders(opts).await }) } /// Get US order detail (blocking) @@ -477,7 +478,8 @@ impl TradeContextSync { /// Get the full US account asset overview (blocking) pub fn us_asset_overview(&self) -> Result { - self.rt.call(move |ctx| async move { ctx.us_asset_overview().await }) + self.rt + .call(move |ctx| async move { ctx.us_asset_overview().await }) } /// Get realized P&L for the US account (blocking) diff --git a/rust/src/fundamental/context.rs b/rust/src/fundamental/context.rs index 59788cc10b..c3425c3a4f 100644 --- a/rust/src/fundamental/context.rs +++ b/rust/src/fundamental/context.rs @@ -1057,7 +1057,8 @@ impl FundamentalContext { /// /// Path: `GET /v1/stock-info/company-overview` /// - /// US token required — returns [`longbridge_httpcli::HttpClientError::DcRegionRestricted`] + /// US token required — returns + /// [`longbridge_httpcli::HttpClientError::DcRegionRestricted`] /// for non-US credentials. pub async fn us_company_overview( &self, @@ -1069,7 +1070,9 @@ impl FundamentalContext { } self.get_dc( "/v1/stock-info/company-overview", - Query { counter_id: counter_id.into() }, + Query { + counter_id: counter_id.into(), + }, DcRegion::Us, ) .await @@ -1090,7 +1093,9 @@ impl FundamentalContext { } self.get_dc( "/v1/stock-info/valuation-overview", - Query { counter_id: counter_id.into() }, + Query { + counter_id: counter_id.into(), + }, DcRegion::Us, ) .await @@ -1115,7 +1120,10 @@ impl FundamentalContext { } self.get_dc( "/v1/stock-info/finn-overview", - Query { counter_id: counter_id.into(), report: report.into() }, + Query { + counter_id: counter_id.into(), + report: report.into(), + }, DcRegion::Us, ) .await @@ -1123,8 +1131,8 @@ impl FundamentalContext { /// Get US financial statement detail (IS / BS / CF). /// - /// `kind`: `"IS"` (income statement), `"BS"` (balance sheet), `"CF"` (cash flow). - /// `report`: `"annual"` or `"quarterly"`. + /// `kind`: `"IS"` (income statement), `"BS"` (balance sheet), `"CF"` (cash + /// flow). `report`: `"annual"` or `"quarterly"`. /// /// Path: `GET /v1/us/quote/financials/statements` /// @@ -1172,7 +1180,10 @@ impl FundamentalContext { } self.get_dc( "/v1/stock-info/fin-keyfactor", - Query { counter_id: counter_id.into(), report: report.into() }, + Query { + counter_id: counter_id.into(), + report: report.into(), + }, DcRegion::Us, ) .await @@ -1197,7 +1208,10 @@ impl FundamentalContext { } self.get_dc( "/v1/stock-info/fin-consensus", - Query { counter_id: counter_id.into(), report: report.into() }, + Query { + counter_id: counter_id.into(), + report: report.into(), + }, DcRegion::Us, ) .await @@ -1218,7 +1232,9 @@ impl FundamentalContext { } self.get_dc( "/v1/stock-info/etf-dividend-info", - Query { counter_id: counter_id.into() }, + Query { + counter_id: counter_id.into(), + }, DcRegion::Us, ) .await @@ -1239,7 +1255,9 @@ impl FundamentalContext { } self.get_dc( "/v1/stock-info/company-dividends", - Query { counter_id: counter_id.into() }, + Query { + counter_id: counter_id.into(), + }, DcRegion::Us, ) .await @@ -1247,7 +1265,8 @@ impl FundamentalContext { /// Get ETF document list (prospectus, annual reports, etc.). /// - /// `size`: number of files to return; `None` returns all (server default 0 = all). + /// `size`: number of files to return; `None` returns all (server default 0 + /// = all). /// /// Path: `GET /v1/stock-info/etf-files` /// @@ -1265,7 +1284,10 @@ impl FundamentalContext { } self.get_dc( "/v1/stock-info/etf-files", - Query { counter_id: counter_id.into(), size }, + Query { + counter_id: counter_id.into(), + size, + }, DcRegion::Us, ) .await diff --git a/rust/src/fundamental/types.rs b/rust/src/fundamental/types.rs index 9280f91b2f..02e34a3616 100644 --- a/rust/src/fundamental/types.rs +++ b/rust/src/fundamental/types.rs @@ -1720,9 +1720,11 @@ pub struct MacroeconomicResponse { pub count: i32, } -// ── US-market types ─────────────────────────────────────────────────────────── +// ── US-market types +// ─────────────────────────────────────────────────────────── -/// Industry rank tag returned by [`crate::FundamentalContext::us_company_overview`]. +/// Industry rank tag returned by +/// [`crate::FundamentalContext::us_company_overview`]. #[derive(Debug, Clone, Serialize, Deserialize)] pub struct USRankTag { /// Tag name diff --git a/rust/src/lib.rs b/rust/src/lib.rs index 4bb622053d..f9a0e2d9eb 100644 --- a/rust/src/lib.rs +++ b/rust/src/lib.rs @@ -44,26 +44,23 @@ pub use content::ContentContext; pub use dca::DCAContext; pub use error::{Error, Result, SimpleError, SimpleErrorKind}; pub use fundamental::FundamentalContext; +// ── US-market type re-exports ───────────────────────────────────────────────── +pub use fundamental::types::{ + USCompanyDividends, USCompanyOverview, USDividendItem, USETFDividendInfo, USETFFile, + USETFFilesResponse, USFinancialStatement, USRankTag, USValuationIndicator, USValuationOverview, +}; pub use longbridge_httpcli as httpclient; pub use longbridge_httpcli::{DC_REGION_HEADER, DcRegion}; pub use longbridge_wscli as wsclient; pub use market::MarketContext; pub use portfolio::PortfolioContext; -pub use quote::QuoteContext; +pub use quote::{QuoteContext, USCryptoOverview}; pub use rust_decimal::Decimal; pub use screener::ScreenerContext; pub use sharelist::SharelistContext; -pub use trade::TradeContext; -pub use types::Market; - -// ── US-market type re-exports ───────────────────────────────────────────────── -pub use fundamental::types::{ - USCompanyDividends, USCompanyOverview, USDividendItem, USETFDividendInfo, USETFFile, - USETFFilesResponse, USFinancialStatement, USRankTag, USValuationIndicator, USValuationOverview, -}; -pub use quote::USCryptoOverview; pub use trade::{ - QueryUSOrdersOptions, QueryUSOrdersResponse, USAssetOverview, USAttachedOrder, USBuyPower, - USCryptoPosition, USOptionPosition, USOrderDetailResponse, USRealizedPL, USRealizedPLItem, - USStockPosition, + QueryUSOrdersOptions, QueryUSOrdersResponse, TradeContext, USAssetOverview, USAttachedOrder, + USBuyPower, USCryptoPosition, USOptionPosition, USOrderDetailResponse, USRealizedPL, + USRealizedPLItem, USStockPosition, }; +pub use types::Market; diff --git a/rust/src/quote/context.rs b/rust/src/quote/context.rs index 3a5e231acb..a7bacbc9c4 100644 --- a/rust/src/quote/context.rs +++ b/rust/src/quote/context.rs @@ -2295,7 +2295,8 @@ impl QuoteContext { /// Path: `GET /v1/gemini/crypto-overview` /// /// US token required — returns - /// [`longbridge_httpcli::HttpClientError::DcRegionRestricted`] for non-US credentials. + /// [`longbridge_httpcli::HttpClientError::DcRegionRestricted`] for non-US + /// credentials. pub async fn us_crypto_overview( &self, counter_id: impl Into, @@ -2309,7 +2310,9 @@ impl QuoteContext { .http_cli .request(Method::GET, "/v1/gemini/crypto-overview") .dc_restrict(DcRegion::Us) - .query_params(Query { counter_id: counter_id.into() }) + .query_params(Query { + counter_id: counter_id.into(), + }) .response::>() .send() .with_subscriber(self.0.log_subscriber.clone()) diff --git a/rust/src/quote/mod.rs b/rust/src/quote/mod.rs index c9931d92c7..39d4260e1a 100644 --- a/rust/src/quote/mod.rs +++ b/rust/src/quote/mod.rs @@ -70,12 +70,12 @@ pub use types::{ TradeSession, TradeSessions, TradingSessionInfo, + USCryptoOverview, WarrantInfo, WarrantQuote, WarrantSortBy, WarrantStatus, WarrantType, - USCryptoOverview, WatchlistGroup, WatchlistSecurity, }; diff --git a/rust/src/quote/types.rs b/rust/src/quote/types.rs index 5c146d4f9d..162a8c2127 100644 --- a/rust/src/quote/types.rs +++ b/rust/src/quote/types.rs @@ -2164,7 +2164,8 @@ pub enum PinnedMode { Remove, } -// ── US-market types ─────────────────────────────────────────────────────────── +// ── US-market types +// ─────────────────────────────────────────────────────────── /// Market overview for a single cryptocurrency. /// diff --git a/rust/src/trade/context.rs b/rust/src/trade/context.rs index b3d7618942..5f059800f2 100644 --- a/rust/src/trade/context.rs +++ b/rust/src/trade/context.rs @@ -861,7 +861,8 @@ impl TradeContext { .0) } - /// Get US order detail, optionally including attached stop-loss/take-profit sub-orders. + /// Get US order detail, optionally including attached stop-loss/take-profit + /// sub-orders. /// /// Path: `GET /v3/orders/{order_id}` /// @@ -897,7 +898,8 @@ impl TradeContext { .0) } - /// Get the full US account asset snapshot (stocks, options, crypto, buying power). + /// Get the full US account asset snapshot (stocks, options, crypto, buying + /// power). /// /// Path: `GET /v1/us/assets/overview` /// @@ -918,7 +920,8 @@ impl TradeContext { /// Get realized profit-and-loss for the US account. /// /// `currency`: required, e.g. `"USD"`. - /// `category`: optional filter — `"ALL"`, `"STOCK"`, `"OPTION"`, or `"CRYPTO"`. + /// `category`: optional filter — `"ALL"`, `"STOCK"`, `"OPTION"`, or + /// `"CRYPTO"`. /// /// Path: `GET /v1/us/assets/pl/realized` /// diff --git a/rust/src/trade/mod.rs b/rust/src/trade/mod.rs index 0cc9c10d96..52a755500d 100644 --- a/rust/src/trade/mod.rs +++ b/rust/src/trade/mod.rs @@ -15,14 +15,47 @@ pub use requests::{ GetTodayExecutionsOptions, GetTodayOrdersOptions, ReplaceOrderOptions, SubmitOrderOptions, }; pub use types::{ - AccountBalance, BalanceType, CashFlow, CashFlowDirection, CashInfo, ChargeCategoryCode, - CommissionFreeStatus, DeductionStatus, Execution, FrozenTransactionFee, FundPosition, - FundPositionChannel, FundPositionsResponse, MarginRatio, Order, OrderChargeDetail, - OrderChargeFee, OrderChargeItem, OrderDetail, OrderHistoryDetail, OrderSide, OrderStatus, - OrderTag, OrderType, OutsideRTH, StockPosition, StockPositionChannel, StockPositionsResponse, - TimeInForceType, TriggerPriceType, TriggerStatus, + AccountBalance, + BalanceType, + CashFlow, + CashFlowDirection, + CashInfo, + ChargeCategoryCode, + CommissionFreeStatus, + DeductionStatus, + Execution, + FrozenTransactionFee, + FundPosition, + FundPositionChannel, + FundPositionsResponse, + MarginRatio, + Order, + OrderChargeDetail, + OrderChargeFee, + OrderChargeItem, + OrderDetail, + OrderHistoryDetail, + OrderSide, + OrderStatus, + OrderTag, + OrderType, + OutsideRTH, // US-market types - QueryUSOrdersOptions, QueryUSOrdersResponse, USAssetOverview, USAttachedOrder, USBuyPower, - USCryptoPosition, USOptionPosition, USOrderDetailResponse, USRealizedPL, USRealizedPLItem, + QueryUSOrdersOptions, + QueryUSOrdersResponse, + StockPosition, + StockPositionChannel, + StockPositionsResponse, + TimeInForceType, + TriggerPriceType, + TriggerStatus, + USAssetOverview, + USAttachedOrder, + USBuyPower, + USCryptoPosition, + USOptionPosition, + USOrderDetailResponse, + USRealizedPL, + USRealizedPLItem, USStockPosition, }; diff --git a/rust/src/trade/types.rs b/rust/src/trade/types.rs index 8d1fc2ef82..6e725641d3 100644 --- a/rust/src/trade/types.rs +++ b/rust/src/trade/types.rs @@ -804,7 +804,8 @@ impl_default_for_enum_string!( ChargeCategoryCode ); -// ── US-market types ─────────────────────────────────────────────────────────── +// ── US-market types +// ─────────────────────────────────────────────────────────── /// Request body for [`crate::TradeContext::us_query_orders`]. #[derive(Debug, Clone, Serialize)] @@ -871,7 +872,8 @@ pub struct USOrderDetailResponse { /// Raw order detail fields (pass-through) #[serde(flatten)] pub detail: serde_json::Value, - /// Attached stop-loss / take-profit orders (populated when `is_attached = true`) + /// Attached stop-loss / take-profit orders (populated when `is_attached = + /// true`) #[serde(default)] pub attached_orders: Vec, } @@ -879,94 +881,147 @@ pub struct USOrderDetailResponse { /// A stock position in a US account. #[derive(Debug, Clone, Serialize, Deserialize)] pub struct USStockPosition { - #[serde(default)] pub symbol: String, - #[serde(default)] pub name: String, - #[serde(default)] pub quantity: String, - #[serde(default)] pub available_quantity: String, - #[serde(default)] pub currency: String, - #[serde(default)] pub cost_price: String, - #[serde(default)] pub market_value: String, - #[serde(default)] pub unrealized_pl: String, - #[serde(default)] pub unrealized_pl_ratio: String, - #[serde(default)] pub last_done: String, - #[serde(default)] pub prev_close: String, - #[serde(default)] pub change_rate: String, + #[serde(default)] + pub symbol: String, + #[serde(default)] + pub name: String, + #[serde(default)] + pub quantity: String, + #[serde(default)] + pub available_quantity: String, + #[serde(default)] + pub currency: String, + #[serde(default)] + pub cost_price: String, + #[serde(default)] + pub market_value: String, + #[serde(default)] + pub unrealized_pl: String, + #[serde(default)] + pub unrealized_pl_ratio: String, + #[serde(default)] + pub last_done: String, + #[serde(default)] + pub prev_close: String, + #[serde(default)] + pub change_rate: String, /// Overnight/night-session last price (US-specific) - #[serde(default)] pub night_last_done: String, + #[serde(default)] + pub night_last_done: String, /// Pre-market close price (US-specific) - #[serde(default)] pub pretrade_close: String, + #[serde(default)] + pub pretrade_close: String, /// Trading session status (US-specific) - #[serde(default)] pub trade_status: String, + #[serde(default)] + pub trade_status: String, /// Individual quantity after multi-leg exclusion (US-specific) - #[serde(default)] pub individual_quantity: String, + #[serde(default)] + pub individual_quantity: String, } /// An option position in a US account. #[derive(Debug, Clone, Serialize, Deserialize)] pub struct USOptionPosition { - #[serde(default)] pub symbol: String, - #[serde(default)] pub strike_price: String, - #[serde(default)] pub due_date: String, - #[serde(default)] pub contract_multiplier: i32, - #[serde(default, rename = "type")] pub option_type: String, - #[serde(default)] pub quantity: String, - #[serde(default)] pub market_value: String, - #[serde(default)] pub unrealized_pl: String, + #[serde(default)] + pub symbol: String, + #[serde(default)] + pub strike_price: String, + #[serde(default)] + pub due_date: String, + #[serde(default)] + pub contract_multiplier: i32, + #[serde(default, rename = "type")] + pub option_type: String, + #[serde(default)] + pub quantity: String, + #[serde(default)] + pub market_value: String, + #[serde(default)] + pub unrealized_pl: String, } /// A cryptocurrency position in a US account. #[derive(Debug, Clone, Serialize, Deserialize)] pub struct USCryptoPosition { - #[serde(default)] pub symbol: String, - #[serde(default)] pub quantity: String, - #[serde(default)] pub market_value: String, - #[serde(default)] pub unrealized_pl: String, - #[serde(default)] pub cost_price: String, + #[serde(default)] + pub symbol: String, + #[serde(default)] + pub quantity: String, + #[serde(default)] + pub market_value: String, + #[serde(default)] + pub unrealized_pl: String, + #[serde(default)] + pub cost_price: String, } /// Purchasing power breakdown for a US account. #[derive(Debug, Clone, Serialize, Deserialize, Default)] pub struct USBuyPower { - #[serde(default)] pub cash_buy_power: String, - #[serde(default)] pub overnight_buy_power: String, + #[serde(default)] + pub cash_buy_power: String, + #[serde(default)] + pub overnight_buy_power: String, /// Day-trade buying power (margin accounts only) - #[serde(default)] pub day_trade_buy_power: String, - #[serde(default)] pub option_buy_power: String, - #[serde(default)] pub crypto_buy_power: String, + #[serde(default)] + pub day_trade_buy_power: String, + #[serde(default)] + pub option_buy_power: String, + #[serde(default)] + pub crypto_buy_power: String, } /// Response for [`crate::TradeContext::us_asset_overview`]. #[derive(Debug, Clone, Serialize, Deserialize)] pub struct USAssetOverview { - #[serde(default)] pub account_type: String, - #[serde(default)] pub net_assets: String, - #[serde(default)] pub total_cash: String, - #[serde(default)] pub unrealized_pl: String, - #[serde(default)] pub positions: Vec, - #[serde(default)] pub option_positions: Vec, - #[serde(default)] pub multi_legs: Vec, - #[serde(default)] pub crypto_positions: Vec, - #[serde(default)] pub buy_power: USBuyPower, + #[serde(default)] + pub account_type: String, + #[serde(default)] + pub net_assets: String, + #[serde(default)] + pub total_cash: String, + #[serde(default)] + pub unrealized_pl: String, + #[serde(default)] + pub positions: Vec, + #[serde(default)] + pub option_positions: Vec, + #[serde(default)] + pub multi_legs: Vec, + #[serde(default)] + pub crypto_positions: Vec, + #[serde(default)] + pub buy_power: USBuyPower, } /// A single realized P&L entry. #[derive(Debug, Clone, Serialize, Deserialize)] pub struct USRealizedPLItem { - #[serde(default)] pub symbol: String, - #[serde(default)] pub name: String, - #[serde(default)] pub category: String, - #[serde(default)] pub realized_pl: String, - #[serde(default)] pub quantity_sold: String, - #[serde(default)] pub avg_cost: String, - #[serde(default)] pub avg_sell_price: String, + #[serde(default)] + pub symbol: String, + #[serde(default)] + pub name: String, + #[serde(default)] + pub category: String, + #[serde(default)] + pub realized_pl: String, + #[serde(default)] + pub quantity_sold: String, + #[serde(default)] + pub avg_cost: String, + #[serde(default)] + pub avg_sell_price: String, } /// Response for [`crate::TradeContext::us_realized_pl`]. #[derive(Debug, Clone, Serialize, Deserialize)] pub struct USRealizedPL { - #[serde(default)] pub total_realized_pl: String, - #[serde(default)] pub currency: String, - #[serde(default)] pub items: Vec, + #[serde(default)] + pub total_realized_pl: String, + #[serde(default)] + pub currency: String, + #[serde(default)] + pub items: Vec, } #[cfg(test)] From 21e67ca5229440e4c12dc286ac364a2b478f6072 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=A2=81=E7=AB=A0=E6=B4=AA?= Date: Thu, 2 Jul 2026 18:14:58 +0800 Subject: [PATCH 13/44] fix: use symbol param (e.g. AAPL.US) instead of counter_id for all US APIs All US methods now accept the user-facing symbol format (e.g. "AAPL.US", "SPY.US", "BTC.US") and convert to counter_id internally via symbol_to_counter_id(), matching the convention of all existing HK/CN APIs. Affected: FundamentalContext (9 methods), QuoteContext.us_crypto_overview, Python/Node.js bindings, index.d.ts, openapi.pyi Co-Authored-By: Claude Sonnet 4.6 (1M context) --- nodejs/index.d.ts | 20 +++++++------- nodejs/src/fundamental/context.rs | 8 +++--- nodejs/src/quote/context.rs | 2 +- python/pysrc/longbridge/openapi.pyi | 22 +++++++-------- python/src/fundamental/context.rs | 8 +++--- python/src/fundamental/context_async.rs | 8 +++--- python/src/quote/context.rs | 2 +- python/src/quote/context_async.rs | 2 +- rust/src/fundamental/context.rs | 36 ++++++++++++------------- rust/src/quote/context.rs | 5 ++-- 10 files changed, 57 insertions(+), 56 deletions(-) diff --git a/nodejs/index.d.ts b/nodejs/index.d.ts index af14edb458..d7f2d2e187 100644 --- a/nodejs/index.d.ts +++ b/nodejs/index.d.ts @@ -623,23 +623,23 @@ export declare class FundamentalContext { /** Get historical data for a macroeconomic indicator */ macroeconomic(indicatorCode: string, startDate?: string | undefined | null, endDate?: string | undefined | null, offset?: number | undefined | null, limit?: number | undefined | null): Promise /** Get US company overview. US token required. counterID format: "ST/US/AAPL" */ - usCompanyOverview(counterId: string): Promise + usCompanyOverview(symbol: string): Promise /** Get US valuation snapshot (PE/PB/PS). US token required. */ - usValuationOverview(counterId: string): Promise + usValuationOverview(symbol: string): Promise /** Get US financial overview (revenue/net income/EPS). Returns JSON string. US token required. */ - usFinancialOverview(counterId: string, report: string): Promise + usFinancialOverview(symbol: string, report: string): Promise /** Get US financial statement (IS/BS/CF). kind: "IS" | "BS" | "CF". US token required. */ - usFinancialStatementV3(counterId: string, kind: string, report: string): Promise + usFinancialStatementV3(symbol: string, kind: string, report: string): Promise /** Get US key financial metrics (ROE/margins). Returns JSON string. US token required. */ - usKeyFinancialMetrics(counterId: string, report: string): Promise + usKeyFinancialMetrics(symbol: string, report: string): Promise /** Get US analyst consensus estimates. Returns JSON string. US token required. */ - usAnalystConsensus(counterId: string, report: string): Promise + usAnalystConsensus(symbol: string, report: string): Promise /** Get US ETF dividend history. US token required. */ - usEtfDividendInfo(counterId: string): Promise + usEtfDividendInfo(symbol: string): Promise /** Get US company historical dividends. US token required. */ - usCompanyDividends(counterId: string): Promise + usCompanyDividends(symbol: string): Promise /** Get US ETF document list. size=null returns all. US token required. */ - usEtfFiles(counterId: string, size?: number | undefined | null): Promise + usEtfFiles(symbol: string, size?: number | undefined | null): Promise } /** Fund position */ @@ -2049,7 +2049,7 @@ export declare class QuoteContext { /** Get daily historical option volume */ optionVolumeDaily(symbol: string, timestamp: number, count: number): Promise /** Get US cryptocurrency market overview. counterID format: "CY/US/BTC". US token required. */ - usCryptoOverview(counterId: string): Promise + usCryptoOverview(symbol: string): Promise } export declare class QuotePackageDetail { diff --git a/nodejs/src/fundamental/context.rs b/nodejs/src/fundamental/context.rs index abf42ec218..e6cbe5325c 100644 --- a/nodejs/src/fundamental/context.rs +++ b/nodejs/src/fundamental/context.rs @@ -330,7 +330,7 @@ impl FundamentalContext { pub async fn us_company_overview(&self, counter_id: String) -> Result { Ok(self .ctx - .us_company_overview(counter_id) + .us_company_overview(symbol) .await .map_err(ErrorNewType)? .into()) @@ -341,7 +341,7 @@ impl FundamentalContext { pub async fn us_valuation_overview(&self, counter_id: String) -> Result { Ok(self .ctx - .us_valuation_overview(counter_id) + .us_valuation_overview(symbol) .await .map_err(ErrorNewType)? .into()) @@ -409,7 +409,7 @@ impl FundamentalContext { pub async fn us_etf_dividend_info(&self, counter_id: String) -> Result { Ok(self .ctx - .us_etf_dividend_info(counter_id) + .us_etf_dividend_info(symbol) .await .map_err(ErrorNewType)? .into()) @@ -420,7 +420,7 @@ impl FundamentalContext { pub async fn us_company_dividends(&self, counter_id: String) -> Result { Ok(self .ctx - .us_company_dividends(counter_id) + .us_company_dividends(symbol) .await .map_err(ErrorNewType)? .into()) diff --git a/nodejs/src/quote/context.rs b/nodejs/src/quote/context.rs index 41ac25bbf3..052e86acdc 100644 --- a/nodejs/src/quote/context.rs +++ b/nodejs/src/quote/context.rs @@ -1296,7 +1296,7 @@ impl QuoteContext { pub async fn us_crypto_overview(&self, counter_id: String) -> Result { Ok(self .ctx - .us_crypto_overview(counter_id) + .us_crypto_overview(symbol) .await .map_err(ErrorNewType)? .into()) diff --git a/python/pysrc/longbridge/openapi.pyi b/python/pysrc/longbridge/openapi.pyi index d065efff0e..afb0805664 100644 --- a/python/pysrc/longbridge/openapi.pyi +++ b/python/pysrc/longbridge/openapi.pyi @@ -4020,7 +4020,7 @@ class QuoteContext: :class:`ShortTradesResponse` with raw JSON data """ - def us_crypto_overview(self, counter_id: str) -> "USCryptoOverview": + def us_crypto_overview(self, symbol: str) -> "USCryptoOverview": """Get US cryptocurrency market overview. US token required. Args: @@ -5383,7 +5383,7 @@ class AsyncQuoteContext: """ ... - def us_crypto_overview(self, counter_id: str) -> "Awaitable[USCryptoOverview]": + def us_crypto_overview(self, symbol: str) -> "Awaitable[USCryptoOverview]": """Get US cryptocurrency market overview. US token required. Returns awaitable. Args: @@ -10119,7 +10119,7 @@ class FundamentalContext: """ ... - def us_company_overview(self, counter_id: str) -> "USCompanyOverview": + def us_company_overview(self, symbol: str) -> "USCompanyOverview": """Get US company overview. US token required. Args: @@ -10130,7 +10130,7 @@ class FundamentalContext: """ ... - def us_valuation_overview(self, counter_id: str) -> "USValuationOverview": + def us_valuation_overview(self, symbol: str) -> "USValuationOverview": """Get US valuation overview. US token required. Args: @@ -10141,7 +10141,7 @@ class FundamentalContext: """ ... - def us_financial_overview(self, counter_id: str, report: str) -> Any: + def us_financial_overview(self, symbol: str, report: str) -> Any: """Get US financial overview (revenue, net income, EPS, cash flow). US token required. Args: @@ -10153,7 +10153,7 @@ class FundamentalContext: """ ... - def us_financial_statement_v3(self, counter_id: str, kind: str, report: str) -> "USFinancialStatement": + def us_financial_statement_v3(self, symbol: str, kind: str, report: str) -> "USFinancialStatement": """Get US financial statement detail (IS/BS/CF). US token required. Args: @@ -10166,7 +10166,7 @@ class FundamentalContext: """ ... - def us_key_financial_metrics(self, counter_id: str, report: str) -> Any: + def us_key_financial_metrics(self, symbol: str, report: str) -> Any: """Get US key financial metrics (ROE, margins, debt ratio). US token required. Args: @@ -10178,7 +10178,7 @@ class FundamentalContext: """ ... - def us_analyst_consensus(self, counter_id: str, report: str) -> Any: + def us_analyst_consensus(self, symbol: str, report: str) -> Any: """Get US analyst consensus estimates (EPS, revenue forecasts). US token required. Args: @@ -10190,7 +10190,7 @@ class FundamentalContext: """ ... - def us_etf_dividend_info(self, counter_id: str) -> "USETFDividendInfo": + def us_etf_dividend_info(self, symbol: str) -> "USETFDividendInfo": """Get US ETF dividend history. US token required. Args: @@ -10201,7 +10201,7 @@ class FundamentalContext: """ ... - def us_company_dividends(self, counter_id: str) -> "USCompanyDividends": + def us_company_dividends(self, symbol: str) -> "USCompanyDividends": """Get US company historical dividends. US token required. Args: @@ -10212,7 +10212,7 @@ class FundamentalContext: """ ... - def us_etf_files(self, counter_id: str, size: Optional[int] = None) -> "USETFFilesResponse": + def us_etf_files(self, symbol: str, size: Optional[int] = None) -> "USETFFilesResponse": """Get US ETF document list. US token required. Args: diff --git a/python/src/fundamental/context.rs b/python/src/fundamental/context.rs index 66bc19776f..6d64dc618f 100644 --- a/python/src/fundamental/context.rs +++ b/python/src/fundamental/context.rs @@ -248,7 +248,7 @@ impl FundamentalContext { fn us_company_overview(&self, counter_id: String) -> PyResult { Ok(self .ctx - .us_company_overview(counter_id) + .us_company_overview(symbol) .map_err(ErrorNewType)? .into()) } @@ -257,7 +257,7 @@ impl FundamentalContext { fn us_valuation_overview(&self, counter_id: String) -> PyResult { Ok(self .ctx - .us_valuation_overview(counter_id) + .us_valuation_overview(symbol) .map_err(ErrorNewType)? .into()) } @@ -330,7 +330,7 @@ impl FundamentalContext { fn us_etf_dividend_info(&self, counter_id: String) -> PyResult { Ok(self .ctx - .us_etf_dividend_info(counter_id) + .us_etf_dividend_info(symbol) .map_err(ErrorNewType)? .into()) } @@ -339,7 +339,7 @@ impl FundamentalContext { fn us_company_dividends(&self, counter_id: String) -> PyResult { Ok(self .ctx - .us_company_dividends(counter_id) + .us_company_dividends(symbol) .map_err(ErrorNewType)? .into()) } diff --git a/python/src/fundamental/context_async.rs b/python/src/fundamental/context_async.rs index 5d0367c37b..300adace98 100644 --- a/python/src/fundamental/context_async.rs +++ b/python/src/fundamental/context_async.rs @@ -369,7 +369,7 @@ impl AsyncFundamentalContext { let ctx = self.ctx.clone(); pyo3_async_runtimes::tokio::future_into_py(py, async move { Ok(USCompanyOverview::from( - ctx.us_company_overview(counter_id) + ctx.us_company_overview(symbol) .await .map_err(ErrorNewType)?, )) @@ -382,7 +382,7 @@ impl AsyncFundamentalContext { let ctx = self.ctx.clone(); pyo3_async_runtimes::tokio::future_into_py(py, async move { Ok(USValuationOverview::from( - ctx.us_valuation_overview(counter_id) + ctx.us_valuation_overview(symbol) .await .map_err(ErrorNewType)?, )) @@ -480,7 +480,7 @@ impl AsyncFundamentalContext { let ctx = self.ctx.clone(); pyo3_async_runtimes::tokio::future_into_py(py, async move { Ok(USETFDividendInfo::from( - ctx.us_etf_dividend_info(counter_id) + ctx.us_etf_dividend_info(symbol) .await .map_err(ErrorNewType)?, )) @@ -493,7 +493,7 @@ impl AsyncFundamentalContext { let ctx = self.ctx.clone(); pyo3_async_runtimes::tokio::future_into_py(py, async move { Ok(USCompanyDividends::from( - ctx.us_company_dividends(counter_id) + ctx.us_company_dividends(symbol) .await .map_err(ErrorNewType)?, )) diff --git a/python/src/quote/context.rs b/python/src/quote/context.rs index bf785a8a08..de4791b979 100644 --- a/python/src/quote/context.rs +++ b/python/src/quote/context.rs @@ -695,7 +695,7 @@ impl QuoteContext { ) -> PyResult { Ok(self .ctx - .us_crypto_overview(counter_id) + .us_crypto_overview(symbol) .map_err(ErrorNewType)? .into()) } diff --git a/python/src/quote/context_async.rs b/python/src/quote/context_async.rs index 48ad340870..4c5b21d908 100644 --- a/python/src/quote/context_async.rs +++ b/python/src/quote/context_async.rs @@ -902,7 +902,7 @@ impl AsyncQuoteContext { let ctx = self.ctx.clone(); pyo3_async_runtimes::tokio::future_into_py(py, async move { let r: crate::quote::types::USCryptoOverview = ctx - .us_crypto_overview(counter_id) + .us_crypto_overview(symbol) .await .map_err(ErrorNewType)? .into(); diff --git a/rust/src/fundamental/context.rs b/rust/src/fundamental/context.rs index c3425c3a4f..b9a066e002 100644 --- a/rust/src/fundamental/context.rs +++ b/rust/src/fundamental/context.rs @@ -1062,7 +1062,7 @@ impl FundamentalContext { /// for non-US credentials. pub async fn us_company_overview( &self, - counter_id: impl Into, + symbol: impl Into, ) -> Result { #[derive(Serialize)] struct Query { @@ -1071,7 +1071,7 @@ impl FundamentalContext { self.get_dc( "/v1/stock-info/company-overview", Query { - counter_id: counter_id.into(), + counter_id: symbol_to_counter_id(&symbol.into()), }, DcRegion::Us, ) @@ -1085,7 +1085,7 @@ impl FundamentalContext { /// US token required. pub async fn us_valuation_overview( &self, - counter_id: impl Into, + symbol: impl Into, ) -> Result { #[derive(Serialize)] struct Query { @@ -1094,7 +1094,7 @@ impl FundamentalContext { self.get_dc( "/v1/stock-info/valuation-overview", Query { - counter_id: counter_id.into(), + counter_id: symbol_to_counter_id(&symbol.into()), }, DcRegion::Us, ) @@ -1110,7 +1110,7 @@ impl FundamentalContext { /// US token required. Returns raw JSON for maximum flexibility. pub async fn us_financial_overview( &self, - counter_id: impl Into, + symbol: impl Into, report: impl Into, ) -> Result { #[derive(Serialize)] @@ -1121,7 +1121,7 @@ impl FundamentalContext { self.get_dc( "/v1/stock-info/finn-overview", Query { - counter_id: counter_id.into(), + counter_id: symbol_to_counter_id(&symbol.into()), report: report.into(), }, DcRegion::Us, @@ -1139,7 +1139,7 @@ impl FundamentalContext { /// US token required. pub async fn us_financial_statement_v3( &self, - counter_id: impl Into, + symbol: impl Into, kind: impl Into, report: impl Into, ) -> Result { @@ -1152,7 +1152,7 @@ impl FundamentalContext { self.get_dc( "/v1/us/quote/financials/statements", Query { - counter_id: counter_id.into(), + counter_id: symbol_to_counter_id(&symbol.into()), kind: kind.into(), report: report.into(), }, @@ -1170,7 +1170,7 @@ impl FundamentalContext { /// US token required. Returns raw JSON. pub async fn us_key_financial_metrics( &self, - counter_id: impl Into, + symbol: impl Into, report: impl Into, ) -> Result { #[derive(Serialize)] @@ -1181,7 +1181,7 @@ impl FundamentalContext { self.get_dc( "/v1/stock-info/fin-keyfactor", Query { - counter_id: counter_id.into(), + counter_id: symbol_to_counter_id(&symbol.into()), report: report.into(), }, DcRegion::Us, @@ -1198,7 +1198,7 @@ impl FundamentalContext { /// US token required. Returns raw JSON. pub async fn us_analyst_consensus( &self, - counter_id: impl Into, + symbol: impl Into, report: impl Into, ) -> Result { #[derive(Serialize)] @@ -1209,7 +1209,7 @@ impl FundamentalContext { self.get_dc( "/v1/stock-info/fin-consensus", Query { - counter_id: counter_id.into(), + counter_id: symbol_to_counter_id(&symbol.into()), report: report.into(), }, DcRegion::Us, @@ -1224,7 +1224,7 @@ impl FundamentalContext { /// US token required. pub async fn us_etf_dividend_info( &self, - counter_id: impl Into, + symbol: impl Into, ) -> Result { #[derive(Serialize)] struct Query { @@ -1233,7 +1233,7 @@ impl FundamentalContext { self.get_dc( "/v1/stock-info/etf-dividend-info", Query { - counter_id: counter_id.into(), + counter_id: symbol_to_counter_id(&symbol.into()), }, DcRegion::Us, ) @@ -1247,7 +1247,7 @@ impl FundamentalContext { /// US token required. pub async fn us_company_dividends( &self, - counter_id: impl Into, + symbol: impl Into, ) -> Result { #[derive(Serialize)] struct Query { @@ -1256,7 +1256,7 @@ impl FundamentalContext { self.get_dc( "/v1/stock-info/company-dividends", Query { - counter_id: counter_id.into(), + counter_id: symbol_to_counter_id(&symbol.into()), }, DcRegion::Us, ) @@ -1273,7 +1273,7 @@ impl FundamentalContext { /// US token required. pub async fn us_etf_files( &self, - counter_id: impl Into, + symbol: impl Into, size: Option, ) -> Result { #[derive(Serialize)] @@ -1285,7 +1285,7 @@ impl FundamentalContext { self.get_dc( "/v1/stock-info/etf-files", Query { - counter_id: counter_id.into(), + counter_id: symbol_to_counter_id(&symbol.into()), size, }, DcRegion::Us, diff --git a/rust/src/quote/context.rs b/rust/src/quote/context.rs index a7bacbc9c4..a84bd0253f 100644 --- a/rust/src/quote/context.rs +++ b/rust/src/quote/context.rs @@ -2299,8 +2299,9 @@ impl QuoteContext { /// credentials. pub async fn us_crypto_overview( &self, - counter_id: impl Into, + symbol: impl Into, ) -> Result { + use crate::utils::counter::symbol_to_counter_id; #[derive(Serialize)] struct Query { counter_id: String, @@ -2311,7 +2312,7 @@ impl QuoteContext { .request(Method::GET, "/v1/gemini/crypto-overview") .dc_restrict(DcRegion::Us) .query_params(Query { - counter_id: counter_id.into(), + counter_id: symbol_to_counter_id(&symbol.into()), }) .response::>() .send() From a7ead43aa90df06bf4f5a5f8bb97a7e05ebe3b06 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=A2=81=E7=AB=A0=E6=B4=AA?= Date: Thu, 2 Jul 2026 18:19:31 +0800 Subject: [PATCH 14/44] fix(ci): rename counter_id to symbol in Python/Node.js US method bindings The sed rename in the previous commit left parameter declarations as counter_id while the call sites expected symbol, causing E0425 'cannot find value symbol' compile errors in Python/Node.js crates. All US binding methods now consistently use symbol: String as the parameter name, matching the Rust core convention. Co-Authored-By: Claude Sonnet 4.6 (1M context) --- nodejs/src/fundamental/context.rs | 36 ++++++++++--------------- nodejs/src/quote/context.rs | 2 +- python/src/fundamental/context.rs | 28 +++++++++---------- python/src/fundamental/context_async.rs | 30 ++++++++++----------- python/src/quote/context.rs | 2 +- python/src/quote/context_async.rs | 2 +- 6 files changed, 45 insertions(+), 55 deletions(-) diff --git a/nodejs/src/fundamental/context.rs b/nodejs/src/fundamental/context.rs index e6cbe5325c..c7e759d0bd 100644 --- a/nodejs/src/fundamental/context.rs +++ b/nodejs/src/fundamental/context.rs @@ -327,7 +327,7 @@ impl FundamentalContext { /// Get US company overview. US token required. #[napi] - pub async fn us_company_overview(&self, counter_id: String) -> Result { + pub async fn us_company_overview(&self, symbol: String) -> Result { Ok(self .ctx .us_company_overview(symbol) @@ -338,7 +338,7 @@ impl FundamentalContext { /// Get US valuation overview. US token required. #[napi] - pub async fn us_valuation_overview(&self, counter_id: String) -> Result { + pub async fn us_valuation_overview(&self, symbol: String) -> Result { Ok(self .ctx .us_valuation_overview(symbol) @@ -349,14 +349,10 @@ impl FundamentalContext { /// Get US financial overview (JSON string). US token required. #[napi] - pub async fn us_financial_overview( - &self, - counter_id: String, - report: String, - ) -> Result { + pub async fn us_financial_overview(&self, symbol: String, report: String) -> Result { let v = self .ctx - .us_financial_overview(counter_id, report) + .us_financial_overview(symbol, report) .await .map_err(ErrorNewType)?; serde_json::to_string(&v).map_err(|e| napi::Error::from_reason(e.to_string())) @@ -366,13 +362,13 @@ impl FundamentalContext { #[napi] pub async fn us_financial_statement_v3( &self, - counter_id: String, + symbol: String, kind: String, report: String, ) -> Result { Ok(self .ctx - .us_financial_statement_v3(counter_id, kind, report) + .us_financial_statement_v3(symbol, kind, report) .await .map_err(ErrorNewType)? .into()) @@ -380,14 +376,10 @@ impl FundamentalContext { /// Get US key financial metrics (JSON string). US token required. #[napi] - pub async fn us_key_financial_metrics( - &self, - counter_id: String, - report: String, - ) -> Result { + pub async fn us_key_financial_metrics(&self, symbol: String, report: String) -> Result { let v = self .ctx - .us_key_financial_metrics(counter_id, report) + .us_key_financial_metrics(symbol, report) .await .map_err(ErrorNewType)?; serde_json::to_string(&v).map_err(|e| napi::Error::from_reason(e.to_string())) @@ -395,10 +387,10 @@ impl FundamentalContext { /// Get US analyst consensus (JSON string). US token required. #[napi] - pub async fn us_analyst_consensus(&self, counter_id: String, report: String) -> Result { + pub async fn us_analyst_consensus(&self, symbol: String, report: String) -> Result { let v = self .ctx - .us_analyst_consensus(counter_id, report) + .us_analyst_consensus(symbol, report) .await .map_err(ErrorNewType)?; serde_json::to_string(&v).map_err(|e| napi::Error::from_reason(e.to_string())) @@ -406,7 +398,7 @@ impl FundamentalContext { /// Get US ETF dividend history. US token required. #[napi] - pub async fn us_etf_dividend_info(&self, counter_id: String) -> Result { + pub async fn us_etf_dividend_info(&self, symbol: String) -> Result { Ok(self .ctx .us_etf_dividend_info(symbol) @@ -417,7 +409,7 @@ impl FundamentalContext { /// Get US company dividends. US token required. #[napi] - pub async fn us_company_dividends(&self, counter_id: String) -> Result { + pub async fn us_company_dividends(&self, symbol: String) -> Result { Ok(self .ctx .us_company_dividends(symbol) @@ -430,12 +422,12 @@ impl FundamentalContext { #[napi] pub async fn us_etf_files( &self, - counter_id: String, + symbol: String, size: Option, ) -> Result { Ok(self .ctx - .us_etf_files(counter_id, size) + .us_etf_files(symbol, size) .await .map_err(ErrorNewType)? .into()) diff --git a/nodejs/src/quote/context.rs b/nodejs/src/quote/context.rs index 052e86acdc..5cf1f831d4 100644 --- a/nodejs/src/quote/context.rs +++ b/nodejs/src/quote/context.rs @@ -1293,7 +1293,7 @@ impl QuoteContext { /// Get US cryptocurrency market overview. US token required. #[napi] - pub async fn us_crypto_overview(&self, counter_id: String) -> Result { + pub async fn us_crypto_overview(&self, symbol: String) -> Result { Ok(self .ctx .us_crypto_overview(symbol) diff --git a/python/src/fundamental/context.rs b/python/src/fundamental/context.rs index 6d64dc618f..f165f53078 100644 --- a/python/src/fundamental/context.rs +++ b/python/src/fundamental/context.rs @@ -245,7 +245,7 @@ impl FundamentalContext { // ── US-market methods ───────────────────────────────────────────────────── /// Get US company overview. US token required. - fn us_company_overview(&self, counter_id: String) -> PyResult { + fn us_company_overview(&self, symbol: String) -> PyResult { Ok(self .ctx .us_company_overview(symbol) @@ -254,7 +254,7 @@ impl FundamentalContext { } /// Get US valuation overview. US token required. - fn us_valuation_overview(&self, counter_id: String) -> PyResult { + fn us_valuation_overview(&self, symbol: String) -> PyResult { Ok(self .ctx .us_valuation_overview(symbol) @@ -267,12 +267,12 @@ impl FundamentalContext { fn us_financial_overview( &self, py: Python<'_>, - counter_id: String, + symbol: String, report: String, ) -> PyResult> { let v = self .ctx - .us_financial_overview(counter_id, report) + .us_financial_overview(symbol, report) .map_err(ErrorNewType)?; pythonize::pythonize(py, &v) .map(|b| b.unbind()) @@ -283,13 +283,13 @@ impl FundamentalContext { /// required. fn us_financial_statement_v3( &self, - counter_id: String, + symbol: String, kind: String, report: String, ) -> PyResult { Ok(self .ctx - .us_financial_statement_v3(counter_id, kind, report) + .us_financial_statement_v3(symbol, kind, report) .map_err(ErrorNewType)? .into()) } @@ -298,12 +298,12 @@ impl FundamentalContext { fn us_key_financial_metrics( &self, py: Python<'_>, - counter_id: String, + symbol: String, report: String, ) -> PyResult> { let v = self .ctx - .us_key_financial_metrics(counter_id, report) + .us_key_financial_metrics(symbol, report) .map_err(ErrorNewType)?; pythonize::pythonize(py, &v) .map(|b| b.unbind()) @@ -314,12 +314,12 @@ impl FundamentalContext { fn us_analyst_consensus( &self, py: Python<'_>, - counter_id: String, + symbol: String, report: String, ) -> PyResult> { let v = self .ctx - .us_analyst_consensus(counter_id, report) + .us_analyst_consensus(symbol, report) .map_err(ErrorNewType)?; pythonize::pythonize(py, &v) .map(|b| b.unbind()) @@ -327,7 +327,7 @@ impl FundamentalContext { } /// Get US ETF dividend history. US token required. - fn us_etf_dividend_info(&self, counter_id: String) -> PyResult { + fn us_etf_dividend_info(&self, symbol: String) -> PyResult { Ok(self .ctx .us_etf_dividend_info(symbol) @@ -336,7 +336,7 @@ impl FundamentalContext { } /// Get US company historical dividends. US token required. - fn us_company_dividends(&self, counter_id: String) -> PyResult { + fn us_company_dividends(&self, symbol: String) -> PyResult { Ok(self .ctx .us_company_dividends(symbol) @@ -345,10 +345,10 @@ impl FundamentalContext { } /// Get US ETF document list. `size`: None = all. US token required. - fn us_etf_files(&self, counter_id: String, size: Option) -> PyResult { + fn us_etf_files(&self, symbol: String, size: Option) -> PyResult { Ok(self .ctx - .us_etf_files(counter_id, size) + .us_etf_files(symbol, size) .map_err(ErrorNewType)? .into()) } diff --git a/python/src/fundamental/context_async.rs b/python/src/fundamental/context_async.rs index 300adace98..ff54ca653f 100644 --- a/python/src/fundamental/context_async.rs +++ b/python/src/fundamental/context_async.rs @@ -365,7 +365,7 @@ impl AsyncFundamentalContext { // ── US-market async methods ─────────────────────────────────────────────── /// Get US company overview. US token required. Returns awaitable. - fn us_company_overview(&self, py: Python<'_>, counter_id: String) -> PyResult> { + fn us_company_overview(&self, py: Python<'_>, symbol: String) -> PyResult> { let ctx = self.ctx.clone(); pyo3_async_runtimes::tokio::future_into_py(py, async move { Ok(USCompanyOverview::from( @@ -378,7 +378,7 @@ impl AsyncFundamentalContext { } /// Get US valuation overview. US token required. Returns awaitable. - fn us_valuation_overview(&self, py: Python<'_>, counter_id: String) -> PyResult> { + fn us_valuation_overview(&self, py: Python<'_>, symbol: String) -> PyResult> { let ctx = self.ctx.clone(); pyo3_async_runtimes::tokio::future_into_py(py, async move { Ok(USValuationOverview::from( @@ -394,13 +394,13 @@ impl AsyncFundamentalContext { fn us_financial_overview( &self, py: Python<'_>, - counter_id: String, + symbol: String, report: String, ) -> PyResult> { let ctx = self.ctx.clone(); pyo3_async_runtimes::tokio::future_into_py(py, async move { let v = ctx - .us_financial_overview(counter_id, report) + .us_financial_overview(symbol, report) .await .map_err(ErrorNewType)?; Python::attach(|py| { @@ -416,14 +416,14 @@ impl AsyncFundamentalContext { fn us_financial_statement_v3( &self, py: Python<'_>, - counter_id: String, + symbol: String, kind: String, report: String, ) -> PyResult> { let ctx = self.ctx.clone(); pyo3_async_runtimes::tokio::future_into_py(py, async move { Ok(USFinancialStatement::from( - ctx.us_financial_statement_v3(counter_id, kind, report) + ctx.us_financial_statement_v3(symbol, kind, report) .await .map_err(ErrorNewType)?, )) @@ -435,13 +435,13 @@ impl AsyncFundamentalContext { fn us_key_financial_metrics( &self, py: Python<'_>, - counter_id: String, + symbol: String, report: String, ) -> PyResult> { let ctx = self.ctx.clone(); pyo3_async_runtimes::tokio::future_into_py(py, async move { let v = ctx - .us_key_financial_metrics(counter_id, report) + .us_key_financial_metrics(symbol, report) .await .map_err(ErrorNewType)?; Python::attach(|py| { @@ -457,13 +457,13 @@ impl AsyncFundamentalContext { fn us_analyst_consensus( &self, py: Python<'_>, - counter_id: String, + symbol: String, report: String, ) -> PyResult> { let ctx = self.ctx.clone(); pyo3_async_runtimes::tokio::future_into_py(py, async move { let v = ctx - .us_analyst_consensus(counter_id, report) + .us_analyst_consensus(symbol, report) .await .map_err(ErrorNewType)?; Python::attach(|py| { @@ -476,7 +476,7 @@ impl AsyncFundamentalContext { } /// Get US ETF dividend info. Returns awaitable. - fn us_etf_dividend_info(&self, py: Python<'_>, counter_id: String) -> PyResult> { + fn us_etf_dividend_info(&self, py: Python<'_>, symbol: String) -> PyResult> { let ctx = self.ctx.clone(); pyo3_async_runtimes::tokio::future_into_py(py, async move { Ok(USETFDividendInfo::from( @@ -489,7 +489,7 @@ impl AsyncFundamentalContext { } /// Get US company dividends. Returns awaitable. - fn us_company_dividends(&self, py: Python<'_>, counter_id: String) -> PyResult> { + fn us_company_dividends(&self, py: Python<'_>, symbol: String) -> PyResult> { let ctx = self.ctx.clone(); pyo3_async_runtimes::tokio::future_into_py(py, async move { Ok(USCompanyDividends::from( @@ -505,15 +505,13 @@ impl AsyncFundamentalContext { fn us_etf_files( &self, py: Python<'_>, - counter_id: String, + symbol: String, size: Option, ) -> PyResult> { let ctx = self.ctx.clone(); pyo3_async_runtimes::tokio::future_into_py(py, async move { Ok(USETFFilesResponse::from( - ctx.us_etf_files(counter_id, size) - .await - .map_err(ErrorNewType)?, + ctx.us_etf_files(symbol, size).await.map_err(ErrorNewType)?, )) }) .map(|b| b.unbind()) diff --git a/python/src/quote/context.rs b/python/src/quote/context.rs index de4791b979..a87ee29be4 100644 --- a/python/src/quote/context.rs +++ b/python/src/quote/context.rs @@ -691,7 +691,7 @@ impl QuoteContext { /// Get US cryptocurrency market overview. US token required. fn us_crypto_overview( &self, - counter_id: String, + symbol: String, ) -> PyResult { Ok(self .ctx diff --git a/python/src/quote/context_async.rs b/python/src/quote/context_async.rs index 4c5b21d908..7d6e3cbe9e 100644 --- a/python/src/quote/context_async.rs +++ b/python/src/quote/context_async.rs @@ -898,7 +898,7 @@ impl AsyncQuoteContext { /// Get US cryptocurrency market overview. US token required. Returns /// awaitable. - fn us_crypto_overview(&self, py: Python<'_>, counter_id: String) -> PyResult> { + fn us_crypto_overview(&self, py: Python<'_>, symbol: String) -> PyResult> { let ctx = self.ctx.clone(); pyo3_async_runtimes::tokio::future_into_py(py, async move { let r: crate::quote::types::USCryptoOverview = ctx From 493118c8bbefc6f6a40ae0852080e58f914f84c8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=A2=81=E7=AB=A0=E6=B4=AA?= Date: Thu, 2 Jul 2026 18:41:15 +0800 Subject: [PATCH 15/44] fix: correct CryptoOverview path to /v1/gemini/us/crypto-overview Co-Authored-By: Claude Sonnet 4.6 (1M context) --- rust/src/quote/context.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/rust/src/quote/context.rs b/rust/src/quote/context.rs index a84bd0253f..647ce61dcb 100644 --- a/rust/src/quote/context.rs +++ b/rust/src/quote/context.rs @@ -2292,7 +2292,7 @@ impl QuoteContext { /// /// `counter_id`: crypto counter_id, e.g. `"CY/US/BTC"`. /// - /// Path: `GET /v1/gemini/crypto-overview` + /// Path: `GET /v1/gemini/us/crypto-overview` /// /// US token required — returns /// [`longbridge_httpcli::HttpClientError::DcRegionRestricted`] for non-US @@ -2309,7 +2309,7 @@ impl QuoteContext { Ok(self .0 .http_cli - .request(Method::GET, "/v1/gemini/crypto-overview") + .request(Method::GET, "/v1/gemini/us/crypto-overview") .dc_restrict(DcRegion::Us) .query_params(Query { counter_id: symbol_to_counter_id(&symbol.into()), From 60de0b6b3e49d162de8e5dfdc3cc2f634c3297a7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=A2=81=E7=AB=A0=E6=B4=AA?= Date: Thu, 2 Jul 2026 19:05:34 +0800 Subject: [PATCH 16/44] fix: CryptoOverview accepts BTCUSD/BTC/USD, converts to VA/HAS/BTCUSD Co-Authored-By: Claude Sonnet 4.6 (1M context) --- rust/src/quote/context.rs | 16 +++++++++++++--- 1 file changed, 13 insertions(+), 3 deletions(-) diff --git a/rust/src/quote/context.rs b/rust/src/quote/context.rs index 647ce61dcb..3dc9894780 100644 --- a/rust/src/quote/context.rs +++ b/rust/src/quote/context.rs @@ -2290,7 +2290,9 @@ impl QuoteContext { /// Get cryptocurrency market overview. /// - /// `counter_id`: crypto counter_id, e.g. `"CY/US/BTC"`. + /// `symbol` accepts user-facing trading-pair format: `"BTCUSD"` or + /// `"BTC/USD"`. Both are converted to the internal `VA/HAS/BTCUSDT` + /// counter_id automatically (`/` removed, `USD` → `USDT`). /// /// Path: `GET /v1/gemini/us/crypto-overview` /// @@ -2301,7 +2303,6 @@ impl QuoteContext { &self, symbol: impl Into, ) -> Result { - use crate::utils::counter::symbol_to_counter_id; #[derive(Serialize)] struct Query { counter_id: String, @@ -2312,7 +2313,7 @@ impl QuoteContext { .request(Method::GET, "/v1/gemini/us/crypto-overview") .dc_restrict(DcRegion::Us) .query_params(Query { - counter_id: symbol_to_counter_id(&symbol.into()), + counter_id: crypto_symbol_to_counter_id(&symbol.into()), }) .response::>() .send() @@ -2328,3 +2329,12 @@ fn normalize_symbol(symbol: &str) -> &str { _ => symbol, } } + +/// Convert a user-facing crypto trading-pair symbol to the internal +/// `VA/HAS/…` counter_id. +/// +/// Accepts `"BTCUSD"` or `"BTC/USD"` → `"VA/HAS/BTCUSD"`. +/// Only the `/` separator is removed; no other transformation is applied. +fn crypto_symbol_to_counter_id(symbol: &str) -> String { + format!("VA/HAS/{}", symbol.replace('/', "")) +} From 2050c941142f962dd2e5be9afbc5767fb8d0d8a4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=A2=81=E7=AB=A0=E6=B4=AA?= Date: Thu, 2 Jul 2026 19:15:39 +0800 Subject: [PATCH 17/44] =?UTF-8?q?feat:=20CryptoOverview=20supports=20BTCUS?= =?UTF-8?q?D.HAS=20=E2=86=92=20VA/HAS/BTCUSD=20format?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Sonnet 4.6 (1M context) --- rust/src/quote/context.rs | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/rust/src/quote/context.rs b/rust/src/quote/context.rs index 3dc9894780..954109bdcd 100644 --- a/rust/src/quote/context.rs +++ b/rust/src/quote/context.rs @@ -2330,11 +2330,18 @@ fn normalize_symbol(symbol: &str) -> &str { } } -/// Convert a user-facing crypto trading-pair symbol to the internal -/// `VA/HAS/…` counter_id. +/// Convert a user-facing crypto symbol to the internal `VA/{EXCHANGE}/{PAIR}` +/// counter_id. /// -/// Accepts `"BTCUSD"` or `"BTC/USD"` → `"VA/HAS/BTCUSD"`. -/// Only the `/` separator is removed; no other transformation is applied. +/// Supported formats: +/// - `"BTCUSD.HAS"` → `"VA/HAS/BTCUSD"` (`{PAIR}.{EXCHANGE}`) +/// - `"BTCUSD"` → `"VA/HAS/BTCUSD"` (default exchange `HAS`) +/// - `"BTC/USD"` → `"VA/HAS/BTCUSD"` (slash removed, default exchange `HAS`) fn crypto_symbol_to_counter_id(symbol: &str) -> String { + if let Some(dot) = symbol.rfind('.') { + let pair = symbol[..dot].replace('/', ""); + let exchange = symbol[dot + 1..].to_uppercase(); + return format!("VA/{}/{}", exchange, pair); + } format!("VA/HAS/{}", symbol.replace('/', "")) } From c905912535e9d702c39e769b515032abc06d2980 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=A2=81=E7=AB=A0=E6=B4=AA?= Date: Thu, 2 Jul 2026 19:22:06 +0800 Subject: [PATCH 18/44] refactor: CryptoOverview only accepts PAIR.EXCHANGE format (e.g. BTCUSD.HAS) Co-Authored-By: Claude Sonnet 4.6 (1M context) --- rust/src/quote/context.rs | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) diff --git a/rust/src/quote/context.rs b/rust/src/quote/context.rs index 954109bdcd..334722f8f4 100644 --- a/rust/src/quote/context.rs +++ b/rust/src/quote/context.rs @@ -2330,18 +2330,16 @@ fn normalize_symbol(symbol: &str) -> &str { } } -/// Convert a user-facing crypto symbol to the internal `VA/{EXCHANGE}/{PAIR}` -/// counter_id. +/// Convert a crypto symbol in `PAIR.EXCHANGE` format to the internal +/// `VA/{EXCHANGE}/{PAIR}` counter_id. /// -/// Supported formats: -/// - `"BTCUSD.HAS"` → `"VA/HAS/BTCUSD"` (`{PAIR}.{EXCHANGE}`) -/// - `"BTCUSD"` → `"VA/HAS/BTCUSD"` (default exchange `HAS`) -/// - `"BTC/USD"` → `"VA/HAS/BTCUSD"` (slash removed, default exchange `HAS`) +/// Example: `"BTCUSD.HAS"` → `"VA/HAS/BTCUSD"`. +/// If no `.EXCHANGE` suffix is present the value is passed through unchanged. fn crypto_symbol_to_counter_id(symbol: &str) -> String { if let Some(dot) = symbol.rfind('.') { - let pair = symbol[..dot].replace('/', ""); + let pair = &symbol[..dot]; let exchange = symbol[dot + 1..].to_uppercase(); return format!("VA/{}/{}", exchange, pair); } - format!("VA/HAS/{}", symbol.replace('/', "")) + symbol.to_string() } From f7e718efd345bbad4a018c6fe23034df73421c2b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=A2=81=E7=AB=A0=E6=B4=AA?= Date: Thu, 2 Jul 2026 19:24:39 +0800 Subject: [PATCH 19/44] =?UTF-8?q?refactor:=20consolidate=20crypto=20symbol?= =?UTF-8?q?=E2=86=94counter=5Fid=20into=20symbol=5Fto=5Fcounter=5Fid/count?= =?UTF-8?q?er=5Fid=5Fto=5Fsymbol?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - symbol_to_counter_id: BTCUSD.HAS → VA/HAS/BTCUSD - counter_id_to_symbol: VA/HAS/BTCUSD → BTCUSD.HAS - us_crypto_overview uses symbol_to_counter_id - Remove crypto_symbol_to_counter_id Co-Authored-By: Claude Sonnet 4.6 (1M context) --- rust/src/quote/context.rs | 23 +++++------------------ rust/src/utils/counter.rs | 34 ++++++++++++++++++++++------------ 2 files changed, 27 insertions(+), 30 deletions(-) diff --git a/rust/src/quote/context.rs b/rust/src/quote/context.rs index 334722f8f4..0e24025a14 100644 --- a/rust/src/quote/context.rs +++ b/rust/src/quote/context.rs @@ -2290,9 +2290,9 @@ impl QuoteContext { /// Get cryptocurrency market overview. /// - /// `symbol` accepts user-facing trading-pair format: `"BTCUSD"` or - /// `"BTC/USD"`. Both are converted to the internal `VA/HAS/BTCUSDT` - /// counter_id automatically (`/` removed, `USD` → `USDT`). + /// `symbol` must be in `PAIR.EXCHANGE` format, e.g. `"BTCUSD.HAS"` → + /// `"VA/HAS/BTCUSD"`. Uses `symbol_to_counter_id`, consistent with all + /// other symbol-based methods. /// /// Path: `GET /v1/gemini/us/crypto-overview` /// @@ -2303,6 +2303,7 @@ impl QuoteContext { &self, symbol: impl Into, ) -> Result { + use crate::utils::counter::symbol_to_counter_id; #[derive(Serialize)] struct Query { counter_id: String, @@ -2313,7 +2314,7 @@ impl QuoteContext { .request(Method::GET, "/v1/gemini/us/crypto-overview") .dc_restrict(DcRegion::Us) .query_params(Query { - counter_id: crypto_symbol_to_counter_id(&symbol.into()), + counter_id: symbol_to_counter_id(&symbol.into()), }) .response::>() .send() @@ -2329,17 +2330,3 @@ fn normalize_symbol(symbol: &str) -> &str { _ => symbol, } } - -/// Convert a crypto symbol in `PAIR.EXCHANGE` format to the internal -/// `VA/{EXCHANGE}/{PAIR}` counter_id. -/// -/// Example: `"BTCUSD.HAS"` → `"VA/HAS/BTCUSD"`. -/// If no `.EXCHANGE` suffix is present the value is passed through unchanged. -fn crypto_symbol_to_counter_id(symbol: &str) -> String { - if let Some(dot) = symbol.rfind('.') { - let pair = &symbol[..dot]; - let exchange = symbol[dot + 1..].to_uppercase(); - return format!("VA/{}/{}", exchange, pair); - } - symbol.to_string() -} diff --git a/rust/src/utils/counter.rs b/rust/src/utils/counter.rs index beacec3e01..4e9c9c8f34 100644 --- a/rust/src/utils/counter.rs +++ b/rust/src/utils/counter.rs @@ -148,22 +148,27 @@ pub fn lookup_counter_id(symbol: &str) -> Option { /// Leading-dot symbols (e.g. `.DJI.US`) are US market indexes and always map /// to `IX/`. All other symbols are checked against the embedded /// ETF + index + warrant set and the remote-resolved cache; a matching entry -/// is returned as-is. Unmatched symbols default to `ST/`. +/// is returned as-is. Crypto symbols in `PAIR.EXCHANGE` format (non-standard +/// market suffixes such as `HAS`) are converted to `VA/{EXCHANGE}/{PAIR}`. +/// Unmatched symbols default to `ST/`. pub fn symbol_to_counter_id(symbol: &str) -> String { if let Some((code, market)) = symbol.rsplit_once('.') { if let Some(counter_id) = lookup_counter_id(symbol) { return counter_id; } - let market = market.to_uppercase(); + let market_upper = market.to_uppercase(); + // Non-standard market suffixes are crypto exchange identifiers. + match market_upper.as_str() { + "US" | "HK" | "CN" | "SH" | "SZ" | "A" | "B" => {} + _ => return format!("VA/{market_upper}/{code}"), + } // Strip leading zeros from numeric HK codes (e.g. `00700` → `700`). - // Other markets keep their codes verbatim (A-share codes such as - // `000001.SZ` have significant leading zeros). - let code = if market == "HK" && code.chars().all(|c| c.is_ascii_digit()) { + let code = if market_upper == "HK" && code.chars().all(|c| c.is_ascii_digit()) { code.trim_start_matches('0') } else { code }; - format!("ST/{market}/{code}") + format!("ST/{market_upper}/{code}") } else { symbol.to_string() } @@ -179,16 +184,21 @@ pub fn index_symbol_to_counter_id(symbol: &str) -> String { } } -/// Convert a counter_id (e.g. `ST/US/TSLA`, `ETF/US/SPY`, `IX/US/.DJI`, -/// `ST/HK/700`) back to a display symbol (e.g. `TSLA.US`, `SPY.US`, -/// `.DJI.US`, `700.HK`). +/// Convert a counter_id back to a display symbol. /// -/// US index counter IDs (`IX/US/...`) preserve the leading dot in the code -/// part (e.g. `IX/US/.DJI` → `.DJI.US`). +/// - `ST/US/TSLA` → `TSLA.US` +/// - `ETF/US/SPY` → `SPY.US` +/// - `IX/US/.DJI` → `.DJI.US` +/// - `VA/HAS/BTCUSD` → `BTCUSD.HAS` (crypto: `PAIR.EXCHANGE`) pub fn counter_id_to_symbol(counter_id: &str) -> String { let parts: Vec<&str> = counter_id.splitn(3, '/').collect(); if parts.len() == 3 { - format!("{}.{}", parts[2], parts[1]) + let (prefix, market, code) = (parts[0], parts[1], parts[2]); + if prefix.eq_ignore_ascii_case("VA") { + // Crypto: return PAIR.EXCHANGE format + return format!("{code}.{market}"); + } + format!("{code}.{market}") } else { counter_id.to_string() } From cf4e97799c412170f31ddca9a542204a90f21d3d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=A2=81=E7=AB=A0=E6=B4=AA?= Date: Thu, 2 Jul 2026 19:26:05 +0800 Subject: [PATCH 20/44] fix: detect crypto symbols by CRYPTO_EXCHANGES list (HAS) not market exclusion Co-Authored-By: Claude Sonnet 4.6 (1M context) --- rust/src/utils/counter.rs | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) diff --git a/rust/src/utils/counter.rs b/rust/src/utils/counter.rs index 4e9c9c8f34..3230a48255 100644 --- a/rust/src/utils/counter.rs +++ b/rust/src/utils/counter.rs @@ -147,9 +147,13 @@ pub fn lookup_counter_id(symbol: &str) -> Option { /// /// Leading-dot symbols (e.g. `.DJI.US`) are US market indexes and always map /// to `IX/`. All other symbols are checked against the embedded +/// Known crypto exchange identifiers used as symbol suffixes. +/// `"BTCUSD.HAS"` → `"VA/HAS/BTCUSD"`. +const CRYPTO_EXCHANGES: &[&str] = &["HAS"]; + /// ETF + index + warrant set and the remote-resolved cache; a matching entry -/// is returned as-is. Crypto symbols in `PAIR.EXCHANGE` format (non-standard -/// market suffixes such as `HAS`) are converted to `VA/{EXCHANGE}/{PAIR}`. +/// is returned as-is. Crypto symbols whose suffix matches a known exchange +/// (e.g. `HAS`) are converted to `VA/{EXCHANGE}/{PAIR}`. /// Unmatched symbols default to `ST/`. pub fn symbol_to_counter_id(symbol: &str) -> String { if let Some((code, market)) = symbol.rsplit_once('.') { @@ -157,10 +161,12 @@ pub fn symbol_to_counter_id(symbol: &str) -> String { return counter_id; } let market_upper = market.to_uppercase(); - // Non-standard market suffixes are crypto exchange identifiers. - match market_upper.as_str() { - "US" | "HK" | "CN" | "SH" | "SZ" | "A" | "B" => {} - _ => return format!("VA/{market_upper}/{code}"), + // Known crypto exchange suffix → VA/{EXCHANGE}/{PAIR} + if CRYPTO_EXCHANGES + .iter() + .any(|e| e.eq_ignore_ascii_case(&market_upper)) + { + return format!("VA/{market_upper}/{code}"); } // Strip leading zeros from numeric HK codes (e.g. `00700` → `700`). let code = if market_upper == "HK" && code.chars().all(|c| c.is_ascii_digit()) { From 67d54f9ec4c727d345e0ae2c8572e5224256d460 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=A2=81=E7=AB=A0=E6=B4=AA?= Date: Fri, 3 Jul 2026 09:59:52 +0800 Subject: [PATCH 21/44] feat: add BKKT to CRYPTO_EXCHANGES alongside HAS Co-Authored-By: Claude Sonnet 4.6 (1M context) --- rust/src/utils/counter.rs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/rust/src/utils/counter.rs b/rust/src/utils/counter.rs index 3230a48255..6ac965ad03 100644 --- a/rust/src/utils/counter.rs +++ b/rust/src/utils/counter.rs @@ -148,8 +148,9 @@ pub fn lookup_counter_id(symbol: &str) -> Option { /// Leading-dot symbols (e.g. `.DJI.US`) are US market indexes and always map /// to `IX/`. All other symbols are checked against the embedded /// Known crypto exchange identifiers used as symbol suffixes. -/// `"BTCUSD.HAS"` → `"VA/HAS/BTCUSD"`. -const CRYPTO_EXCHANGES: &[&str] = &["HAS"]; +/// e.g. `"BTCUSD.HAS"` → `"VA/HAS/BTCUSD"`, `"BTCUSD.BKKT"` → +/// `"VA/BKKT/BTCUSD"`. +const CRYPTO_EXCHANGES: &[&str] = &["HAS", "BKKT"]; /// ETF + index + warrant set and the remote-resolved cache; a matching entry /// is returned as-is. Crypto symbols whose suffix matches a known exchange From 92515e5b9568a43e7952d5e2bc6a0d3a946a9ea4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=A2=81=E7=AB=A0=E6=B4=AA?= Date: Fri, 3 Jul 2026 10:00:21 +0800 Subject: [PATCH 22/44] feat: add OSL to CRYPTO_EXCHANGES (HAS, OSL, BKKT) Co-Authored-By: Claude Sonnet 4.6 (1M context) --- rust/src/utils/counter.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/rust/src/utils/counter.rs b/rust/src/utils/counter.rs index 6ac965ad03..cebce4d166 100644 --- a/rust/src/utils/counter.rs +++ b/rust/src/utils/counter.rs @@ -150,7 +150,7 @@ pub fn lookup_counter_id(symbol: &str) -> Option { /// Known crypto exchange identifiers used as symbol suffixes. /// e.g. `"BTCUSD.HAS"` → `"VA/HAS/BTCUSD"`, `"BTCUSD.BKKT"` → /// `"VA/BKKT/BTCUSD"`. -const CRYPTO_EXCHANGES: &[&str] = &["HAS", "BKKT"]; +const CRYPTO_EXCHANGES: &[&str] = &["HAS", "OSL", "BKKT"]; /// ETF + index + warrant set and the remote-resolved cache; a matching entry /// is returned as-is. Crypto symbols whose suffix matches a known exchange From 0822fc961c1e536992bf943482acbd8fde358917 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=A2=81=E7=AB=A0=E6=B4=AA?= Date: Fri, 3 Jul 2026 10:06:27 +0800 Subject: [PATCH 23/44] fix: update USAssetOverview and USRealizedPL types to match real API response - USAssetOverview: cash_list/crypto_list (was positions/buy_power) - USRealizedPL: realized_pl_list[]{category,metrics} (was items[]) - Remove USStockPosition/USOptionPosition/USCryptoPosition/USBuyPower/USRealizedPLItem Co-Authored-By: Claude Sonnet 4.6 (1M context) --- rust/src/lib.rs | 4 +- rust/src/trade/mod.rs | 9 ++- rust/src/trade/types.rs | 151 ++++++++++++---------------------------- 3 files changed, 51 insertions(+), 113 deletions(-) diff --git a/rust/src/lib.rs b/rust/src/lib.rs index f9a0e2d9eb..f8e54e7d86 100644 --- a/rust/src/lib.rs +++ b/rust/src/lib.rs @@ -60,7 +60,7 @@ pub use screener::ScreenerContext; pub use sharelist::SharelistContext; pub use trade::{ QueryUSOrdersOptions, QueryUSOrdersResponse, TradeContext, USAssetOverview, USAttachedOrder, - USBuyPower, USCryptoPosition, USOptionPosition, USOrderDetailResponse, USRealizedPL, - USRealizedPLItem, USStockPosition, + USCashEntry, USCryptoEntry, USOrderDetailResponse, USRealizedPL, USRealizedPLEntry, + USRealizedPLMetric, }; pub use types::Market; diff --git a/rust/src/trade/mod.rs b/rust/src/trade/mod.rs index 52a755500d..d4383b5ed4 100644 --- a/rust/src/trade/mod.rs +++ b/rust/src/trade/mod.rs @@ -51,11 +51,10 @@ pub use types::{ TriggerStatus, USAssetOverview, USAttachedOrder, - USBuyPower, - USCryptoPosition, - USOptionPosition, + USCashEntry, + USCryptoEntry, USOrderDetailResponse, USRealizedPL, - USRealizedPLItem, - USStockPosition, + USRealizedPLEntry, + USRealizedPLMetric, }; diff --git a/rust/src/trade/types.rs b/rust/src/trade/types.rs index 6e725641d3..72085d4c0d 100644 --- a/rust/src/trade/types.rs +++ b/rust/src/trade/types.rs @@ -878,150 +878,89 @@ pub struct USOrderDetailResponse { pub attached_orders: Vec, } -/// A stock position in a US account. -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct USStockPosition { - #[serde(default)] - pub symbol: String, - #[serde(default)] - pub name: String, - #[serde(default)] - pub quantity: String, - #[serde(default)] - pub available_quantity: String, +/// One cash currency entry in [`USAssetOverview`]. +#[derive(Debug, Clone, Serialize, Deserialize, Default)] +pub struct USCashEntry { #[serde(default)] pub currency: String, #[serde(default)] - pub cost_price: String, + pub frozen_buy_cash: String, #[serde(default)] - pub market_value: String, + pub outstanding: String, #[serde(default)] - pub unrealized_pl: String, + pub settled_cash: String, #[serde(default)] - pub unrealized_pl_ratio: String, + pub total_amount: String, #[serde(default)] - pub last_done: String, - #[serde(default)] - pub prev_close: String, - #[serde(default)] - pub change_rate: String, - /// Overnight/night-session last price (US-specific) - #[serde(default)] - pub night_last_done: String, - /// Pre-market close price (US-specific) - #[serde(default)] - pub pretrade_close: String, - /// Trading session status (US-specific) - #[serde(default)] - pub trade_status: String, - /// Individual quantity after multi-leg exclusion (US-specific) - #[serde(default)] - pub individual_quantity: String, -} - -/// An option position in a US account. -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct USOptionPosition { - #[serde(default)] - pub symbol: String, - #[serde(default)] - pub strike_price: String, - #[serde(default)] - pub due_date: String, - #[serde(default)] - pub contract_multiplier: i32, - #[serde(default, rename = "type")] - pub option_type: String, - #[serde(default)] - pub quantity: String, - #[serde(default)] - pub market_value: String, - #[serde(default)] - pub unrealized_pl: String, -} - -/// A cryptocurrency position in a US account. -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct USCryptoPosition { - #[serde(default)] - pub symbol: String, - #[serde(default)] - pub quantity: String, - #[serde(default)] - pub market_value: String, - #[serde(default)] - pub unrealized_pl: String, - #[serde(default)] - pub cost_price: String, + pub total_cash: String, } -/// Purchasing power breakdown for a US account. +/// One cryptocurrency holding in [`USAssetOverview`]. #[derive(Debug, Clone, Serialize, Deserialize, Default)] -pub struct USBuyPower { +pub struct USCryptoEntry { #[serde(default)] - pub cash_buy_power: String, + pub asset_type: String, #[serde(default)] - pub overnight_buy_power: String, - /// Day-trade buying power (margin accounts only) + pub average_cost: String, + /// Internal counter_id, e.g. `"VA/BKKT/BTCUSD"`. #[serde(default)] - pub day_trade_buy_power: String, + pub counter_id: String, + #[serde(default)] + pub currency: String, #[serde(default)] - pub option_buy_power: String, + pub industry_counter_id: String, #[serde(default)] - pub crypto_buy_power: String, + pub industry_name: String, } /// Response for [`crate::TradeContext::us_asset_overview`]. -#[derive(Debug, Clone, Serialize, Deserialize)] +/// Field names match the actual API response from `GET /v1/us/assets/overview`. +#[derive(Debug, Clone, Serialize, Deserialize, Default)] pub struct USAssetOverview { #[serde(default)] pub account_type: String, #[serde(default)] - pub net_assets: String, - #[serde(default)] - pub total_cash: String, - #[serde(default)] - pub unrealized_pl: String, - #[serde(default)] - pub positions: Vec, - #[serde(default)] - pub option_positions: Vec, + pub asset_timestamp: String, + /// Cash buying power (top-level convenience field). #[serde(default)] - pub multi_legs: Vec, + pub cash_buy_power: String, #[serde(default)] - pub crypto_positions: Vec, + pub cash_list: Vec, #[serde(default)] - pub buy_power: USBuyPower, + pub crypto_list: Vec, } -/// A single realized P&L entry. -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct USRealizedPLItem { - #[serde(default)] - pub symbol: String, +/// One time-period metric in a [`USRealizedPLEntry`]. +#[derive(Debug, Clone, Serialize, Deserialize, Default)] +pub struct USRealizedPLMetric { #[serde(default)] - pub name: String, + pub amount: String, + /// Period code (server-defined; 2 = current month observed in testing). #[serde(default)] - pub category: String, + pub period: i32, #[serde(default)] - pub realized_pl: String, + pub rate: String, +} + +/// One asset-category entry in [`USRealizedPL`]. +/// `category`: 0 = all, 1 = stock, 2 = option, 3 = crypto (server-defined). +#[derive(Debug, Clone, Serialize, Deserialize, Default)] +pub struct USRealizedPLEntry { #[serde(default)] - pub quantity_sold: String, + pub category: i32, #[serde(default)] - pub avg_cost: String, + pub currency: String, #[serde(default)] - pub avg_sell_price: String, + pub metrics: Vec, } /// Response for [`crate::TradeContext::us_realized_pl`]. -#[derive(Debug, Clone, Serialize, Deserialize)] +/// Field name matches the actual API response from `GET +/// /v1/us/assets/pl/realized`. +#[derive(Debug, Clone, Serialize, Deserialize, Default)] pub struct USRealizedPL { #[serde(default)] - pub total_realized_pl: String, - #[serde(default)] - pub currency: String, - #[serde(default)] - pub items: Vec, + pub realized_pl_list: Vec, } #[cfg(test)] From b6ddb1f7ea30970305ad411c3af5fd3b4155b6e6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=A2=81=E7=AB=A0=E6=B4=AA?= Date: Fri, 3 Jul 2026 10:10:29 +0800 Subject: [PATCH 24/44] fix: update USAssetOverview and USRealizedPL types to match real API response MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - USAssetOverview: account_type, asset_timestamp, cash_buy_power, cash_list (USCashEntry), crypto_list (USCryptoEntry) (was: positions/option_positions/buy_power — did not match actual API) - USRealizedPL: realized_pl_list with USRealizedPLEntry{category,currency,metrics} (was: items[] with symbol/realized_pl — did not match actual API) - Update Python binding types.rs, mod.rs, context_async (via types) - Update Node.js binding types.rs - Update nodejs/index.d.ts interfaces - Update python/pysrc/longbridge/openapi.pyi stubs Co-Authored-By: Claude Sonnet 4.6 (1M context) --- nodejs/index.d.ts | 86 ++++------- nodejs/src/trade/types.rs | 214 +++++++++------------------- python/pysrc/longbridge/openapi.pyi | 173 +++++++--------------- python/src/trade/mod.rs | 9 +- python/src/trade/types.rs | 213 +++++++++------------------ 5 files changed, 228 insertions(+), 467 deletions(-) diff --git a/nodejs/index.d.ts b/nodejs/index.d.ts index d7f2d2e187..f0797d0c52 100644 --- a/nodejs/index.d.ts +++ b/nodejs/index.d.ts @@ -6178,76 +6178,44 @@ export interface USCryptoOverview { profile: unknown } -export interface USStockPosition { - symbol: string - name: string - quantity: string - availableQuantity: string +export interface USCashEntry { currency: string - costPrice: string - marketValue: string - unrealizedPl: string - unrealizedPlRatio: string - lastDone: string - prevClose: string - changeRate: string - nightLastDone: string - pretradeClose: string - tradeStatus: string - individualQuantity: string -} - -export interface USOptionPosition { - symbol: string - strikePrice: string - dueDate: string - contractMultiplier: number - optionType: string - quantity: string - marketValue: string - unrealizedPl: string + frozenBuyCash: string + outstanding: string + settledCash: string + totalAmount: string + totalCash: string } -export interface USCryptoPosition { - symbol: string - quantity: string - marketValue: string - unrealizedPl: string - costPrice: string +export interface USCryptoEntry { + assetType: string + averageCost: string + counterId: string + currency: string + industryCounterId: string + industryName: string } -export interface USBuyPower { +export interface USAssetOverview { + accountType: string + assetTimestamp: string cashBuyPower: string - overnightBuyPower: string - dayTradeBuyPower: string - optionBuyPower: string - cryptoBuyPower: string + cashList: Array + cryptoList: Array } -export interface USAssetOverview { - accountType: string - netAssets: string - totalCash: string - unrealizedPl: string - positions: Array - optionPositions: Array - multiLegs: Array - cryptoPositions: Array - buyPower: USBuyPower +export interface USRealizedPLMetric { + amount: string + period: number + rate: string } -export interface USRealizedPLItem { - symbol: string - name: string - category: string - realizedPl: string - quantitySold: string - avgCost: string - avgSellPrice: string +export interface USRealizedPLEntry { + category: number + currency: string + metrics: Array } export interface USRealizedPL { - totalRealizedPl: string - currency: string - items: Array + realizedPlList: Array } diff --git a/nodejs/src/trade/types.rs b/nodejs/src/trade/types.rs index 0d8b6deb8a..4f8ab06733 100644 --- a/nodejs/src/trade/types.rs +++ b/nodejs/src/trade/types.rs @@ -798,200 +798,128 @@ pub struct EstimateMaxPurchaseQuantityResponse { // ── US-market types ────────────────────────────────────────────────────────── -/// A stock position in a US account +/// One cash currency entry in USAssetOverview #[napi_derive::napi(object)] -#[derive(Debug, Clone)] -pub struct USStockPosition { - pub symbol: String, - pub name: String, - pub quantity: String, - pub available_quantity: String, +#[derive(Debug, Clone, Default)] +pub struct USCashEntry { pub currency: String, - pub cost_price: String, - pub market_value: String, - pub unrealized_pl: String, - pub unrealized_pl_ratio: String, - pub last_done: String, - pub prev_close: String, - pub change_rate: String, - pub night_last_done: String, - pub pretrade_close: String, - pub trade_status: String, - pub individual_quantity: String, -} - -impl From for USStockPosition { - fn from(v: longbridge::trade::USStockPosition) -> Self { - Self { - symbol: v.symbol, - name: v.name, - quantity: v.quantity, - available_quantity: v.available_quantity, - currency: v.currency, - cost_price: v.cost_price, - market_value: v.market_value, - unrealized_pl: v.unrealized_pl, - unrealized_pl_ratio: v.unrealized_pl_ratio, - last_done: v.last_done, - prev_close: v.prev_close, - change_rate: v.change_rate, - night_last_done: v.night_last_done, - pretrade_close: v.pretrade_close, - trade_status: v.trade_status, - individual_quantity: v.individual_quantity, - } - } + pub frozen_buy_cash: String, + pub outstanding: String, + pub settled_cash: String, + pub total_amount: String, + pub total_cash: String, } -/// An option position in a US account -#[napi_derive::napi(object)] -#[derive(Debug, Clone)] -pub struct USOptionPosition { - pub symbol: String, - pub strike_price: String, - pub due_date: String, - pub contract_multiplier: i32, - pub option_type: String, - pub quantity: String, - pub market_value: String, - pub unrealized_pl: String, -} - -impl From for USOptionPosition { - fn from(v: longbridge::trade::USOptionPosition) -> Self { +impl From for USCashEntry { + fn from(v: longbridge::trade::USCashEntry) -> Self { Self { - symbol: v.symbol, - strike_price: v.strike_price, - due_date: v.due_date, - contract_multiplier: v.contract_multiplier, - option_type: v.option_type, - quantity: v.quantity, - market_value: v.market_value, - unrealized_pl: v.unrealized_pl, + currency: v.currency, + frozen_buy_cash: v.frozen_buy_cash, + outstanding: v.outstanding, + settled_cash: v.settled_cash, + total_amount: v.total_amount, + total_cash: v.total_cash, } } } -/// A cryptocurrency position in a US account +/// One cryptocurrency holding in USAssetOverview #[napi_derive::napi(object)] -#[derive(Debug, Clone)] -pub struct USCryptoPosition { - pub symbol: String, - pub quantity: String, - pub market_value: String, - pub unrealized_pl: String, - pub cost_price: String, +#[derive(Debug, Clone, Default)] +pub struct USCryptoEntry { + pub asset_type: String, + pub average_cost: String, + pub counter_id: String, + pub currency: String, + pub industry_counter_id: String, + pub industry_name: String, } -impl From for USCryptoPosition { - fn from(v: longbridge::trade::USCryptoPosition) -> Self { +impl From for USCryptoEntry { + fn from(v: longbridge::trade::USCryptoEntry) -> Self { Self { - symbol: v.symbol, - quantity: v.quantity, - market_value: v.market_value, - unrealized_pl: v.unrealized_pl, - cost_price: v.cost_price, + asset_type: v.asset_type, + average_cost: v.average_cost, + counter_id: v.counter_id, + currency: v.currency, + industry_counter_id: v.industry_counter_id, + industry_name: v.industry_name, } } } -/// Purchasing power breakdown for a US account +/// US account asset snapshot #[napi_derive::napi(object)] #[derive(Debug, Clone, Default)] -pub struct USBuyPower { +pub struct USAssetOverview { + pub account_type: String, + pub asset_timestamp: String, pub cash_buy_power: String, - pub overnight_buy_power: String, - pub day_trade_buy_power: String, - pub option_buy_power: String, - pub crypto_buy_power: String, + pub cash_list: Vec, + pub crypto_list: Vec, } -impl From for USBuyPower { - fn from(v: longbridge::trade::USBuyPower) -> Self { +impl From for USAssetOverview { + fn from(v: longbridge::trade::USAssetOverview) -> Self { Self { + account_type: v.account_type, + asset_timestamp: v.asset_timestamp, cash_buy_power: v.cash_buy_power, - overnight_buy_power: v.overnight_buy_power, - day_trade_buy_power: v.day_trade_buy_power, - option_buy_power: v.option_buy_power, - crypto_buy_power: v.crypto_buy_power, + cash_list: v.cash_list.into_iter().map(Into::into).collect(), + crypto_list: v.crypto_list.into_iter().map(Into::into).collect(), } } } -/// Full US account asset snapshot +/// One time-period metric in USRealizedPLEntry #[napi_derive::napi(object)] -#[derive(Debug, Clone)] -pub struct USAssetOverview { - pub account_type: String, - pub net_assets: String, - pub total_cash: String, - pub unrealized_pl: String, - pub positions: Vec, - pub option_positions: Vec, - /// Multi-leg strategies as JSON string - pub multi_legs: String, - pub crypto_positions: Vec, - pub buy_power: USBuyPower, +#[derive(Debug, Clone, Default)] +pub struct USRealizedPLMetric { + pub amount: String, + pub period: i32, + pub rate: String, } -impl From for USAssetOverview { - fn from(v: longbridge::trade::USAssetOverview) -> Self { +impl From for USRealizedPLMetric { + fn from(v: longbridge::trade::USRealizedPLMetric) -> Self { Self { - account_type: v.account_type, - net_assets: v.net_assets, - total_cash: v.total_cash, - unrealized_pl: v.unrealized_pl, - positions: v.positions.into_iter().map(Into::into).collect(), - option_positions: v.option_positions.into_iter().map(Into::into).collect(), - multi_legs: serde_json::to_string(&v.multi_legs).unwrap_or_default(), - crypto_positions: v.crypto_positions.into_iter().map(Into::into).collect(), - buy_power: v.buy_power.into(), + amount: v.amount, + period: v.period, + rate: v.rate, } } } -/// A single realized P&L entry +/// One asset-category entry in USRealizedPL #[napi_derive::napi(object)] -#[derive(Debug, Clone)] -pub struct USRealizedPLItem { - pub symbol: String, - pub name: String, - pub category: String, - pub realized_pl: String, - pub quantity_sold: String, - pub avg_cost: String, - pub avg_sell_price: String, -} - -impl From for USRealizedPLItem { - fn from(v: longbridge::trade::USRealizedPLItem) -> Self { +#[derive(Debug, Clone, Default)] +pub struct USRealizedPLEntry { + pub category: i32, + pub currency: String, + pub metrics: Vec, +} + +impl From for USRealizedPLEntry { + fn from(v: longbridge::trade::USRealizedPLEntry) -> Self { Self { - symbol: v.symbol, - name: v.name, category: v.category, - realized_pl: v.realized_pl, - quantity_sold: v.quantity_sold, - avg_cost: v.avg_cost, - avg_sell_price: v.avg_sell_price, + currency: v.currency, + metrics: v.metrics.into_iter().map(Into::into).collect(), } } } /// Realized P&L response for a US account #[napi_derive::napi(object)] -#[derive(Debug, Clone)] +#[derive(Debug, Clone, Default)] pub struct USRealizedPL { - pub total_realized_pl: String, - pub currency: String, - pub items: Vec, + pub realized_pl_list: Vec, } impl From for USRealizedPL { fn from(v: longbridge::trade::USRealizedPL) -> Self { Self { - total_realized_pl: v.total_realized_pl, - currency: v.currency, - items: v.items.into_iter().map(Into::into).collect(), + realized_pl_list: v.realized_pl_list.into_iter().map(Into::into).collect(), } } } diff --git a/python/pysrc/longbridge/openapi.pyi b/python/pysrc/longbridge/openapi.pyi index afb0805664..f8f6713e61 100644 --- a/python/pysrc/longbridge/openapi.pyi +++ b/python/pysrc/longbridge/openapi.pyi @@ -12403,145 +12403,82 @@ class USCryptoOverview: """Extended profile as JSON string""" -class USStockPosition: - """A stock holding in the US account.""" +class USCashEntry: + """One cash currency entry in USAssetOverview.""" - symbol: str - """Security symbol""" - name: str - """Security name""" - quantity: str - """Total quantity held""" - available_quantity: str - """Available (tradable) quantity""" currency: str - """Settlement currency""" - cost_price: str - """Average cost price""" - market_value: str - """Current market value""" - unrealized_pl: str - """Unrealized profit/loss""" - unrealized_pl_ratio: str - """Unrealized P&L ratio""" - last_done: str - """Last trade price""" - prev_close: str - """Previous close price""" - change_rate: str - """Price change rate""" - night_last_done: str - """After-hours last trade price""" - pretrade_close: str - """Pre-trade close price""" - trade_status: str - """Trade status code""" - individual_quantity: str - """Individual account quantity""" - - -class USOptionPosition: - """An option holding in the US account.""" - - symbol: str - """Option contract symbol""" - strike_price: str - """Strike price""" - due_date: str - """Expiry date""" - contract_multiplier: int - """Contract multiplier (typically 100)""" - option_type: str - """Option type: ``"call"`` or ``"put"``""" - quantity: str - """Quantity held""" - market_value: str - """Current market value""" - unrealized_pl: str - """Unrealized profit/loss""" + """Currency code, e.g. ``"USD"``""" + frozen_buy_cash: str + """Cash frozen for pending buy orders""" + outstanding: str + """Unsettled cash (positive = receivable, negative = payable)""" + settled_cash: str + """Settled cash balance""" + total_amount: str + """Total cash amount""" + total_cash: str + """Total available cash""" -class USCryptoPosition: - """A cryptocurrency holding in the US account.""" +class USCryptoEntry: + """One cryptocurrency holding in USAssetOverview.""" - symbol: str - """Cryptocurrency symbol""" - quantity: str - """Quantity held""" - market_value: str - """Current market value""" - unrealized_pl: str - """Unrealized profit/loss""" - cost_price: str + asset_type: str + """Asset type, e.g. ``"CRYPTO"``""" + average_cost: str """Average cost price""" + counter_id: str + """Internal counter_id, e.g. ``"VA/BKKT/BTCUSD"``""" + currency: str + """Settlement currency""" + industry_counter_id: str + """Industry counter_id""" + industry_name: str + """Industry name""" -class USBuyPower: - """Purchasing power breakdown for a US account.""" +class USAssetOverview: + """US account asset snapshot. Returned by TradeContext.us_asset_overview.""" + account_type: str + """Account type code""" + asset_timestamp: str + """Snapshot timestamp (Unix seconds string)""" cash_buy_power: str - """Cash buying power""" - overnight_buy_power: str - """Overnight buying power""" - day_trade_buy_power: str - """Day-trade buying power""" - option_buy_power: str - """Option buying power""" - crypto_buy_power: str - """Crypto buying power""" + """Available cash buying power""" + cash_list: List[USCashEntry] + """Cash balances by currency""" + crypto_list: List[USCryptoEntry] + """Cryptocurrency positions""" -class USAssetOverview: - """Full US account asset snapshot. Returned by TradeContext.us_asset_overview.""" +class USRealizedPLMetric: + """One time-period metric within a USRealizedPLEntry.""" - account_type: str - """Account type string""" - net_assets: str - """Total net assets""" - total_cash: str - """Total cash balance""" - unrealized_pl: str - """Total unrealized profit/loss""" - positions: List[USStockPosition] - """Stock positions""" - option_positions: List[USOptionPosition] - """Option positions""" - multi_legs: str - """Multi-leg positions as JSON string""" - crypto_positions: List[USCryptoPosition] - """Cryptocurrency positions""" - buy_power: USBuyPower - """Purchasing power breakdown""" + amount: str + """Realized P&L amount""" + period: int + """Period code (server-defined)""" + rate: str + """Realized P&L rate""" -class USRealizedPLItem: - """A single realized P&L entry by symbol.""" +class USRealizedPLEntry: + """One asset-category entry in USRealizedPL. category: 0=all, 1=stock, 2=option, 3=crypto.""" - symbol: str - """Security symbol""" - name: str - """Security name""" - category: str - """Asset category""" - realized_pl: str - """Realized profit/loss""" - quantity_sold: str - """Total quantity sold""" - avg_cost: str - """Average cost price""" - avg_sell_price: str - """Average sell price""" + category: int + """Asset category code""" + currency: str + """Currency""" + metrics: List[USRealizedPLMetric] + """Time-period metrics""" class USRealizedPL: """Realized P&L response for a US account. Returned by TradeContext.us_realized_pl.""" - total_realized_pl: str - """Total realized profit/loss""" - currency: str - """Currency""" - items: List[USRealizedPLItem] - """Per-symbol breakdown""" + realized_pl_list: List[USRealizedPLEntry] + """Per-category realized P&L entries""" class USRankTag: diff --git a/python/src/trade/mod.rs b/python/src/trade/mod.rs index 3065fa1bc0..eaaa7b8925 100644 --- a/python/src/trade/mod.rs +++ b/python/src/trade/mod.rs @@ -41,12 +41,11 @@ pub(crate) fn register_types(parent: &Bound) -> PyResult<()> { parent.add_class::()?; parent.add_class::()?; - parent.add_class::()?; - parent.add_class::()?; - parent.add_class::()?; - parent.add_class::()?; + parent.add_class::()?; + parent.add_class::()?; parent.add_class::()?; - parent.add_class::()?; + parent.add_class::()?; + parent.add_class::()?; parent.add_class::()?; parent.add_class::()?; diff --git a/python/src/trade/types.rs b/python/src/trade/types.rs index 0395447d61..193abe20d8 100644 --- a/python/src/trade/types.rs +++ b/python/src/trade/types.rs @@ -794,199 +794,128 @@ pub(crate) struct EstimateMaxPurchaseQuantityResponse { // ── US-market types ────────────────────────────────────────────────────────── -/// A stock position in a US account +/// One cash currency entry in USAssetOverview #[pyclass(get_all, skip_from_py_object)] -#[derive(Debug, Clone)] -pub(crate) struct USStockPosition { - pub symbol: String, - pub name: String, - pub quantity: String, - pub available_quantity: String, +#[derive(Debug, Clone, Default)] +pub(crate) struct USCashEntry { pub currency: String, - pub cost_price: String, - pub market_value: String, - pub unrealized_pl: String, - pub unrealized_pl_ratio: String, - pub last_done: String, - pub prev_close: String, - pub change_rate: String, - pub night_last_done: String, - pub pretrade_close: String, - pub trade_status: String, - pub individual_quantity: String, -} - -impl From for USStockPosition { - fn from(v: longbridge::trade::USStockPosition) -> Self { - Self { - symbol: v.symbol, - name: v.name, - quantity: v.quantity, - available_quantity: v.available_quantity, - currency: v.currency, - cost_price: v.cost_price, - market_value: v.market_value, - unrealized_pl: v.unrealized_pl, - unrealized_pl_ratio: v.unrealized_pl_ratio, - last_done: v.last_done, - prev_close: v.prev_close, - change_rate: v.change_rate, - night_last_done: v.night_last_done, - pretrade_close: v.pretrade_close, - trade_status: v.trade_status, - individual_quantity: v.individual_quantity, - } - } + pub frozen_buy_cash: String, + pub outstanding: String, + pub settled_cash: String, + pub total_amount: String, + pub total_cash: String, } -/// An option position in a US account -#[pyclass(get_all, skip_from_py_object)] -#[derive(Debug, Clone)] -pub(crate) struct USOptionPosition { - pub symbol: String, - pub strike_price: String, - pub due_date: String, - pub contract_multiplier: i32, - pub option_type: String, - pub quantity: String, - pub market_value: String, - pub unrealized_pl: String, -} - -impl From for USOptionPosition { - fn from(v: longbridge::trade::USOptionPosition) -> Self { +impl From for USCashEntry { + fn from(v: longbridge::trade::USCashEntry) -> Self { Self { - symbol: v.symbol, - strike_price: v.strike_price, - due_date: v.due_date, - contract_multiplier: v.contract_multiplier, - option_type: v.option_type, - quantity: v.quantity, - market_value: v.market_value, - unrealized_pl: v.unrealized_pl, + currency: v.currency, + frozen_buy_cash: v.frozen_buy_cash, + outstanding: v.outstanding, + settled_cash: v.settled_cash, + total_amount: v.total_amount, + total_cash: v.total_cash, } } } -/// A cryptocurrency position in a US account +/// One cryptocurrency holding in USAssetOverview #[pyclass(get_all, skip_from_py_object)] -#[derive(Debug, Clone)] -pub(crate) struct USCryptoPosition { - pub symbol: String, - pub quantity: String, - pub market_value: String, - pub unrealized_pl: String, - pub cost_price: String, +#[derive(Debug, Clone, Default)] +pub(crate) struct USCryptoEntry { + pub asset_type: String, + pub average_cost: String, + pub counter_id: String, + pub currency: String, + pub industry_counter_id: String, + pub industry_name: String, } -impl From for USCryptoPosition { - fn from(v: longbridge::trade::USCryptoPosition) -> Self { +impl From for USCryptoEntry { + fn from(v: longbridge::trade::USCryptoEntry) -> Self { Self { - symbol: v.symbol, - quantity: v.quantity, - market_value: v.market_value, - unrealized_pl: v.unrealized_pl, - cost_price: v.cost_price, + asset_type: v.asset_type, + average_cost: v.average_cost, + counter_id: v.counter_id, + currency: v.currency, + industry_counter_id: v.industry_counter_id, + industry_name: v.industry_name, } } } -/// Purchasing power breakdown for a US account +/// US account asset snapshot #[pyclass(get_all, skip_from_py_object)] #[derive(Debug, Clone, Default)] -pub(crate) struct USBuyPower { +pub(crate) struct USAssetOverview { + pub account_type: String, + pub asset_timestamp: String, pub cash_buy_power: String, - pub overnight_buy_power: String, - pub day_trade_buy_power: String, - pub option_buy_power: String, - pub crypto_buy_power: String, + pub cash_list: Vec, + pub crypto_list: Vec, } -impl From for USBuyPower { - fn from(v: longbridge::trade::USBuyPower) -> Self { +impl From for USAssetOverview { + fn from(v: longbridge::trade::USAssetOverview) -> Self { Self { + account_type: v.account_type, + asset_timestamp: v.asset_timestamp, cash_buy_power: v.cash_buy_power, - overnight_buy_power: v.overnight_buy_power, - day_trade_buy_power: v.day_trade_buy_power, - option_buy_power: v.option_buy_power, - crypto_buy_power: v.crypto_buy_power, + cash_list: v.cash_list.into_iter().map(Into::into).collect(), + crypto_list: v.crypto_list.into_iter().map(Into::into).collect(), } } } -/// Full US account asset snapshot +/// One time-period metric in USRealizedPLEntry #[pyclass(get_all, skip_from_py_object)] -#[derive(Debug, Clone)] -pub(crate) struct USAssetOverview { - pub account_type: String, - pub net_assets: String, - pub total_cash: String, - pub unrealized_pl: String, - pub positions: Vec, - pub option_positions: Vec, - pub multi_legs: String, - pub crypto_positions: Vec, - pub buy_power: USBuyPower, +#[derive(Debug, Clone, Default)] +pub(crate) struct USRealizedPLMetric { + pub amount: String, + pub period: i32, + pub rate: String, } -impl From for USAssetOverview { - fn from(v: longbridge::trade::USAssetOverview) -> Self { +impl From for USRealizedPLMetric { + fn from(v: longbridge::trade::USRealizedPLMetric) -> Self { Self { - account_type: v.account_type, - net_assets: v.net_assets, - total_cash: v.total_cash, - unrealized_pl: v.unrealized_pl, - positions: v.positions.into_iter().map(Into::into).collect(), - option_positions: v.option_positions.into_iter().map(Into::into).collect(), - multi_legs: serde_json::to_string(&v.multi_legs).unwrap_or_default(), - crypto_positions: v.crypto_positions.into_iter().map(Into::into).collect(), - buy_power: v.buy_power.into(), + amount: v.amount, + period: v.period, + rate: v.rate, } } } -/// A single realized P&L entry +/// One asset-category entry in USRealizedPL #[pyclass(get_all, skip_from_py_object)] -#[derive(Debug, Clone)] -pub(crate) struct USRealizedPLItem { - pub symbol: String, - pub name: String, - pub category: String, - pub realized_pl: String, - pub quantity_sold: String, - pub avg_cost: String, - pub avg_sell_price: String, -} - -impl From for USRealizedPLItem { - fn from(v: longbridge::trade::USRealizedPLItem) -> Self { +#[derive(Debug, Clone, Default)] +pub(crate) struct USRealizedPLEntry { + pub category: i32, + pub currency: String, + pub metrics: Vec, +} + +impl From for USRealizedPLEntry { + fn from(v: longbridge::trade::USRealizedPLEntry) -> Self { Self { - symbol: v.symbol, - name: v.name, category: v.category, - realized_pl: v.realized_pl, - quantity_sold: v.quantity_sold, - avg_cost: v.avg_cost, - avg_sell_price: v.avg_sell_price, + currency: v.currency, + metrics: v.metrics.into_iter().map(Into::into).collect(), } } } /// Realized P&L response for a US account #[pyclass(get_all, skip_from_py_object)] -#[derive(Debug, Clone)] +#[derive(Debug, Clone, Default)] pub(crate) struct USRealizedPL { - pub total_realized_pl: String, - pub currency: String, - pub items: Vec, + pub realized_pl_list: Vec, } impl From for USRealizedPL { fn from(v: longbridge::trade::USRealizedPL) -> Self { Self { - total_realized_pl: v.total_realized_pl, - currency: v.currency, - items: v.items.into_iter().map(Into::into).collect(), + realized_pl_list: v.realized_pl_list.into_iter().map(Into::into).collect(), } } } From f0aa0daeec9a3077f2f221680d59bc1d1597db34 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=A2=81=E7=AB=A0=E6=B4=AA?= Date: Fri, 3 Jul 2026 10:58:20 +0800 Subject: [PATCH 25/44] =?UTF-8?q?fix:=20convert=20counter=5Fid=E2=86=92sym?= =?UTF-8?q?bol=20and=20asset=5Ftimestamp=E2=86=92OffsetDateTime=20in=20USA?= =?UTF-8?q?ssetOverview?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Sonnet 4.6 (1M context) --- rust/src/trade/types.rs | 25 ++++++++++++++++++------- 1 file changed, 18 insertions(+), 7 deletions(-) diff --git a/rust/src/trade/types.rs b/rust/src/trade/types.rs index 72085d4c0d..85470bef55 100644 --- a/rust/src/trade/types.rs +++ b/rust/src/trade/types.rs @@ -902,13 +902,23 @@ pub struct USCryptoEntry { pub asset_type: String, #[serde(default)] pub average_cost: String, - /// Internal counter_id, e.g. `"VA/BKKT/BTCUSD"`. - #[serde(default)] - pub counter_id: String, + /// User-facing trading-pair symbol (e.g. `"BTCUSD.BKKT"`), converted from + /// the API's `counter_id` field (e.g. `"VA/BKKT/BTCUSD"`). + #[serde( + default, + rename = "counter_id", + deserialize_with = "crate::utils::counter::deserialize_counter_id_as_symbol" + )] + pub symbol: String, #[serde(default)] pub currency: String, - #[serde(default)] - pub industry_counter_id: String, + /// Industry symbol converted from `industry_counter_id`. + #[serde( + default, + rename = "industry_counter_id", + deserialize_with = "crate::utils::counter::deserialize_counter_id_as_symbol" + )] + pub industry_symbol: String, #[serde(default)] pub industry_name: String, } @@ -919,8 +929,9 @@ pub struct USCryptoEntry { pub struct USAssetOverview { #[serde(default)] pub account_type: String, - #[serde(default)] - pub asset_timestamp: String, + /// Account snapshot timestamp (Unix-second string → OffsetDateTime). + #[serde(default, with = "crate::serde_utils::timestamp_opt")] + pub asset_timestamp: Option, /// Cash buying power (top-level convenience field). #[serde(default)] pub cash_buy_power: String, From 189d7032acd4e9b455189a5b7ac52fea36031d6f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=A2=81=E7=AB=A0=E6=B4=AA?= Date: Fri, 3 Jul 2026 11:22:58 +0800 Subject: [PATCH 26/44] fix: strip us_m_/hk_m_ routing prefix before sending JWT in Authorization header MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Server validates only bare JWT (eyJ…); sending 'us_m_eyJ…' causes '\xba' decode error. strip_region_prefix now removes everything before 'eyJ' in addition to Bearer. App keys (no eyJ) are unchanged. Co-Authored-By: Claude Sonnet 4.6 (1M context) --- rust/crates/geo/src/lib.rs | 39 ++++++++++++++++++++------- rust/crates/httpclient/src/request.rs | 5 +++- 2 files changed, 33 insertions(+), 11 deletions(-) diff --git a/rust/crates/geo/src/lib.rs b/rust/crates/geo/src/lib.rs index dad4436056..6b81a86e83 100644 --- a/rust/crates/geo/src/lib.rs +++ b/rust/crates/geo/src/lib.rs @@ -133,13 +133,27 @@ impl DcRegion { } /// Strip any leading `Bearer ` from a credential. + /// Strip the routing prefix from a credential before sending it in the + /// `Authorization` header. /// - /// Region prefixes (`hk_m_`, `us_m_`, `ap_m_`, …) are routing metadata - /// consumed by [`from_credential`] to derive the [`DC_REGION_HEADER`]. - /// The gateway accepts the full prefixed token and routes via the header, - /// so **no region prefix is stripped** — only `Bearer ` is removed. + /// Access tokens carry a data-center prefix (`us_m_`, `hk_m_`, `ap_m_`, + /// …) that is routing metadata consumed by [`from_credential`]. The gateway + /// validates only the bare JWT (`eyJ…`); sending the full prefixed string + /// causes JWT header decode failure (`invalid character '\xba'`). + /// + /// Stripping order: + /// 1. Remove any leading `Bearer ` OAuth wrapper. + /// 2. Remove everything before the JWT start (`eyJ`). + /// + /// App keys (hex strings, no `eyJ`) are returned unchanged. pub fn strip_region_prefix(credential: &str) -> &str { - credential.strip_prefix("Bearer ").unwrap_or(credential) + let credential = credential.strip_prefix("Bearer ").unwrap_or(credential); + if let Some(idx) = credential.find("eyJ") { + if idx > 0 { + return &credential[idx..]; + } + } + credential } } @@ -207,15 +221,20 @@ mod dc_region_tests { } #[test] - fn strip_region_prefix_only_removes_bearer() { - // Region prefixes are kept as-is; only "Bearer " is stripped. - assert_eq!(DcRegion::strip_region_prefix("us_m_eyJabc"), "us_m_eyJabc"); - assert_eq!(DcRegion::strip_region_prefix("hk_m_eyJabc"), "hk_m_eyJabc"); + fn strip_region_prefix_strips_routing_prefix_before_jwt() { + // Region prefix (e.g. "us_m_") is stripped; the bare JWT is returned. + assert_eq!(DcRegion::strip_region_prefix("us_m_eyJabc"), "eyJabc"); + assert_eq!(DcRegion::strip_region_prefix("hk_m_eyJabc"), "eyJabc"); assert_eq!( DcRegion::strip_region_prefix("Bearer us_m_eyJabc"), - "us_m_eyJabc" + "eyJabc" ); assert_eq!(DcRegion::strip_region_prefix("Bearer eyJabc"), "eyJabc"); + // Already-bare JWT and app keys are returned unchanged. assert_eq!(DcRegion::strip_region_prefix("eyJabc"), "eyJabc"); + assert_eq!( + DcRegion::strip_region_prefix("f56dd0886267f801"), + "f56dd0886267f801" + ); } } diff --git a/rust/crates/httpclient/src/request.rs b/rust/crates/httpclient/src/request.rs index 8477a7fc3e..3f1b3ed0ef 100644 --- a/rust/crates/httpclient/src/request.rs +++ b/rust/crates/httpclient/src/request.rs @@ -264,7 +264,10 @@ where access_token, } => ( app_key.clone(), - access_token.clone(), + // Strip data-center routing prefix (e.g. "us_m_") before + // sending. The prefix is routing metadata for dc_region + // detection; the gateway validates only the bare JWT ("eyJ…"). + DcRegion::strip_region_prefix(access_token).to_string(), Some(app_secret.clone()), DcRegion::from_credentials(&[app_key, access_token, app_secret]), ), From 69018489d2037b1726079960699aa12c25dcb011 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=A2=81=E7=AB=A0=E6=B4=AA?= Date: Fri, 3 Jul 2026 11:32:51 +0800 Subject: [PATCH 27/44] Revert "fix: strip us_m_/hk_m_ routing prefix before sending JWT in Authorization header" This reverts commit 189d7032acd4e9b455189a5b7ac52fea36031d6f. --- rust/crates/geo/src/lib.rs | 39 +++++++-------------------- rust/crates/httpclient/src/request.rs | 5 +--- 2 files changed, 11 insertions(+), 33 deletions(-) diff --git a/rust/crates/geo/src/lib.rs b/rust/crates/geo/src/lib.rs index 6b81a86e83..dad4436056 100644 --- a/rust/crates/geo/src/lib.rs +++ b/rust/crates/geo/src/lib.rs @@ -133,27 +133,13 @@ impl DcRegion { } /// Strip any leading `Bearer ` from a credential. - /// Strip the routing prefix from a credential before sending it in the - /// `Authorization` header. /// - /// Access tokens carry a data-center prefix (`us_m_`, `hk_m_`, `ap_m_`, - /// …) that is routing metadata consumed by [`from_credential`]. The gateway - /// validates only the bare JWT (`eyJ…`); sending the full prefixed string - /// causes JWT header decode failure (`invalid character '\xba'`). - /// - /// Stripping order: - /// 1. Remove any leading `Bearer ` OAuth wrapper. - /// 2. Remove everything before the JWT start (`eyJ`). - /// - /// App keys (hex strings, no `eyJ`) are returned unchanged. + /// Region prefixes (`hk_m_`, `us_m_`, `ap_m_`, …) are routing metadata + /// consumed by [`from_credential`] to derive the [`DC_REGION_HEADER`]. + /// The gateway accepts the full prefixed token and routes via the header, + /// so **no region prefix is stripped** — only `Bearer ` is removed. pub fn strip_region_prefix(credential: &str) -> &str { - let credential = credential.strip_prefix("Bearer ").unwrap_or(credential); - if let Some(idx) = credential.find("eyJ") { - if idx > 0 { - return &credential[idx..]; - } - } - credential + credential.strip_prefix("Bearer ").unwrap_or(credential) } } @@ -221,20 +207,15 @@ mod dc_region_tests { } #[test] - fn strip_region_prefix_strips_routing_prefix_before_jwt() { - // Region prefix (e.g. "us_m_") is stripped; the bare JWT is returned. - assert_eq!(DcRegion::strip_region_prefix("us_m_eyJabc"), "eyJabc"); - assert_eq!(DcRegion::strip_region_prefix("hk_m_eyJabc"), "eyJabc"); + fn strip_region_prefix_only_removes_bearer() { + // Region prefixes are kept as-is; only "Bearer " is stripped. + assert_eq!(DcRegion::strip_region_prefix("us_m_eyJabc"), "us_m_eyJabc"); + assert_eq!(DcRegion::strip_region_prefix("hk_m_eyJabc"), "hk_m_eyJabc"); assert_eq!( DcRegion::strip_region_prefix("Bearer us_m_eyJabc"), - "eyJabc" + "us_m_eyJabc" ); assert_eq!(DcRegion::strip_region_prefix("Bearer eyJabc"), "eyJabc"); - // Already-bare JWT and app keys are returned unchanged. assert_eq!(DcRegion::strip_region_prefix("eyJabc"), "eyJabc"); - assert_eq!( - DcRegion::strip_region_prefix("f56dd0886267f801"), - "f56dd0886267f801" - ); } } diff --git a/rust/crates/httpclient/src/request.rs b/rust/crates/httpclient/src/request.rs index 3f1b3ed0ef..8477a7fc3e 100644 --- a/rust/crates/httpclient/src/request.rs +++ b/rust/crates/httpclient/src/request.rs @@ -264,10 +264,7 @@ where access_token, } => ( app_key.clone(), - // Strip data-center routing prefix (e.g. "us_m_") before - // sending. The prefix is routing metadata for dc_region - // detection; the gateway validates only the bare JWT ("eyJ…"). - DcRegion::strip_region_prefix(access_token).to_string(), + access_token.clone(), Some(app_secret.clone()), DcRegion::from_credentials(&[app_key, access_token, app_secret]), ), From 9f720d7f6ed3c0f8228d1129e0c291613927c277 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=A2=81=E7=AB=A0=E6=B4=AA?= Date: Fri, 3 Jul 2026 11:34:27 +0800 Subject: [PATCH 28/44] fix(ci): update USCryptoEntry field names and asset_timestamp type in Python/Node.js bindings MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - counter_id → symbol, industry_counter_id → industry_symbol (match Rust core rename) - asset_timestamp: String → i64 (unix seconds, from Option) Co-Authored-By: Claude Sonnet 4.6 (1M context) --- nodejs/src/trade/types.rs | 15 +++++++++------ python/src/trade/types.rs | 15 +++++++++------ 2 files changed, 18 insertions(+), 12 deletions(-) diff --git a/nodejs/src/trade/types.rs b/nodejs/src/trade/types.rs index 4f8ab06733..3502963a78 100644 --- a/nodejs/src/trade/types.rs +++ b/nodejs/src/trade/types.rs @@ -829,9 +829,9 @@ impl From for USCashEntry { pub struct USCryptoEntry { pub asset_type: String, pub average_cost: String, - pub counter_id: String, + pub symbol: String, pub currency: String, - pub industry_counter_id: String, + pub industry_symbol: String, pub industry_name: String, } @@ -840,9 +840,9 @@ impl From for USCryptoEntry { Self { asset_type: v.asset_type, average_cost: v.average_cost, - counter_id: v.counter_id, + symbol: v.symbol, currency: v.currency, - industry_counter_id: v.industry_counter_id, + industry_symbol: v.industry_symbol, industry_name: v.industry_name, } } @@ -853,7 +853,7 @@ impl From for USCryptoEntry { #[derive(Debug, Clone, Default)] pub struct USAssetOverview { pub account_type: String, - pub asset_timestamp: String, + pub asset_timestamp: i64, pub cash_buy_power: String, pub cash_list: Vec, pub crypto_list: Vec, @@ -863,7 +863,10 @@ impl From for USAssetOverview { fn from(v: longbridge::trade::USAssetOverview) -> Self { Self { account_type: v.account_type, - asset_timestamp: v.asset_timestamp, + asset_timestamp: v + .asset_timestamp + .map(|t| t.unix_timestamp()) + .unwrap_or_default(), cash_buy_power: v.cash_buy_power, cash_list: v.cash_list.into_iter().map(Into::into).collect(), crypto_list: v.crypto_list.into_iter().map(Into::into).collect(), diff --git a/python/src/trade/types.rs b/python/src/trade/types.rs index 193abe20d8..0a5bb3c089 100644 --- a/python/src/trade/types.rs +++ b/python/src/trade/types.rs @@ -825,9 +825,9 @@ impl From for USCashEntry { pub(crate) struct USCryptoEntry { pub asset_type: String, pub average_cost: String, - pub counter_id: String, + pub symbol: String, pub currency: String, - pub industry_counter_id: String, + pub industry_symbol: String, pub industry_name: String, } @@ -836,9 +836,9 @@ impl From for USCryptoEntry { Self { asset_type: v.asset_type, average_cost: v.average_cost, - counter_id: v.counter_id, + symbol: v.symbol, currency: v.currency, - industry_counter_id: v.industry_counter_id, + industry_symbol: v.industry_symbol, industry_name: v.industry_name, } } @@ -849,7 +849,7 @@ impl From for USCryptoEntry { #[derive(Debug, Clone, Default)] pub(crate) struct USAssetOverview { pub account_type: String, - pub asset_timestamp: String, + pub asset_timestamp: i64, pub cash_buy_power: String, pub cash_list: Vec, pub crypto_list: Vec, @@ -859,7 +859,10 @@ impl From for USAssetOverview { fn from(v: longbridge::trade::USAssetOverview) -> Self { Self { account_type: v.account_type, - asset_timestamp: v.asset_timestamp, + asset_timestamp: v + .asset_timestamp + .map(|t| t.unix_timestamp()) + .unwrap_or_default(), cash_buy_power: v.cash_buy_power, cash_list: v.cash_list.into_iter().map(Into::into).collect(), crypto_list: v.crypto_list.into_iter().map(Into::into).collect(), From 42e229d6bf2475c3345e09e8aa45623eb6aa14d7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=A2=81=E7=AB=A0=E6=B4=AA?= Date: Fri, 3 Jul 2026 11:39:12 +0800 Subject: [PATCH 29/44] fix: remove industry_symbol/industry_counter_id from USCryptoEntry (always empty) Co-Authored-By: Claude Sonnet 4.6 (1M context) --- nodejs/src/trade/types.rs | 2 -- python/src/trade/types.rs | 2 -- rust/src/trade/types.rs | 7 ------- 3 files changed, 11 deletions(-) diff --git a/nodejs/src/trade/types.rs b/nodejs/src/trade/types.rs index 3502963a78..98edc390c8 100644 --- a/nodejs/src/trade/types.rs +++ b/nodejs/src/trade/types.rs @@ -831,7 +831,6 @@ pub struct USCryptoEntry { pub average_cost: String, pub symbol: String, pub currency: String, - pub industry_symbol: String, pub industry_name: String, } @@ -842,7 +841,6 @@ impl From for USCryptoEntry { average_cost: v.average_cost, symbol: v.symbol, currency: v.currency, - industry_symbol: v.industry_symbol, industry_name: v.industry_name, } } diff --git a/python/src/trade/types.rs b/python/src/trade/types.rs index 0a5bb3c089..249d5f7e13 100644 --- a/python/src/trade/types.rs +++ b/python/src/trade/types.rs @@ -827,7 +827,6 @@ pub(crate) struct USCryptoEntry { pub average_cost: String, pub symbol: String, pub currency: String, - pub industry_symbol: String, pub industry_name: String, } @@ -838,7 +837,6 @@ impl From for USCryptoEntry { average_cost: v.average_cost, symbol: v.symbol, currency: v.currency, - industry_symbol: v.industry_symbol, industry_name: v.industry_name, } } diff --git a/rust/src/trade/types.rs b/rust/src/trade/types.rs index 85470bef55..8f4ddf6fd6 100644 --- a/rust/src/trade/types.rs +++ b/rust/src/trade/types.rs @@ -912,13 +912,6 @@ pub struct USCryptoEntry { pub symbol: String, #[serde(default)] pub currency: String, - /// Industry symbol converted from `industry_counter_id`. - #[serde( - default, - rename = "industry_counter_id", - deserialize_with = "crate::utils::counter::deserialize_counter_id_as_symbol" - )] - pub industry_symbol: String, #[serde(default)] pub industry_name: String, } From 9abbe640725e4074bdff9c948810a94b3d55af00 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=A2=81=E7=AB=A0=E6=B4=AA?= Date: Fri, 3 Jul 2026 18:50:39 +0800 Subject: [PATCH 30/44] fix: USOrderDetail path /v1/orders/:order_id, remove is_attached param Co-Authored-By: Claude Sonnet 4.6 (1M context) --- java/src/trade_context.rs | 4 +--- nodejs/index.d.ts | 2 +- nodejs/src/trade/context.rs | 6 +----- python/pysrc/longbridge/openapi.pyi | 4 ++-- python/src/trade/context.rs | 7 ++----- python/src/trade/context_async.rs | 12 ++---------- rust/src/blocking/trade.rs | 3 +-- rust/src/trade/context.rs | 19 +++---------------- 8 files changed, 13 insertions(+), 44 deletions(-) diff --git a/java/src/trade_context.rs b/java/src/trade_context.rs index a9c483c7f8..1535a04e23 100644 --- a/java/src/trade_context.rs +++ b/java/src/trade_context.rs @@ -678,15 +678,13 @@ pub unsafe extern "system" fn Java_com_longbridge_SdkNative_tradeContextUsOrderD _class: JClass, context: i64, order_id: JObject, - is_attached: jboolean, callback: JObject, ) { jni_result(&mut env, (), |env| { let context = &*(context as *const ContextObj); let order_id: String = FromJValue::from_jvalue(env, order_id.into())?; - let is_attached = is_attached != 0; async_util::execute(env, callback, async move { - let resp = context.ctx.us_order_detail(order_id, is_attached).await?; + let resp = context.ctx.us_order_detail(order_id).await?; Ok(serde_json::to_string(&resp).unwrap_or_default()) })?; Ok(()) diff --git a/nodejs/index.d.ts b/nodejs/index.d.ts index f0797d0c52..598e50a9ea 100644 --- a/nodejs/index.d.ts +++ b/nodejs/index.d.ts @@ -2823,7 +2823,7 @@ export declare class TradeContext { /** Query US order list (paginated). Returns JSON string. US token required. */ usQueryOrders(accountChannel: string, action: number, startAt: number, endAt: number, counterIds: string[], securityTypes: string[], queryType: number, page: number, limit: number, queryVersion: number): Promise /** Get US order detail. isAttached=true includes take-profit/stop-loss sub-orders. Returns JSON string. US token required. */ - usOrderDetail(orderId: string, isAttached: boolean): Promise + usOrderDetail(orderId: string): Promise /** Get US account asset overview (stocks/options/crypto/buy power). US token required. */ usAssetOverview(): Promise /** Get US realized P&L. category: "ALL"|"STOCK"|"OPTION"|"CRYPTO". US token required. */ diff --git a/nodejs/src/trade/context.rs b/nodejs/src/trade/context.rs index 2171200c34..ce83932b83 100644 --- a/nodejs/src/trade/context.rs +++ b/nodejs/src/trade/context.rs @@ -547,14 +547,10 @@ impl TradeContext { &self, env: &'env Env, order_id: String, - is_attached: bool, ) -> Result> { let ctx = self.ctx.clone(); env.spawn_future(async move { - let resp = ctx - .us_order_detail(order_id, is_attached) - .await - .map_err(ErrorNewType)?; + let resp = ctx.us_order_detail(order_id).await.map_err(ErrorNewType)?; serde_json::to_string(&resp).map_err(|e| napi::Error::from_reason(e.to_string())) }) } diff --git a/python/pysrc/longbridge/openapi.pyi b/python/pysrc/longbridge/openapi.pyi index f8f6713e61..bb7b49524f 100644 --- a/python/pysrc/longbridge/openapi.pyi +++ b/python/pysrc/longbridge/openapi.pyi @@ -7373,7 +7373,7 @@ class TradeContext: """ ... - def us_order_detail(self, order_id: str, is_attached: bool) -> str: + def us_order_detail(self, order_id: str) -> str: """Get US order detail. Returns JSON string. US token required. When ``is_attached`` is ``True``, attached take-profit/stop-loss @@ -8108,7 +8108,7 @@ class AsyncTradeContext: """ ... - def us_order_detail(self, order_id: str, is_attached: bool) -> "Awaitable[str]": + def us_order_detail(self, order_id: str) -> "Awaitable[str]": """Get US order detail. Returns awaitable JSON string. US token required. When ``is_attached`` is ``True``, attached take-profit/stop-loss diff --git a/python/src/trade/context.rs b/python/src/trade/context.rs index 96ef040dba..89421de566 100644 --- a/python/src/trade/context.rs +++ b/python/src/trade/context.rs @@ -446,11 +446,8 @@ impl TradeContext { } /// Get US order detail (returns JSON string). US token required. - fn us_order_detail(&self, order_id: String, is_attached: bool) -> PyResult { - let resp = self - .ctx - .us_order_detail(order_id, is_attached) - .map_err(ErrorNewType)?; + fn us_order_detail(&self, order_id: String) -> PyResult { + let resp = self.ctx.us_order_detail(order_id).map_err(ErrorNewType)?; serde_json::to_string(&resp) .map_err(|e| pyo3::exceptions::PyValueError::new_err(e.to_string())) } diff --git a/python/src/trade/context_async.rs b/python/src/trade/context_async.rs index 4ba1a79d77..abeb63fdf6 100644 --- a/python/src/trade/context_async.rs +++ b/python/src/trade/context_async.rs @@ -529,18 +529,10 @@ impl AsyncTradeContext { } /// Get US order detail (JSON string). US token required. Returns awaitable. - fn us_order_detail( - &self, - py: Python<'_>, - order_id: String, - is_attached: bool, - ) -> PyResult> { + fn us_order_detail(&self, py: Python<'_>, order_id: String) -> PyResult> { let ctx = self.ctx.clone(); pyo3_async_runtimes::tokio::future_into_py(py, async move { - let resp = ctx - .us_order_detail(order_id, is_attached) - .await - .map_err(ErrorNewType)?; + let resp = ctx.us_order_detail(order_id).await.map_err(ErrorNewType)?; let s = serde_json::to_string(&resp) .map_err(|e| pyo3::exceptions::PyValueError::new_err(e.to_string()))?; Ok(s) diff --git a/rust/src/blocking/trade.rs b/rust/src/blocking/trade.rs index 5347d7d933..10087527ad 100644 --- a/rust/src/blocking/trade.rs +++ b/rust/src/blocking/trade.rs @@ -470,10 +470,9 @@ impl TradeContextSync { pub fn us_order_detail( &self, order_id: impl Into + Send + 'static, - is_attached: bool, ) -> Result { self.rt - .call(move |ctx| async move { ctx.us_order_detail(order_id, is_attached).await }) + .call(move |ctx| async move { ctx.us_order_detail(order_id).await }) } /// Get the full US account asset overview (blocking) diff --git a/rust/src/trade/context.rs b/rust/src/trade/context.rs index 5f059800f2..b90027a005 100644 --- a/rust/src/trade/context.rs +++ b/rust/src/trade/context.rs @@ -861,36 +861,23 @@ impl TradeContext { .0) } - /// Get US order detail, optionally including attached stop-loss/take-profit - /// sub-orders. + /// Get US order detail. /// - /// Path: `GET /v3/orders/{order_id}` + /// Path: `GET /v1/orders/{order_id}` /// /// US token required. pub async fn us_order_detail( &self, order_id: impl Into, - is_attached: bool, ) -> Result { let order_id = order_id.into(); - let path = format!("/v3/orders/{order_id}"); - - #[derive(Serialize)] - struct Query { - order_id_str: String, - #[serde(skip_serializing_if = "Option::is_none")] - is_attached: Option, - } + let path = format!("/v1/orders/{order_id}"); Ok(self .0 .http_cli .request(Method::GET, path.as_str()) .dc_restrict(DcRegion::Us) - .query_params(Query { - order_id_str: order_id, - is_attached: if is_attached { Some(true) } else { None }, - }) .response::>() .send() .with_subscriber(self.0.log_subscriber.clone()) From dd77774080018a215e067a75af46edf3f5367b51 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=A2=81=E7=AB=A0=E6=B4=AA?= Date: Fri, 3 Jul 2026 20:58:39 +0800 Subject: [PATCH 31/44] docs: annotate crypto exchanges with region (US:BKKT, HK:HAS/OSL) Co-Authored-By: Claude Sonnet 4.6 (1M context) --- rust/src/utils/counter.rs | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/rust/src/utils/counter.rs b/rust/src/utils/counter.rs index cebce4d166..f6f69b46bd 100644 --- a/rust/src/utils/counter.rs +++ b/rust/src/utils/counter.rs @@ -148,9 +148,14 @@ pub fn lookup_counter_id(symbol: &str) -> Option { /// Leading-dot symbols (e.g. `.DJI.US`) are US market indexes and always map /// to `IX/`. All other symbols are checked against the embedded /// Known crypto exchange identifiers used as symbol suffixes. -/// e.g. `"BTCUSD.HAS"` → `"VA/HAS/BTCUSD"`, `"BTCUSD.BKKT"` → -/// `"VA/BKKT/BTCUSD"`. -const CRYPTO_EXCHANGES: &[&str] = &["HAS", "OSL", "BKKT"]; +/// +/// Region mapping: +/// - US DC: `BKKT` +/// - HK DC: `HAS`, `OSL` +/// +/// e.g. `"BTCUSD.BKKT"` → `"VA/BKKT/BTCUSD"`, `"BTCUSD.HAS"` → +/// `"VA/HAS/BTCUSD"`. +const CRYPTO_EXCHANGES: &[&str] = &["BKKT", "HAS", "OSL"]; /// ETF + index + warrant set and the remote-resolved cache; a matching entry /// is returned as-is. Crypto symbols whose suffix matches a known exchange From 44b834fb67e0c5d19ba9929d6855908e10229ea5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=A2=81=E7=AB=A0=E6=B4=AA?= Date: Fri, 3 Jul 2026 21:04:48 +0800 Subject: [PATCH 32/44] fix(review): P0/P1 issues from PR review - QueryUSOrdersResponse: add total_count field - USOrderDetailResponse: replace USAttachedOrder dead code with order/order_histories/current_attached_order matching real API response - Remove USAttachedOrder, export USOrderHistory instead - CryptoOverview USCryptoOverview: add counter_id, base_asset, logo, wiki_url; profile changed to String (API returns JSON string) Co-Authored-By: Claude Sonnet 4.6 (1M context) --- rust/src/lib.rs | 4 ++-- rust/src/quote/types.rs | 16 ++++++++++++-- rust/src/trade/mod.rs | 2 +- rust/src/trade/types.rs | 49 +++++++++++++++++++---------------------- 4 files changed, 40 insertions(+), 31 deletions(-) diff --git a/rust/src/lib.rs b/rust/src/lib.rs index f8e54e7d86..7b4640f014 100644 --- a/rust/src/lib.rs +++ b/rust/src/lib.rs @@ -59,8 +59,8 @@ pub use rust_decimal::Decimal; pub use screener::ScreenerContext; pub use sharelist::SharelistContext; pub use trade::{ - QueryUSOrdersOptions, QueryUSOrdersResponse, TradeContext, USAssetOverview, USAttachedOrder, - USCashEntry, USCryptoEntry, USOrderDetailResponse, USRealizedPL, USRealizedPLEntry, + QueryUSOrdersOptions, QueryUSOrdersResponse, TradeContext, USAssetOverview, USCashEntry, + USCryptoEntry, USOrderDetailResponse, USOrderHistory, USRealizedPL, USRealizedPLEntry, USRealizedPLMetric, }; pub use types::Market; diff --git a/rust/src/quote/types.rs b/rust/src/quote/types.rs index 162a8c2127..70457f8fc1 100644 --- a/rust/src/quote/types.rs +++ b/rust/src/quote/types.rs @@ -2202,12 +2202,24 @@ pub struct USCryptoOverview { /// Circulating supply #[serde(default)] pub shares: String, + /// Internal counter_id (e.g. `"VA/BKKT/BTCUSD"`) + #[serde(default)] + pub counter_id: String, + /// Base asset code (e.g. `"BTC"`) + #[serde(default)] + pub base_asset: String, /// Official website URL #[serde(default)] pub official_web_address: String, - /// Multi-language profile / description + /// Logo image URL + #[serde(default)] + pub logo: String, + /// In-app wiki URL + #[serde(default)] + pub wiki_url: String, + /// Multi-language profile / description (JSON string) #[serde(default)] - pub profile: serde_json::Value, + pub profile: String, } #[cfg(test)] diff --git a/rust/src/trade/mod.rs b/rust/src/trade/mod.rs index d4383b5ed4..9cdd15d9ef 100644 --- a/rust/src/trade/mod.rs +++ b/rust/src/trade/mod.rs @@ -50,10 +50,10 @@ pub use types::{ TriggerPriceType, TriggerStatus, USAssetOverview, - USAttachedOrder, USCashEntry, USCryptoEntry, USOrderDetailResponse, + USOrderHistory, USRealizedPL, USRealizedPLEntry, USRealizedPLMetric, diff --git a/rust/src/trade/types.rs b/rust/src/trade/types.rs index 8f4ddf6fd6..56e769987d 100644 --- a/rust/src/trade/types.rs +++ b/rust/src/trade/types.rs @@ -833,49 +833,46 @@ pub struct QueryUSOrdersOptions { } /// Response for [`crate::TradeContext::us_query_orders`]. -#[derive(Debug, Clone, Serialize, Deserialize)] +#[derive(Debug, Clone, Serialize, Deserialize, Default)] pub struct QueryUSOrdersResponse { - /// Order list (raw JSON for forward compatibility) + /// Order list (raw JSON for forward compatibility). + /// Order ID field is `id` (not `order_id`). #[serde(default)] pub orders: Vec, + /// Total number of orders matching the query. + #[serde(default)] + pub total_count: i32, } -/// An attached take-profit or stop-loss sub-order. -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct USAttachedOrder { - /// Sub-order ID +/// One order state-transition entry in [`USOrderDetailResponse`]. +#[derive(Debug, Clone, Serialize, Deserialize, Default)] +pub struct USOrderHistory { #[serde(default)] - pub order_id: String, - /// Type: `TAKE_PROFIT` or `STOP_LOSS` - #[serde(default, rename = "type")] - pub order_type: String, - /// Direction: `BUY` or `SELL` + pub exec_type: i32, #[serde(default)] - pub side: String, - /// Limit price + pub status: String, #[serde(default)] pub price: String, - /// Trailing stop amount #[serde(default)] - pub trail_amount: String, - /// Trailing stop percentage + pub qty: String, #[serde(default)] - pub trail_percent: String, - /// Order status + pub time: String, #[serde(default)] - pub status: String, + pub msg: String, } /// Response for [`crate::TradeContext::us_order_detail`]. -#[derive(Debug, Clone, Serialize, Deserialize)] +/// Path: `GET /v1/orders/{order_id}` +#[derive(Debug, Clone, Serialize, Deserialize, Default)] pub struct USOrderDetailResponse { - /// Raw order detail fields (pass-through) - #[serde(flatten)] - pub detail: serde_json::Value, - /// Attached stop-loss / take-profit orders (populated when `is_attached = - /// true`) + /// Full order object (raw JSON — field set varies by order type). + pub order: serde_json::Value, + /// Order state-transition history. + #[serde(default)] + pub order_histories: Vec, + /// Current attached stop-loss/take-profit sub-order, if any. #[serde(default)] - pub attached_orders: Vec, + pub current_attached_order: serde_json::Value, } /// One cash currency entry in [`USAssetOverview`]. From 653eedd767600b4f31cc939f6f93328f4ded1ee0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=A2=81=E7=AB=A0=E6=B4=AA?= Date: Fri, 3 Jul 2026 21:28:00 +0800 Subject: [PATCH 33/44] =?UTF-8?q?fix(review-2):=20P0=20fixes=20=E2=80=94?= =?UTF-8?q?=20stubs/bindings=20sync=20for=20USCryptoOverview,=20USCryptoEn?= =?UTF-8?q?try,=20asset=5Ftimestamp?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit P0: - Python/Node.js USCryptoOverview: add counter_id, base_asset, logo, wiki_url - index.d.ts USCryptoEntry: counterId→symbol, remove industryCounterId - Python stubs USCryptoEntry: same - asset_timestamp type: str→int (i64 unix seconds) - index.d.ts: add USOrderDetailResponse/USOrderHistory interfaces; usOrderDetail return type Promise→Promise - Python stubs: fix CryptoOverview doc from CY/US/BTC to BTCUSD.BKKT Co-Authored-By: Claude Sonnet 4.6 (1M context) --- nodejs/index.d.ts | 31 ++++++++++++++++++++++++----- nodejs/src/quote/types.rs | 8 ++++++++ python/pysrc/longbridge/openapi.pyi | 6 +++--- python/src/quote/types.rs | 8 ++++++++ 4 files changed, 45 insertions(+), 8 deletions(-) diff --git a/nodejs/index.d.ts b/nodejs/index.d.ts index 598e50a9ea..a143bc966e 100644 --- a/nodejs/index.d.ts +++ b/nodejs/index.d.ts @@ -2823,7 +2823,7 @@ export declare class TradeContext { /** Query US order list (paginated). Returns JSON string. US token required. */ usQueryOrders(accountChannel: string, action: number, startAt: number, endAt: number, counterIds: string[], securityTypes: string[], queryType: number, page: number, limit: number, queryVersion: number): Promise /** Get US order detail. isAttached=true includes take-profit/stop-loss sub-orders. Returns JSON string. US token required. */ - usOrderDetail(orderId: string): Promise + usOrderDetail(orderId: string): Promise /** Get US account asset overview (stocks/options/crypto/buy power). US token required. */ usAssetOverview(): Promise /** Get US realized P&L. category: "ALL"|"STOCK"|"OPTION"|"CRYPTO". US token required. */ @@ -6088,6 +6088,21 @@ export declare const enum WarrantType { Inline = 5 } +export interface USOrderHistory { + execType: number + status: string + price: string + qty: string + time: string + msg: string +} + +export interface USOrderDetailResponse { + order: Record + orderHistories: Array + currentAttachedOrder: Record | null +} + // ── US-market types ──────────────────────────────────────────────────────── export interface USRankTag { @@ -6164,8 +6179,10 @@ export interface USETFFilesResponse { } export interface USCryptoOverview { + counterId: string name: string ticker: string + baseAsset: string currency: string allTimeHigh: string allTimeHighDate: string @@ -6175,7 +6192,10 @@ export interface USCryptoOverview { issuePrice: string shares: string officialWebAddress: string - profile: unknown + logo: string + wikiUrl: string + /** Profile / description as a JSON string */ + profile: string } export interface USCashEntry { @@ -6190,15 +6210,16 @@ export interface USCashEntry { export interface USCryptoEntry { assetType: string averageCost: string - counterId: string + /** User-facing symbol, e.g. "BTCUSD.BKKT" */ + symbol: string currency: string - industryCounterId: string industryName: string } export interface USAssetOverview { accountType: string - assetTimestamp: string + /** Unix timestamp (seconds) */ + assetTimestamp: number cashBuyPower: string cashList: Array cryptoList: Array diff --git a/nodejs/src/quote/types.rs b/nodejs/src/quote/types.rs index 13ab083a2d..db63d03ec3 100644 --- a/nodejs/src/quote/types.rs +++ b/nodejs/src/quote/types.rs @@ -1679,6 +1679,10 @@ pub struct USCryptoOverview { pub issue_price: String, pub shares: String, pub official_web_address: String, + pub counter_id: String, + pub base_asset: String, + pub logo: String, + pub wiki_url: String, /// Profile serialized as JSON string pub profile: String, } @@ -1697,6 +1701,10 @@ impl From for USCryptoOverview { issue_price: v.issue_price, shares: v.shares, official_web_address: v.official_web_address, + counter_id: v.counter_id, + base_asset: v.base_asset, + logo: v.logo, + wiki_url: v.wiki_url, profile: serde_json::to_string(&v.profile).unwrap_or_default(), } } diff --git a/python/pysrc/longbridge/openapi.pyi b/python/pysrc/longbridge/openapi.pyi index bb7b49524f..5995969612 100644 --- a/python/pysrc/longbridge/openapi.pyi +++ b/python/pysrc/longbridge/openapi.pyi @@ -4024,7 +4024,7 @@ class QuoteContext: """Get US cryptocurrency market overview. US token required. Args: - counter_id: Crypto counter_id, e.g. ``"CY/US/BTC"`` + symbol: Trading-pair symbol, e.g. ``"BTCUSD.BKKT"`` Returns: :class:`USCryptoOverview` @@ -5387,7 +5387,7 @@ class AsyncQuoteContext: """Get US cryptocurrency market overview. US token required. Returns awaitable. Args: - counter_id: Crypto counter_id, e.g. ``"CY/US/BTC"`` + symbol: Trading-pair symbol, e.g. ``"BTCUSD.BKKT"`` Returns: Awaitable resolving to :class:`USCryptoOverview` @@ -12442,7 +12442,7 @@ class USAssetOverview: account_type: str """Account type code""" - asset_timestamp: str + asset_timestamp: int """Snapshot timestamp (Unix seconds string)""" cash_buy_power: str """Available cash buying power""" diff --git a/python/src/quote/types.rs b/python/src/quote/types.rs index e94d01e7a4..103cfdb881 100644 --- a/python/src/quote/types.rs +++ b/python/src/quote/types.rs @@ -1637,6 +1637,10 @@ pub(crate) struct USCryptoOverview { pub issue_price: String, pub shares: String, pub official_web_address: String, + pub counter_id: String, + pub base_asset: String, + pub logo: String, + pub wiki_url: String, /// Profile as JSON string pub profile: String, } @@ -1655,6 +1659,10 @@ impl From for USCryptoOverview { issue_price: v.issue_price, shares: v.shares, official_web_address: v.official_web_address, + counter_id: v.counter_id, + base_asset: v.base_asset, + logo: v.logo, + wiki_url: v.wiki_url, profile: serde_json::to_string(&v.profile).unwrap_or_default(), } } From d65c9d78cd3f254af3a1d88dfab2cded14536c33 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=A2=81=E7=AB=A0=E6=B4=AA?= Date: Fri, 3 Jul 2026 21:31:03 +0800 Subject: [PATCH 34/44] fix: convert CryptoOverview counter_id to symbol in Rust/Python/Node.js/stubs Co-Authored-By: Claude Sonnet 4.6 (1M context) --- nodejs/index.d.ts | 2 +- nodejs/src/quote/types.rs | 5 +++-- python/pysrc/longbridge/openapi.pyi | 2 +- python/src/quote/types.rs | 5 +++-- rust/src/quote/types.rs | 11 ++++++++--- 5 files changed, 16 insertions(+), 9 deletions(-) diff --git a/nodejs/index.d.ts b/nodejs/index.d.ts index a143bc966e..546f4b3567 100644 --- a/nodejs/index.d.ts +++ b/nodejs/index.d.ts @@ -6179,7 +6179,7 @@ export interface USETFFilesResponse { } export interface USCryptoOverview { - counterId: string + symbol: string name: string ticker: string baseAsset: string diff --git a/nodejs/src/quote/types.rs b/nodejs/src/quote/types.rs index db63d03ec3..2affc9b0bc 100644 --- a/nodejs/src/quote/types.rs +++ b/nodejs/src/quote/types.rs @@ -1679,7 +1679,8 @@ pub struct USCryptoOverview { pub issue_price: String, pub shares: String, pub official_web_address: String, - pub counter_id: String, + /// User-facing symbol (e.g. "BTCUSD.BKKT"), converted from counter_id + pub symbol: String, pub base_asset: String, pub logo: String, pub wiki_url: String, @@ -1701,7 +1702,7 @@ impl From for USCryptoOverview { issue_price: v.issue_price, shares: v.shares, official_web_address: v.official_web_address, - counter_id: v.counter_id, + symbol: v.symbol, base_asset: v.base_asset, logo: v.logo, wiki_url: v.wiki_url, diff --git a/python/pysrc/longbridge/openapi.pyi b/python/pysrc/longbridge/openapi.pyi index 5995969612..3665a220c1 100644 --- a/python/pysrc/longbridge/openapi.pyi +++ b/python/pysrc/longbridge/openapi.pyi @@ -12427,7 +12427,7 @@ class USCryptoEntry: """Asset type, e.g. ``"CRYPTO"``""" average_cost: str """Average cost price""" - counter_id: str + symbol: str """Internal counter_id, e.g. ``"VA/BKKT/BTCUSD"``""" currency: str """Settlement currency""" diff --git a/python/src/quote/types.rs b/python/src/quote/types.rs index 103cfdb881..89ab0736fe 100644 --- a/python/src/quote/types.rs +++ b/python/src/quote/types.rs @@ -1637,7 +1637,8 @@ pub(crate) struct USCryptoOverview { pub issue_price: String, pub shares: String, pub official_web_address: String, - pub counter_id: String, + /// User-facing symbol (e.g. "BTCUSD.BKKT"), converted from counter_id + pub symbol: String, pub base_asset: String, pub logo: String, pub wiki_url: String, @@ -1659,7 +1660,7 @@ impl From for USCryptoOverview { issue_price: v.issue_price, shares: v.shares, official_web_address: v.official_web_address, - counter_id: v.counter_id, + symbol: v.symbol, base_asset: v.base_asset, logo: v.logo, wiki_url: v.wiki_url, diff --git a/rust/src/quote/types.rs b/rust/src/quote/types.rs index 70457f8fc1..dbb556ac0d 100644 --- a/rust/src/quote/types.rs +++ b/rust/src/quote/types.rs @@ -2202,9 +2202,14 @@ pub struct USCryptoOverview { /// Circulating supply #[serde(default)] pub shares: String, - /// Internal counter_id (e.g. `"VA/BKKT/BTCUSD"`) - #[serde(default)] - pub counter_id: String, + /// User-facing symbol (e.g. `"BTCUSD.BKKT"`), converted from the API's + /// `counter_id` field (e.g. `"VA/BKKT/BTCUSD"`). + #[serde( + default, + rename = "counter_id", + deserialize_with = "crate::utils::counter::deserialize_counter_id_as_symbol" + )] + pub symbol: String, /// Base asset code (e.g. `"BTC"`) #[serde(default)] pub base_asset: String, From 582c660a9b6dea5b86b49803ee718ed1d7acfd1f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=A2=81=E7=AB=A0=E6=B4=AA?= Date: Fri, 3 Jul 2026 21:44:52 +0800 Subject: [PATCH 35/44] fix(review-3): P0/P1 fixes MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit P0: - Fix profile double-serialization: v.profile is String; drop serde_json::to_string() - Python stub USCryptoOverview: add symbol, base_asset, logo, wiki_url fields P1: - Blocking us_crypto_overview: rename param counter_id → symbol - Java JNI us_crypto_overview: rename local var counter_id → symbol Co-Authored-By: Claude Sonnet 4.6 (1M context) --- java/src/quote_context.rs | 6 +++--- nodejs/src/quote/types.rs | 2 +- python/pysrc/longbridge/openapi.pyi | 10 +++++++++- python/src/quote/types.rs | 2 +- rust/src/blocking/quote.rs | 4 ++-- 5 files changed, 16 insertions(+), 8 deletions(-) diff --git a/java/src/quote_context.rs b/java/src/quote_context.rs index e12d99befd..1fa8377fa4 100644 --- a/java/src/quote_context.rs +++ b/java/src/quote_context.rs @@ -1288,14 +1288,14 @@ pub unsafe extern "system" fn Java_com_longbridge_SdkNative_quoteContextUsCrypto mut env: JNIEnv, _class: JClass, context: i64, - counter_id: JObject, + symbol: JObject, callback: JObject, ) { jni_result(&mut env, (), |env| { let context = &*(context as *const ContextObj); - let counter_id: String = FromJValue::from_jvalue(env, counter_id.into())?; + let symbol: String = FromJValue::from_jvalue(env, symbol.into())?; async_util::execute(env, callback, async move { - let resp = context.ctx.us_crypto_overview(counter_id).await?; + let resp = context.ctx.us_crypto_overview(symbol).await?; Ok(serde_json::to_string(&resp).unwrap_or_default()) })?; Ok(()) diff --git a/nodejs/src/quote/types.rs b/nodejs/src/quote/types.rs index 2affc9b0bc..647fcd1e6c 100644 --- a/nodejs/src/quote/types.rs +++ b/nodejs/src/quote/types.rs @@ -1706,7 +1706,7 @@ impl From for USCryptoOverview { base_asset: v.base_asset, logo: v.logo, wiki_url: v.wiki_url, - profile: serde_json::to_string(&v.profile).unwrap_or_default(), + profile: v.profile, } } } diff --git a/python/pysrc/longbridge/openapi.pyi b/python/pysrc/longbridge/openapi.pyi index 3665a220c1..41a86f8f67 100644 --- a/python/pysrc/longbridge/openapi.pyi +++ b/python/pysrc/longbridge/openapi.pyi @@ -12377,10 +12377,14 @@ class OptionVolumeDaily: class USCryptoOverview: """US cryptocurrency market overview. Returned by QuoteContext.us_crypto_overview.""" + symbol: str + """User-facing trading-pair symbol, e.g. ``"BTCUSD.BKKT"``""" name: str """Cryptocurrency full name""" ticker: str - """Ticker symbol""" + """Ticker / pair code, e.g. ``"BTCUSD"``""" + base_asset: str + """Base asset code, e.g. ``"BTC"``""" currency: str """Quote currency""" all_time_high: str @@ -12399,6 +12403,10 @@ class USCryptoOverview: """Total supply""" official_web_address: str """Official website URL""" + logo: str + """Logo image URL""" + wiki_url: str + """In-app wiki URL""" profile: str """Extended profile as JSON string""" diff --git a/python/src/quote/types.rs b/python/src/quote/types.rs index 89ab0736fe..a77b3563ff 100644 --- a/python/src/quote/types.rs +++ b/python/src/quote/types.rs @@ -1664,7 +1664,7 @@ impl From for USCryptoOverview { base_asset: v.base_asset, logo: v.logo, wiki_url: v.wiki_url, - profile: serde_json::to_string(&v.profile).unwrap_or_default(), + profile: v.profile, } } } diff --git a/rust/src/blocking/quote.rs b/rust/src/blocking/quote.rs index 19ba760660..3bab1c95ed 100644 --- a/rust/src/blocking/quote.rs +++ b/rust/src/blocking/quote.rs @@ -1238,9 +1238,9 @@ impl QuoteContextSync { /// Get cryptocurrency market overview (blocking) pub fn us_crypto_overview( &self, - counter_id: impl Into + Send + 'static, + symbol: impl Into + Send + 'static, ) -> Result { self.rt - .call(move |ctx| async move { ctx.us_crypto_overview(counter_id).await }) + .call(move |ctx| async move { ctx.us_crypto_overview(symbol).await }) } } From c62dc2b1b9ea3bf78b721e6a9ea877d52d48486f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=A2=81=E7=AB=A0=E6=B4=AA?= Date: Fri, 3 Jul 2026 21:50:54 +0800 Subject: [PATCH 36/44] docs: clarify us_order_detail and usQueryOrders return types in stubs - Python stub: us_order_detail docstring describes JSON shape - index.d.ts: usQueryOrders JSDoc describes JSON shape and queryType values Co-Authored-By: Claude Sonnet 4.6 (1M context) --- nodejs/index.d.ts | 5 +++++ python/pysrc/longbridge/openapi.pyi | 26 ++++++++++++++++++++------ 2 files changed, 25 insertions(+), 6 deletions(-) diff --git a/nodejs/index.d.ts b/nodejs/index.d.ts index 546f4b3567..36cc161867 100644 --- a/nodejs/index.d.ts +++ b/nodejs/index.d.ts @@ -2821,6 +2821,11 @@ export declare class TradeContext { */ estimateMaxPurchaseQuantity(opts: EstimateMaxPurchaseQuantityOptions): Promise /** Query US order list (paginated). Returns JSON string. US token required. */ + /** + * Query US order list. Returns JSON string with shape `{orders: USOrder[], total_count: number}`. + * queryType: 0=all (incl. Rejected), 1=pending, 2=history (filled only). + * US token required. + */ usQueryOrders(accountChannel: string, action: number, startAt: number, endAt: number, counterIds: string[], securityTypes: string[], queryType: number, page: number, limit: number, queryVersion: number): Promise /** Get US order detail. isAttached=true includes take-profit/stop-loss sub-orders. Returns JSON string. US token required. */ usOrderDetail(orderId: string): Promise diff --git a/python/pysrc/longbridge/openapi.pyi b/python/pysrc/longbridge/openapi.pyi index 41a86f8f67..d6a7d28e01 100644 --- a/python/pysrc/longbridge/openapi.pyi +++ b/python/pysrc/longbridge/openapi.pyi @@ -7374,10 +7374,17 @@ class TradeContext: ... def us_order_detail(self, order_id: str) -> str: - """Get US order detail. Returns JSON string. US token required. + """Get US order detail. US token required. - When ``is_attached`` is ``True``, attached take-profit/stop-loss - sub-orders are included in the response. + Returns a JSON string with shape:: + + { + "order": {...}, + "order_histories": [...], + "current_attached_order": null + } + + Path: ``GET /v1/orders/{order_id}`` (no query parameters). Args: order_id: Order ID @@ -8109,10 +8116,17 @@ class AsyncTradeContext: ... def us_order_detail(self, order_id: str) -> "Awaitable[str]": - """Get US order detail. Returns awaitable JSON string. US token required. + """Get US order detail (async). US token required. + + Returns a JSON string with shape:: + + { + "order": {...}, + "order_histories": [...], + "current_attached_order": null + } - When ``is_attached`` is ``True``, attached take-profit/stop-loss - sub-orders are included in the response. + Path: ``GET /v1/orders/{order_id}`` (no query parameters). Args: order_id: Order ID From 2750828c0b5970151f33024cb7c2e1337b4b7854 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=A2=81=E7=AB=A0=E6=B4=AA?= Date: Fri, 3 Jul 2026 22:13:42 +0800 Subject: [PATCH 37/44] fix: USRankTag.rank_type is i32/number (API returns integer, not string) Co-Authored-By: Claude Sonnet 4.6 (1M context) --- nodejs/index.d.ts | 2 +- nodejs/src/fundamental/types.rs | 2 +- python/pysrc/longbridge/openapi.pyi | 2 +- python/src/fundamental/types.rs | 2 +- rust/src/fundamental/types.rs | 2 +- 5 files changed, 5 insertions(+), 5 deletions(-) diff --git a/nodejs/index.d.ts b/nodejs/index.d.ts index 36cc161867..7a7bfe54f6 100644 --- a/nodejs/index.d.ts +++ b/nodejs/index.d.ts @@ -6113,7 +6113,7 @@ export interface USOrderDetailResponse { export interface USRankTag { name: string chg: string - rankType: string + rankType: number } export interface USCompanyOverview { diff --git a/nodejs/src/fundamental/types.rs b/nodejs/src/fundamental/types.rs index 9a61e71a3d..5ca0bbc4b7 100644 --- a/nodejs/src/fundamental/types.rs +++ b/nodejs/src/fundamental/types.rs @@ -2069,7 +2069,7 @@ use longbridge::fundamental::types as lb_us; pub struct USRankTag { pub name: String, pub chg: String, - pub rank_type: String, + pub rank_type: i32, } impl From for USRankTag { diff --git a/python/pysrc/longbridge/openapi.pyi b/python/pysrc/longbridge/openapi.pyi index d6a7d28e01..80acaad726 100644 --- a/python/pysrc/longbridge/openapi.pyi +++ b/python/pysrc/longbridge/openapi.pyi @@ -12510,7 +12510,7 @@ class USRankTag: """Tag name""" chg: str """Change value""" - rank_type: str + rank_type: int """Rank type""" diff --git a/python/src/fundamental/types.rs b/python/src/fundamental/types.rs index acb1ecacb8..baf113627e 100644 --- a/python/src/fundamental/types.rs +++ b/python/src/fundamental/types.rs @@ -2123,7 +2123,7 @@ use longbridge::fundamental::types as lb_us; pub(crate) struct USRankTag { pub name: String, pub chg: String, - pub rank_type: String, + pub rank_type: i32, } impl From for USRankTag { diff --git a/rust/src/fundamental/types.rs b/rust/src/fundamental/types.rs index 02e34a3616..96c8895a80 100644 --- a/rust/src/fundamental/types.rs +++ b/rust/src/fundamental/types.rs @@ -1735,7 +1735,7 @@ pub struct USRankTag { pub chg: String, /// Rank type identifier #[serde(default)] - pub rank_type: String, + pub rank_type: i32, } /// Response for [`crate::FundamentalContext::us_company_overview`]. From 414b11529fdc2b1cd2419cd7ac40a2f197b015ae Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=A2=81=E7=AB=A0=E6=B4=AA?= Date: Fri, 3 Jul 2026 22:48:35 +0800 Subject: [PATCH 38/44] refactor: Rust us_query_orders uses GetUSHistoryOrders (ergonomic, matches Go) - GetUSHistoryOrders: Symbol/Side/StartAt/EndAt/QueryType/Page/Limit typed fields; SDK builds POST body with defaults internally - QueryUSOrdersOptions kept as type alias for backward compat - USQueryOrdersBody is the internal serialization struct (pub(crate)) - Blocking wrapper and Python/Node.js bindings updated to use GetUSHistoryOrders (still accept old individual params from user, build GetUSHistoryOrders internally) Co-Authored-By: Claude Sonnet 4.6 (1M context) --- nodejs/src/trade/context.rs | 25 ++++++------ python/src/trade/context.rs | 25 ++++++------ python/src/trade/context_async.rs | 25 ++++++------ rust/src/blocking/trade.rs | 11 +++--- rust/src/lib.rs | 6 +-- rust/src/trade/context.rs | 63 ++++++++++++++++++++++++++----- rust/src/trade/mod.rs | 3 +- rust/src/trade/types.rs | 44 +++++++++++++++------ 8 files changed, 136 insertions(+), 66 deletions(-) diff --git a/nodejs/src/trade/context.rs b/nodejs/src/trade/context.rs index ce83932b83..f5f09eae18 100644 --- a/nodejs/src/trade/context.rs +++ b/nodejs/src/trade/context.rs @@ -523,18 +523,19 @@ impl TradeContext { query_version: f64, ) -> Result> { let ctx = self.ctx.clone(); - let opts = QueryUSOrdersOptions { - account_channel, - action, - start_at, - end_at, - counter_ids, - security_types, - query_type, - page, - limit, - query_version, - }; + let opts = longbridge::trade::GetUSHistoryOrders { + symbol: if counter_ids.is_empty() { None } else { Some(counter_ids[0].clone()) }, + side: match action { + 1 => longbridge::trade::OrderSide::Buy, + 2 => longbridge::trade::OrderSide::Sell, + _ => longbridge::trade::OrderSide::Unknown, + }, + start_at: start_at as i64, + end_at: end_at as i64, + query_type, + page, + limit, + }; env.spawn_future(async move { let resp = ctx.us_query_orders(opts).await.map_err(ErrorNewType)?; serde_json::to_string(&resp).map_err(|e| napi::Error::from_reason(e.to_string())) diff --git a/python/src/trade/context.rs b/python/src/trade/context.rs index 89421de566..d31d30a660 100644 --- a/python/src/trade/context.rs +++ b/python/src/trade/context.rs @@ -428,18 +428,19 @@ impl TradeContext { limit: i32, query_version: f64, ) -> PyResult { - let opts = QueryUSOrdersOptions { - account_channel, - action, - start_at, - end_at, - counter_ids, - security_types, - query_type, - page, - limit, - query_version, - }; + let opts = longbridge::trade::GetUSHistoryOrders { + symbol: if counter_ids.is_empty() { None } else { Some(counter_ids[0].clone()) }, + side: match action { + 1 => longbridge::trade::OrderSide::Buy, + 2 => longbridge::trade::OrderSide::Sell, + _ => longbridge::trade::OrderSide::Unknown, + }, + start_at: start_at as i64, + end_at: end_at as i64, + query_type, + page, + limit, + }; let resp = self.ctx.us_query_orders(opts).map_err(ErrorNewType)?; serde_json::to_string(&resp) .map_err(|e| pyo3::exceptions::PyValueError::new_err(e.to_string())) diff --git a/python/src/trade/context_async.rs b/python/src/trade/context_async.rs index abeb63fdf6..c6784623e9 100644 --- a/python/src/trade/context_async.rs +++ b/python/src/trade/context_async.rs @@ -507,18 +507,19 @@ impl AsyncTradeContext { query_version: f64, ) -> PyResult> { let ctx = self.ctx.clone(); - let opts = QueryUSOrdersOptions { - account_channel, - action, - start_at, - end_at, - counter_ids, - security_types, - query_type, - page, - limit, - query_version, - }; + let opts = longbridge::trade::GetUSHistoryOrders { + symbol: if counter_ids.is_empty() { None } else { Some(counter_ids[0].clone()) }, + side: match action { + 1 => longbridge::trade::OrderSide::Buy, + 2 => longbridge::trade::OrderSide::Sell, + _ => longbridge::trade::OrderSide::Unknown, + }, + start_at: start_at as i64, + end_at: end_at as i64, + query_type, + page, + limit, + }; pyo3_async_runtimes::tokio::future_into_py(py, async move { let resp = ctx.us_query_orders(opts).await.map_err(ErrorNewType)?; let s = serde_json::to_string(&resp) diff --git a/rust/src/blocking/trade.rs b/rust/src/blocking/trade.rs index 10087527ad..6a750723bf 100644 --- a/rust/src/blocking/trade.rs +++ b/rust/src/blocking/trade.rs @@ -7,10 +7,11 @@ use crate::{ AccountBalance, CashFlow, EstimateMaxPurchaseQuantityOptions, EstimateMaxPurchaseQuantityResponse, Execution, FundPositionsResponse, GetCashFlowOptions, GetFundPositionsOptions, GetHistoryExecutionsOptions, GetHistoryOrdersOptions, - GetStockPositionsOptions, GetTodayExecutionsOptions, GetTodayOrdersOptions, MarginRatio, - Order, OrderDetail, PushEvent, QueryUSOrdersOptions, QueryUSOrdersResponse, - ReplaceOrderOptions, StockPositionsResponse, SubmitOrderOptions, SubmitOrderResponse, - TopicType, TradeContext, USAssetOverview, USOrderDetailResponse, USRealizedPL, + GetStockPositionsOptions, GetTodayExecutionsOptions, GetTodayOrdersOptions, + GetUSHistoryOrders, MarginRatio, Order, OrderDetail, PushEvent, QueryUSOrdersOptions, + QueryUSOrdersResponse, ReplaceOrderOptions, StockPositionsResponse, SubmitOrderOptions, + SubmitOrderResponse, TopicType, TradeContext, USAssetOverview, USOrderDetailResponse, + USRealizedPL, }, }; @@ -461,7 +462,7 @@ impl TradeContextSync { // ── US-market blocking wrappers ─────────────────────────────────────────── /// Query the paginated US order list (blocking) - pub fn us_query_orders(&self, opts: QueryUSOrdersOptions) -> Result { + pub fn us_query_orders(&self, opts: GetUSHistoryOrders) -> Result { self.rt .call(move |ctx| async move { ctx.us_query_orders(opts).await }) } diff --git a/rust/src/lib.rs b/rust/src/lib.rs index 7b4640f014..f3d89caf44 100644 --- a/rust/src/lib.rs +++ b/rust/src/lib.rs @@ -59,8 +59,8 @@ pub use rust_decimal::Decimal; pub use screener::ScreenerContext; pub use sharelist::SharelistContext; pub use trade::{ - QueryUSOrdersOptions, QueryUSOrdersResponse, TradeContext, USAssetOverview, USCashEntry, - USCryptoEntry, USOrderDetailResponse, USOrderHistory, USRealizedPL, USRealizedPLEntry, - USRealizedPLMetric, + GetUSHistoryOrders, QueryUSOrdersOptions, QueryUSOrdersResponse, TradeContext, USAssetOverview, + USCashEntry, USCryptoEntry, USOrderDetailResponse, USOrderHistory, USRealizedPL, + USRealizedPLEntry, USRealizedPLMetric, }; pub use types::Market; diff --git a/rust/src/trade/context.rs b/rust/src/trade/context.rs index b90027a005..3d7d11c73f 100644 --- a/rust/src/trade/context.rs +++ b/rust/src/trade/context.rs @@ -13,10 +13,10 @@ use crate::{ AccountBalance, CashFlow, EstimateMaxPurchaseQuantityOptions, Execution, FundPositionsResponse, GetCashFlowOptions, GetFundPositionsOptions, GetHistoryExecutionsOptions, GetHistoryOrdersOptions, GetStockPositionsOptions, - GetTodayExecutionsOptions, GetTodayOrdersOptions, MarginRatio, Order, OrderDetail, - PushEvent, QueryUSOrdersOptions, QueryUSOrdersResponse, ReplaceOrderOptions, - StockPositionsResponse, SubmitOrderOptions, TopicType, USAssetOverview, - USOrderDetailResponse, USRealizedPL, + GetTodayExecutionsOptions, GetTodayOrdersOptions, GetUSHistoryOrders, MarginRatio, Order, + OrderDetail, OrderSide, PushEvent, QueryUSOrdersOptions, QueryUSOrdersResponse, + ReplaceOrderOptions, StockPositionsResponse, SubmitOrderOptions, TopicType, + USAssetOverview, USOrderDetailResponse, USRealizedPL, core::{Command, Core}, }, }; @@ -844,16 +844,61 @@ impl TradeContext { /// Path: `POST /v1/orders/query` /// /// US token required. - pub async fn us_query_orders( - &self, - opts: QueryUSOrdersOptions, - ) -> Result { + pub async fn us_query_orders(&self, opts: GetUSHistoryOrders) -> Result { + use std::time::{SystemTime, UNIX_EPOCH}; + + use crate::utils::counter::symbol_to_counter_id; + + let now = SystemTime::now() + .duration_since(UNIX_EPOCH) + .unwrap_or_default() + .as_secs() as i64; + + let action = match opts.side { + OrderSide::Buy => 1, + OrderSide::Sell => 2, + _ => 0, + }; + + let counter_ids = opts + .symbol + .as_deref() + .filter(|s| !s.is_empty()) + .map(|s| vec![symbol_to_counter_id(s)]) + .unwrap_or_default(); + + let start_at = if opts.start_at == 0 { + (now - 90 * 24 * 3600) as f64 + } else { + opts.start_at as f64 + }; + let end_at = if opts.end_at == 0 { + now as f64 + } else { + opts.end_at as f64 + }; + let page = if opts.page <= 0 { 1 } else { opts.page }; + let limit = if opts.limit <= 0 { 20 } else { opts.limit }; + + let body = super::types::USQueryOrdersBody { + account_channel: String::new(), + action, + start_at, + end_at, + counter_ids, + security_types: vec![], + query_type: opts.query_type, + page, + limit, + query_version: now as f64, + }; + Ok(self .0 .http_cli .request(Method::POST, "/v1/orders/query") .dc_restrict(DcRegion::Us) - .body(Json(opts)) + .body(Json(body)) .response::>() .send() .with_subscriber(self.0.log_subscriber.clone()) diff --git a/rust/src/trade/mod.rs b/rust/src/trade/mod.rs index 9cdd15d9ef..dcb6ee6035 100644 --- a/rust/src/trade/mod.rs +++ b/rust/src/trade/mod.rs @@ -28,6 +28,8 @@ pub use types::{ FundPosition, FundPositionChannel, FundPositionsResponse, + // US-market types + GetUSHistoryOrders, MarginRatio, Order, OrderChargeDetail, @@ -40,7 +42,6 @@ pub use types::{ OrderTag, OrderType, OutsideRTH, - // US-market types QueryUSOrdersOptions, QueryUSOrdersResponse, StockPosition, diff --git a/rust/src/trade/types.rs b/rust/src/trade/types.rs index 56e769987d..8988db08e3 100644 --- a/rust/src/trade/types.rs +++ b/rust/src/trade/types.rs @@ -807,28 +807,48 @@ impl_default_for_enum_string!( // ── US-market types // ─────────────────────────────────────────────────────────── -/// Request body for [`crate::TradeContext::us_query_orders`]. +/// Request for [`crate::TradeContext::us_query_orders`], modelled after +/// [`crate::GetHistoryOrdersOptions`] for HK/CN orders. +/// +/// `query_type`: 0 = all (includes Rejected), 1 = pending, 2 = history (filled +/// only). Default 0 matches what the app shows as "past orders". +/// +/// `symbol` accepts a user-facing symbol e.g. `"AAPL.US"` or `"DOGEUSD.BKKT"`. +/// The SDK converts it to the internal `counter_id` format automatically. +#[derive(Debug, Clone, Default)] +pub struct GetUSHistoryOrders { + /// Optional symbol filter, e.g. `"AAPL.US"`. Converted to counter_id + /// internally. + pub symbol: Option, + /// Direction filter. [`crate::OrderSide::Unknown`] = all (default). + pub side: OrderSide, + /// Start timestamp (seconds). Defaults to 90 days ago. + pub start_at: i64, + /// End timestamp (seconds). Defaults to now. + pub end_at: i64, + /// 0 = all, 1 = pending, 2 = history (filled only). Default 0. + pub query_type: i32, + /// Page number, 1-based. Default 1. + pub page: i32, + /// Page size. Default 20. + pub limit: i32, +} + +/// Alias kept for backward compatibility. +pub type QueryUSOrdersOptions = GetUSHistoryOrders; + +/// Internal JSON body sent to POST /v1/orders/query. #[derive(Debug, Clone, Serialize)] -pub struct QueryUSOrdersOptions { - /// Account channel (injected by the framework) +pub(crate) struct USQueryOrdersBody { pub account_channel: String, - /// Direction filter: 0 = all, 1 = buy, 2 = sell pub action: i32, - /// Start timestamp (seconds) pub start_at: f64, - /// End timestamp (seconds) pub end_at: f64, - /// Counter-ID filter (empty = all) pub counter_ids: Vec, - /// Security-type filter (empty = all) pub security_types: Vec, - /// Status: 0 = all, 1 = pending, 2 = history pub query_type: i32, - /// Page number (1-based) pub page: i32, - /// Page size pub limit: i32, - /// Epoch-seconds timestamp from the first request in a paging series pub query_version: f64, } From 1e468b0f22e70793be943cb3e2e69046738876c6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=A2=81=E7=AB=A0=E6=B4=AA?= Date: Fri, 3 Jul 2026 22:56:19 +0800 Subject: [PATCH 39/44] fix(review-7): simplify us_query_orders to ergonomic params across all layers MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit P1-1: Python/Node.js/Java binding us_query_orders now accepts symbol (optional), action (0/1/2), start_at, end_at, query_type, page, limit — same ergonomic style as Go GetUSHistoryOrders. Raw params (account_channel, counter_ids, security_types, query_version) are now internal SDK concerns. P1-2: Java us_query_orders was building QueryUSOrdersOptions (now alias) with old fields — compile error fixed by switching to GetUSHistoryOrders. Co-Authored-By: Claude Sonnet 4.6 (1M context) --- java/src/trade_context.rs | 32 ++++++++++++-------------- nodejs/index.d.ts | 4 +++- nodejs/src/trade/context.rs | 34 +++++++++++++-------------- python/src/trade/context.rs | 38 ++++++++++++++----------------- python/src/trade/context_async.rs | 37 ++++++++++++++---------------- 5 files changed, 68 insertions(+), 77 deletions(-) diff --git a/java/src/trade_context.rs b/java/src/trade_context.rs index 1535a04e23..1723957761 100644 --- a/java/src/trade_context.rs +++ b/java/src/trade_context.rs @@ -633,36 +633,34 @@ pub unsafe extern "system" fn Java_com_longbridge_SdkNative_tradeContextUsQueryO mut env: JNIEnv, _class: JClass, context: i64, - account_channel: JObject, + symbol: JObject, action: i64, start_at: i64, end_at: i64, - counter_ids: JObject, - security_types: JObject, query_type: i64, page: i64, limit: i64, - query_version: i64, callback: JObject, ) { - use crate::types::ObjectArray; jni_result(&mut env, (), |env| { let context = &*(context as *const ContextObj); - let account_channel: String = FromJValue::from_jvalue(env, account_channel.into())?; - let counter_ids: ObjectArray = FromJValue::from_jvalue(env, counter_ids.into())?; - let security_types: ObjectArray = - FromJValue::from_jvalue(env, security_types.into())?; - let us_opts = QueryUSOrdersOptions { - account_channel, - action: action as i32, - start_at: start_at as f64, - end_at: end_at as f64, - counter_ids: counter_ids.0, - security_types: security_types.0, + let symbol_str: String = FromJValue::from_jvalue(env, symbol.into())?; + let us_opts = longbridge::trade::GetUSHistoryOrders { + symbol: if symbol_str.is_empty() { + None + } else { + Some(symbol_str) + }, + side: match action { + 1 => longbridge::trade::OrderSide::Buy, + 2 => longbridge::trade::OrderSide::Sell, + _ => longbridge::trade::OrderSide::Unknown, + }, + start_at, + end_at, query_type: query_type as i32, page: page as i32, limit: limit as i32, - query_version: query_version as f64, }; async_util::execute(env, callback, async move { let resp = context.ctx.us_query_orders(us_opts).await?; diff --git a/nodejs/index.d.ts b/nodejs/index.d.ts index 7a7bfe54f6..1347f52999 100644 --- a/nodejs/index.d.ts +++ b/nodejs/index.d.ts @@ -2823,10 +2823,12 @@ export declare class TradeContext { /** Query US order list (paginated). Returns JSON string. US token required. */ /** * Query US order list. Returns JSON string with shape `{orders: USOrder[], total_count: number}`. + * symbol: user-facing symbol e.g. "AAPL.US" (optional). + * action: 0=all, 1=buy, 2=sell. * queryType: 0=all (incl. Rejected), 1=pending, 2=history (filled only). * US token required. */ - usQueryOrders(accountChannel: string, action: number, startAt: number, endAt: number, counterIds: string[], securityTypes: string[], queryType: number, page: number, limit: number, queryVersion: number): Promise + usQueryOrders(symbol?: string | null, action?: number, startAt?: number, endAt?: number, queryType?: number, page?: number, limit?: number): Promise /** Get US order detail. isAttached=true includes take-profit/stop-loss sub-orders. Returns JSON string. US token required. */ usOrderDetail(orderId: string): Promise /** Get US account asset overview (stocks/options/crypto/buy power). US token required. */ diff --git a/nodejs/src/trade/context.rs b/nodejs/src/trade/context.rs index f5f09eae18..4c00974b22 100644 --- a/nodejs/src/trade/context.rs +++ b/nodejs/src/trade/context.rs @@ -507,35 +507,33 @@ impl TradeContext { // ── US-market methods ───────────────────────────────────────────────── /// Query US order list. Returns JSON string. US token required. + /// symbol: user-facing symbol e.g. "AAPL.US"; action: 0=all/1=buy/2=sell. #[napi] pub fn us_query_orders<'env>( &self, env: &'env Env, - account_channel: String, + symbol: Option, action: i32, - start_at: f64, - end_at: f64, - counter_ids: Vec, - security_types: Vec, + start_at: i64, + end_at: i64, query_type: i32, page: i32, limit: i32, - query_version: f64, ) -> Result> { let ctx = self.ctx.clone(); let opts = longbridge::trade::GetUSHistoryOrders { - symbol: if counter_ids.is_empty() { None } else { Some(counter_ids[0].clone()) }, - side: match action { - 1 => longbridge::trade::OrderSide::Buy, - 2 => longbridge::trade::OrderSide::Sell, - _ => longbridge::trade::OrderSide::Unknown, - }, - start_at: start_at as i64, - end_at: end_at as i64, - query_type, - page, - limit, - }; + symbol, + side: match action { + 1 => longbridge::trade::OrderSide::Buy, + 2 => longbridge::trade::OrderSide::Sell, + _ => longbridge::trade::OrderSide::Unknown, + }, + start_at, + end_at, + query_type, + page, + limit, + }; env.spawn_future(async move { let resp = ctx.us_query_orders(opts).await.map_err(ErrorNewType)?; serde_json::to_string(&resp).map_err(|e| napi::Error::from_reason(e.to_string())) diff --git a/python/src/trade/context.rs b/python/src/trade/context.rs index d31d30a660..89fe7359ae 100644 --- a/python/src/trade/context.rs +++ b/python/src/trade/context.rs @@ -413,39 +413,35 @@ impl TradeContext { // ── US-market methods ───────────────────────────────────────────────── /// Query US order list (returns JSON string). US token required. - #[pyo3(signature = (account_channel, action, start_at, end_at, counter_ids, security_types, query_type, page, limit, query_version))] - #[allow(clippy::too_many_arguments)] + /// symbol: user-facing symbol e.g. "AAPL.US"; action: 0=all/1=buy/2=sell. + #[pyo3(signature = (symbol = None, action = 0, start_at = 0, end_at = 0, query_type = 0, page = 1, limit = 20))] fn us_query_orders( &self, - account_channel: String, + symbol: Option, action: i32, - start_at: f64, - end_at: f64, - counter_ids: Vec, - security_types: Vec, + start_at: i64, + end_at: i64, query_type: i32, page: i32, limit: i32, - query_version: f64, ) -> PyResult { let opts = longbridge::trade::GetUSHistoryOrders { - symbol: if counter_ids.is_empty() { None } else { Some(counter_ids[0].clone()) }, - side: match action { - 1 => longbridge::trade::OrderSide::Buy, - 2 => longbridge::trade::OrderSide::Sell, - _ => longbridge::trade::OrderSide::Unknown, - }, - start_at: start_at as i64, - end_at: end_at as i64, - query_type, - page, - limit, - }; + symbol, + side: match action { + 1 => longbridge::trade::OrderSide::Buy, + 2 => longbridge::trade::OrderSide::Sell, + _ => longbridge::trade::OrderSide::Unknown, + }, + start_at, + end_at, + query_type, + page, + limit, + }; let resp = self.ctx.us_query_orders(opts).map_err(ErrorNewType)?; serde_json::to_string(&resp) .map_err(|e| pyo3::exceptions::PyValueError::new_err(e.to_string())) } - /// Get US order detail (returns JSON string). US token required. fn us_order_detail(&self, order_id: String) -> PyResult { let resp = self.ctx.us_order_detail(order_id).map_err(ErrorNewType)?; diff --git a/python/src/trade/context_async.rs b/python/src/trade/context_async.rs index c6784623e9..3aa20563c0 100644 --- a/python/src/trade/context_async.rs +++ b/python/src/trade/context_async.rs @@ -490,36 +490,33 @@ impl AsyncTradeContext { // ── US-market async methods ─────────────────────────────────────────── /// Query US order list (JSON string). US token required. Returns awaitable. - #[pyo3(signature = (account_channel, action, start_at, end_at, counter_ids, security_types, query_type, page, limit, query_version))] - #[allow(clippy::too_many_arguments)] + /// symbol: user-facing symbol e.g. "AAPL.US"; action: 0=all/1=buy/2=sell. + #[pyo3(signature = (symbol = None, action = 0, start_at = 0, end_at = 0, query_type = 0, page = 1, limit = 20))] fn us_query_orders( &self, py: Python<'_>, - account_channel: String, + symbol: Option, action: i32, - start_at: f64, - end_at: f64, - counter_ids: Vec, - security_types: Vec, + start_at: i64, + end_at: i64, query_type: i32, page: i32, limit: i32, - query_version: f64, ) -> PyResult> { let ctx = self.ctx.clone(); let opts = longbridge::trade::GetUSHistoryOrders { - symbol: if counter_ids.is_empty() { None } else { Some(counter_ids[0].clone()) }, - side: match action { - 1 => longbridge::trade::OrderSide::Buy, - 2 => longbridge::trade::OrderSide::Sell, - _ => longbridge::trade::OrderSide::Unknown, - }, - start_at: start_at as i64, - end_at: end_at as i64, - query_type, - page, - limit, - }; + symbol, + side: match action { + 1 => longbridge::trade::OrderSide::Buy, + 2 => longbridge::trade::OrderSide::Sell, + _ => longbridge::trade::OrderSide::Unknown, + }, + start_at, + end_at, + query_type, + page, + limit, + }; pyo3_async_runtimes::tokio::future_into_py(py, async move { let resp = ctx.us_query_orders(opts).await.map_err(ErrorNewType)?; let s = serde_json::to_string(&resp) From be8ffb6897bf696460fdcb6289b7b51924426588 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=A2=81=E7=AB=A0=E6=B4=AA?= Date: Fri, 3 Jul 2026 23:00:59 +0800 Subject: [PATCH 40/44] fix: update Python stub us_query_orders to ergonomic signature Matches the binding: (symbol?, action, start_at, end_at, query_type, page, limit) Removes: account_channel, counter_ids, security_types, query_version Co-Authored-By: Claude Sonnet 4.6 (1M context) --- python/pysrc/longbridge/openapi.pyi | 34 ++++++++++++----------------- 1 file changed, 14 insertions(+), 20 deletions(-) diff --git a/python/pysrc/longbridge/openapi.pyi b/python/pysrc/longbridge/openapi.pyi index 80acaad726..8560649b54 100644 --- a/python/pysrc/longbridge/openapi.pyi +++ b/python/pysrc/longbridge/openapi.pyi @@ -7343,16 +7343,13 @@ class TradeContext: def us_query_orders( self, - account_channel: str, - action: int, - start_at: float, - end_at: float, - counter_ids: list, - security_types: list, - query_type: int, - page: int, - limit: int, - query_version: float, + symbol: Optional[str] = None, + action: int = 0, + start_at: int = 0, + end_at: int = 0, + query_type: int = 0, + page: int = 1, + limit: int = 20, ) -> str: """Query US order list (paginated). Returns JSON string. US token required. @@ -8085,16 +8082,13 @@ class AsyncTradeContext: def us_query_orders( self, - account_channel: str, - action: int, - start_at: float, - end_at: float, - counter_ids: list, - security_types: list, - query_type: int, - page: int, - limit: int, - query_version: float, + symbol: Optional[str] = None, + action: int = 0, + start_at: int = 0, + end_at: int = 0, + query_type: int = 0, + page: int = 1, + limit: int = 20, ) -> "Awaitable[str]": """Query US order list (paginated). Returns awaitable JSON string. US token required. From bfa1eb985eea182c3e4410ea4cce2f925e83c29a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=A2=81=E7=AB=A0=E6=B4=AA?= Date: Sun, 5 Jul 2026 09:06:43 +0800 Subject: [PATCH 41/44] docs: fix stale us_query_orders Args docstring in Python stubs Remove old raw params (account_channel, counter_ids, security_types, query_version) from docstring; replace with ergonomic params. Co-Authored-By: Claude Sonnet 4.6 (1M context) --- python/pysrc/longbridge/openapi.pyi | 30 ++++++++++++----------------- 1 file changed, 12 insertions(+), 18 deletions(-) diff --git a/python/pysrc/longbridge/openapi.pyi b/python/pysrc/longbridge/openapi.pyi index 8560649b54..8f2dc62365 100644 --- a/python/pysrc/longbridge/openapi.pyi +++ b/python/pysrc/longbridge/openapi.pyi @@ -7354,16 +7354,13 @@ class TradeContext: """Query US order list (paginated). Returns JSON string. US token required. Args: - account_channel: Account channel (e.g. ``"regular"``) - action: Order action filter - start_at: Start timestamp (unix seconds) - end_at: End timestamp (unix seconds) - counter_ids: Filter by counter IDs - security_types: Filter by security types (``"stock"``, ``"option"``, ``"crypto"``) - query_type: Query type code - page: Page number (1-based) + symbol: Optional symbol filter, e.g. ``"AAPL.US"`` + action: Direction filter: 0=all, 1=buy, 2=sell + start_at: Start timestamp (unix seconds); 0 = last 90 days + end_at: End timestamp (unix seconds); 0 = now + query_type: 0=all (incl. Rejected), 1=pending, 2=history (filled only) + page: Page number, 1-based limit: Page size - query_version: Query version (pass ``0`` for latest) Returns: JSON string with order list @@ -8093,16 +8090,13 @@ class AsyncTradeContext: """Query US order list (paginated). Returns awaitable JSON string. US token required. Args: - account_channel: Account channel (e.g. ``"regular"``) - action: Order action filter - start_at: Start timestamp (unix seconds) - end_at: End timestamp (unix seconds) - counter_ids: Filter by counter IDs - security_types: Filter by security types (``"stock"``, ``"option"``, ``"crypto"``) - query_type: Query type code - page: Page number (1-based) + symbol: Optional symbol filter, e.g. ``"AAPL.US"`` + action: Direction filter: 0=all, 1=buy, 2=sell + start_at: Start timestamp (unix seconds); 0 = last 90 days + end_at: End timestamp (unix seconds); 0 = now + query_type: 0=all (incl. Rejected), 1=pending, 2=history (filled only) + page: Page number, 1-based limit: Page size - query_version: Query version (pass ``0`` for latest) Returns: Awaitable resolving to JSON string with order list From 73c25f5c6b171110df4f490120673e21ef3947f0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=A2=81=E7=AB=A0=E6=B4=AA?= Date: Sun, 5 Jul 2026 11:40:09 +0800 Subject: [PATCH 42/44] =?UTF-8?q?fix(review-10):=20add=20GetUSRealizedPLOp?= =?UTF-8?q?tions=20struct=20=E2=80=94=20consistent=20with=20GetUSHistoryOr?= =?UTF-8?q?ders?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Rust us_realized_pl now takes GetUSRealizedPLOptions{currency, category} instead of raw strings, matching Go's *GetUSRealizedPL pattern. Empty currency defaults to 'USD'; empty category = all assets. Co-Authored-By: Claude Sonnet 4.6 (1M context) --- java/src/trade_context.rs | 2 +- nodejs/src/trade/context.rs | 2 +- python/src/trade/context.rs | 2 +- python/src/trade/context_async.rs | 2 +- rust/src/blocking/trade.rs | 16 ++++++---------- rust/src/lib.rs | 6 +++--- rust/src/trade/context.rs | 30 +++++++++++++++++------------- rust/src/trade/mod.rs | 1 + rust/src/trade/types.rs | 10 ++++++++++ 9 files changed, 41 insertions(+), 30 deletions(-) diff --git a/java/src/trade_context.rs b/java/src/trade_context.rs index 1723957761..66a7b49fdb 100644 --- a/java/src/trade_context.rs +++ b/java/src/trade_context.rs @@ -720,7 +720,7 @@ pub unsafe extern "system" fn Java_com_longbridge_SdkNative_tradeContextUsRealiz let currency: String = FromJValue::from_jvalue(env, currency.into())?; let category: Option = FromJValue::from_jvalue(env, category.into()).ok(); async_util::execute(env, callback, async move { - let resp = context.ctx.us_realized_pl(currency, category).await?; + let resp = context.ctx.us_realized_pl(longbridge::trade::GetUSRealizedPLOptions { currency, category: category.unwrap_or_default() }).await?; Ok(serde_json::to_string(&resp).unwrap_or_default()) })?; Ok(()) diff --git a/nodejs/src/trade/context.rs b/nodejs/src/trade/context.rs index 4c00974b22..036807b5cf 100644 --- a/nodejs/src/trade/context.rs +++ b/nodejs/src/trade/context.rs @@ -577,7 +577,7 @@ impl TradeContext { let ctx = self.ctx.clone(); env.spawn_future(async move { Ok(ctx - .us_realized_pl(currency, category) + .us_realized_pl(longbridge::trade::GetUSRealizedPLOptions { currency, category: category.unwrap_or_default() }) .await .map_err(ErrorNewType)? .into()) diff --git a/python/src/trade/context.rs b/python/src/trade/context.rs index 89fe7359ae..6395ac577f 100644 --- a/python/src/trade/context.rs +++ b/python/src/trade/context.rs @@ -462,7 +462,7 @@ impl TradeContext { ) -> PyResult { Ok(self .ctx - .us_realized_pl(currency, category) + .us_realized_pl(longbridge::trade::GetUSRealizedPLOptions { currency, category: category.unwrap_or_default() }) .map_err(ErrorNewType)? .into()) } diff --git a/python/src/trade/context_async.rs b/python/src/trade/context_async.rs index 3aa20563c0..7ed850708a 100644 --- a/python/src/trade/context_async.rs +++ b/python/src/trade/context_async.rs @@ -559,7 +559,7 @@ impl AsyncTradeContext { let ctx = self.ctx.clone(); pyo3_async_runtimes::tokio::future_into_py(py, async move { let r: crate::trade::types::USRealizedPL = ctx - .us_realized_pl(currency, category) + .us_realized_pl(longbridge::trade::GetUSRealizedPLOptions { currency, category: category.unwrap_or_default() }) .await .map_err(ErrorNewType)? .into(); diff --git a/rust/src/blocking/trade.rs b/rust/src/blocking/trade.rs index 6a750723bf..75940a820f 100644 --- a/rust/src/blocking/trade.rs +++ b/rust/src/blocking/trade.rs @@ -8,10 +8,10 @@ use crate::{ EstimateMaxPurchaseQuantityResponse, Execution, FundPositionsResponse, GetCashFlowOptions, GetFundPositionsOptions, GetHistoryExecutionsOptions, GetHistoryOrdersOptions, GetStockPositionsOptions, GetTodayExecutionsOptions, GetTodayOrdersOptions, - GetUSHistoryOrders, MarginRatio, Order, OrderDetail, PushEvent, QueryUSOrdersOptions, - QueryUSOrdersResponse, ReplaceOrderOptions, StockPositionsResponse, SubmitOrderOptions, - SubmitOrderResponse, TopicType, TradeContext, USAssetOverview, USOrderDetailResponse, - USRealizedPL, + GetUSHistoryOrders, GetUSRealizedPLOptions, MarginRatio, Order, OrderDetail, PushEvent, + QueryUSOrdersOptions, QueryUSOrdersResponse, ReplaceOrderOptions, StockPositionsResponse, + SubmitOrderOptions, SubmitOrderResponse, TopicType, TradeContext, USAssetOverview, + USOrderDetailResponse, USRealizedPL, }, }; @@ -483,12 +483,8 @@ impl TradeContextSync { } /// Get realized P&L for the US account (blocking) - pub fn us_realized_pl( - &self, - currency: impl Into + Send + 'static, - category: Option + Send + 'static>, - ) -> Result { + pub fn us_realized_pl(&self, opts: GetUSRealizedPLOptions) -> Result { self.rt - .call(move |ctx| async move { ctx.us_realized_pl(currency, category).await }) + .call(move |ctx| async move { ctx.us_realized_pl(opts).await }) } } diff --git a/rust/src/lib.rs b/rust/src/lib.rs index f3d89caf44..45bcb840a3 100644 --- a/rust/src/lib.rs +++ b/rust/src/lib.rs @@ -59,8 +59,8 @@ pub use rust_decimal::Decimal; pub use screener::ScreenerContext; pub use sharelist::SharelistContext; pub use trade::{ - GetUSHistoryOrders, QueryUSOrdersOptions, QueryUSOrdersResponse, TradeContext, USAssetOverview, - USCashEntry, USCryptoEntry, USOrderDetailResponse, USOrderHistory, USRealizedPL, - USRealizedPLEntry, USRealizedPLMetric, + GetUSHistoryOrders, GetUSRealizedPLOptions, QueryUSOrdersOptions, QueryUSOrdersResponse, + TradeContext, USAssetOverview, USCashEntry, USCryptoEntry, USOrderDetailResponse, + USOrderHistory, USRealizedPL, USRealizedPLEntry, USRealizedPLMetric, }; pub use types::Market; diff --git a/rust/src/trade/context.rs b/rust/src/trade/context.rs index 3d7d11c73f..cfd8ad4176 100644 --- a/rust/src/trade/context.rs +++ b/rust/src/trade/context.rs @@ -13,10 +13,10 @@ use crate::{ AccountBalance, CashFlow, EstimateMaxPurchaseQuantityOptions, Execution, FundPositionsResponse, GetCashFlowOptions, GetFundPositionsOptions, GetHistoryExecutionsOptions, GetHistoryOrdersOptions, GetStockPositionsOptions, - GetTodayExecutionsOptions, GetTodayOrdersOptions, GetUSHistoryOrders, MarginRatio, Order, - OrderDetail, OrderSide, PushEvent, QueryUSOrdersOptions, QueryUSOrdersResponse, - ReplaceOrderOptions, StockPositionsResponse, SubmitOrderOptions, TopicType, - USAssetOverview, USOrderDetailResponse, USRealizedPL, + GetTodayExecutionsOptions, GetTodayOrdersOptions, GetUSHistoryOrders, + GetUSRealizedPLOptions, MarginRatio, Order, OrderDetail, OrderSide, PushEvent, + QueryUSOrdersOptions, QueryUSOrdersResponse, ReplaceOrderOptions, StockPositionsResponse, + SubmitOrderOptions, TopicType, USAssetOverview, USOrderDetailResponse, USRealizedPL, core::{Command, Core}, }, }; @@ -958,11 +958,7 @@ impl TradeContext { /// Path: `GET /v1/us/assets/pl/realized` /// /// US token required. - pub async fn us_realized_pl( - &self, - currency: impl Into, - category: Option>, - ) -> Result { + pub async fn us_realized_pl(&self, opts: GetUSRealizedPLOptions) -> Result { #[derive(Serialize)] struct Query { currency: String, @@ -970,15 +966,23 @@ impl TradeContext { category: Option, } + let currency = if opts.currency.is_empty() { + "USD".to_string() + } else { + opts.currency + }; + let category = if opts.category.is_empty() { + None + } else { + Some(opts.category) + }; + Ok(self .0 .http_cli .request(Method::GET, "/v1/us/assets/pl/realized") .dc_restrict(DcRegion::Us) - .query_params(Query { - currency: currency.into(), - category: category.map(Into::into), - }) + .query_params(Query { currency, category }) .response::>() .send() .with_subscriber(self.0.log_subscriber.clone()) diff --git a/rust/src/trade/mod.rs b/rust/src/trade/mod.rs index dcb6ee6035..4f1b070409 100644 --- a/rust/src/trade/mod.rs +++ b/rust/src/trade/mod.rs @@ -30,6 +30,7 @@ pub use types::{ FundPositionsResponse, // US-market types GetUSHistoryOrders, + GetUSRealizedPLOptions, MarginRatio, Order, OrderChargeDetail, diff --git a/rust/src/trade/types.rs b/rust/src/trade/types.rs index 8988db08e3..b7215ccb62 100644 --- a/rust/src/trade/types.rs +++ b/rust/src/trade/types.rs @@ -975,6 +975,16 @@ pub struct USRealizedPLEntry { pub metrics: Vec, } +/// Request for [`crate::TradeContext::us_realized_pl`], modelled after +/// [`crate::GetUSHistoryOrders`]. +#[derive(Debug, Clone, Default)] +pub struct GetUSRealizedPLOptions { + /// Currency, e.g. `"USD"`. Defaults to `"USD"` if empty. + pub currency: String, + /// Asset category filter: `""` = all, `"STOCK"`, `"OPTION"`, `"CRYPTO"`. + pub category: String, +} + /// Response for [`crate::TradeContext::us_realized_pl`]. /// Field name matches the actual API response from `GET /// /v1/us/assets/pl/realized`. From 1815cf80dc795b39a42f36c47f5a7b46176a76de Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=A2=81=E7=AB=A0=E6=B4=AA?= Date: Sun, 5 Jul 2026 12:17:07 +0800 Subject: [PATCH 43/44] revert: CryptoOverview back to explicit PAIR.EXCHANGE symbol format Co-Authored-By: Claude Sonnet 4.6 (1M context) --- rust/src/quote/context.rs | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/rust/src/quote/context.rs b/rust/src/quote/context.rs index 0e24025a14..6d686cc9b9 100644 --- a/rust/src/quote/context.rs +++ b/rust/src/quote/context.rs @@ -2290,15 +2290,12 @@ impl QuoteContext { /// Get cryptocurrency market overview. /// - /// `symbol` must be in `PAIR.EXCHANGE` format, e.g. `"BTCUSD.HAS"` → - /// `"VA/HAS/BTCUSD"`. Uses `symbol_to_counter_id`, consistent with all - /// other symbol-based methods. + /// `symbol` must be in `PAIR.EXCHANGE` format, e.g. `"BTCUSD.BKKT"`. + /// US DC uses **BKKT** exchange; pass the exchange suffix explicitly. /// /// Path: `GET /v1/gemini/us/crypto-overview` /// - /// US token required — returns - /// [`longbridge_httpcli::HttpClientError::DcRegionRestricted`] for non-US - /// credentials. + /// US token required. pub async fn us_crypto_overview( &self, symbol: impl Into, From 68c5be4432915a9f466f90eca47b892fb4f7c032 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=A2=81=E7=AB=A0=E6=B4=AA?= Date: Sun, 5 Jul 2026 12:27:00 +0800 Subject: [PATCH 44/44] docs: add BTCUSD.BKKT example to us_crypto_overview Rust doc Co-Authored-By: Claude Sonnet 4.6 (1M context) --- rust/src/quote/context.rs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/rust/src/quote/context.rs b/rust/src/quote/context.rs index 6d686cc9b9..85be46679f 100644 --- a/rust/src/quote/context.rs +++ b/rust/src/quote/context.rs @@ -2290,8 +2290,9 @@ impl QuoteContext { /// Get cryptocurrency market overview. /// - /// `symbol` must be in `PAIR.EXCHANGE` format, e.g. `"BTCUSD.BKKT"`. - /// US DC uses **BKKT** exchange; pass the exchange suffix explicitly. + /// `symbol` must be in `PAIR.EXCHANGE` format. + /// US DC uses the **BKKT** exchange: e.g. `"BTCUSD.BKKT"` → + /// `"VA/BKKT/BTCUSD"`. Pass the exchange suffix explicitly. /// /// Path: `GET /v1/gemini/us/crypto-overview` ///