From 97c0c276e5bb9d4d042043ef63df84d88a50836c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=A2=81=E7=AB=A0=E6=B4=AA?= Date: Wed, 20 May 2026 14:12:17 +0800 Subject: [PATCH 01/12] feat(fundamental,quote,market,screener): add 13 new Rust SDK API methods - FundamentalContext: shareholder_top, shareholder_detail, valuation_comparison - QuoteContext: hk_short_positions, short_trades (auto-detects HK vs US endpoint) - MarketContext: stock_events (POST), rank_categories, rank_list - New ScreenerContext: screener_recommend_strategies, screener_user_strategies, screener_strategy, screener_search (POST), screener_indicators - All methods have matching blocking (sync) wrappers - All new response types use serde_json::Value for flexible raw-JSON payloads Co-Authored-By: Claude Sonnet 4.6 (1M context) --- CHANGELOG.md | 25 ++--- rust/src/blocking/fundamental.rs | 32 ++++++ rust/src/blocking/market.rs | 38 ++++++- rust/src/blocking/mod.rs | 2 + rust/src/blocking/quote.rs | 36 +++++-- rust/src/blocking/screener.rs | 73 ++++++++++++++ rust/src/fundamental/context.rs | 93 +++++++++++++++-- rust/src/fundamental/types.rs | 27 +++++ rust/src/lib.rs | 2 + rust/src/market/context.rs | 90 +++++++++++++++++ rust/src/market/types.rs | 37 +++++++ rust/src/quote/context.rs | 100 ++++++++++++++++-- rust/src/quote/mod.rs | 2 + rust/src/quote/types.rs | 24 +++++ rust/src/screener/context.rs | 168 +++++++++++++++++++++++++++++++ rust/src/screener/mod.rs | 7 ++ rust/src/screener/types.rs | 64 ++++++++++++ 17 files changed, 786 insertions(+), 34 deletions(-) create mode 100644 rust/src/blocking/screener.rs create mode 100644 rust/src/screener/context.rs create mode 100644 rust/src/screener/mod.rs create mode 100644 rust/src/screener/types.rs diff --git a/CHANGELOG.md b/CHANGELOG.md index 2b88801a1..23d9a50cf 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,18 +10,19 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - **Rust, Python, Node.js:** Document normalization formulas for `SecurityCalcIndex` Greeks fields: `theta` (divide by 252 for per-trading-day), `vega` and `rho` (divide by 100 for per-unit change). The raw API values differ from Longbridge app display values by these factors. - **Rust:** `Config::header(key, value)` builder method to inject custom headers into every HTTP request and WebSocket upgrade request. -- **Rust, Python:** `ContentContext` adds three new methods: - - `topic_detail(topic_id)` — get detail of a single topic. - - `list_topic_replies(opts)` — list replies for a topic, with optional page/size filtering. - - `create_topic_reply(opts)` — create a reply under a topic. -- **Rust, Python:** New types `ListTopicRepliesOptions`, `CreateReplyOptions`, and `TopicReply` to support the above methods. -- **All languages (Rust/Python/Node.js/Java/C/C++):** Six new `FundamentalContext` methods: - - `BusinessSegments` — GET `/v1/quote/fundamentals/business-segments`: latest business segment breakdown. - - `BusinessSegmentsHistory` — GET `/v1/quote/fundamentals/business-segments/history`: historical business and regional segment breakdowns with optional `report` and `cate` filters. - - `InstitutionRatingViews` — GET `/v1/quote/ratings/institutional`: historical rating distribution time-series (buy/over/hold/under/sell per date). - - `IndustryRank` — GET `/v1/quote/industry/rank`: industry leaderboard; exposes `IndustryRankIndicator` and `IndustryRankSortType` enum constants. - - `IndustryPeers` — GET `/v1/quote/industries/peers`: recursive industry peer chain; accepts both symbol-style (`AAPL.US`) and raw counter IDs (`BK/US/123`). - - `FinancialReportSnapshot` — GET `/v1/quote/financials/earnings-snapshot`: earnings snapshot with forecast and reported metrics. +- **Rust, Python:** `ContentContext` adds three new methods: `topic_detail`, `list_topic_replies`, `create_topic_reply`. +- **All languages (Rust/Python/Node.js/Java/C/C++):** Six new `FundamentalContext` methods (PR #526): + - `BusinessSegments` — GET `/v1/quote/fundamentals/business-segments` + - `BusinessSegmentsHistory` — GET `/v1/quote/fundamentals/business-segments/history` + - `InstitutionRatingViews` — GET `/v1/quote/ratings/institutional` + - `IndustryRank` — GET `/v1/quote/industry/rank` + - `IndustryPeers` — GET `/v1/quote/industries/peers` + - `FinancialReportSnapshot` — GET `/v1/quote/financials/earnings-snapshot` +- **All languages (Rust/Python/Node.js/Java/C/C++):** 13 new APIs across FundamentalContext, QuoteContext, MarketContext, and new ScreenerContext: + - `FundamentalContext`: `shareholder_top`, `shareholder_detail`, `valuation_comparison` + - `QuoteContext`: `short_positions` (extended to HK+US), `short_trades` + - `MarketContext`: `top_movers`, `rank_categories`, `rank_list` + - `ScreenerContext` (new): `screener_recommend_strategies`, `screener_user_strategies`, `screener_strategy`, `screener_search`, `screener_indicators` # [4.1.0] diff --git a/rust/src/blocking/fundamental.rs b/rust/src/blocking/fundamental.rs index b0e539c51..90b209f48 100644 --- a/rust/src/blocking/fundamental.rs +++ b/rust/src/blocking/fundamental.rs @@ -248,4 +248,36 @@ impl FundamentalContextSync { .await }) } + + /// Get ranked list of top shareholders + pub fn shareholder_top( + &self, + symbol: impl Into + Send + 'static, + ) -> Result { + self.rt + .call(move |ctx| async move { ctx.shareholder_top(symbol).await }) + } + + /// Get holding history and detail for one shareholder object + pub fn shareholder_detail( + &self, + symbol: impl Into + Send + 'static, + object_id: i64, + ) -> Result { + self.rt + .call(move |ctx| async move { ctx.shareholder_detail(symbol, object_id).await }) + } + + /// Get valuation comparison between a security and optional peers + pub fn valuation_comparison( + &self, + symbol: impl Into + Send + 'static, + currency: impl Into + Send + 'static, + comparison_symbols: Option>, + ) -> Result { + self.rt.call(move |ctx| async move { + ctx.valuation_comparison(symbol, currency, comparison_symbols) + .await + }) + } } diff --git a/rust/src/blocking/market.rs b/rust/src/blocking/market.rs index 534758e72..ad486c3d6 100644 --- a/rust/src/blocking/market.rs +++ b/rust/src/blocking/market.rs @@ -5,7 +5,15 @@ use tokio::sync::mpsc; use crate::{ Config, Result, blocking::runtime::BlockingRuntime, - market::{MarketContext, types::*}, + market::{ + MarketContext, + types::{ + AhPremiumIntraday, AhPremiumKlines, AhPremiumPeriod, AnomalyResponse, + BrokerHoldingDailyHistory, BrokerHoldingDetail, BrokerHoldingPeriod, BrokerHoldingTop, + IndexConstituents, MarketStatusResponse, RankCategoriesResponse, RankListResponse, + StockEventsResponse, TradeStatsResponse, + }, + }, }; /// Blocking market data context @@ -105,4 +113,32 @@ impl MarketContextSync { self.rt .call(move |ctx| async move { ctx.constituent(symbol).await }) } + + /// Get stock events across one or more markets + pub fn stock_events( + &self, + markets: Vec, + sort: u32, + date: Option, + limit: u32, + ) -> Result { + self.rt + .call(move |ctx| async move { ctx.stock_events(markets, sort, date, limit).await }) + } + + /// Get all available rank category keys and labels + pub fn rank_categories(&self) -> Result { + self.rt + .call(|ctx| async move { ctx.rank_categories().await }) + } + + /// Get a ranked list of securities for the given category key + pub fn rank_list( + &self, + key: impl Into + Send + 'static, + need_article: bool, + ) -> Result { + self.rt + .call(move |ctx| async move { ctx.rank_list(key, need_article).await }) + } } diff --git a/rust/src/blocking/mod.rs b/rust/src/blocking/mod.rs index f01b95554..82f70862f 100644 --- a/rust/src/blocking/mod.rs +++ b/rust/src/blocking/mod.rs @@ -11,6 +11,7 @@ mod market; mod portfolio; mod quote; mod runtime; +mod screener; mod sharelist; mod trade; @@ -24,5 +25,6 @@ pub use fundamental::FundamentalContextSync; pub use market::MarketContextSync; pub use portfolio::PortfolioContextSync; pub use quote::QuoteContextSync; +pub use screener::ScreenerContextSync; pub use sharelist::SharelistContextSync; pub use trade::TradeContextSync; diff --git a/rust/src/blocking/quote.rs b/rust/src/blocking/quote.rs index d0966d74c..06cd9e57a 100644 --- a/rust/src/blocking/quote.rs +++ b/rust/src/blocking/quote.rs @@ -8,14 +8,14 @@ use crate::{ quote::{ AdjustType, CalcIndex, Candlestick, CapitalDistributionResponse, CapitalFlowLine, FilingItem, FilterWarrantExpiryDate, FilterWarrantInOutBoundsType, - HistoryMarketTemperatureResponse, IntradayLine, IssuerInfo, MarketTemperature, - MarketTradingDays, MarketTradingSession, OptionQuote, OptionVolumeDaily, OptionVolumeStats, - ParticipantInfo, Period, PinnedMode, PushEvent, QuotePackageDetail, RealtimeQuote, - RequestCreateWatchlistGroup, RequestUpdateWatchlistGroup, Security, SecurityBrokers, - SecurityCalcIndex, SecurityDepth, SecurityListCategory, SecurityQuote, SecurityStaticInfo, - ShortPositionsResponse, SortOrderType, StrikePriceInfo, SubFlags, Subscription, Trade, - TradeSessions, WarrantInfo, WarrantQuote, WarrantSortBy, WarrantStatus, WarrantType, - WatchlistGroup, + HistoryMarketTemperatureResponse, HkShortPositionsResponse, IntradayLine, IssuerInfo, + MarketTemperature, MarketTradingDays, MarketTradingSession, OptionQuote, OptionVolumeDaily, + OptionVolumeStats, ParticipantInfo, Period, PinnedMode, PushEvent, QuotePackageDetail, + RealtimeQuote, RequestCreateWatchlistGroup, RequestUpdateWatchlistGroup, Security, + SecurityBrokers, SecurityCalcIndex, SecurityDepth, SecurityListCategory, SecurityQuote, + SecurityStaticInfo, ShortPositionsResponse, ShortTradesResponse, SortOrderType, + StrikePriceInfo, SubFlags, Subscription, Trade, TradeSessions, WarrantInfo, WarrantQuote, + WarrantSortBy, WarrantStatus, WarrantType, WatchlistGroup, }, }; @@ -1202,4 +1202,24 @@ impl QuoteContextSync { self.rt .call(move |ctx| async move { ctx.update_pinned(mode, symbols).await }) } + + /// Get HK short interest / position data for a security + pub fn hk_short_positions( + &self, + symbol: impl Into + Send + 'static, + count: u32, + ) -> Result { + self.rt + .call(move |ctx| async move { ctx.hk_short_positions(symbol, count).await }) + } + + /// Get short trade records for a HK or US security + pub fn short_trades( + &self, + symbol: impl Into + Send + 'static, + count: u32, + ) -> Result { + self.rt + .call(move |ctx| async move { ctx.short_trades(symbol, count).await }) + } } diff --git a/rust/src/blocking/screener.rs b/rust/src/blocking/screener.rs new file mode 100644 index 000000000..b5cbd3e1b --- /dev/null +++ b/rust/src/blocking/screener.rs @@ -0,0 +1,73 @@ +use std::sync::Arc; + +use tokio::sync::mpsc; + +use crate::{ + Config, Result, + blocking::runtime::BlockingRuntime, + screener::{ + ScreenerContext, + types::{ + ScreenerIndicatorsResponse, ScreenerRecommendStrategiesResponse, + ScreenerSearchResponse, ScreenerStrategyResponse, ScreenerUserStrategiesResponse, + }, + }, +}; + +/// Blocking screener context +pub struct ScreenerContextSync { + rt: BlockingRuntime, +} + +impl ScreenerContextSync { + /// Create a [`ScreenerContextSync`] + pub fn new(config: Arc) -> Result { + let rt = BlockingRuntime::try_new( + move || { + let ctx = ScreenerContext::new(config); + let (tx, rx) = mpsc::unbounded_channel::(); + std::mem::forget(tx); + Ok::<_, crate::Error>((ctx, rx)) + }, + |_: std::convert::Infallible| {}, + )?; + Ok(Self { rt }) + } + + /// Get recommended built-in screener strategies + pub fn screener_recommend_strategies(&self) -> Result { + self.rt + .call(|ctx| async move { ctx.screener_recommend_strategies().await }) + } + + /// Get the current user's saved screener strategies + pub fn screener_user_strategies(&self) -> Result { + self.rt + .call(|ctx| async move { ctx.screener_user_strategies().await }) + } + + /// Get detail for one screener strategy by ID + pub fn screener_strategy(&self, id: i64) -> Result { + self.rt + .call(move |ctx| async move { ctx.screener_strategy(id).await }) + } + + /// Search / screen securities using a strategy + pub fn screener_search( + &self, + market: impl Into + Send + 'static, + strategy_id: Option, + page: u32, + size: u32, + ) -> Result { + self.rt.call(move |ctx| async move { + ctx.screener_search(market, strategy_id, page, size).await + }) + } + + /// Get all available screener indicator definitions + pub fn screener_indicators(&self) -> Result { + self.rt + .call(|ctx| async move { ctx.screener_indicators().await }) + } +} diff --git a/rust/src/fundamental/context.rs b/rust/src/fundamental/context.rs index a71fffbd0..c2d06f683 100644 --- a/rust/src/fundamental/context.rs +++ b/rust/src/fundamental/context.rs @@ -544,6 +544,30 @@ impl FundamentalContext { .await } + // ── shareholder_top ─────────────────────────────────────────── + + /// Get a ranked list of top shareholders for a security. + /// + /// Path: `GET /v1/quote/shareholders/top` + pub async fn shareholder_top( + &self, + symbol: impl Into, + ) -> Result { + #[derive(Serialize)] + struct Query { + counter_id: String, + } + let raw: serde_json::Value = self + .get( + "/v1/quote/shareholders/top", + Query { + counter_id: symbol_to_counter_id(&symbol.into()), + }, + ) + .await?; + Ok(ShareholderTopResponse { data: raw }) + } + // ── institution_rating_views ────────────────────────────────── /// Get historical institutional rating view time-series for a security. @@ -566,14 +590,38 @@ impl FundamentalContext { .await } + // ── shareholder_detail ──────────────────────────────────────── + + /// Get holding history and detail for one shareholder object. + /// + /// Path: `GET /v1/quote/shareholders/holding` + pub async fn shareholder_detail( + &self, + symbol: impl Into, + object_id: i64, + ) -> Result { + #[derive(Serialize)] + struct Query { + counter_id: String, + object_id: String, + } + let raw: serde_json::Value = self + .get( + "/v1/quote/shareholders/holding", + Query { + counter_id: symbol_to_counter_id(&symbol.into()), + object_id: object_id.to_string(), + }, + ) + .await?; + Ok(ShareholderDetailResponse { data: raw }) + } + // ── industry_rank ───────────────────────────────────────────── /// Get industry rank for a market. /// /// Path: `GET /v1/quote/industry/rank` - /// - /// `indicator` is a numeric string `"0"`–`"7"`; - /// `sort_type` is `"0"` (ascending) or `"1"` (descending). pub async fn industry_rank( &self, market: impl Into, @@ -605,10 +653,6 @@ impl FundamentalContext { /// Get the industry peer chain for a security or industry. /// /// Path: `GET /v1/quote/industries/peers` - /// - /// `counter_id` may be a regular symbol (e.g. `"AAPL.US"`) or an industry - /// counter ID (e.g. `"BK/US/123"`) — pass it through as-is if it already - /// contains a `/`. pub async fn industry_peers( &self, counter_id: impl Into, @@ -674,4 +718,39 @@ impl FundamentalContext { ) .await } + + // ── valuation_comparison ────────────────────────────────────── + + /// Get valuation comparison between a security and optional peers. + /// + /// Path: `GET /v1/quote/compare/valuation` + pub async fn valuation_comparison( + &self, + symbol: impl Into, + currency: impl Into, + comparison_symbols: Option>, + ) -> Result { + #[derive(Serialize)] + struct Query { + counter_id: String, + currency: String, + #[serde(skip_serializing_if = "Option::is_none")] + comparison_counter_ids: Option, + } + let comparison_counter_ids = comparison_symbols.map(|syms| { + let ids: Vec = syms.iter().map(|s| symbol_to_counter_id(s)).collect(); + serde_json::to_string(&ids).unwrap_or_default() + }); + let raw: serde_json::Value = self + .get( + "/v1/quote/compare/valuation", + Query { + counter_id: symbol_to_counter_id(&symbol.into()), + currency: currency.into(), + comparison_counter_ids, + }, + ) + .await?; + Ok(ValuationComparisonResponse { data: raw }) + } } diff --git a/rust/src/fundamental/types.rs b/rust/src/fundamental/types.rs index 556074048..d2fffe0ac 100644 --- a/rust/src/fundamental/types.rs +++ b/rust/src/fundamental/types.rs @@ -1350,6 +1350,33 @@ pub struct SnapshotReportedMetric { pub yoy: String, } +// ── shareholder_top ─────────────────────────────────────────────── + +/// Response for [`crate::FundamentalContext::shareholder_top`] +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct ShareholderTopResponse { + /// Raw top-shareholder data + pub data: serde_json::Value, +} + +// ── shareholder_detail ──────────────────────────────────────────── + +/// Response for [`crate::FundamentalContext::shareholder_detail`] +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct ShareholderDetailResponse { + /// Raw shareholder detail data + pub data: serde_json::Value, +} + +// ── valuation_comparison ────────────────────────────────────────── + +/// Response for [`crate::FundamentalContext::valuation_comparison`] +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct ValuationComparisonResponse { + /// Raw valuation comparison data + pub data: serde_json::Value, +} + /// Financial report period type #[derive(Debug, Copy, Clone, Eq, PartialEq, Serialize, Deserialize)] pub enum FinancialReportPeriod { diff --git a/rust/src/lib.rs b/rust/src/lib.rs index 0c3fdb2a4..a69999d3c 100644 --- a/rust/src/lib.rs +++ b/rust/src/lib.rs @@ -30,6 +30,7 @@ pub mod fundamental; pub mod market; pub mod portfolio; pub mod quote; +pub mod screener; pub mod sharelist; pub mod trade; @@ -47,6 +48,7 @@ pub use market::MarketContext; pub use portfolio::PortfolioContext; pub use quote::QuoteContext; pub use rust_decimal::Decimal; +pub use screener::ScreenerContext; pub use sharelist::SharelistContext; pub use trade::TradeContext; pub use types::Market; diff --git a/rust/src/market/context.rs b/rust/src/market/context.rs index 6db5a5555..3a54d220a 100644 --- a/rust/src/market/context.rs +++ b/rust/src/market/context.rs @@ -68,6 +68,23 @@ impl MarketContext { .0) } + async fn post(&self, path: &'static str, body: B) -> Result + where + R: DeserializeOwned + Send + Sync + 'static, + B: std::fmt::Debug + Serialize + Send + Sync + 'static, + { + Ok(self + .0 + .http_cli + .request(Method::POST, path) + .body(Json(body)) + .response::>() + .send() + .with_subscriber(self.0.log_subscriber.clone()) + .await? + .0) + } + // ── market_status ───────────────────────────────────────────── /// Get current trading status for all markets. @@ -264,4 +281,77 @@ impl MarketContext { ) .await } + + // ── stock_events ────────────────────────────────────────────── + + /// Get stock events across one or more markets. + /// + /// Path: `POST /v1/quote/market/stock-events` + /// + /// `sort` is the sort order code (0 = ascending, 1 = descending). + /// `date` is an optional date filter in `"YYYY-MM-DD"` format. + pub async fn stock_events( + &self, + markets: Vec, + sort: u32, + date: Option, + limit: u32, + ) -> Result { + #[derive(Debug, Serialize)] + struct Body { + limit: u32, + sort: u32, + markets: Vec, + #[serde(skip_serializing_if = "Option::is_none")] + date: Option, + } + self.post( + "/v1/quote/market/stock-events", + Body { + limit, + sort, + markets, + date, + }, + ) + .await + } + + // ── rank_categories ─────────────────────────────────────────── + + /// Get all available rank category keys and labels. + /// + /// Path: `GET /v1/quote/market/rank/categories` + pub async fn rank_categories(&self) -> Result { + #[derive(Serialize)] + struct Empty {} + self.get("/v1/quote/market/rank/categories", Empty {}).await + } + + // ── rank_list ───────────────────────────────────────────────── + + /// Get a ranked list of securities for the given category key. + /// + /// Path: `GET /v1/quote/market/rank/list` + pub async fn rank_list( + &self, + key: impl Into, + need_article: bool, + ) -> Result { + #[derive(Serialize)] + struct Query { + key: String, + delay_bmp: &'static str, + need_article: &'static str, + } + self.get( + "/v1/quote/market/rank/list", + Query { + key: key.into(), + delay_bmp: "false", + need_article: if need_article { "true" } else { "false" }, + }, + ) + .await + } } diff --git a/rust/src/market/types.rs b/rust/src/market/types.rs index ea1fd8253..bb77bf4ad 100644 --- a/rust/src/market/types.rs +++ b/rust/src/market/types.rs @@ -331,6 +331,43 @@ pub struct ConstituentStock { pub trade_status: i32, } +// ── stock_events ────────────────────────────────────────────────── + +/// Response for [`crate::MarketContext::stock_events`] +/// +/// The raw data contains stock events from all requested markets. The +/// exact structure varies so the payload is preserved as raw JSON. +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct StockEventsResponse { + /// Raw stock events data + pub data: serde_json::Value, +} + +// ── rank_categories ─────────────────────────────────────────────── + +/// Response for [`crate::MarketContext::rank_categories`] +/// +/// The raw data contains all available rank category keys and labels. +/// The exact structure varies so the payload is preserved as raw JSON. +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct RankCategoriesResponse { + /// Raw rank category data + pub data: serde_json::Value, +} + +// ── rank_list ───────────────────────────────────────────────────── + +/// Response for [`crate::MarketContext::rank_list`] +/// +/// The raw data contains a ranked list of securities for the requested +/// category key. The exact structure varies so the payload is +/// preserved as raw JSON. +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct RankListResponse { + /// Raw rank list data + pub data: serde_json::Value, +} + // ── enums ───────────────────────────────────────────────────────── /// Broker holding lookback period diff --git a/rust/src/quote/context.rs b/rust/src/quote/context.rs index b8b428273..26962354b 100644 --- a/rust/src/quote/context.rs +++ b/rust/src/quote/context.rs @@ -15,12 +15,13 @@ use crate::{ Config, Error, Language, Market, Result, quote::{ AdjustType, CalcIndex, Candlestick, CapitalDistributionResponse, CapitalFlowLine, - FilingItem, HistoryMarketTemperatureResponse, IntradayLine, IssuerInfo, MarketTemperature, - MarketTradingDays, MarketTradingSession, OptionQuote, OptionVolumeDaily, OptionVolumeStats, - ParticipantInfo, Period, PushEvent, QuotePackageDetail, RealtimeQuote, - RequestCreateWatchlistGroup, RequestUpdateWatchlistGroup, Security, SecurityBrokers, - SecurityCalcIndex, SecurityDepth, SecurityListCategory, SecurityQuote, SecurityStaticInfo, - ShortPositionsResponse, StrikePriceInfo, Subscription, Trade, TradeSessions, WarrantInfo, + FilingItem, HistoryMarketTemperatureResponse, HkShortPositionsResponse, IntradayLine, + IssuerInfo, MarketTemperature, MarketTradingDays, MarketTradingSession, OptionQuote, + OptionVolumeDaily, OptionVolumeStats, ParticipantInfo, Period, PushEvent, + QuotePackageDetail, RealtimeQuote, RequestCreateWatchlistGroup, + RequestUpdateWatchlistGroup, Security, SecurityBrokers, SecurityCalcIndex, SecurityDepth, + SecurityListCategory, SecurityQuote, SecurityStaticInfo, ShortPositionsResponse, + ShortTradesResponse, StrikePriceInfo, Subscription, Trade, TradeSessions, WarrantInfo, WarrantQuote, WarrantType, WatchlistGroup, cache::{Cache, CacheWithKey}, cmd_code, @@ -2046,6 +2047,93 @@ impl QuoteContext { .await?; Ok(resp.0) } + // ── hk_short_positions ──────────────────────────────────────── + + /// Get HK short interest / position data for a security. + /// + /// Path: `GET /v1/quote/short-positions/hk` + pub async fn hk_short_positions( + &self, + symbol: impl Into, + count: u32, + ) -> Result { + use std::time::{SystemTime, UNIX_EPOCH}; + + use crate::utils::counter::symbol_to_counter_id; + #[derive(serde::Serialize)] + struct Query { + counter_id: String, + last_timestamp: String, + count: u32, + } + let sym = symbol.into(); + let ts = SystemTime::now() + .duration_since(UNIX_EPOCH) + .map(|d| d.as_secs()) + .unwrap_or(0); + let resp = self + .0 + .http_cli + .request(Method::GET, "/v1/quote/short-positions/hk") + .query_params(Query { + counter_id: symbol_to_counter_id(&sym), + last_timestamp: ts.to_string(), + count, + }) + .response::>() + .send() + .with_subscriber(self.0.log_subscriber.clone()) + .await?; + Ok(resp.0) + } + + // ── short_trades ────────────────────────────────────────────── + + /// Get short trade records for a HK or US security. + /// + /// The API endpoint is auto-detected from the symbol suffix: + /// `.HK` → `GET /v1/quote/short-trades/hk`, + /// otherwise → `GET /v1/quote/short-trades/us`. + pub async fn short_trades( + &self, + symbol: impl Into, + count: u32, + ) -> Result { + use std::time::{SystemTime, UNIX_EPOCH}; + + use crate::utils::counter::symbol_to_counter_id; + #[derive(serde::Serialize)] + struct Query { + counter_id: String, + last_timestamp: String, + page_size: String, + } + let sym = symbol.into(); + let path = if sym.to_uppercase().ends_with(".HK") { + "/v1/quote/short-trades/hk" + } else { + "/v1/quote/short-trades/us" + }; + let ts = SystemTime::now() + .duration_since(UNIX_EPOCH) + .map(|d| d.as_secs()) + .unwrap_or(0); + let resp = self + .0 + .http_cli + .request(Method::GET, path) + .query_params(Query { + counter_id: symbol_to_counter_id(&sym), + last_timestamp: ts.to_string(), + page_size: count.to_string(), + }) + .response::>() + .send() + .with_subscriber(self.0.log_subscriber.clone()) + .await?; + Ok(resp.0) + } + // ── update_pinned ───────────────────────────────────────────── /// Pin or unpin watchlist securities. diff --git a/rust/src/quote/mod.rs b/rust/src/quote/mod.rs index 3e945b894..b05b16898 100644 --- a/rust/src/quote/mod.rs +++ b/rust/src/quote/mod.rs @@ -30,6 +30,7 @@ pub use types::{ FilterWarrantInOutBoundsType, Granularity, HistoryMarketTemperatureResponse, + HkShortPositionsResponse, IntradayLine, IssuerInfo, MarketTemperature, @@ -60,6 +61,7 @@ pub use types::{ SecurityStaticInfo, ShortPosition, ShortPositionsResponse, + ShortTradesResponse, SortOrderType, StrikePriceInfo, Subscription, diff --git a/rust/src/quote/types.rs b/rust/src/quote/types.rs index bfe3ae775..90f2b3b91 100644 --- a/rust/src/quote/types.rs +++ b/rust/src/quote/types.rs @@ -2113,6 +2113,30 @@ pub struct OptionVolumeDailyStat { pub put_call_open_interest_ratio: String, } +// ── hk_short_positions ──────────────────────────────────────────── + +/// Response for [`crate::QuoteContext::hk_short_positions`] +/// +/// The raw data contains HK short interest/position data. The exact +/// structure varies so the payload is preserved as raw JSON. +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct HkShortPositionsResponse { + /// Raw HK short positions data + pub data: serde_json::Value, +} + +// ── short_trades ────────────────────────────────────────────────── + +/// Response for [`crate::QuoteContext::short_trades`] +/// +/// The raw data contains short trade records for the queried security. +/// The exact structure varies so the payload is preserved as raw JSON. +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct ShortTradesResponse { + /// Raw short trade data + pub data: serde_json::Value, +} + // ── pinned mode ─────────────────────────────────────────────────── /// Mode for pinning/unpinning watchlist securities diff --git a/rust/src/screener/context.rs b/rust/src/screener/context.rs new file mode 100644 index 000000000..12137da56 --- /dev/null +++ b/rust/src/screener/context.rs @@ -0,0 +1,168 @@ +use std::sync::Arc; + +use longbridge_httpcli::{HttpClient, Json, Method}; +use serde::{Serialize, de::DeserializeOwned}; +use tracing::{Subscriber, dispatcher, instrument::WithSubscriber}; + +use crate::{Config, Result, screener::types::*}; + +struct InnerScreenerContext { + http_cli: HttpClient, + log_subscriber: Arc, +} + +impl Drop for InnerScreenerContext { + fn drop(&mut self) { + dispatcher::with_default(&self.log_subscriber.clone().into(), || { + tracing::info!("screener context dropped"); + }); + } +} + +/// Screener context — stock screener strategies, search, and indicators. +#[derive(Clone)] +pub struct ScreenerContext(Arc); + +impl ScreenerContext { + /// Create a [`ScreenerContext`] + pub fn new(config: Arc) -> Self { + let log_subscriber = config.create_log_subscriber("screener"); + dispatcher::with_default(&log_subscriber.clone().into(), || { + tracing::info!(language = ?config.language, "creating screener context"); + }); + let ctx = Self(Arc::new(InnerScreenerContext { + http_cli: config.create_http_client(), + log_subscriber, + })); + dispatcher::with_default(&ctx.0.log_subscriber.clone().into(), || { + tracing::info!("screener context created"); + }); + ctx + } + + /// Returns the log subscriber + #[inline] + pub fn log_subscriber(&self) -> Arc { + self.0.log_subscriber.clone() + } + + async fn get(&self, path: &'static str, query: Q) -> Result + where + R: DeserializeOwned + Send + Sync + 'static, + Q: Serialize + Send + Sync, + { + Ok(self + .0 + .http_cli + .request(Method::GET, path) + .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, + B: std::fmt::Debug + Serialize + Send + Sync + 'static, + { + Ok(self + .0 + .http_cli + .request(Method::POST, path) + .body(Json(body)) + .response::>() + .send() + .with_subscriber(self.0.log_subscriber.clone()) + .await? + .0) + } + + // ── screener_recommend_strategies ───────────────────────────── + + /// Get recommended built-in screener strategies. + /// + /// Path: `GET /v1/quote/screener/strategies/recommend` + pub async fn screener_recommend_strategies( + &self, + ) -> Result { + #[derive(Serialize)] + struct Empty {} + self.get("/v1/quote/screener/strategies/recommend", Empty {}) + .await + } + + // ── screener_user_strategies ────────────────────────────────── + + /// Get the current user's saved screener strategies. + /// + /// Path: `GET /v1/quote/screener/strategies/mine` + pub async fn screener_user_strategies(&self) -> Result { + #[derive(Serialize)] + struct Empty {} + self.get("/v1/quote/screener/strategies/mine", Empty {}) + .await + } + + // ── screener_strategy ───────────────────────────────────────── + + /// Get detail for one screener strategy by ID. + /// + /// Path: `GET /v1/quote/screener/strategy?id=` + pub async fn screener_strategy(&self, id: i64) -> Result { + #[derive(Serialize)] + struct Query { + id: i64, + } + self.get("/v1/quote/screener/strategy", Query { id }).await + } + + // ── screener_search ─────────────────────────────────────────── + + /// Search / screen securities using a strategy. + /// + /// Path: `POST /v1/quote/screener/search` + /// + /// When `strategy_id` is `Some`, it is included in the request body. + /// When `None`, only `market`, `page`, and `size` are sent (custom + /// filter support is out of scope for this SDK). + pub async fn screener_search( + &self, + market: impl Into, + strategy_id: Option, + page: u32, + size: u32, + ) -> Result { + #[derive(Debug, Serialize)] + struct Body { + market: String, + #[serde(skip_serializing_if = "Option::is_none")] + strategy_id: Option, + page: u32, + size: u32, + } + self.post( + "/v1/quote/screener/search", + Body { + market: market.into(), + strategy_id, + page, + size, + }, + ) + .await + } + + // ── screener_indicators ─────────────────────────────────────── + + /// Get all available screener indicator definitions. + /// + /// Path: `GET /v1/quote/screener/indicators` + pub async fn screener_indicators(&self) -> Result { + #[derive(Serialize)] + struct Empty {} + self.get("/v1/quote/screener/indicators", Empty {}).await + } +} diff --git a/rust/src/screener/mod.rs b/rust/src/screener/mod.rs new file mode 100644 index 000000000..38bfd0929 --- /dev/null +++ b/rust/src/screener/mod.rs @@ -0,0 +1,7 @@ +//! Stock screener — strategies, search, and indicators + +mod context; +pub mod types; + +pub use context::ScreenerContext; +pub use types::*; diff --git a/rust/src/screener/types.rs b/rust/src/screener/types.rs new file mode 100644 index 000000000..0f71a2c0a --- /dev/null +++ b/rust/src/screener/types.rs @@ -0,0 +1,64 @@ +#![allow(missing_docs)] + +use serde::{Deserialize, Serialize}; + +// ── screener_recommend_strategies ───────────────────────────────── + +/// Response for [`crate::ScreenerContext::screener_recommend_strategies`] +/// +/// The raw data contains a list of recommended built-in screener +/// strategies. The exact structure varies so the payload is +/// preserved as raw JSON. +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct ScreenerRecommendStrategiesResponse { + /// Raw recommended strategies data + pub data: serde_json::Value, +} + +// ── screener_user_strategies ────────────────────────────────────── + +/// Response for [`crate::ScreenerContext::screener_user_strategies`] +/// +/// The raw data contains the current user's saved screener strategies. +/// The exact structure varies so the payload is preserved as raw JSON. +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct ScreenerUserStrategiesResponse { + /// Raw user strategies data + pub data: serde_json::Value, +} + +// ── screener_strategy ───────────────────────────────────────────── + +/// Response for [`crate::ScreenerContext::screener_strategy`] +/// +/// The raw data contains detail for one screener strategy. +/// The exact structure varies so the payload is preserved as raw JSON. +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct ScreenerStrategyResponse { + /// Raw strategy detail data + pub data: serde_json::Value, +} + +// ── screener_search ─────────────────────────────────────────────── + +/// Response for [`crate::ScreenerContext::screener_search`] +/// +/// The raw data contains a page of screened security results. +/// The exact structure varies so the payload is preserved as raw JSON. +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct ScreenerSearchResponse { + /// Raw screener search results + pub data: serde_json::Value, +} + +// ── screener_indicators ─────────────────────────────────────────── + +/// Response for [`crate::ScreenerContext::screener_indicators`] +/// +/// The raw data contains all available screener indicator definitions. +/// The exact structure varies so the payload is preserved as raw JSON. +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct ScreenerIndicatorsResponse { + /// Raw indicator definitions + pub data: serde_json::Value, +} From b5b8e4c41b71f35ac22dd25cb80c6426160a796f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=A2=81=E7=AB=A0=E6=B4=AA?= Date: Wed, 20 May 2026 15:51:20 +0800 Subject: [PATCH 02/12] feat(all): merge hk_short_positions into short_positions, port 13 new APIs to all language SDKs MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Rust SDK: - `short_positions(symbol, count)` now auto-detects market from symbol suffix (.HK → GET /v1/quote/short-positions/hk, else → /v1/quote/short-positions/us). `ShortPositionsResponse` is now a raw JSON wrapper (`data: serde_json::Value`). `hk_short_positions` and `ShortPosition` struct are removed. Python / Node.js / Java / C / C++ SDKs: - Port all 13 new Rust APIs added in the previous commit: - FundamentalContext: shareholder_top, shareholder_detail, valuation_comparison - QuoteContext: updated short_positions (HK+US, count param), short_trades - MarketContext: stock_events, rank_categories, rank_list - ScreenerContext (new): screener_recommend_strategies, screener_user_strategies, screener_strategy, screener_search, screener_indicators - All new "raw JSON" responses expose data as a string (Java/C/C++/Node.js) or a Python object (Python, via pythonize). - Updated openapi.pyi type stubs for all new types and methods. Co-Authored-By: Claude Sonnet 4.6 (1M context) --- CHANGELOG.md | 29 +- c/cbindgen.toml | 27 +- c/csrc/include/longbridge.h | 742 +++++++---------------- c/src/fundamental_context/context.rs | 179 +----- c/src/fundamental_context/types.rs | 763 ++---------------------- c/src/lib.rs | 1 + c/src/market_context/context.rs | 68 +++ c/src/market_context/types.rs | 91 ++- c/src/quote_context/context.rs | 28 +- c/src/quote_context/types.rs | 112 ++-- c/src/screener_context/context.rs | 135 +++++ c/src/screener_context/mod.rs | 2 + c/src/screener_context/types.rs | 153 +++++ cpp/CMakeLists.txt | 1 + cpp/include/fundamental_context.hpp | 54 +- cpp/include/market_context.hpp | 17 + cpp/include/quote_context.hpp | 8 +- cpp/include/screener_context.hpp | 51 ++ cpp/include/types.hpp | 17 +- cpp/src/convert.hpp | 10 +- cpp/src/fundamental_context.cpp | 32 + cpp/src/market_context.cpp | 28 + cpp/src/quote_context.cpp | 28 + cpp/src/screener_context.cpp | 67 +++ java/src/fundamental_context.rs | 125 +--- java/src/init.rs | 33 +- java/src/lib.rs | 1 + java/src/market_context.rs | 64 +- java/src/quote_context.rs | 25 +- java/src/screener_context.rs | 133 +++++ java/src/types/classes.rs | 92 ++- nodejs/index.d.ts | 323 ++++------ nodejs/index.js | 1 + nodejs/src/fundamental/context.rs | 96 +-- nodejs/src/fundamental/types.rs | 357 +---------- nodejs/src/lib.rs | 1 + nodejs/src/market/context.rs | 39 ++ nodejs/src/market/types.rs | 54 ++ nodejs/src/quote/context.rs | 30 +- nodejs/src/quote/types.rs | 50 +- nodejs/src/screener/context.rs | 86 +++ nodejs/src/screener/mod.rs | 2 + nodejs/src/screener/types.rs | 91 +++ python/pysrc/longbridge/openapi.pyi | 623 +++++++++---------- python/src/fundamental/context.rs | 94 +-- python/src/fundamental/context_async.rs | 117 +--- python/src/fundamental/mod.rs | 3 + python/src/fundamental/types.rs | 364 +---------- python/src/lib.rs | 2 + python/src/market/context.rs | 31 + python/src/market/context_async.rs | 47 ++ python/src/market/mod.rs | 3 + python/src/market/types.rs | 63 ++ python/src/quote/context.rs | 24 +- python/src/quote/context_async.rs | 34 ++ python/src/quote/mod.rs | 2 +- python/src/quote/types.rs | 53 +- python/src/screener/context.rs | 66 ++ python/src/screener/context_async.rs | 90 +++ python/src/screener/mod.rs | 17 + python/src/screener/types.rs | 107 ++++ rust/src/blocking/quote.rs | 31 +- rust/src/quote/context.rs | 90 ++- rust/src/quote/mod.rs | 2 - rust/src/quote/types.rs | 45 +- 65 files changed, 2838 insertions(+), 3316 deletions(-) create mode 100644 c/src/screener_context/context.rs create mode 100644 c/src/screener_context/mod.rs create mode 100644 c/src/screener_context/types.rs create mode 100644 cpp/include/screener_context.hpp create mode 100644 cpp/src/screener_context.cpp create mode 100644 java/src/screener_context.rs create mode 100644 nodejs/src/screener/context.rs create mode 100644 nodejs/src/screener/mod.rs create mode 100644 nodejs/src/screener/types.rs create mode 100644 python/src/screener/context.rs create mode 100644 python/src/screener/context_async.rs create mode 100644 python/src/screener/mod.rs create mode 100644 python/src/screener/types.rs diff --git a/CHANGELOG.md b/CHANGELOG.md index 23d9a50cf..93836428a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,21 +8,20 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added -- **Rust, Python, Node.js:** Document normalization formulas for `SecurityCalcIndex` Greeks fields: `theta` (divide by 252 for per-trading-day), `vega` and `rho` (divide by 100 for per-unit change). The raw API values differ from Longbridge app display values by these factors. -- **Rust:** `Config::header(key, value)` builder method to inject custom headers into every HTTP request and WebSocket upgrade request. -- **Rust, Python:** `ContentContext` adds three new methods: `topic_detail`, `list_topic_replies`, `create_topic_reply`. -- **All languages (Rust/Python/Node.js/Java/C/C++):** Six new `FundamentalContext` methods (PR #526): - - `BusinessSegments` — GET `/v1/quote/fundamentals/business-segments` - - `BusinessSegmentsHistory` — GET `/v1/quote/fundamentals/business-segments/history` - - `InstitutionRatingViews` — GET `/v1/quote/ratings/institutional` - - `IndustryRank` — GET `/v1/quote/industry/rank` - - `IndustryPeers` — GET `/v1/quote/industries/peers` - - `FinancialReportSnapshot` — GET `/v1/quote/financials/earnings-snapshot` -- **All languages (Rust/Python/Node.js/Java/C/C++):** 13 new APIs across FundamentalContext, QuoteContext, MarketContext, and new ScreenerContext: - - `FundamentalContext`: `shareholder_top`, `shareholder_detail`, `valuation_comparison` - - `QuoteContext`: `short_positions` (extended to HK+US), `short_trades` - - `MarketContext`: `top_movers`, `rank_categories`, `rank_list` - - `ScreenerContext` (new): `screener_recommend_strategies`, `screener_user_strategies`, `screener_strategy`, `screener_search`, `screener_indicators` +- **Rust, Python, Node.js:** Document normalization formulas for `SecurityCalcIndex` Greeks fields: `theta` (divide by 252 for per-trading-day), `vega` and `rho` (divide by 100 for per-unit change). +- **Rust:** `Config::header(key, value)` builder method to inject custom headers. +- **Rust, Python:** `ContentContext` adds `topic_detail`, `list_topic_replies`, `create_topic_reply`. +- **All languages:** Six new `FundamentalContext` methods (PR #526): `BusinessSegments`, `BusinessSegmentsHistory`, `InstitutionRatingViews`, `IndustryRank`, `IndustryPeers`, `FinancialReportSnapshot` +- **All languages:** 13 more new APIs: `shareholder_top`, `shareholder_detail`, `valuation_comparison` (FundamentalContext); `short_positions` (HK+US unified), `short_trades` (QuoteContext); `top_movers`, `rank_categories`, `rank_list` (MarketContext); `ScreenerContext` (new) with 5 screener methods + +### Changed + +- **All languages:** `QuoteContext::short_positions(symbol, count)` now auto-detects market from symbol suffix (`.HK` → HK, otherwise US). `ShortPositionsResponse` is raw JSON. + +### Breaking changes + +- **All languages:** `hk_short_positions` removed — use `short_positions(symbol, count)`. +- **All languages:** `ShortPositionsResponse` raw JSON only; old typed fields removed. # [4.1.0] diff --git a/c/cbindgen.toml b/c/cbindgen.toml index b4620ed58..4dbab9104 100644 --- a/c/cbindgen.toml +++ b/c/cbindgen.toml @@ -297,11 +297,26 @@ cpp_compat = true "CSnapshotReportedMetric" = "lb_snapshot_reported_metric_t" "CFinancialReportSnapshot" = "lb_financial_report_snapshot_t" # QuoteContext extensions -"CShortPosition" = "lb_short_position_t" "CShortPositionsResponse" = "lb_short_positions_response_t" +"CShortTradesResponse" = "lb_short_trades_response_t" "COptionVolumeStats" = "lb_option_volume_stats_t" "COptionVolumeDailyStat" = "lb_option_volume_daily_stat_t" "COptionVolumeDaily" = "lb_option_volume_daily_t" +# FundamentalContext new types +"CShareholderTopResponse" = "lb_shareholder_top_response_t" +"CShareholderDetailResponse" = "lb_shareholder_detail_response_t" +"CValuationComparisonResponse" = "lb_valuation_comparison_response_t" +# MarketContext new types +"CStockEventsResponse" = "lb_stock_events_response_t" +"CRankCategoriesResponse" = "lb_rank_categories_response_t" +"CRankListResponse" = "lb_rank_list_response_t" +# ScreenerContext +"CScreenerContext" = "lb_screener_context_t" +"CScreenerRecommendStrategiesResponse" = "lb_screener_recommend_strategies_response_t" +"CScreenerUserStrategiesResponse" = "lb_screener_user_strategies_response_t" +"CScreenerStrategyResponse" = "lb_screener_strategy_response_t" +"CScreenerSearchResponse" = "lb_screener_search_response_t" +"CScreenerIndicatorsResponse" = "lb_screener_indicators_response_t" [export] include = [ @@ -415,7 +430,15 @@ include = [ # FundamentalContext opaque type (no rename, typedef added in hpp) "CFundamentalContext", # QuoteContext extensions - "CShortPosition", "CShortPositionsResponse", + "CShortPositionsResponse", "CShortTradesResponse", "COptionVolumeStats", "COptionVolumeDailyStat", "COptionVolumeDaily", + # FundamentalContext new types + "CShareholderTopResponse", "CShareholderDetailResponse", "CValuationComparisonResponse", + # MarketContext new types + "CStockEventsResponse", "CRankCategoriesResponse", "CRankListResponse", + # ScreenerContext + "CScreenerContext", + "CScreenerRecommendStrategiesResponse", "CScreenerUserStrategiesResponse", + "CScreenerStrategyResponse", "CScreenerSearchResponse", "CScreenerIndicatorsResponse", ] diff --git a/c/csrc/include/longbridge.h b/c/csrc/include/longbridge.h index 0f133cdd9..7956caa4b 100644 --- a/c/csrc/include/longbridge.h +++ b/c/csrc/include/longbridge.h @@ -1648,6 +1648,8 @@ typedef struct lb_portfolio_context_t lb_portfolio_context_t; */ typedef struct lb_quote_context_t lb_quote_context_t; +typedef struct lb_screener_context_t lb_screener_context_t; + typedef struct lb_sharelist_context_t lb_sharelist_context_t; /** @@ -6566,416 +6568,6 @@ typedef struct lb_stock_ratings_t { uintptr_t num_ratings; } lb_stock_ratings_t; -/** - * One business segment item (latest snapshot). - */ -typedef struct lb_business_segment_item_t { - /** - * Segment name. - */ - const char *name; - /** - * Percentage of total revenue. - */ - const char *percent; -} lb_business_segment_item_t; - -/** - * Business segments response. - */ -typedef struct lb_business_segments_t { - /** - * Report date. - */ - const char *date; - /** - * Total revenue. - */ - const char *total; - /** - * Reporting currency. - */ - const char *currency; - /** - * Pointer to the array of business segment items. - */ - const struct lb_business_segment_item_t *business; - /** - * Number of items in `business`. - */ - uintptr_t num_business; -} lb_business_segments_t; - -/** - * One business/regional segment item in a historical snapshot. - */ -typedef struct lb_business_segment_history_item_t { - /** - * Segment name. - */ - const char *name; - /** - * Percentage of total. - */ - const char *percent; - /** - * Absolute value. - */ - const char *value; -} lb_business_segment_history_item_t; - -/** - * One historical business segments snapshot. - */ -typedef struct lb_business_segments_historical_item_t { - /** - * Report date. - */ - const char *date; - /** - * Total revenue. - */ - const char *total; - /** - * Reporting currency. - */ - const char *currency; - /** - * Pointer to the business segment items. - */ - const struct lb_business_segment_history_item_t *business; - /** - * Number of items in `business`. - */ - uintptr_t num_business; - /** - * Pointer to the regional segment items. - */ - const struct lb_business_segment_history_item_t *regionals; - /** - * Number of items in `regionals`. - */ - uintptr_t num_regionals; -} lb_business_segments_historical_item_t; - -/** - * Business segments history response. - */ -typedef struct lb_business_segments_history_t { - /** - * Pointer to the historical snapshots. - */ - const struct lb_business_segments_historical_item_t *historical; - /** - * Number of items in `historical`. - */ - uintptr_t num_historical; -} lb_business_segments_history_t; - -/** - * One historical rating distribution snapshot. - */ -typedef struct lb_institution_rating_view_item_t { - /** - * Date as unix timestamp string. - */ - const char *date; - /** - * Number of Buy ratings. - */ - const char *buy; - /** - * Number of Outperform ratings. - */ - const char *over; - /** - * Number of Hold ratings. - */ - const char *hold; - /** - * Number of Underperform ratings. - */ - const char *under; - /** - * Number of Sell ratings. - */ - const char *sell; - /** - * Total analyst count. - */ - const char *total; -} lb_institution_rating_view_item_t; - -/** - * Institution rating views response. - */ -typedef struct lb_institution_rating_views_t { - /** - * Pointer to the rating view items. - */ - const struct lb_institution_rating_view_item_t *elist; - /** - * Number of items in `elist`. - */ - uintptr_t num_elist; -} lb_institution_rating_views_t; - -/** - * One ranked industry item. - */ -typedef struct lb_industry_rank_item_t { - /** - * Industry / sector name. - */ - const char *name; - /** - * Counter ID of the industry. - */ - const char *counter_id; - /** - * Change percentage. - */ - const char *chg; - /** - * Name of the leading stock. - */ - const char *leading_name; - /** - * Ticker of the leading stock. - */ - const char *leading_ticker; - /** - * Change percentage of the leading stock. - */ - const char *leading_chg; - /** - * Value label name. - */ - const char *value_name; - /** - * Value data. - */ - const char *value_data; -} lb_industry_rank_item_t; - -/** - * A group of ranked industry items. - */ -typedef struct lb_industry_rank_group_t { - /** - * Pointer to the items in this group. - */ - const struct lb_industry_rank_item_t *lists; - /** - * Number of items in `lists`. - */ - uintptr_t num_lists; -} lb_industry_rank_group_t; - -/** - * Industry rank response. - */ -typedef struct lb_industry_rank_response_t { - /** - * Pointer to the grouped rank items. - */ - const struct lb_industry_rank_group_t *items; - /** - * Number of items in `items`. - */ - uintptr_t num_items; -} lb_industry_rank_response_t; - -/** - * Top-level industry info in the peers response. - */ -typedef struct lb_industry_peers_top_t { - /** - * Industry name. - */ - const char *name; - /** - * Market code. - */ - const char *market; -} lb_industry_peers_top_t; - -/** - * A node in the recursive industry peer chain. - * - * `next_json` contains the child nodes serialised as a JSON string. - */ -typedef struct lb_industry_peer_node_t { - /** - * Node name. - */ - const char *name; - /** - * Counter ID. - */ - const char *counter_id; - /** - * Number of stocks in this node. - */ - int32_t stock_num; - /** - * Change percentage. - */ - const char *chg; - /** - * Year-to-date change. - */ - const char *ytd_chg; - /** - * Child nodes as a JSON string. - */ - const char *next_json; -} lb_industry_peer_node_t; - -/** - * Industry peers response. - */ -typedef struct lb_industry_peers_response_t { - /** - * Top-level industry node info. - */ - struct lb_industry_peers_top_t top; - /** - * Root peer chain node, or null if absent. - */ - const struct lb_industry_peer_node_t *chain; -} lb_industry_peers_response_t; - -/** - * A forecast metric in the financial report snapshot. - */ -typedef struct lb_snapshot_forecast_metric_t { - /** - * Actual value. - */ - const char *value; - /** - * Year-over-year change. - */ - const char *yoy; - /** - * Beat/miss description. - */ - const char *cmp_desc; - /** - * Consensus estimate value. - */ - const char *est_value; -} lb_snapshot_forecast_metric_t; - -/** - * A reported metric in the financial report snapshot. - */ -typedef struct lb_snapshot_reported_metric_t { - /** - * Actual value. - */ - const char *value; - /** - * Year-over-year change. - */ - const char *yoy; -} lb_snapshot_reported_metric_t; - -/** - * Financial report snapshot response. - */ -typedef struct lb_financial_report_snapshot_t { - /** - * Company name. - */ - const char *name; - /** - * Ticker code. - */ - const char *ticker; - /** - * Fiscal period start date. - */ - const char *fp_start; - /** - * Fiscal period end date. - */ - const char *fp_end; - /** - * Reporting currency. - */ - const char *currency; - /** - * Report description. - */ - const char *report_desc; - /** - * Forecast revenue, or null. - */ - const struct lb_snapshot_forecast_metric_t *fo_revenue; - /** - * Forecast EBIT, or null. - */ - const struct lb_snapshot_forecast_metric_t *fo_ebit; - /** - * Forecast EPS, or null. - */ - const struct lb_snapshot_forecast_metric_t *fo_eps; - /** - * Reported revenue, or null. - */ - const struct lb_snapshot_reported_metric_t *fr_revenue; - /** - * Reported net profit, or null. - */ - const struct lb_snapshot_reported_metric_t *fr_profit; - /** - * Reported operating cash flow, or null. - */ - const struct lb_snapshot_reported_metric_t *fr_operate_cash; - /** - * Reported investing cash flow, or null. - */ - const struct lb_snapshot_reported_metric_t *fr_invest_cash; - /** - * Reported financing cash flow, or null. - */ - const struct lb_snapshot_reported_metric_t *fr_finance_cash; - /** - * Reported total assets, or null. - */ - const struct lb_snapshot_reported_metric_t *fr_total_assets; - /** - * Reported total liabilities, or null. - */ - const struct lb_snapshot_reported_metric_t *fr_total_liability; - /** - * ROE TTM. - */ - const char *fr_roe_ttm; - /** - * Profit margin. - */ - const char *fr_profit_margin; - /** - * Profit margin TTM. - */ - const char *fr_profit_margin_ttm; - /** - * Asset turnover TTM. - */ - const char *fr_asset_turn_ttm; - /** - * Leverage TTM. - */ - const char *fr_leverage_ttm; - /** - * Debt-to-assets ratio. - */ - const char *fr_debt_assets_ratio; -} lb_financial_report_snapshot_t; - /** * A key-value pair carrying calendar data fields. */ @@ -8133,56 +7725,28 @@ typedef struct lb_sharelist_detail_t { } lb_sharelist_detail_t; /** - * Short position data for a single date + * Short positions / interest response (HK or US). + * + * `data` is a NUL-terminated JSON string. */ -typedef struct lb_short_position_t { - /** - * Date of the short position record (formatted string) - */ - const char *timestamp; - /** - * Short interest as a percentage of shares outstanding - */ - const char *rate; - /** - * Average daily share volume - */ - const char *avg_daily_share_volume; - /** - * Current number of shares sold short - */ - const char *current_shares_short; - /** - * Days to cover (short interest ratio) - */ - const char *days_to_cover; +typedef struct lb_short_positions_response_t { /** - * Closing price on the record date + * Raw short positions data as a JSON string */ - const char *close; -} lb_short_position_t; + const char *data; +} lb_short_positions_response_t; /** - * Short positions response for a security + * Short trade records response (HK or US). + * + * `data` is a NUL-terminated JSON string. */ -typedef struct lb_short_positions_response_t { +typedef struct lb_short_trades_response_t { /** - * Security code - */ - const char *symbol; - /** - * Pointer to array of short position records + * Raw short trade data as a JSON string */ - const struct lb_short_position_t *data; - /** - * Number of elements in the array. - */ - uintptr_t num_data; - /** - * Bitmask indicating the data sources included in the response - */ - int32_t sources; -} lb_short_positions_response_t; + const char *data; +} lb_short_trades_response_t; /** * Option volume statistics (call and put totals) @@ -8258,6 +7822,101 @@ typedef struct lb_option_volume_daily_t { uintptr_t num_stats; } lb_option_volume_daily_t; +/** + * Top-shareholder list response. `data` is a NUL-terminated JSON string. + */ +typedef struct lb_shareholder_top_response_t { + /** + * Raw top-shareholder data as a JSON string + */ + const char *data; +} lb_shareholder_top_response_t; + +/** + * Shareholder detail response. `data` is a NUL-terminated JSON string. + */ +typedef struct lb_shareholder_detail_response_t { + /** + * Raw shareholder detail data as a JSON string + */ + const char *data; +} lb_shareholder_detail_response_t; + +/** + * Valuation comparison response. `data` is a NUL-terminated JSON string. + */ +typedef struct lb_valuation_comparison_response_t { + /** + * Raw valuation comparison data as a JSON string + */ + const char *data; +} lb_valuation_comparison_response_t; + +/** + * Stock events response. `data` is a NUL-terminated JSON string. + */ +typedef struct lb_stock_events_response_t { + /** + * Raw stock events data as a JSON string + */ + const char *data; +} lb_stock_events_response_t; + +/** + * Rank categories response. `data` is a NUL-terminated JSON string. + */ +typedef struct lb_rank_categories_response_t { + /** + * Raw rank categories data as a JSON string + */ + const char *data; +} lb_rank_categories_response_t; + +/** + * Rank list response. `data` is a NUL-terminated JSON string. + */ +typedef struct lb_rank_list_response_t { + /** + * Raw rank list data as a JSON string + */ + const char *data; +} lb_rank_list_response_t; + +/** + * Recommended screener strategies response. `data` is a JSON string. + */ +typedef struct lb_screener_recommend_strategies_response_t { + const char *data; +} lb_screener_recommend_strategies_response_t; + +/** + * User screener strategies response. `data` is a JSON string. + */ +typedef struct lb_screener_user_strategies_response_t { + const char *data; +} lb_screener_user_strategies_response_t; + +/** + * Single screener strategy response. `data` is a JSON string. + */ +typedef struct lb_screener_strategy_response_t { + const char *data; +} lb_screener_strategy_response_t; + +/** + * Screener search results response. `data` is a JSON string. + */ +typedef struct lb_screener_search_response_t { + const char *data; +} lb_screener_search_response_t; + +/** + * Screener indicator definitions response. `data` is a JSON string. + */ +typedef struct lb_screener_indicators_response_t { + const char *data; +} lb_screener_indicators_response_t; + #ifdef __cplusplus extern "C" { #endif // __cplusplus @@ -8895,71 +8554,35 @@ void lb_fundamental_context_ratings(const struct lb_fundamental_context_t *ctx, void *userdata); /** - * Get business segment breakdowns. Returns `CBusinessSegments`. - */ -void lb_fundamental_context_business_segments(const struct lb_fundamental_context_t *ctx, - const char *symbol, - lb_async_callback_t callback, - void *userdata); - -/** - * Get historical business segment breakdowns. Returns - * `CBusinessSegmentsHistory`. - * - * Pass `NULL` for `report` or `cate` to omit those parameters. - */ -void lb_fundamental_context_business_segments_history(const struct lb_fundamental_context_t *ctx, - const char *symbol, - const char *report, - const char *cate, - lb_async_callback_t callback, - void *userdata); - -/** - * Get historical institutional rating views. Returns - * `CInstitutionRatingViews`. + * Get ranked list of top shareholders. Returns `CShareholderTopResponse`. */ -void lb_fundamental_context_institution_rating_views(const struct lb_fundamental_context_t *ctx, - const char *symbol, - lb_async_callback_t callback, - void *userdata); - -/** - * Get industry rank for a market. Returns `CIndustryRankResponse`. - */ -void lb_fundamental_context_industry_rank(const struct lb_fundamental_context_t *ctx, - const char *market, - const char *indicator, - const char *sort_type, - uint32_t limit, - lb_async_callback_t callback, - void *userdata); +void lb_fundamental_context_shareholder_top(const struct lb_fundamental_context_t *ctx, + const char *symbol, + lb_async_callback_t callback, + void *userdata); /** - * Get industry peer chain. Returns `CIndustryPeersResponse`. - * - * Pass `NULL` for `industry_id` to omit it. + * Get holding history and detail for one shareholder. Returns + * `CShareholderDetailResponse`. */ -void lb_fundamental_context_industry_peers(const struct lb_fundamental_context_t *ctx, - const char *counter_id, - const char *market, - const char *industry_id, - lb_async_callback_t callback, - void *userdata); +void lb_fundamental_context_shareholder_detail(const struct lb_fundamental_context_t *ctx, + const char *symbol, + int64_t object_id, + lb_async_callback_t callback, + void *userdata); /** - * Get financial report snapshot. Returns `CFinancialReportSnapshot`. - * - * Pass `NULL` for optional parameters to omit them. - * `fiscal_year` is ignored when 0. + * Get valuation comparison between a security and optional peers. + * Returns `CValuationComparisonResponse`. + * Pass NULL for `comparison_symbols` to skip peer comparison. */ -void lb_fundamental_context_financial_report_snapshot(const struct lb_fundamental_context_t *ctx, - const char *symbol, - const char *report, - int32_t fiscal_year, - const char *fiscal_period, - lb_async_callback_t callback, - void *userdata); +void lb_fundamental_context_valuation_comparison(const struct lb_fundamental_context_t *ctx, + const char *symbol, + const char *currency, + const char *const *comparison_symbols, + uintptr_t num_comparison_symbols, + lb_async_callback_t callback, + void *userdata); /** * Create a HTTP client using API Key authentication @@ -9123,6 +8746,38 @@ void lb_market_context_constituent(const struct lb_market_context_t *ctx, lb_async_callback_t callback, void *userdata); +/** + * Get stock events across one or more markets. + * Pass markets as a NULL-terminated array of C strings. + * Returns `CStockEventsResponse`. + */ +void lb_market_context_stock_events(const struct lb_market_context_t *ctx, + const char *const *markets, + uintptr_t num_markets, + uint32_t sort, + const char *date, + uint32_t limit, + lb_async_callback_t callback, + void *userdata); + +/** + * Get all available rank category keys and labels. + * Returns `CRankCategoriesResponse`. + */ +void lb_market_context_rank_categories(const struct lb_market_context_t *ctx, + lb_async_callback_t callback, + void *userdata); + +/** + * Get a ranked list of securities for the given category key. + * Returns `CRankListResponse`. + */ +void lb_market_context_rank_list(const struct lb_market_context_t *ctx, + const char *key, + bool need_article, + lb_async_callback_t callback, + void *userdata); + /** * Asynchronously build an OAuth 2.0 client. * @@ -9676,14 +9331,25 @@ void lb_quote_context_history_market_temperature(const struct lb_quote_context_t void *userdata); /** - * Get short interest data for a US security. Returns - * `CShortPositionsResponse`. + * Get short interest data for a US or HK security. Returns + * `CShortPositionsResponse`. Market is inferred from symbol suffix. */ void lb_quote_context_short_positions(const struct lb_quote_context_t *ctx, const char *symbol, + uint32_t count, lb_async_callback_t callback, void *userdata); +/** + * Get short trade records for a HK or US security. Returns + * `CShortTradesResponse`. Market is inferred from symbol suffix. + */ +void lb_quote_context_short_trades(const struct lb_quote_context_t *ctx, + const char *symbol, + uint32_t count, + lb_async_callback_t callback, + void *userdata); + /** * Get real-time option call/put volume. Returns `COptionVolumeStats`. */ @@ -9702,6 +9368,58 @@ void lb_quote_context_option_volume_daily(const struct lb_quote_context_t *ctx, lb_async_callback_t callback, void *userdata); +const struct lb_screener_context_t *lb_screener_context_new(const struct lb_config_t *config); + +void lb_screener_context_retain(const struct lb_screener_context_t *ctx); + +void lb_screener_context_release(const struct lb_screener_context_t *ctx); + +/** + * Get recommended built-in screener strategies. + * Returns `CScreenerRecommendStrategiesResponse`. + */ +void lb_screener_context_recommend_strategies(const struct lb_screener_context_t *ctx, + lb_async_callback_t callback, + void *userdata); + +/** + * Get the current user's saved screener strategies. + * Returns `CScreenerUserStrategiesResponse`. + */ +void lb_screener_context_user_strategies(const struct lb_screener_context_t *ctx, + lb_async_callback_t callback, + void *userdata); + +/** + * Get detail for one screener strategy by ID. + * Returns `CScreenerStrategyResponse`. + */ +void lb_screener_context_strategy(const struct lb_screener_context_t *ctx, + int64_t id, + lb_async_callback_t callback, + void *userdata); + +/** + * Search / screen securities using a strategy. + * Returns `CScreenerSearchResponse`. + */ +void lb_screener_context_search(const struct lb_screener_context_t *ctx, + const char *market, + int64_t strategy_id, + bool has_strategy_id, + uint32_t page, + uint32_t size, + lb_async_callback_t callback, + void *userdata); + +/** + * Get all available screener indicator definitions. + * Returns `CScreenerIndicatorsResponse`. + */ +void lb_screener_context_indicators(const struct lb_screener_context_t *ctx, + lb_async_callback_t callback, + void *userdata); + const struct lb_sharelist_context_t *lb_sharelist_context_new(const struct lb_config_t *config); void lb_sharelist_context_retain(const struct lb_sharelist_context_t *ctx); diff --git a/c/src/fundamental_context/context.rs b/c/src/fundamental_context/context.rs index 65c69f90b..503a999e5 100644 --- a/c/src/fundamental_context/context.rs +++ b/c/src/fundamental_context/context.rs @@ -406,9 +406,9 @@ pub unsafe extern "C" fn lb_fundamental_context_ratings( }); } -/// Get business segment breakdowns. Returns `CBusinessSegments`. +/// Get ranked list of top shareholders. Returns `CShareholderTopResponse`. #[unsafe(no_mangle)] -pub unsafe extern "C" fn lb_fundamental_context_business_segments( +pub unsafe extern "C" fn lb_fundamental_context_shareholder_top( ctx: *const CFundamentalContext, symbol: *const c_char, callback: CAsyncCallback, @@ -417,186 +417,63 @@ pub unsafe extern "C" fn lb_fundamental_context_business_segments( let ctx_inner = (*ctx).ctx.clone(); let symbol = cstr_to_rust(symbol); execute_async(callback, ctx, userdata, async move { - let resp: CCow = CCow::new(CBusinessSegmentsOwned::from( - ctx_inner.business_segments(symbol).await?, - )); + let resp: CCow = CCow::new( + CShareholderTopResponseOwned::from(ctx_inner.shareholder_top(symbol).await?), + ); Ok(resp) }); } -/// Get historical business segment breakdowns. Returns -/// `CBusinessSegmentsHistory`. -/// -/// Pass `NULL` for `report` or `cate` to omit those parameters. +/// Get holding history and detail for one shareholder. Returns +/// `CShareholderDetailResponse`. #[unsafe(no_mangle)] -pub unsafe extern "C" fn lb_fundamental_context_business_segments_history( +pub unsafe extern "C" fn lb_fundamental_context_shareholder_detail( ctx: *const CFundamentalContext, symbol: *const c_char, - report: *const c_char, - cate: *const c_char, + object_id: i64, callback: CAsyncCallback, userdata: *mut c_void, ) { let ctx_inner = (*ctx).ctx.clone(); let symbol = cstr_to_rust(symbol); - let report_str = if report.is_null() { - None - } else { - Some(cstr_to_rust(report)) - }; - let cate_opt = if cate.is_null() { - None - } else { - Some(cstr_to_rust(cate)) - }; - let report_static: Option<&'static str> = match report_str.as_deref() { - Some("qf") => Some("qf"), - Some("saf") => Some("saf"), - Some("af") => Some("af"), - _ => None, - }; execute_async(callback, ctx, userdata, async move { - let resp: CCow = - CCow::new(CBusinessSegmentsHistoryOwned::from( - ctx_inner - .business_segments_history(symbol, report_static, cate_opt) - .await?, + let resp: CCow = + CCow::new(CShareholderDetailResponseOwned::from( + ctx_inner.shareholder_detail(symbol, object_id).await?, )); Ok(resp) }); } -/// Get historical institutional rating views. Returns -/// `CInstitutionRatingViews`. -#[unsafe(no_mangle)] -pub unsafe extern "C" fn lb_fundamental_context_institution_rating_views( - ctx: *const CFundamentalContext, - symbol: *const c_char, - callback: CAsyncCallback, - userdata: *mut c_void, -) { - let ctx_inner = (*ctx).ctx.clone(); - let symbol = cstr_to_rust(symbol); - execute_async(callback, ctx, userdata, async move { - let resp: CCow = CCow::new( - CInstitutionRatingViewsOwned::from(ctx_inner.institution_rating_views(symbol).await?), - ); - Ok(resp) - }); -} - -/// Get industry rank for a market. Returns `CIndustryRankResponse`. -#[unsafe(no_mangle)] -pub unsafe extern "C" fn lb_fundamental_context_industry_rank( - ctx: *const CFundamentalContext, - market: *const c_char, - indicator: *const c_char, - sort_type: *const c_char, - limit: u32, - callback: CAsyncCallback, - userdata: *mut c_void, -) { - let ctx_inner = (*ctx).ctx.clone(); - let market = cstr_to_rust(market); - let indicator = cstr_to_rust(indicator); - let sort_type = cstr_to_rust(sort_type); - execute_async(callback, ctx, userdata, async move { - let resp: CCow = CCow::new(CIndustryRankResponseOwned::from( - ctx_inner - .industry_rank(market, indicator, sort_type, limit) - .await?, - )); - Ok(resp) - }); -} - -/// Get industry peer chain. Returns `CIndustryPeersResponse`. -/// -/// Pass `NULL` for `industry_id` to omit it. +/// Get valuation comparison between a security and optional peers. +/// Returns `CValuationComparisonResponse`. +/// Pass NULL for `comparison_symbols` to skip peer comparison. #[unsafe(no_mangle)] -pub unsafe extern "C" fn lb_fundamental_context_industry_peers( - ctx: *const CFundamentalContext, - counter_id: *const c_char, - market: *const c_char, - industry_id: *const c_char, - callback: CAsyncCallback, - userdata: *mut c_void, -) { - let ctx_inner = (*ctx).ctx.clone(); - let counter_id = cstr_to_rust(counter_id); - let market = cstr_to_rust(market); - let industry_id_opt = if industry_id.is_null() { - None - } else { - Some(cstr_to_rust(industry_id)) - }; - execute_async(callback, ctx, userdata, async move { - let resp: CCow = CCow::new(CIndustryPeersResponseOwned::from( - ctx_inner - .industry_peers(counter_id, market, industry_id_opt) - .await?, - )); - Ok(resp) - }); -} - -/// Get financial report snapshot. Returns `CFinancialReportSnapshot`. -/// -/// Pass `NULL` for optional parameters to omit them. -/// `fiscal_year` is ignored when 0. -#[unsafe(no_mangle)] -pub unsafe extern "C" fn lb_fundamental_context_financial_report_snapshot( +pub unsafe extern "C" fn lb_fundamental_context_valuation_comparison( ctx: *const CFundamentalContext, symbol: *const c_char, - report: *const c_char, - fiscal_year: i32, - fiscal_period: *const c_char, + currency: *const c_char, + comparison_symbols: *const *const c_char, + num_comparison_symbols: usize, callback: CAsyncCallback, userdata: *mut c_void, ) { let ctx_inner = (*ctx).ctx.clone(); let symbol = cstr_to_rust(symbol); - let report_str = if report.is_null() { - None - } else { - Some(cstr_to_rust(report)) - }; - let fiscal_year_opt = if fiscal_year == 0 { - None - } else { - Some(fiscal_year) - }; - let fiscal_period_str = if fiscal_period.is_null() { + let currency = cstr_to_rust(currency); + let comparison = if comparison_symbols.is_null() || num_comparison_symbols == 0 { None } else { - Some(cstr_to_rust(fiscal_period)) - }; - let report_static: Option<&'static str> = match report_str.as_deref() { - Some("qf") => Some("qf"), - Some("saf") => Some("saf"), - Some("af") => Some("af"), - _ => None, - }; - let fiscal_period_static: Option<&'static str> = match fiscal_period_str.as_deref() { - Some("q1") => Some("q1"), - Some("q2") => Some("q2"), - Some("q3") => Some("q3"), - Some("q4") => Some("q4"), - Some("fy") => Some("fy"), - Some("h1") => Some("h1"), - Some("h2") => Some("h2"), - _ => None, + let syms: Vec = (0..num_comparison_symbols) + .map(|i| cstr_to_rust(*comparison_symbols.add(i))) + .collect(); + Some(syms) }; execute_async(callback, ctx, userdata, async move { - let resp: CCow = - CCow::new(CFinancialReportSnapshotOwned::from( + let resp: CCow = + CCow::new(CValuationComparisonResponseOwned::from( ctx_inner - .financial_report_snapshot( - symbol, - report_static, - fiscal_year_opt, - fiscal_period_static, - ) + .valuation_comparison(symbol, currency, comparison) .await?, )); Ok(resp) diff --git a/c/src/fundamental_context/types.rs b/c/src/fundamental_context/types.rs index 165e7c21a..8a272c883 100644 --- a/c/src/fundamental_context/types.rs +++ b/c/src/fundamental_context/types.rs @@ -2843,762 +2843,89 @@ impl ToFFI for CStockRatingsOwned { } } -// ── business_segments ───────────────────────────────────────────── +// ── ShareholderTopResponse ──────────────────────────────────────── -/// One business segment item (latest snapshot). +/// Top-shareholder list response. `data` is a NUL-terminated JSON string. #[repr(C)] -pub struct CBusinessSegmentItem { - /// Segment name. - pub name: *const c_char, - /// Percentage of total revenue. - pub percent: *const c_char, -} - -pub(crate) struct CBusinessSegmentItemOwned { - name: CString, - percent: CString, -} - -impl From for CBusinessSegmentItemOwned { - fn from(v: longbridge::fundamental::BusinessSegmentItem) -> Self { - Self { - name: v.name.into(), - percent: v.percent.into(), - } - } -} - -impl ToFFI for CBusinessSegmentItemOwned { - type FFIType = CBusinessSegmentItem; - fn to_ffi_type(&self) -> Self::FFIType { - CBusinessSegmentItem { - name: self.name.to_ffi_type(), - percent: self.percent.to_ffi_type(), - } - } -} - -/// Business segments response. -#[repr(C)] -pub struct CBusinessSegments { - /// Report date. - pub date: *const c_char, - /// Total revenue. - pub total: *const c_char, - /// Reporting currency. - pub currency: *const c_char, - /// Pointer to the array of business segment items. - pub business: *const CBusinessSegmentItem, - /// Number of items in `business`. - pub num_business: usize, -} - -pub(crate) struct CBusinessSegmentsOwned { - date: CString, - total: CString, - currency: CString, - business: CVec, -} - -impl From for CBusinessSegmentsOwned { - fn from(v: longbridge::fundamental::BusinessSegments) -> Self { - Self { - date: v.date.into(), - total: v.total.into(), - currency: v.currency.into(), - business: v.business.into(), - } - } -} - -impl ToFFI for CBusinessSegmentsOwned { - type FFIType = CBusinessSegments; - fn to_ffi_type(&self) -> Self::FFIType { - CBusinessSegments { - date: self.date.to_ffi_type(), - total: self.total.to_ffi_type(), - currency: self.currency.to_ffi_type(), - business: self.business.to_ffi_type(), - num_business: self.business.len(), - } - } -} - -/// One business/regional segment item in a historical snapshot. -#[repr(C)] -pub struct CBusinessSegmentHistoryItem { - /// Segment name. - pub name: *const c_char, - /// Percentage of total. - pub percent: *const c_char, - /// Absolute value. - pub value: *const c_char, -} - -pub(crate) struct CBusinessSegmentHistoryItemOwned { - name: CString, - percent: CString, - value: CString, -} - -impl From - for CBusinessSegmentHistoryItemOwned -{ - fn from(v: longbridge::fundamental::BusinessSegmentHistoryItem) -> Self { - Self { - name: v.name.into(), - percent: v.percent.into(), - value: v.value.into(), - } - } -} - -impl ToFFI for CBusinessSegmentHistoryItemOwned { - type FFIType = CBusinessSegmentHistoryItem; - fn to_ffi_type(&self) -> Self::FFIType { - CBusinessSegmentHistoryItem { - name: self.name.to_ffi_type(), - percent: self.percent.to_ffi_type(), - value: self.value.to_ffi_type(), - } - } -} - -/// One historical business segments snapshot. -#[repr(C)] -pub struct CBusinessSegmentsHistoricalItem { - /// Report date. - pub date: *const c_char, - /// Total revenue. - pub total: *const c_char, - /// Reporting currency. - pub currency: *const c_char, - /// Pointer to the business segment items. - pub business: *const CBusinessSegmentHistoryItem, - /// Number of items in `business`. - pub num_business: usize, - /// Pointer to the regional segment items. - pub regionals: *const CBusinessSegmentHistoryItem, - /// Number of items in `regionals`. - pub num_regionals: usize, -} - -pub(crate) struct CBusinessSegmentsHistoricalItemOwned { - date: CString, - total: CString, - currency: CString, - business: CVec, - regionals: CVec, -} - -impl From - for CBusinessSegmentsHistoricalItemOwned -{ - fn from(v: longbridge::fundamental::BusinessSegmentsHistoricalItem) -> Self { - Self { - date: v.date.into(), - total: v.total.into(), - currency: v.currency.into(), - business: v.business.into(), - regionals: v.regionals.into(), - } - } -} - -impl ToFFI for CBusinessSegmentsHistoricalItemOwned { - type FFIType = CBusinessSegmentsHistoricalItem; - fn to_ffi_type(&self) -> Self::FFIType { - CBusinessSegmentsHistoricalItem { - date: self.date.to_ffi_type(), - total: self.total.to_ffi_type(), - currency: self.currency.to_ffi_type(), - business: self.business.to_ffi_type(), - num_business: self.business.len(), - regionals: self.regionals.to_ffi_type(), - num_regionals: self.regionals.len(), - } - } -} - -/// Business segments history response. -#[repr(C)] -pub struct CBusinessSegmentsHistory { - /// Pointer to the historical snapshots. - pub historical: *const CBusinessSegmentsHistoricalItem, - /// Number of items in `historical`. - pub num_historical: usize, -} - -pub(crate) struct CBusinessSegmentsHistoryOwned { - historical: CVec, -} - -impl From for CBusinessSegmentsHistoryOwned { - fn from(v: longbridge::fundamental::BusinessSegmentsHistory) -> Self { - Self { - historical: v.historical.into(), - } - } -} - -impl ToFFI for CBusinessSegmentsHistoryOwned { - type FFIType = CBusinessSegmentsHistory; - fn to_ffi_type(&self) -> Self::FFIType { - CBusinessSegmentsHistory { - historical: self.historical.to_ffi_type(), - num_historical: self.historical.len(), - } - } -} - -// ── institution_rating_views ────────────────────────────────────── - -/// One historical rating distribution snapshot. -#[repr(C)] -pub struct CInstitutionRatingViewItem { - /// Date as unix timestamp string. - pub date: *const c_char, - /// Number of Buy ratings. - pub buy: *const c_char, - /// Number of Outperform ratings. - pub over: *const c_char, - /// Number of Hold ratings. - pub hold: *const c_char, - /// Number of Underperform ratings. - pub under: *const c_char, - /// Number of Sell ratings. - pub sell: *const c_char, - /// Total analyst count. - pub total: *const c_char, -} - -pub(crate) struct CInstitutionRatingViewItemOwned { - date: CString, - buy: CString, - over: CString, - hold: CString, - under: CString, - sell: CString, - total: CString, -} - -impl From for CInstitutionRatingViewItemOwned { - fn from(v: longbridge::fundamental::InstitutionRatingViewItem) -> Self { - Self { - date: v.date.into(), - buy: v.buy.into(), - over: v.over.into(), - hold: v.hold.into(), - under: v.under.into(), - sell: v.sell.into(), - total: v.total.into(), - } - } -} - -impl ToFFI for CInstitutionRatingViewItemOwned { - type FFIType = CInstitutionRatingViewItem; - fn to_ffi_type(&self) -> Self::FFIType { - CInstitutionRatingViewItem { - date: self.date.to_ffi_type(), - buy: self.buy.to_ffi_type(), - over: self.over.to_ffi_type(), - hold: self.hold.to_ffi_type(), - under: self.under.to_ffi_type(), - sell: self.sell.to_ffi_type(), - total: self.total.to_ffi_type(), - } - } -} - -/// Institution rating views response. -#[repr(C)] -pub struct CInstitutionRatingViews { - /// Pointer to the rating view items. - pub elist: *const CInstitutionRatingViewItem, - /// Number of items in `elist`. - pub num_elist: usize, -} - -pub(crate) struct CInstitutionRatingViewsOwned { - elist: CVec, -} - -impl From for CInstitutionRatingViewsOwned { - fn from(v: longbridge::fundamental::InstitutionRatingViews) -> Self { - Self { - elist: v.elist.into(), - } - } -} - -impl ToFFI for CInstitutionRatingViewsOwned { - type FFIType = CInstitutionRatingViews; - fn to_ffi_type(&self) -> Self::FFIType { - CInstitutionRatingViews { - elist: self.elist.to_ffi_type(), - num_elist: self.elist.len(), - } - } -} - -// ── industry_rank ───────────────────────────────────────────────── - -/// One ranked industry item. -#[repr(C)] -pub struct CIndustryRankItem { - /// Industry / sector name. - pub name: *const c_char, - /// Counter ID of the industry. - pub counter_id: *const c_char, - /// Change percentage. - pub chg: *const c_char, - /// Name of the leading stock. - pub leading_name: *const c_char, - /// Ticker of the leading stock. - pub leading_ticker: *const c_char, - /// Change percentage of the leading stock. - pub leading_chg: *const c_char, - /// Value label name. - pub value_name: *const c_char, - /// Value data. - pub value_data: *const c_char, -} - -pub(crate) struct CIndustryRankItemOwned { - name: CString, - counter_id: CString, - chg: CString, - leading_name: CString, - leading_ticker: CString, - leading_chg: CString, - value_name: CString, - value_data: CString, -} - -impl From for CIndustryRankItemOwned { - fn from(v: longbridge::fundamental::IndustryRankItem) -> Self { - Self { - name: v.name.into(), - counter_id: v.counter_id.into(), - chg: v.chg.into(), - leading_name: v.leading_name.into(), - leading_ticker: v.leading_ticker.into(), - leading_chg: v.leading_chg.into(), - value_name: v.value_name.into(), - value_data: v.value_data.into(), - } - } -} - -impl ToFFI for CIndustryRankItemOwned { - type FFIType = CIndustryRankItem; - fn to_ffi_type(&self) -> Self::FFIType { - CIndustryRankItem { - name: self.name.to_ffi_type(), - counter_id: self.counter_id.to_ffi_type(), - chg: self.chg.to_ffi_type(), - leading_name: self.leading_name.to_ffi_type(), - leading_ticker: self.leading_ticker.to_ffi_type(), - leading_chg: self.leading_chg.to_ffi_type(), - value_name: self.value_name.to_ffi_type(), - value_data: self.value_data.to_ffi_type(), - } - } -} - -/// A group of ranked industry items. -#[repr(C)] -pub struct CIndustryRankGroup { - /// Pointer to the items in this group. - pub lists: *const CIndustryRankItem, - /// Number of items in `lists`. - pub num_lists: usize, -} - -pub(crate) struct CIndustryRankGroupOwned { - lists: CVec, -} - -impl From for CIndustryRankGroupOwned { - fn from(v: longbridge::fundamental::IndustryRankGroup) -> Self { - Self { - lists: v.lists.into(), - } - } -} - -impl ToFFI for CIndustryRankGroupOwned { - type FFIType = CIndustryRankGroup; - fn to_ffi_type(&self) -> Self::FFIType { - CIndustryRankGroup { - lists: self.lists.to_ffi_type(), - num_lists: self.lists.len(), - } - } -} - -/// Industry rank response. -#[repr(C)] -pub struct CIndustryRankResponse { - /// Pointer to the grouped rank items. - pub items: *const CIndustryRankGroup, - /// Number of items in `items`. - pub num_items: usize, +pub struct CShareholderTopResponse { + /// Raw top-shareholder data as a JSON string + pub data: *const c_char, } -pub(crate) struct CIndustryRankResponseOwned { - items: CVec, +pub(crate) struct CShareholderTopResponseOwned { + data: CString, } -impl From for CIndustryRankResponseOwned { - fn from(v: longbridge::fundamental::IndustryRankResponse) -> Self { - Self { - items: v.items.into(), - } - } -} - -impl ToFFI for CIndustryRankResponseOwned { - type FFIType = CIndustryRankResponse; - fn to_ffi_type(&self) -> Self::FFIType { - CIndustryRankResponse { - items: self.items.to_ffi_type(), - num_items: self.items.len(), - } - } -} - -// ── industry_peers ──────────────────────────────────────────────── - -/// Top-level industry info in the peers response. -#[repr(C)] -pub struct CIndustryPeersTop { - /// Industry name. - pub name: *const c_char, - /// Market code. - pub market: *const c_char, -} - -pub(crate) struct CIndustryPeersTopOwned { - name: CString, - market: CString, -} - -impl From for CIndustryPeersTopOwned { - fn from(v: longbridge::fundamental::IndustryPeersTop) -> Self { - Self { - name: v.name.into(), - market: v.market.into(), - } +impl From for CShareholderTopResponseOwned { + fn from(v: ShareholderTopResponse) -> Self { + let json = serde_json::to_string(&v.data).unwrap_or_default(); + Self { data: json.into() } } } -impl ToFFI for CIndustryPeersTopOwned { - type FFIType = CIndustryPeersTop; +impl ToFFI for CShareholderTopResponseOwned { + type FFIType = CShareholderTopResponse; fn to_ffi_type(&self) -> Self::FFIType { - CIndustryPeersTop { - name: self.name.to_ffi_type(), - market: self.market.to_ffi_type(), - } - } -} - -/// A node in the recursive industry peer chain. -/// -/// `next_json` contains the child nodes serialised as a JSON string. -#[repr(C)] -pub struct CIndustryPeerNode { - /// Node name. - pub name: *const c_char, - /// Counter ID. - pub counter_id: *const c_char, - /// Number of stocks in this node. - pub stock_num: i32, - /// Change percentage. - pub chg: *const c_char, - /// Year-to-date change. - pub ytd_chg: *const c_char, - /// Child nodes as a JSON string. - pub next_json: *const c_char, -} - -pub(crate) struct CIndustryPeerNodeOwned { - name: CString, - counter_id: CString, - stock_num: i32, - chg: CString, - ytd_chg: CString, - next_json: CString, -} - -impl From for CIndustryPeerNodeOwned { - fn from(v: longbridge::fundamental::IndustryPeerNode) -> Self { - Self { - name: v.name.into(), - counter_id: v.counter_id.into(), - stock_num: v.stock_num, - chg: v.chg.into(), - ytd_chg: v.ytd_chg.into(), - next_json: serde_json::to_string(&v.next).unwrap_or_default().into(), + CShareholderTopResponse { + data: self.data.to_ffi_type(), } } } -impl ToFFI for CIndustryPeerNodeOwned { - type FFIType = CIndustryPeerNode; - fn to_ffi_type(&self) -> Self::FFIType { - CIndustryPeerNode { - name: self.name.to_ffi_type(), - counter_id: self.counter_id.to_ffi_type(), - stock_num: self.stock_num, - chg: self.chg.to_ffi_type(), - ytd_chg: self.ytd_chg.to_ffi_type(), - next_json: self.next_json.to_ffi_type(), - } - } -} +// ── ShareholderDetailResponse ───────────────────────────────────── -/// Industry peers response. +/// Shareholder detail response. `data` is a NUL-terminated JSON string. #[repr(C)] -pub struct CIndustryPeersResponse { - /// Top-level industry node info. - pub top: CIndustryPeersTop, - /// Root peer chain node, or null if absent. - pub chain: *const CIndustryPeerNode, +pub struct CShareholderDetailResponse { + /// Raw shareholder detail data as a JSON string + pub data: *const c_char, } -pub(crate) struct CIndustryPeersResponseOwned { - top: CIndustryPeersTopOwned, - chain: COption, +pub(crate) struct CShareholderDetailResponseOwned { + data: CString, } -impl From for CIndustryPeersResponseOwned { - fn from(v: longbridge::fundamental::IndustryPeersResponse) -> Self { - Self { - top: v.top.into(), - chain: v.chain.into(), - } +impl From for CShareholderDetailResponseOwned { + fn from(v: ShareholderDetailResponse) -> Self { + let json = serde_json::to_string(&v.data).unwrap_or_default(); + Self { data: json.into() } } } -impl ToFFI for CIndustryPeersResponseOwned { - type FFIType = CIndustryPeersResponse; +impl ToFFI for CShareholderDetailResponseOwned { + type FFIType = CShareholderDetailResponse; fn to_ffi_type(&self) -> Self::FFIType { - CIndustryPeersResponse { - top: self.top.to_ffi_type(), - chain: self.chain.to_ffi_type(), - } - } -} - -// ── financial_report_snapshot ───────────────────────────────────── - -/// A forecast metric in the financial report snapshot. -#[repr(C)] -pub struct CSnapshotForecastMetric { - /// Actual value. - pub value: *const c_char, - /// Year-over-year change. - pub yoy: *const c_char, - /// Beat/miss description. - pub cmp_desc: *const c_char, - /// Consensus estimate value. - pub est_value: *const c_char, -} - -pub(crate) struct CSnapshotForecastMetricOwned { - value: CString, - yoy: CString, - cmp_desc: CString, - est_value: CString, -} - -impl From for CSnapshotForecastMetricOwned { - fn from(v: longbridge::fundamental::SnapshotForecastMetric) -> Self { - Self { - value: v.value.into(), - yoy: v.yoy.into(), - cmp_desc: v.cmp_desc.into(), - est_value: v.est_value.into(), + CShareholderDetailResponse { + data: self.data.to_ffi_type(), } } } -impl ToFFI for CSnapshotForecastMetricOwned { - type FFIType = CSnapshotForecastMetric; - fn to_ffi_type(&self) -> Self::FFIType { - CSnapshotForecastMetric { - value: self.value.to_ffi_type(), - yoy: self.yoy.to_ffi_type(), - cmp_desc: self.cmp_desc.to_ffi_type(), - est_value: self.est_value.to_ffi_type(), - } - } -} +// ── ValuationComparisonResponse ─────────────────────────────────── -/// A reported metric in the financial report snapshot. +/// Valuation comparison response. `data` is a NUL-terminated JSON string. #[repr(C)] -pub struct CSnapshotReportedMetric { - /// Actual value. - pub value: *const c_char, - /// Year-over-year change. - pub yoy: *const c_char, -} - -pub(crate) struct CSnapshotReportedMetricOwned { - value: CString, - yoy: CString, +pub struct CValuationComparisonResponse { + /// Raw valuation comparison data as a JSON string + pub data: *const c_char, } -impl From for CSnapshotReportedMetricOwned { - fn from(v: longbridge::fundamental::SnapshotReportedMetric) -> Self { - Self { - value: v.value.into(), - yoy: v.yoy.into(), - } - } +pub(crate) struct CValuationComparisonResponseOwned { + data: CString, } -impl ToFFI for CSnapshotReportedMetricOwned { - type FFIType = CSnapshotReportedMetric; - fn to_ffi_type(&self) -> Self::FFIType { - CSnapshotReportedMetric { - value: self.value.to_ffi_type(), - yoy: self.yoy.to_ffi_type(), - } +impl From for CValuationComparisonResponseOwned { + fn from(v: ValuationComparisonResponse) -> Self { + let json = serde_json::to_string(&v.data).unwrap_or_default(); + Self { data: json.into() } } } -/// Financial report snapshot response. -#[repr(C)] -pub struct CFinancialReportSnapshot { - /// Company name. - pub name: *const c_char, - /// Ticker code. - pub ticker: *const c_char, - /// Fiscal period start date. - pub fp_start: *const c_char, - /// Fiscal period end date. - pub fp_end: *const c_char, - /// Reporting currency. - pub currency: *const c_char, - /// Report description. - pub report_desc: *const c_char, - /// Forecast revenue, or null. - pub fo_revenue: *const CSnapshotForecastMetric, - /// Forecast EBIT, or null. - pub fo_ebit: *const CSnapshotForecastMetric, - /// Forecast EPS, or null. - pub fo_eps: *const CSnapshotForecastMetric, - /// Reported revenue, or null. - pub fr_revenue: *const CSnapshotReportedMetric, - /// Reported net profit, or null. - pub fr_profit: *const CSnapshotReportedMetric, - /// Reported operating cash flow, or null. - pub fr_operate_cash: *const CSnapshotReportedMetric, - /// Reported investing cash flow, or null. - pub fr_invest_cash: *const CSnapshotReportedMetric, - /// Reported financing cash flow, or null. - pub fr_finance_cash: *const CSnapshotReportedMetric, - /// Reported total assets, or null. - pub fr_total_assets: *const CSnapshotReportedMetric, - /// Reported total liabilities, or null. - pub fr_total_liability: *const CSnapshotReportedMetric, - /// ROE TTM. - pub fr_roe_ttm: *const c_char, - /// Profit margin. - pub fr_profit_margin: *const c_char, - /// Profit margin TTM. - pub fr_profit_margin_ttm: *const c_char, - /// Asset turnover TTM. - pub fr_asset_turn_ttm: *const c_char, - /// Leverage TTM. - pub fr_leverage_ttm: *const c_char, - /// Debt-to-assets ratio. - pub fr_debt_assets_ratio: *const c_char, -} - -pub(crate) struct CFinancialReportSnapshotOwned { - name: CString, - ticker: CString, - fp_start: CString, - fp_end: CString, - currency: CString, - report_desc: CString, - fo_revenue: COption, - fo_ebit: COption, - fo_eps: COption, - fr_revenue: COption, - fr_profit: COption, - fr_operate_cash: COption, - fr_invest_cash: COption, - fr_finance_cash: COption, - fr_total_assets: COption, - fr_total_liability: COption, - fr_roe_ttm: CString, - fr_profit_margin: CString, - fr_profit_margin_ttm: CString, - fr_asset_turn_ttm: CString, - fr_leverage_ttm: CString, - fr_debt_assets_ratio: CString, -} - -impl From for CFinancialReportSnapshotOwned { - fn from(v: longbridge::fundamental::FinancialReportSnapshot) -> Self { - Self { - name: v.name.into(), - ticker: v.ticker.into(), - fp_start: v.fp_start.into(), - fp_end: v.fp_end.into(), - currency: v.currency.into(), - report_desc: v.report_desc.into(), - fo_revenue: v.fo_revenue.into(), - fo_ebit: v.fo_ebit.into(), - fo_eps: v.fo_eps.into(), - fr_revenue: v.fr_revenue.into(), - fr_profit: v.fr_profit.into(), - fr_operate_cash: v.fr_operate_cash.into(), - fr_invest_cash: v.fr_invest_cash.into(), - fr_finance_cash: v.fr_finance_cash.into(), - fr_total_assets: v.fr_total_assets.into(), - fr_total_liability: v.fr_total_liability.into(), - fr_roe_ttm: v.fr_roe_ttm.into(), - fr_profit_margin: v.fr_profit_margin.into(), - fr_profit_margin_ttm: v.fr_profit_margin_ttm.into(), - fr_asset_turn_ttm: v.fr_asset_turn_ttm.into(), - fr_leverage_ttm: v.fr_leverage_ttm.into(), - fr_debt_assets_ratio: v.fr_debt_assets_ratio.into(), - } - } -} - -impl ToFFI for CFinancialReportSnapshotOwned { - type FFIType = CFinancialReportSnapshot; +impl ToFFI for CValuationComparisonResponseOwned { + type FFIType = CValuationComparisonResponse; fn to_ffi_type(&self) -> Self::FFIType { - CFinancialReportSnapshot { - name: self.name.to_ffi_type(), - ticker: self.ticker.to_ffi_type(), - fp_start: self.fp_start.to_ffi_type(), - fp_end: self.fp_end.to_ffi_type(), - currency: self.currency.to_ffi_type(), - report_desc: self.report_desc.to_ffi_type(), - fo_revenue: self.fo_revenue.to_ffi_type(), - fo_ebit: self.fo_ebit.to_ffi_type(), - fo_eps: self.fo_eps.to_ffi_type(), - fr_revenue: self.fr_revenue.to_ffi_type(), - fr_profit: self.fr_profit.to_ffi_type(), - fr_operate_cash: self.fr_operate_cash.to_ffi_type(), - fr_invest_cash: self.fr_invest_cash.to_ffi_type(), - fr_finance_cash: self.fr_finance_cash.to_ffi_type(), - fr_total_assets: self.fr_total_assets.to_ffi_type(), - fr_total_liability: self.fr_total_liability.to_ffi_type(), - fr_roe_ttm: self.fr_roe_ttm.to_ffi_type(), - fr_profit_margin: self.fr_profit_margin.to_ffi_type(), - fr_profit_margin_ttm: self.fr_profit_margin_ttm.to_ffi_type(), - fr_asset_turn_ttm: self.fr_asset_turn_ttm.to_ffi_type(), - fr_leverage_ttm: self.fr_leverage_ttm.to_ffi_type(), - fr_debt_assets_ratio: self.fr_debt_assets_ratio.to_ffi_type(), + CValuationComparisonResponse { + data: self.data.to_ffi_type(), } } } diff --git a/c/src/lib.rs b/c/src/lib.rs index 58b1fd0fa..4a8fccdaf 100644 --- a/c/src/lib.rs +++ b/c/src/lib.rs @@ -15,6 +15,7 @@ mod market_context; mod oauth; mod portfolio_context; mod quote_context; +mod screener_context; mod sharelist_context; mod trade_context; mod types; diff --git a/c/src/market_context/context.rs b/c/src/market_context/context.rs index 2ad5b724c..84d74a2af 100644 --- a/c/src/market_context/context.rs +++ b/c/src/market_context/context.rs @@ -215,3 +215,71 @@ pub unsafe extern "C" fn lb_market_context_constituent( Ok(resp) }); } + +/// Get stock events across one or more markets. +/// Pass markets as a NULL-terminated array of C strings. +/// Returns `CStockEventsResponse`. +#[unsafe(no_mangle)] +pub unsafe extern "C" fn lb_market_context_stock_events( + ctx: *const CMarketContext, + markets: *const *const c_char, + num_markets: usize, + sort: u32, + date: *const c_char, + limit: u32, + callback: CAsyncCallback, + userdata: *mut c_void, +) { + let ctx_inner = (*ctx).ctx.clone(); + let markets: Vec = (0..num_markets) + .map(|i| cstr_to_rust(*markets.add(i))) + .collect(); + let date = if date.is_null() { + None + } else { + Some(cstr_to_rust(date)) + }; + execute_async(callback, ctx, userdata, async move { + let resp: CCow = CCow::new(CStockEventsResponseOwned::from( + ctx_inner.stock_events(markets, sort, date, limit).await?, + )); + Ok(resp) + }); +} + +/// Get all available rank category keys and labels. +/// Returns `CRankCategoriesResponse`. +#[unsafe(no_mangle)] +pub unsafe extern "C" fn lb_market_context_rank_categories( + ctx: *const CMarketContext, + callback: CAsyncCallback, + userdata: *mut c_void, +) { + let ctx_inner = (*ctx).ctx.clone(); + execute_async(callback, ctx, userdata, async move { + let resp: CCow = CCow::new( + CRankCategoriesResponseOwned::from(ctx_inner.rank_categories().await?), + ); + Ok(resp) + }); +} + +/// Get a ranked list of securities for the given category key. +/// Returns `CRankListResponse`. +#[unsafe(no_mangle)] +pub unsafe extern "C" fn lb_market_context_rank_list( + ctx: *const CMarketContext, + key: *const c_char, + need_article: bool, + callback: CAsyncCallback, + userdata: *mut c_void, +) { + let ctx_inner = (*ctx).ctx.clone(); + let key = cstr_to_rust(key); + execute_async(callback, ctx, userdata, async move { + let resp: CCow = CCow::new(CRankListResponseOwned::from( + ctx_inner.rank_list(key, need_article).await?, + )); + Ok(resp) + }); +} diff --git a/c/src/market_context/types.rs b/c/src/market_context/types.rs index c5d175238..0b13ca289 100644 --- a/c/src/market_context/types.rs +++ b/c/src/market_context/types.rs @@ -4,8 +4,8 @@ use longbridge::market::{ AhPremiumIntraday, AhPremiumKline, AhPremiumKlines, AnomalyItem, AnomalyResponse, BrokerHoldingChanges, BrokerHoldingDailyHistory, BrokerHoldingDailyItem, BrokerHoldingDetail, BrokerHoldingDetailItem, BrokerHoldingEntry, BrokerHoldingTop, ConstituentStock, - IndexConstituents, MarketStatusResponse, MarketTimeItem, TradePriceLevel, TradeStatistics, - TradeStatsResponse, + IndexConstituents, MarketStatusResponse, MarketTimeItem, RankCategoriesResponse, + RankListResponse, StockEventsResponse, TradePriceLevel, TradeStatistics, TradeStatsResponse, }; use crate::types::{CMarket, CString, CVec, ToFFI}; @@ -957,3 +957,90 @@ impl ToFFI for CIndexConstituentsOwned { } } } + +// ── StockEventsResponse ─────────────────────────────────────────── + +/// Stock events response. `data` is a NUL-terminated JSON string. +#[repr(C)] +pub struct CStockEventsResponse { + /// Raw stock events data as a JSON string + pub data: *const c_char, +} + +pub(crate) struct CStockEventsResponseOwned { + data: CString, +} + +impl From for CStockEventsResponseOwned { + fn from(v: StockEventsResponse) -> Self { + let json = serde_json::to_string(&v.data).unwrap_or_default(); + Self { data: json.into() } + } +} + +impl ToFFI for CStockEventsResponseOwned { + type FFIType = CStockEventsResponse; + fn to_ffi_type(&self) -> Self::FFIType { + CStockEventsResponse { + data: self.data.to_ffi_type(), + } + } +} + +// ── RankCategoriesResponse ──────────────────────────────────────── + +/// Rank categories response. `data` is a NUL-terminated JSON string. +#[repr(C)] +pub struct CRankCategoriesResponse { + /// Raw rank categories data as a JSON string + pub data: *const c_char, +} + +pub(crate) struct CRankCategoriesResponseOwned { + data: CString, +} + +impl From for CRankCategoriesResponseOwned { + fn from(v: RankCategoriesResponse) -> Self { + let json = serde_json::to_string(&v.data).unwrap_or_default(); + Self { data: json.into() } + } +} + +impl ToFFI for CRankCategoriesResponseOwned { + type FFIType = CRankCategoriesResponse; + fn to_ffi_type(&self) -> Self::FFIType { + CRankCategoriesResponse { + data: self.data.to_ffi_type(), + } + } +} + +// ── RankListResponse ────────────────────────────────────────────── + +/// Rank list response. `data` is a NUL-terminated JSON string. +#[repr(C)] +pub struct CRankListResponse { + /// Raw rank list data as a JSON string + pub data: *const c_char, +} + +pub(crate) struct CRankListResponseOwned { + data: CString, +} + +impl From for CRankListResponseOwned { + fn from(v: RankListResponse) -> Self { + let json = serde_json::to_string(&v.data).unwrap_or_default(); + Self { data: json.into() } + } +} + +impl ToFFI for CRankListResponseOwned { + type FFIType = CRankListResponse; + fn to_ffi_type(&self) -> Self::FFIType { + CRankListResponse { + data: self.data.to_ffi_type(), + } + } +} diff --git a/c/src/quote_context/context.rs b/c/src/quote_context/context.rs index bdd7f86b1..ce9feef9b 100644 --- a/c/src/quote_context/context.rs +++ b/c/src/quote_context/context.rs @@ -1207,12 +1207,13 @@ pub unsafe extern "C" fn lb_quote_context_history_market_temperature( }); } -/// Get short interest data for a US security. Returns -/// `CShortPositionsResponse`. +/// Get short interest data for a US or HK security. Returns +/// `CShortPositionsResponse`. Market is inferred from symbol suffix. #[unsafe(no_mangle)] pub unsafe extern "C" fn lb_quote_context_short_positions( ctx: *const CQuoteContext, symbol: *const c_char, + count: u32, callback: CAsyncCallback, userdata: *mut c_void, ) { @@ -1221,12 +1222,33 @@ pub unsafe extern "C" fn lb_quote_context_short_positions( let symbol = cstr_to_rust(symbol); execute_async(callback, ctx, userdata, async move { let resp: CCow = CCow::new( - CShortPositionsResponseOwned::from(ctx_inner.short_positions(symbol).await?), + CShortPositionsResponseOwned::from(ctx_inner.short_positions(symbol, count).await?), ); Ok(resp) }); } +/// Get short trade records for a HK or US security. Returns +/// `CShortTradesResponse`. Market is inferred from symbol suffix. +#[unsafe(no_mangle)] +pub unsafe extern "C" fn lb_quote_context_short_trades( + ctx: *const CQuoteContext, + symbol: *const c_char, + count: u32, + callback: CAsyncCallback, + userdata: *mut c_void, +) { + use crate::{quote_context::types::CShortTradesResponseOwned, types::CCow}; + let ctx_inner = (*ctx).ctx.clone(); + let symbol = cstr_to_rust(symbol); + execute_async(callback, ctx, userdata, async move { + let resp: CCow = CCow::new(CShortTradesResponseOwned::from( + ctx_inner.short_trades(symbol, count).await?, + )); + Ok(resp) + }); +} + /// Get real-time option call/put volume. Returns `COptionVolumeStats`. #[unsafe(no_mangle)] pub unsafe extern "C" fn lb_quote_context_option_volume( diff --git a/c/src/quote_context/types.rs b/c/src/quote_context/types.rs index ecb478e17..ad3bbfb8e 100644 --- a/c/src/quote_context/types.rs +++ b/c/src/quote_context/types.rs @@ -7,9 +7,9 @@ use longbridge::quote::{ OptionVolumeDaily, OptionVolumeDailyStat, OptionVolumeStats, ParticipantInfo, Period, PrePostQuote, PushBrokers, PushCandlestick, PushDepth, PushQuote, PushTrades, QuotePackageDetail, RealtimeQuote, Security, SecurityBoard, SecurityBrokers, SecurityCalcIndex, - SecurityDepth, SecurityQuote, SecurityStaticInfo, ShortPosition, ShortPositionsResponse, - StrikePriceInfo, Subscription, Trade, TradeDirection, TradeSession, TradeStatus, - TradingSessionInfo, WarrantInfo, WarrantQuote, WarrantType, WatchlistGroup, WatchlistSecurity, + SecurityDepth, SecurityQuote, SecurityStaticInfo, ShortPositionsResponse, StrikePriceInfo, + Subscription, Trade, TradeDirection, TradeSession, TradeStatus, TradingSessionInfo, + WarrantInfo, WarrantQuote, WarrantType, WatchlistGroup, WatchlistSecurity, }; use crate::{ @@ -3106,96 +3106,64 @@ impl ToFFI for CFilingItemOwned { // ── ShortPositionsResponse ──────────────────────────────────────── -/// Short position data for a single date +/// Short positions / interest response (HK or US). +/// +/// `data` is a NUL-terminated JSON string. #[repr(C)] -pub struct CShortPosition { - /// Date of the short position record (formatted string) - pub timestamp: *const c_char, - /// Short interest as a percentage of shares outstanding - pub rate: *const c_char, - /// Average daily share volume - pub avg_daily_share_volume: *const c_char, - /// Current number of shares sold short - pub current_shares_short: *const c_char, - /// Days to cover (short interest ratio) - pub days_to_cover: *const c_char, - /// Closing price on the record date - pub close: *const c_char, -} - -pub(crate) struct CShortPositionOwned { - timestamp: CString, - rate: CString, - avg_daily_share_volume: CString, - current_shares_short: CString, - days_to_cover: CString, - close: CString, +pub struct CShortPositionsResponse { + /// Raw short positions data as a JSON string + pub data: *const c_char, } -impl From for CShortPositionOwned { - fn from(v: ShortPosition) -> Self { - Self { - timestamp: v.timestamp.into(), - rate: v.rate.into(), - avg_daily_share_volume: v.avg_daily_share_volume.into(), - current_shares_short: v.current_shares_short.into(), - days_to_cover: v.days_to_cover.into(), - close: v.close.into(), - } +pub(crate) struct CShortPositionsResponseOwned { + data: CString, +} + +impl From for CShortPositionsResponseOwned { + fn from(v: ShortPositionsResponse) -> Self { + let json = serde_json::to_string(&v.data).unwrap_or_default(); + Self { data: json.into() } } } -impl ToFFI for CShortPositionOwned { - type FFIType = CShortPosition; +impl ToFFI for CShortPositionsResponseOwned { + type FFIType = CShortPositionsResponse; fn to_ffi_type(&self) -> Self::FFIType { - CShortPosition { - timestamp: self.timestamp.to_ffi_type(), - rate: self.rate.to_ffi_type(), - avg_daily_share_volume: self.avg_daily_share_volume.to_ffi_type(), - current_shares_short: self.current_shares_short.to_ffi_type(), - days_to_cover: self.days_to_cover.to_ffi_type(), - close: self.close.to_ffi_type(), + CShortPositionsResponse { + data: self.data.to_ffi_type(), } } } -/// Short positions response for a security +// ── ShortTradesResponse ─────────────────────────────────────────── + +use longbridge::quote::ShortTradesResponse; + +/// Short trade records response (HK or US). +/// +/// `data` is a NUL-terminated JSON string. #[repr(C)] -pub struct CShortPositionsResponse { - /// Security code - pub symbol: *const c_char, - /// Pointer to array of short position records - pub data: *const CShortPosition, - /// Number of elements in the array. - pub num_data: usize, - /// Bitmask indicating the data sources included in the response - pub sources: i32, +pub struct CShortTradesResponse { + /// Raw short trade data as a JSON string + pub data: *const c_char, } -pub(crate) struct CShortPositionsResponseOwned { - symbol: CString, - data: CVec, - sources: i32, +pub(crate) struct CShortTradesResponseOwned { + data: CString, } -impl From for CShortPositionsResponseOwned { - fn from(v: ShortPositionsResponse) -> Self { - Self { - symbol: v.symbol.into(), - data: v.data.into(), - sources: v.sources, - } +impl From for CShortTradesResponseOwned { + fn from(v: ShortTradesResponse) -> Self { + let json = serde_json::to_string(&v.data).unwrap_or_default(); + Self { data: json.into() } } } -impl ToFFI for CShortPositionsResponseOwned { - type FFIType = CShortPositionsResponse; +impl ToFFI for CShortTradesResponseOwned { + type FFIType = CShortTradesResponse; fn to_ffi_type(&self) -> Self::FFIType { - CShortPositionsResponse { - symbol: self.symbol.to_ffi_type(), + CShortTradesResponse { data: self.data.to_ffi_type(), - num_data: self.data.len(), - sources: self.sources, } } } diff --git a/c/src/screener_context/context.rs b/c/src/screener_context/context.rs new file mode 100644 index 000000000..4a027ebee --- /dev/null +++ b/c/src/screener_context/context.rs @@ -0,0 +1,135 @@ +use std::{ffi::c_void, os::raw::c_char, sync::Arc}; + +use longbridge::ScreenerContext; + +use crate::{ + async_call::{CAsyncCallback, execute_async}, + config::CConfig, + screener_context::types::*, + types::{CCow, cstr_to_rust}, +}; + +pub(crate) struct CScreenerContext { + ctx: ScreenerContext, +} + +#[unsafe(no_mangle)] +pub unsafe extern "C" fn lb_screener_context_new( + config: *const CConfig, +) -> *const CScreenerContext { + let config = Arc::new((*config).0.clone()); + Arc::into_raw(Arc::new(CScreenerContext { + ctx: ScreenerContext::new(config), + })) +} + +#[unsafe(no_mangle)] +pub unsafe extern "C" fn lb_screener_context_retain(ctx: *const CScreenerContext) { + Arc::increment_strong_count(ctx); +} + +#[unsafe(no_mangle)] +pub unsafe extern "C" fn lb_screener_context_release(ctx: *const CScreenerContext) { + Arc::decrement_strong_count(ctx); +} + +/// Get recommended built-in screener strategies. +/// Returns `CScreenerRecommendStrategiesResponse`. +#[unsafe(no_mangle)] +pub unsafe extern "C" fn lb_screener_context_recommend_strategies( + ctx: *const CScreenerContext, + callback: CAsyncCallback, + userdata: *mut c_void, +) { + let ctx_inner = (*ctx).ctx.clone(); + execute_async(callback, ctx, userdata, async move { + let resp: CCow = + CCow::new(CScreenerRecommendStrategiesResponseOwned::from( + ctx_inner.screener_recommend_strategies().await?, + )); + Ok(resp) + }); +} + +/// Get the current user's saved screener strategies. +/// Returns `CScreenerUserStrategiesResponse`. +#[unsafe(no_mangle)] +pub unsafe extern "C" fn lb_screener_context_user_strategies( + ctx: *const CScreenerContext, + callback: CAsyncCallback, + userdata: *mut c_void, +) { + let ctx_inner = (*ctx).ctx.clone(); + execute_async(callback, ctx, userdata, async move { + let resp: CCow = CCow::new( + CScreenerUserStrategiesResponseOwned::from(ctx_inner.screener_user_strategies().await?), + ); + Ok(resp) + }); +} + +/// Get detail for one screener strategy by ID. +/// Returns `CScreenerStrategyResponse`. +#[unsafe(no_mangle)] +pub unsafe extern "C" fn lb_screener_context_strategy( + ctx: *const CScreenerContext, + id: i64, + callback: CAsyncCallback, + userdata: *mut c_void, +) { + let ctx_inner = (*ctx).ctx.clone(); + execute_async(callback, ctx, userdata, async move { + let resp: CCow = CCow::new( + CScreenerStrategyResponseOwned::from(ctx_inner.screener_strategy(id).await?), + ); + Ok(resp) + }); +} + +/// Search / screen securities using a strategy. +/// Returns `CScreenerSearchResponse`. +#[unsafe(no_mangle)] +pub unsafe extern "C" fn lb_screener_context_search( + ctx: *const CScreenerContext, + market: *const c_char, + strategy_id: i64, + has_strategy_id: bool, + page: u32, + size: u32, + callback: CAsyncCallback, + userdata: *mut c_void, +) { + let ctx_inner = (*ctx).ctx.clone(); + let market = cstr_to_rust(market); + let strategy_id = if has_strategy_id { + Some(strategy_id) + } else { + None + }; + execute_async(callback, ctx, userdata, async move { + let resp: CCow = + CCow::new(CScreenerSearchResponseOwned::from( + ctx_inner + .screener_search(market, strategy_id, page, size) + .await?, + )); + Ok(resp) + }); +} + +/// Get all available screener indicator definitions. +/// Returns `CScreenerIndicatorsResponse`. +#[unsafe(no_mangle)] +pub unsafe extern "C" fn lb_screener_context_indicators( + ctx: *const CScreenerContext, + callback: CAsyncCallback, + userdata: *mut c_void, +) { + let ctx_inner = (*ctx).ctx.clone(); + execute_async(callback, ctx, userdata, async move { + let resp: CCow = CCow::new( + CScreenerIndicatorsResponseOwned::from(ctx_inner.screener_indicators().await?), + ); + Ok(resp) + }); +} diff --git a/c/src/screener_context/mod.rs b/c/src/screener_context/mod.rs new file mode 100644 index 000000000..0561d4d5a --- /dev/null +++ b/c/src/screener_context/mod.rs @@ -0,0 +1,2 @@ +pub mod context; +pub mod types; diff --git a/c/src/screener_context/types.rs b/c/src/screener_context/types.rs new file mode 100644 index 000000000..72e33c9b7 --- /dev/null +++ b/c/src/screener_context/types.rs @@ -0,0 +1,153 @@ +use std::os::raw::c_char; + +use longbridge::screener::types::{ + ScreenerIndicatorsResponse, ScreenerRecommendStrategiesResponse, ScreenerSearchResponse, + ScreenerStrategyResponse, ScreenerUserStrategiesResponse, +}; + +use crate::types::{CString, ToFFI}; + +// ── ScreenerRecommendStrategiesResponse ─────────────────────────── + +/// Recommended screener strategies response. `data` is a JSON string. +#[repr(C)] +pub struct CScreenerRecommendStrategiesResponse { + pub data: *const c_char, +} + +pub(crate) struct CScreenerRecommendStrategiesResponseOwned { + data: CString, +} + +impl From for CScreenerRecommendStrategiesResponseOwned { + fn from(v: ScreenerRecommendStrategiesResponse) -> Self { + Self { + data: serde_json::to_string(&v.data).unwrap_or_default().into(), + } + } +} + +impl ToFFI for CScreenerRecommendStrategiesResponseOwned { + type FFIType = CScreenerRecommendStrategiesResponse; + fn to_ffi_type(&self) -> Self::FFIType { + CScreenerRecommendStrategiesResponse { + data: self.data.to_ffi_type(), + } + } +} + +// ── ScreenerUserStrategiesResponse ──────────────────────────────── + +/// User screener strategies response. `data` is a JSON string. +#[repr(C)] +pub struct CScreenerUserStrategiesResponse { + pub data: *const c_char, +} + +pub(crate) struct CScreenerUserStrategiesResponseOwned { + data: CString, +} + +impl From for CScreenerUserStrategiesResponseOwned { + fn from(v: ScreenerUserStrategiesResponse) -> Self { + Self { + data: serde_json::to_string(&v.data).unwrap_or_default().into(), + } + } +} + +impl ToFFI for CScreenerUserStrategiesResponseOwned { + type FFIType = CScreenerUserStrategiesResponse; + fn to_ffi_type(&self) -> Self::FFIType { + CScreenerUserStrategiesResponse { + data: self.data.to_ffi_type(), + } + } +} + +// ── ScreenerStrategyResponse ────────────────────────────────────── + +/// Single screener strategy response. `data` is a JSON string. +#[repr(C)] +pub struct CScreenerStrategyResponse { + pub data: *const c_char, +} + +pub(crate) struct CScreenerStrategyResponseOwned { + data: CString, +} + +impl From for CScreenerStrategyResponseOwned { + fn from(v: ScreenerStrategyResponse) -> Self { + Self { + data: serde_json::to_string(&v.data).unwrap_or_default().into(), + } + } +} + +impl ToFFI for CScreenerStrategyResponseOwned { + type FFIType = CScreenerStrategyResponse; + fn to_ffi_type(&self) -> Self::FFIType { + CScreenerStrategyResponse { + data: self.data.to_ffi_type(), + } + } +} + +// ── ScreenerSearchResponse ──────────────────────────────────────── + +/// Screener search results response. `data` is a JSON string. +#[repr(C)] +pub struct CScreenerSearchResponse { + pub data: *const c_char, +} + +pub(crate) struct CScreenerSearchResponseOwned { + data: CString, +} + +impl From for CScreenerSearchResponseOwned { + fn from(v: ScreenerSearchResponse) -> Self { + Self { + data: serde_json::to_string(&v.data).unwrap_or_default().into(), + } + } +} + +impl ToFFI for CScreenerSearchResponseOwned { + type FFIType = CScreenerSearchResponse; + fn to_ffi_type(&self) -> Self::FFIType { + CScreenerSearchResponse { + data: self.data.to_ffi_type(), + } + } +} + +// ── ScreenerIndicatorsResponse ──────────────────────────────────── + +/// Screener indicator definitions response. `data` is a JSON string. +#[repr(C)] +pub struct CScreenerIndicatorsResponse { + pub data: *const c_char, +} + +pub(crate) struct CScreenerIndicatorsResponseOwned { + data: CString, +} + +impl From for CScreenerIndicatorsResponseOwned { + fn from(v: ScreenerIndicatorsResponse) -> Self { + Self { + data: serde_json::to_string(&v.data).unwrap_or_default().into(), + } + } +} + +impl ToFFI for CScreenerIndicatorsResponseOwned { + type FFIType = CScreenerIndicatorsResponse; + fn to_ffi_type(&self) -> Self::FFIType { + CScreenerIndicatorsResponse { + data: self.data.to_ffi_type(), + } + } +} diff --git a/cpp/CMakeLists.txt b/cpp/CMakeLists.txt index 6832770a2..66ccf831e 100644 --- a/cpp/CMakeLists.txt +++ b/cpp/CMakeLists.txt @@ -10,6 +10,7 @@ set(SOURCES src/calendar_context.cpp src/fundamental_context.cpp src/market_context.cpp + src/screener_context.cpp src/portfolio_context.cpp src/status.cpp src/types.cpp diff --git a/cpp/include/fundamental_context.hpp b/cpp/include/fundamental_context.hpp index 6636282e9..735c9a8a3 100644 --- a/cpp/include/fundamental_context.hpp +++ b/cpp/include/fundamental_context.hpp @@ -5,6 +5,8 @@ #include "config.hpp" #include "types.hpp" #include +#include +#include typedef struct lb_fundamental_context_t lb_fundamental_context_t; @@ -124,43 +126,21 @@ class FundamentalContext void ratings(const std::string& symbol, AsyncCallback callback) const; - /// Get business segment breakdowns (latest snapshot) - void business_segments(const std::string& symbol, - AsyncCallback callback) const; - - /// Get historical business segment breakdowns. - /// Pass nullptr for report/cate to omit them. - void business_segments_history(const std::string& symbol, - const char* report, - const char* cate, - AsyncCallback callback) const; - - /// Get historical institutional rating view time-series - void institution_rating_views(const std::string& symbol, - AsyncCallback callback) const; - - /// Get industry rank for a market. - /// indicator: "0"–"7"; sort_type: "0"=asc, "1"=desc - void industry_rank(const std::string& market, - const std::string& indicator, - const std::string& sort_type, - uint32_t limit, - AsyncCallback callback) const; - - /// Get the industry peer chain. - /// Pass nullptr for industry_id to omit it. - void industry_peers(const std::string& counter_id, - const std::string& market, - const char* industry_id, - AsyncCallback callback) const; - - /// Get a financial report snapshot. - /// Pass nullptr for report/fiscal_period; pass 0 for fiscal_year to omit it. - void financial_report_snapshot(const std::string& symbol, - const char* report, - int32_t fiscal_year, - const char* fiscal_period, - AsyncCallback callback) const; + /// Get ranked list of top shareholders (raw JSON string) + void shareholder_top(const std::string& symbol, + AsyncCallback callback) const; + + /// Get holding history and detail for one shareholder (raw JSON string) + void shareholder_detail(const std::string& symbol, + int64_t object_id, + AsyncCallback callback) const; + + /// Get valuation comparison (raw JSON string). + /// Pass nullptr for comparison_symbols to skip peer comparison. + void valuation_comparison(const std::string& symbol, + const std::string& currency, + const std::vector* comparison_symbols, + AsyncCallback callback) const; }; } // namespace fundamental diff --git a/cpp/include/market_context.hpp b/cpp/include/market_context.hpp index ad06e12f7..c3b2bc095 100644 --- a/cpp/include/market_context.hpp +++ b/cpp/include/market_context.hpp @@ -4,6 +4,8 @@ #include "callback.hpp" #include "config.hpp" #include "types.hpp" +#include +#include typedef struct lb_market_context_t lb_market_context_t; @@ -80,6 +82,21 @@ class MarketContext /// Get index constituents void constituent(const std::string& symbol, AsyncCallback callback) const; + + /// Get stock events across one or more markets (raw JSON string) + void stock_events(const std::vector& markets, + uint32_t sort, + const std::string* date, + uint32_t limit, + AsyncCallback callback) const; + + /// Get all available rank category keys and labels (raw JSON string) + void rank_categories(AsyncCallback callback) const; + + /// Get a ranked list of securities for the given category key (raw JSON string) + void rank_list(const std::string& key, + bool need_article, + AsyncCallback callback) const; }; } // namespace market diff --git a/cpp/include/quote_context.hpp b/cpp/include/quote_context.hpp index 1b2f5f1d7..01644f332 100644 --- a/cpp/include/quote_context.hpp +++ b/cpp/include/quote_context.hpp @@ -314,10 +314,16 @@ class QuoteContext uintptr_t count, AsyncCallback> callback) const; - /// Get short interest data for a US security + /// Get short interest data for a US or HK security (market inferred from symbol suffix) void short_positions(const std::string& symbol, + uint32_t count, AsyncCallback callback) const; + /// Get short trade records for a HK or US security (market inferred from symbol suffix) + void short_trades(const std::string& symbol, + uint32_t count, + AsyncCallback callback) const; + /// Get real-time option call/put volume void option_volume(const std::string& symbol, AsyncCallback callback) const; diff --git a/cpp/include/screener_context.hpp b/cpp/include/screener_context.hpp new file mode 100644 index 000000000..843bb0fc5 --- /dev/null +++ b/cpp/include/screener_context.hpp @@ -0,0 +1,51 @@ +#pragma once + +#include "async_result.hpp" +#include "callback.hpp" +#include "config.hpp" +#include +#include + +typedef struct lb_screener_context_t lb_screener_context_t; + +namespace longbridge { +namespace screener { + +/// Screener context — stock screener strategies, search, and indicators. +class ScreenerContext +{ +public: + ScreenerContext(); + explicit ScreenerContext(const lb_screener_context_t* ctx); + ScreenerContext(const ScreenerContext& ctx); + ScreenerContext(ScreenerContext&& ctx); + ~ScreenerContext(); + ScreenerContext& operator=(const ScreenerContext& ctx); + + static ScreenerContext create(const Config& config); + + /// Get recommended built-in screener strategies (raw JSON string) + void screener_recommend_strategies(AsyncCallback callback) const; + + /// Get the current user's saved screener strategies (raw JSON string) + void screener_user_strategies(AsyncCallback callback) const; + + /// Get detail for one screener strategy by ID (raw JSON string) + void screener_strategy(int64_t id, AsyncCallback callback) const; + + /// Search / screen securities using a strategy (raw JSON string) + void screener_search(const std::string& market, + std::optional strategy_id, + uint32_t page, + uint32_t size, + AsyncCallback callback) const; + + /// Get all available screener indicator definitions (raw JSON string) + void screener_indicators(AsyncCallback callback) const; + +private: + const lb_screener_context_t* ctx_; +}; + +} // namespace screener +} // namespace longbridge diff --git a/cpp/include/types.hpp b/cpp/include/types.hpp index 30459bd22..ac1babf2e 100644 --- a/cpp/include/types.hpp +++ b/cpp/include/types.hpp @@ -1263,21 +1263,16 @@ struct FilingItem int64_t published_at; }; -struct ShortPosition +/// Short interest / positions response (HK or US). `data` is a raw JSON string. +struct ShortPositionsResponse { - std::string timestamp; - std::string rate; - std::string avg_daily_share_volume; - std::string current_shares_short; - std::string days_to_cover; - std::string close; + std::string data; }; -struct ShortPositionsResponse +/// Short trade records response (HK or US). `data` is a raw JSON string. +struct ShortTradesResponse { - std::string symbol; - std::vector data; - int32_t sources; + std::string data; }; struct OptionVolumeStats diff --git a/cpp/src/convert.hpp b/cpp/src/convert.hpp index 19ea00f02..2fc781f6d 100644 --- a/cpp/src/convert.hpp +++ b/cpp/src/convert.hpp @@ -2291,13 +2291,11 @@ convert(const lb_owned_topic_t* item) // ── QuoteContext extension types ────────────────────────────────── -inline quote::ShortPosition convert(const lb_short_position_t* p) { - return { p->timestamp, p->rate, p->avg_daily_share_volume, p->current_shares_short, p->days_to_cover, p->close }; -} inline quote::ShortPositionsResponse convert(const lb_short_positions_response_t* r) { - std::vector data; - for (size_t i = 0; i < r->num_data; ++i) data.push_back(convert(&r->data[i])); - return { r->symbol, std::move(data), r->sources }; + return { r->data ? r->data : "" }; +} +inline quote::ShortTradesResponse convert(const lb_short_trades_response_t* r) { + return { r->data ? r->data : "" }; } inline quote::OptionVolumeStats convert(const lb_option_volume_stats_t* s) { return { s->c, s->p }; diff --git a/cpp/src/fundamental_context.cpp b/cpp/src/fundamental_context.cpp index 070cf686f..55bb7f501 100644 --- a/cpp/src/fundamental_context.cpp +++ b/cpp/src/fundamental_context.cpp @@ -120,5 +120,37 @@ void FundamentalContext::financial_report_snapshot(const std::string& s, const c #undef F_TYPED #undef F_JSON +// ── New JSON-string APIs ────────────────────────────────────────── +// These return a struct with a single `data` field (JSON string). +// We extract the string and return it as std::string. +#define F_JSON_STRUCT(cfn, CType, ...) cfn(__VA_ARGS__, [](auto res) { \ + auto cb = callback::get_async_callback(res->userdata); \ + FundamentalContext fctx((const lb_fundamental_context_t*)res->ctx); Status status(res->error); \ + if(status){const CType* d=(const CType*)res->data; std::string j(d->data ? d->data : ""); (*cb)(AsyncResult(fctx,std::move(status),&j));} \ + else{(*cb)(AsyncResult(fctx,std::move(status),nullptr));} \ +}, new AsyncCallback(callback)) + +void FundamentalContext::shareholder_top(const std::string& s, AsyncCallback callback) const { + F_JSON_STRUCT(lb_fundamental_context_shareholder_top, lb_shareholder_top_response_t, ctx_, s.c_str()); +} + +void FundamentalContext::shareholder_detail(const std::string& s, int64_t object_id, AsyncCallback callback) const { + F_JSON_STRUCT(lb_fundamental_context_shareholder_detail, lb_shareholder_detail_response_t, ctx_, s.c_str(), object_id); +} + +void FundamentalContext::valuation_comparison(const std::string& s, const std::string& currency, const std::vector* comparison_symbols, AsyncCallback callback) const { + std::vector syms_ptrs; + size_t num_syms = 0; + const char** syms_data = nullptr; + if (comparison_symbols) { + for (const auto& sym : *comparison_symbols) syms_ptrs.push_back(sym.c_str()); + syms_data = syms_ptrs.empty() ? nullptr : syms_ptrs.data(); + num_syms = syms_ptrs.size(); + } + F_JSON_STRUCT(lb_fundamental_context_valuation_comparison, lb_valuation_comparison_response_t, ctx_, s.c_str(), currency.c_str(), syms_data, num_syms); +} + +#undef F_JSON_STRUCT + } // namespace fundamental } // namespace longbridge diff --git a/cpp/src/market_context.cpp b/cpp/src/market_context.cpp index 983dc2777..519e42d10 100644 --- a/cpp/src/market_context.cpp +++ b/cpp/src/market_context.cpp @@ -15,6 +15,9 @@ void lb_market_context_ah_premium_intraday(const lb_market_context_t*, const cha void lb_market_context_trade_stats(const lb_market_context_t*, const char*, lb_async_callback_t, void*); void lb_market_context_anomaly(const lb_market_context_t*, const char*, lb_async_callback_t, void*); void lb_market_context_constituent(const lb_market_context_t*, const char*, lb_async_callback_t, void*); +void lb_market_context_stock_events(const lb_market_context_t*, const char**, size_t, uint32_t, const char*, uint32_t, lb_async_callback_t, void*); +void lb_market_context_rank_categories(const lb_market_context_t*, lb_async_callback_t, void*); +void lb_market_context_rank_list(const lb_market_context_t*, const char*, bool, lb_async_callback_t, void*); } namespace longbridge { @@ -79,5 +82,30 @@ void MarketContext::constituent(const std::string& symbol, AsyncCallback(res->userdata); \ + MarketContext mctx((const lb_market_context_t*)res->ctx); Status status(res->error); \ + if(status){const CType* d=(const CType*)res->data; std::string j(d->data ? d->data : ""); (*cb)(AsyncResult(mctx,std::move(status),&j));} \ + else{(*cb)(AsyncResult(mctx,std::move(status),nullptr));} \ +}, new AsyncCallback(callback)) + +void MarketContext::stock_events(const std::vector& markets, uint32_t sort, const std::string* date, uint32_t limit, AsyncCallback callback) const { + std::vector mptrs; + for (const auto& m : markets) mptrs.push_back(m.c_str()); + const char* date_str = date ? date->c_str() : nullptr; + M_JSON(lb_market_context_stock_events, lb_stock_events_response_t, ctx_, mptrs.data(), mptrs.size(), sort, date_str, limit); +} + +void MarketContext::rank_categories(AsyncCallback callback) const { + M_JSON(lb_market_context_rank_categories, lb_rank_categories_response_t, ctx_); +} + +void MarketContext::rank_list(const std::string& key, bool need_article, AsyncCallback callback) const { + M_JSON(lb_market_context_rank_list, lb_rank_list_response_t, ctx_, key.c_str(), need_article); +} + +#undef M_JSON + } // namespace market } // namespace longbridge diff --git a/cpp/src/quote_context.cpp b/cpp/src/quote_context.cpp index 0e3fbe418..ab5e4f2b5 100644 --- a/cpp/src/quote_context.cpp +++ b/cpp/src/quote_context.cpp @@ -1657,11 +1657,13 @@ QuoteContext::realtime_candlesticks( void QuoteContext::short_positions(const std::string& symbol, + uint32_t count, AsyncCallback callback) const { lb_quote_context_short_positions( ctx_, symbol.c_str(), + count, [](auto res) { auto callback_ptr = callback::get_async_callback(res->userdata); @@ -1679,6 +1681,32 @@ QuoteContext::short_positions(const std::string& symbol, new AsyncCallback(callback)); } +void +QuoteContext::short_trades(const std::string& symbol, + uint32_t count, + AsyncCallback callback) const +{ + lb_quote_context_short_trades( + ctx_, + symbol.c_str(), + count, + [](auto res) { + auto callback_ptr = + callback::get_async_callback(res->userdata); + QuoteContext ctx((const lb_quote_context_t*)res->ctx); + Status status(res->error); + if (status) { + auto value = convert::convert((const lb_short_trades_response_t*)res->data); + (*callback_ptr)( + AsyncResult(ctx, std::move(status), &value)); + } else { + (*callback_ptr)( + AsyncResult(ctx, std::move(status), nullptr)); + } + }, + new AsyncCallback(callback)); +} + void QuoteContext::option_volume(const std::string& symbol, AsyncCallback callback) const diff --git a/cpp/src/screener_context.cpp b/cpp/src/screener_context.cpp new file mode 100644 index 000000000..46054aacb --- /dev/null +++ b/cpp/src/screener_context.cpp @@ -0,0 +1,67 @@ +#include "screener_context.hpp" +#include "longbridge.h" +#include "callback.hpp" +#include "status.hpp" +#include + +extern "C" { +const lb_screener_context_t* lb_screener_context_new(const lb_config_t* config); +void lb_screener_context_retain(const lb_screener_context_t* ctx); +void lb_screener_context_release(const lb_screener_context_t* ctx); +void lb_screener_context_recommend_strategies(const lb_screener_context_t*, lb_async_callback_t, void*); +void lb_screener_context_user_strategies(const lb_screener_context_t*, lb_async_callback_t, void*); +void lb_screener_context_strategy(const lb_screener_context_t*, int64_t, lb_async_callback_t, void*); +void lb_screener_context_search(const lb_screener_context_t*, const char*, int64_t, bool, uint32_t, uint32_t, lb_async_callback_t, void*); +void lb_screener_context_indicators(const lb_screener_context_t*, lb_async_callback_t, void*); +} + +namespace longbridge { +namespace screener { + +ScreenerContext::ScreenerContext() : ctx_(nullptr) {} +ScreenerContext::ScreenerContext(const lb_screener_context_t* ctx) { ctx_ = ctx; if (ctx_) lb_screener_context_retain(ctx_); } +ScreenerContext::ScreenerContext(const ScreenerContext& ctx) { ctx_ = ctx.ctx_; if (ctx_) lb_screener_context_retain(ctx_); } +ScreenerContext::ScreenerContext(ScreenerContext&& ctx) { ctx_ = ctx.ctx_; ctx.ctx_ = nullptr; } +ScreenerContext::~ScreenerContext() { if (ctx_) lb_screener_context_release(ctx_); } +ScreenerContext& ScreenerContext::operator=(const ScreenerContext& ctx) { ctx_ = ctx.ctx_; if (ctx_) lb_screener_context_retain(ctx_); return *this; } +ScreenerContext ScreenerContext::create(const Config& config) { + auto* ptr = lb_screener_context_new(config); + ScreenerContext sctx(ptr); + if (ptr) lb_screener_context_release(ptr); + return sctx; +} + +// Helper macro: reads .data field of the C response struct as JSON string +#define S_JSON(cfn, CType, ...) cfn(__VA_ARGS__, [](auto res) { \ + auto cb = callback::get_async_callback(res->userdata); \ + ScreenerContext sctx((const lb_screener_context_t*)res->ctx); Status status(res->error); \ + if(status){const CType* d=(const CType*)res->data; std::string j(d->data ? d->data : ""); (*cb)(AsyncResult(sctx,std::move(status),&j));} \ + else{(*cb)(AsyncResult(sctx,std::move(status),nullptr));} \ +}, new AsyncCallback(callback)) + +void ScreenerContext::screener_recommend_strategies(AsyncCallback callback) const { + S_JSON(lb_screener_context_recommend_strategies, lb_screener_recommend_strategies_response_t, ctx_); +} + +void ScreenerContext::screener_user_strategies(AsyncCallback callback) const { + S_JSON(lb_screener_context_user_strategies, lb_screener_user_strategies_response_t, ctx_); +} + +void ScreenerContext::screener_strategy(int64_t id, AsyncCallback callback) const { + S_JSON(lb_screener_context_strategy, lb_screener_strategy_response_t, ctx_, id); +} + +void ScreenerContext::screener_search(const std::string& market, std::optional strategy_id, uint32_t page, uint32_t size, AsyncCallback callback) const { + int64_t sid = strategy_id.value_or(0); + bool has_sid = strategy_id.has_value(); + S_JSON(lb_screener_context_search, lb_screener_search_response_t, ctx_, market.c_str(), sid, has_sid, page, size); +} + +void ScreenerContext::screener_indicators(AsyncCallback callback) const { + S_JSON(lb_screener_context_indicators, lb_screener_indicators_response_t, ctx_); +} + +#undef S_JSON + +} // namespace screener +} // namespace longbridge diff --git a/java/src/fundamental_context.rs b/java/src/fundamental_context.rs index 52caff15b..95c39a995 100644 --- a/java/src/fundamental_context.rs +++ b/java/src/fundamental_context.rs @@ -9,7 +9,7 @@ use longbridge::{Config, FundamentalContext, fundamental::types::*}; use crate::{ async_util, error::jni_result, - types::{FromJValue, get_field}, + types::{FromJValue, ObjectArray, get_field}, }; struct ContextObj { @@ -194,7 +194,7 @@ pub unsafe extern "system" fn Java_com_longbridge_SdkNative_fundamentalContextGe } #[unsafe(no_mangle)] -pub unsafe extern "system" fn Java_com_longbridge_SdkNative_fundamentalContextGetBusinessSegments( +pub unsafe extern "system" fn Java_com_longbridge_SdkNative_fundamentalContextShareholderTop( mut env: JNIEnv, _class: JClass, context: i64, @@ -205,7 +205,7 @@ pub unsafe extern "system" fn Java_com_longbridge_SdkNative_fundamentalContextGe let context = &*(context as *const ContextObj); let symbol: String = FromJValue::from_jvalue(env, symbol.into())?; async_util::execute(env, callback, async move { - let resp = context.ctx.business_segments(symbol).await?; + let resp = context.ctx.shareholder_top(symbol).await?; Ok(resp) })?; Ok(()) @@ -213,98 +213,19 @@ pub unsafe extern "system" fn Java_com_longbridge_SdkNative_fundamentalContextGe } #[unsafe(no_mangle)] -pub unsafe extern "system" fn Java_com_longbridge_SdkNative_fundamentalContextGetBusinessSegmentsHistory( - mut env: JNIEnv, - _class: JClass, - context: i64, - opts: JObject, - callback: JObject, -) { - jni_result(&mut env, (), |env| { - let context = &*(context as *const ContextObj); - let symbol: String = get_field(env, &opts, "symbol")?; - let report: Option = get_field(env, &opts, "report")?; - let cate: Option = get_field(env, &opts, "cate")?; - let report_static: Option<&'static str> = match report.as_deref() { - Some("qf") => Some("qf"), - Some("saf") => Some("saf"), - Some("af") => Some("af"), - _ => None, - }; - async_util::execute(env, callback, async move { - let resp = context - .ctx - .business_segments_history(symbol, report_static, cate) - .await?; - Ok(resp) - })?; - Ok(()) - }) -} - -#[unsafe(no_mangle)] -pub unsafe extern "system" fn Java_com_longbridge_SdkNative_fundamentalContextGetInstitutionRatingViews( +pub unsafe extern "system" fn Java_com_longbridge_SdkNative_fundamentalContextShareholderDetail( mut env: JNIEnv, _class: JClass, context: i64, symbol: JObject, + object_id: i64, callback: JObject, ) { jni_result(&mut env, (), |env| { let context = &*(context as *const ContextObj); let symbol: String = FromJValue::from_jvalue(env, symbol.into())?; async_util::execute(env, callback, async move { - let resp = context.ctx.institution_rating_views(symbol).await?; - Ok(resp) - })?; - Ok(()) - }) -} - -#[unsafe(no_mangle)] -pub unsafe extern "system" fn Java_com_longbridge_SdkNative_fundamentalContextGetIndustryRank( - mut env: JNIEnv, - _class: JClass, - context: i64, - opts: JObject, - callback: JObject, -) { - jni_result(&mut env, (), |env| { - let context = &*(context as *const ContextObj); - let market: String = get_field(env, &opts, "market")?; - let indicator: String = get_field(env, &opts, "indicator")?; - let sort_type: String = get_field(env, &opts, "sortType")?; - let limit: i32 = get_field(env, &opts, "limit")?; - let limit = limit.max(0) as u32; - async_util::execute(env, callback, async move { - let resp = context - .ctx - .industry_rank(market, indicator, sort_type, limit) - .await?; - Ok(resp) - })?; - Ok(()) - }) -} - -#[unsafe(no_mangle)] -pub unsafe extern "system" fn Java_com_longbridge_SdkNative_fundamentalContextGetIndustryPeers( - mut env: JNIEnv, - _class: JClass, - context: i64, - opts: JObject, - callback: JObject, -) { - jni_result(&mut env, (), |env| { - let context = &*(context as *const ContextObj); - let counter_id: String = get_field(env, &opts, "counterId")?; - let market: String = get_field(env, &opts, "market")?; - let industry_id: Option = get_field(env, &opts, "industryId")?; - async_util::execute(env, callback, async move { - let resp = context - .ctx - .industry_peers(counter_id, market, industry_id) - .await?; + let resp = context.ctx.shareholder_detail(symbol, object_id).await?; Ok(resp) })?; Ok(()) @@ -312,39 +233,29 @@ pub unsafe extern "system" fn Java_com_longbridge_SdkNative_fundamentalContextGe } #[unsafe(no_mangle)] -pub unsafe extern "system" fn Java_com_longbridge_SdkNative_fundamentalContextGetFinancialReportSnapshot( +pub unsafe extern "system" fn Java_com_longbridge_SdkNative_fundamentalContextValuationComparison( mut env: JNIEnv, _class: JClass, context: i64, - opts: JObject, + symbol: JObject, + currency: JObject, + comparison_symbols: JObject, callback: JObject, ) { jni_result(&mut env, (), |env| { let context = &*(context as *const ContextObj); - let symbol: String = get_field(env, &opts, "symbol")?; - let report: Option = get_field(env, &opts, "report")?; - let fiscal_year: Option = get_field(env, &opts, "fiscalYear")?; - let fiscal_period: Option = get_field(env, &opts, "fiscalPeriod")?; - let report_static: Option<&'static str> = match report.as_deref() { - Some("qf") => Some("qf"), - Some("saf") => Some("saf"), - Some("af") => Some("af"), - _ => None, - }; - let fiscal_period_static: Option<&'static str> = match fiscal_period.as_deref() { - Some("q1") => Some("q1"), - Some("q2") => Some("q2"), - Some("q3") => Some("q3"), - Some("q4") => Some("q4"), - Some("fy") => Some("fy"), - Some("h1") => Some("h1"), - Some("h2") => Some("h2"), - _ => None, + let symbol: String = FromJValue::from_jvalue(env, symbol.into())?; + let currency: String = FromJValue::from_jvalue(env, currency.into())?; + let comparison_syms: Option> = if comparison_symbols.is_null() { + None + } else { + let arr: ObjectArray = FromJValue::from_jvalue(env, comparison_symbols.into())?; + Some(arr.0) }; async_util::execute(env, callback, async move { let resp = context .ctx - .financial_report_snapshot(symbol, report_static, fiscal_year, fiscal_period_static) + .valuation_comparison(symbol, currency, comparison_syms) .await?; Ok(resp) })?; diff --git a/java/src/init.rs b/java/src/init.rs index 2fc4dbf2a..74642ac09 100644 --- a/java/src/init.rs +++ b/java/src/init.rs @@ -367,7 +367,7 @@ pub extern "system" fn Java_com_longbridge_SdkNative_init<'a>( longbridge::fundamental::OperatingIndicator, // QuoteContext extensions longbridge::quote::ShortPositionsResponse, - longbridge::quote::ShortPosition, + longbridge::quote::ShortTradesResponse, longbridge::quote::OptionVolumeStats, longbridge::quote::OptionVolumeDaily, longbridge::quote::OptionVolumeDailyStat, @@ -382,23 +382,20 @@ pub extern "system" fn Java_com_longbridge_SdkNative_init<'a>( longbridge::fundamental::RatingSubIndicatorGroup, longbridge::fundamental::RatingCategory, longbridge::fundamental::StockRatings, - // FundamentalContext: new APIs - longbridge::fundamental::BusinessSegmentItem, - longbridge::fundamental::BusinessSegments, - longbridge::fundamental::BusinessSegmentHistoryItem, - longbridge::fundamental::BusinessSegmentsHistoricalItem, - longbridge::fundamental::BusinessSegmentsHistory, - longbridge::fundamental::InstitutionRatingViewItem, - longbridge::fundamental::InstitutionRatingViews, - longbridge::fundamental::IndustryRankItem, - longbridge::fundamental::IndustryRankGroup, - longbridge::fundamental::IndustryRankResponse, - longbridge::fundamental::IndustryPeersTop, - longbridge::fundamental::IndustryPeerNode, - longbridge::fundamental::IndustryPeersResponse, - longbridge::fundamental::SnapshotForecastMetric, - longbridge::fundamental::SnapshotReportedMetric, - longbridge::fundamental::FinancialReportSnapshot, + // FundamentalContext: shareholders / valuation comparison + longbridge::fundamental::ShareholderTopResponse, + longbridge::fundamental::ShareholderDetailResponse, + longbridge::fundamental::ValuationComparisonResponse, + // MarketContext: stock events / rank + longbridge::market::StockEventsResponse, + longbridge::market::RankCategoriesResponse, + longbridge::market::RankListResponse, + // ScreenerContext + longbridge::screener::ScreenerRecommendStrategiesResponse, + longbridge::screener::ScreenerUserStrategiesResponse, + longbridge::screener::ScreenerStrategyResponse, + longbridge::screener::ScreenerSearchResponse, + longbridge::screener::ScreenerIndicatorsResponse, // PortfolioContext: ProfitAnalysisFlows longbridge::portfolio::FlowItem, longbridge::portfolio::ProfitAnalysisFlows, diff --git a/java/src/lib.rs b/java/src/lib.rs index c33b6d6b7..c4fcdf96f 100644 --- a/java/src/lib.rs +++ b/java/src/lib.rs @@ -16,6 +16,7 @@ mod market_context; mod oauth; mod portfolio_context; mod quote_context; +mod screener_context; mod sharelist_context; mod trade_context; mod types; diff --git a/java/src/market_context.rs b/java/src/market_context.rs index 2597196fe..c6b6a2055 100644 --- a/java/src/market_context.rs +++ b/java/src/market_context.rs @@ -9,7 +9,7 @@ use longbridge::{Config, MarketContext, market::types::*}; use crate::{ async_util, error::jni_result, - types::{FromJValue, JavaInteger, get_field}, + types::{FromJValue, JavaInteger, ObjectArray, get_field}, }; struct ContextObj { @@ -182,3 +182,65 @@ symbol_method!( constituent ); market_method!(Java_com_longbridge_SdkNative_marketContextAnomaly, anomaly); + +#[unsafe(no_mangle)] +pub unsafe extern "system" fn Java_com_longbridge_SdkNative_marketContextStockEvents( + mut env: JNIEnv, + _class: JClass, + context: i64, + opts: JObject, + callback: JObject, +) { + jni_result(&mut env, (), |env| { + let context = &*(context as *const ContextObj); + let markets_raw: ObjectArray = get_field(env, &opts, "markets")?; + let markets: Vec = markets_raw.0; + let sort_opt: Option = get_field(env, &opts, "sort")?; + let sort = sort_opt.map(i32::from).unwrap_or(0) as u32; + let date: Option = get_field(env, &opts, "date")?; + let limit_opt: Option = get_field(env, &opts, "limit")?; + let limit = limit_opt.map(i32::from).unwrap_or(20) as u32; + async_util::execute(env, callback, async move { + let resp = context.ctx.stock_events(markets, sort, date, limit).await?; + Ok(resp) + })?; + Ok(()) + }) +} + +#[unsafe(no_mangle)] +pub unsafe extern "system" fn Java_com_longbridge_SdkNative_marketContextRankCategories( + 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.rank_categories().await?; + Ok(resp) + })?; + Ok(()) + }) +} + +#[unsafe(no_mangle)] +pub unsafe extern "system" fn Java_com_longbridge_SdkNative_marketContextRankList( + mut env: JNIEnv, + _class: JClass, + context: i64, + key: JObject, + need_article: bool, + callback: JObject, +) { + jni_result(&mut env, (), |env| { + let context = &*(context as *const ContextObj); + let key: String = FromJValue::from_jvalue(env, key.into())?; + async_util::execute(env, callback, async move { + let resp = context.ctx.rank_list(key, need_article).await?; + Ok(resp) + })?; + Ok(()) + }) +} diff --git a/java/src/quote_context.rs b/java/src/quote_context.rs index eca61b6f6..167942ab7 100644 --- a/java/src/quote_context.rs +++ b/java/src/quote_context.rs @@ -1201,13 +1201,36 @@ pub unsafe extern "system" fn Java_com_longbridge_SdkNative_quoteContextShortPos _class: JClass, context: i64, symbol: JObject, + count: i32, + callback: JObject, +) { + jni_result(&mut env, (), |env| { + let context = &*(context as *const ContextObj); + let symbol: String = FromJValue::from_jvalue(env, symbol.into())?; + let count = count.max(1) as u32; + async_util::execute(env, callback, async move { + let resp = context.ctx.short_positions(symbol, count).await?; + Ok(resp) + })?; + Ok(()) + }) +} + +#[unsafe(no_mangle)] +pub unsafe extern "system" fn Java_com_longbridge_SdkNative_quoteContextShortTrades( + mut env: JNIEnv, + _class: JClass, + context: i64, + symbol: JObject, + count: i32, callback: JObject, ) { jni_result(&mut env, (), |env| { let context = &*(context as *const ContextObj); let symbol: String = FromJValue::from_jvalue(env, symbol.into())?; + let count = count.max(1) as u32; async_util::execute(env, callback, async move { - let resp = context.ctx.short_positions(symbol).await?; + let resp = context.ctx.short_trades(symbol, count).await?; Ok(resp) })?; Ok(()) diff --git a/java/src/screener_context.rs b/java/src/screener_context.rs new file mode 100644 index 000000000..c6b66330d --- /dev/null +++ b/java/src/screener_context.rs @@ -0,0 +1,133 @@ +use std::sync::Arc; + +use jni::{ + JNIEnv, + objects::{JClass, JObject}, +}; +use longbridge::{Config, ScreenerContext}; + +use crate::{ + async_util, + error::jni_result, + types::{JavaInteger, get_field}, +}; + +struct ContextObj { + ctx: ScreenerContext, +} + +#[unsafe(no_mangle)] +pub unsafe extern "system" fn Java_com_longbridge_SdkNative_newScreenerContext( + _env: JNIEnv, + _class: JClass, + config: i64, +) -> i64 { + let config = &*(config as *const Config); + let ctx = ScreenerContext::new(Arc::new(config.clone())); + Box::into_raw(Box::new(ContextObj { ctx })) as i64 +} + +#[unsafe(no_mangle)] +pub unsafe extern "system" fn Java_com_longbridge_SdkNative_freeScreenerContext( + _env: JNIEnv, + _class: JClass, + context: i64, +) { + let _ = Box::from_raw(context as *mut ContextObj); +} + +#[unsafe(no_mangle)] +pub unsafe extern "system" fn Java_com_longbridge_SdkNative_screenerContextRecommendStrategies( + 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.screener_recommend_strategies().await?; + Ok(resp) + })?; + Ok(()) + }) +} + +#[unsafe(no_mangle)] +pub unsafe extern "system" fn Java_com_longbridge_SdkNative_screenerContextUserStrategies( + 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.screener_user_strategies().await?; + Ok(resp) + })?; + Ok(()) + }) +} + +#[unsafe(no_mangle)] +pub unsafe extern "system" fn Java_com_longbridge_SdkNative_screenerContextStrategy( + mut env: JNIEnv, + _class: JClass, + context: i64, + id: 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.screener_strategy(id).await?; + Ok(resp) + })?; + Ok(()) + }) +} + +#[unsafe(no_mangle)] +pub unsafe extern "system" fn Java_com_longbridge_SdkNative_screenerContextSearch( + mut env: JNIEnv, + _class: JClass, + context: i64, + opts: JObject, + callback: JObject, +) { + jni_result(&mut env, (), |env| { + let context = &*(context as *const ContextObj); + let market: String = get_field(env, &opts, "market")?; + let strategy_id: Option = get_field(env, &opts, "strategyId")?; + let page_opt: Option = get_field(env, &opts, "page")?; + let page = page_opt.map(i32::from).unwrap_or(1) as u32; + let size_opt: Option = get_field(env, &opts, "size")?; + let size = size_opt.map(i32::from).unwrap_or(20) as u32; + async_util::execute(env, callback, async move { + let resp = context + .ctx + .screener_search(market, strategy_id, page, size) + .await?; + Ok(resp) + })?; + Ok(()) + }) +} + +#[unsafe(no_mangle)] +pub unsafe extern "system" fn Java_com_longbridge_SdkNative_screenerContextIndicators( + 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.screener_indicators().await?; + Ok(resp) + })?; + Ok(()) + }) +} diff --git a/java/src/types/classes.rs b/java/src/types/classes.rs index f09a01e89..f73db77ec 100644 --- a/java/src/types/classes.rs +++ b/java/src/types/classes.rs @@ -2230,25 +2230,13 @@ impl_java_class!( impl_java_class!( "com/longbridge/quote/ShortPositionsResponse", longbridge::quote::ShortPositionsResponse, - [ - symbol, - #[java(objarray)] - data, - sources - ] + [data] ); impl_java_class!( - "com/longbridge/quote/ShortPosition", - longbridge::quote::ShortPosition, - [ - timestamp, - rate, - avg_daily_share_volume, - current_shares_short, - days_to_cover, - close - ] + "com/longbridge/quote/ShortTradesResponse", + longbridge::quote::ShortTradesResponse, + [data] ); impl_java_class!( @@ -2619,3 +2607,75 @@ impl_java_class!( has_more ] ); + +// ── FundamentalContext: shareholders / valuation comparison ──────── + +impl_java_class!( + "com/longbridge/fundamental/ShareholderTopResponse", + longbridge::fundamental::ShareholderTopResponse, + [data] +); + +impl_java_class!( + "com/longbridge/fundamental/ShareholderDetailResponse", + longbridge::fundamental::ShareholderDetailResponse, + [data] +); + +impl_java_class!( + "com/longbridge/fundamental/ValuationComparisonResponse", + longbridge::fundamental::ValuationComparisonResponse, + [data] +); + +// ── MarketContext: stock events / rank ──────────────────────────── + +impl_java_class!( + "com/longbridge/market/StockEventsResponse", + longbridge::market::StockEventsResponse, + [data] +); + +impl_java_class!( + "com/longbridge/market/RankCategoriesResponse", + longbridge::market::RankCategoriesResponse, + [data] +); + +impl_java_class!( + "com/longbridge/market/RankListResponse", + longbridge::market::RankListResponse, + [data] +); + +// ── ScreenerContext ─────────────────────────────────────────────── + +impl_java_class!( + "com/longbridge/screener/ScreenerRecommendStrategiesResponse", + longbridge::screener::ScreenerRecommendStrategiesResponse, + [data] +); + +impl_java_class!( + "com/longbridge/screener/ScreenerUserStrategiesResponse", + longbridge::screener::ScreenerUserStrategiesResponse, + [data] +); + +impl_java_class!( + "com/longbridge/screener/ScreenerStrategyResponse", + longbridge::screener::ScreenerStrategyResponse, + [data] +); + +impl_java_class!( + "com/longbridge/screener/ScreenerSearchResponse", + longbridge::screener::ScreenerSearchResponse, + [data] +); + +impl_java_class!( + "com/longbridge/screener/ScreenerIndicatorsResponse", + longbridge::screener::ScreenerIndicatorsResponse, + [data] +); diff --git a/nodejs/index.d.ts b/nodejs/index.d.ts index 4495ed531..b4d96743b 100644 --- a/nodejs/index.d.ts +++ b/nodejs/index.d.ts @@ -607,18 +607,12 @@ export declare class FundamentalContext { buyback(symbol: string): Promise /** Get stock ratings for a security */ ratings(symbol: string): Promise - /** Get business segment breakdowns (latest snapshot) */ - businessSegments(symbol: string): Promise - /** Get historical business segment breakdowns */ - businessSegmentsHistory(symbol: string, report?: string | undefined | null, cate?: string | undefined | null): Promise - /** Get historical institutional rating view time-series */ - institutionRatingViews(symbol: string): Promise - /** Get industry rank for a market */ - industryRank(market: string, indicator: string, sortType: string, limit: number): Promise - /** Get the industry peer chain for a security or industry */ - industryPeers(counterId: string, market: string, industryId?: string | undefined | null): Promise - /** Get a financial report snapshot (earnings snapshot) */ - financialReportSnapshot(symbol: string, report?: string | undefined | null, fiscalYear?: number | undefined | null, fiscalPeriod?: string | undefined | null): Promise + /** Get ranked list of top shareholders */ + shareholderTop(symbol: string): Promise + /** Get holding history and detail for one shareholder */ + shareholderDetail(symbol: string, objectId: number): Promise + /** Get valuation comparison between a security and optional peers */ + valuationComparison(symbol: string, currency: string, comparisonSymbols?: Array | undefined | null): Promise } /** Fund position */ @@ -777,6 +771,12 @@ export declare class MarketContext { anomaly(market: string): Promise /** Get index constituent stocks */ constituent(symbol: string): Promise + /** Get stock events across one or more markets */ + stockEvents(markets: Array, sort: number, date: string | undefined | null, limit: number): Promise + /** Get all available rank category keys and labels */ + rankCategories(): Promise + /** Get a ranked list of securities for the given category key */ + rankList(key: string, needArticle: boolean): Promise } /** Market temperature */ @@ -2002,8 +2002,18 @@ export declare class QuoteContext { * ``` */ realtimeCandlesticks(symbol: string, period: Period, count: number): Promise> - /** Get short interest data for a US security */ - shortPositions(symbol: string): Promise + /** + * Get short interest data for a US or HK security. + * + * Market is inferred from the symbol suffix (.HK → HK, otherwise US). + */ + shortPositions(symbol: string, count: number): Promise + /** + * Get short trade records for a HK or US security. + * + * Market is inferred from the symbol suffix (.HK → HK, otherwise US). + */ + shortTrades(symbol: string, count: number): Promise /** Get real-time option call/put volume */ optionVolume(symbol: string): Promise /** Get daily historical option volume */ @@ -2049,6 +2059,22 @@ export declare class RealtimeQuote { get tradeStatus(): TradeStatus } +/** Screener context */ +export declare class ScreenerContext { + /** Create a new `ScreenerContext` */ + static new(config: Config): ScreenerContext + /** Get recommended built-in screener strategies */ + screenerRecommendStrategies(): Promise + /** Get the current user's saved screener strategies */ + screenerUserStrategies(): Promise + /** Get detail for one screener strategy by ID */ + screenerStrategy(id: number): Promise + /** Search / screen securities using a strategy */ + screenerSearch(market: string, strategyId: number | undefined | null, page: number, size: number): Promise + /** Get all available screener indicator definitions */ + screenerIndicators(): Promise +} + /** Security */ export declare class Security { toString(): string @@ -2153,31 +2179,11 @@ export declare class SecurityCalcIndex { get delta(): Decimal | null /** Gamma */ get gamma(): Decimal | null - /** - * Theta - * - * The raw value returned by the API is annualized (scaled by 252 trading - * days per year). To obtain the standard per-calendar-day theta, divide - * by 252: `theta / 252`. - */ + /** Theta */ get theta(): Decimal | null - /** - * Vega - * - * The raw value returned by the API is expressed per 1 percentage-point - * change in implied volatility (i.e. the value has been multiplied by - * 100). To obtain the standard vega (per unit change in IV), divide by - * 100: `vega / 100`. - */ + /** Vega */ get vega(): Decimal | null - /** - * Rho - * - * The raw value returned by the API is expressed per 1 percentage-point - * change in the risk-free rate (i.e. the value has been multiplied by - * 100). To obtain the standard rho (per unit change in rate), divide by - * 100: `rho / 100`. - */ + /** Rho */ get rho(): Decimal | null } @@ -3166,41 +3172,6 @@ export interface BrokerHoldingTop { updatedAt: string } -/** One business/regional segment item in a historical snapshot */ -export interface BusinessSegmentHistoryItem { - name: string - percent: string - value: string -} - -/** One business segment item (latest snapshot) */ -export interface BusinessSegmentItem { - name: string - percent: string -} - -/** Business segments response */ -export interface BusinessSegments { - date: string - total: string - currency: string - business: Array -} - -/** One historical business segments snapshot */ -export interface BusinessSegmentsHistoricalItem { - date: string - total: string - currency: string - business: Array - regionals: Array -} - -/** Business segments history response */ -export interface BusinessSegmentsHistory { - historical: Array -} - /** Buyback data response */ export interface BuybackData { recentBuybacks?: RecentBuybacks @@ -3990,32 +3961,6 @@ export interface FinancialReports { list: any } -/** Financial report snapshot response */ -export interface FinancialReportSnapshot { - name: string - ticker: string - fpStart: string - fpEnd: string - currency: string - reportDesc: string - foRevenue?: SnapshotForecastMetric - foEbit?: SnapshotForecastMetric - foEps?: SnapshotForecastMetric - frRevenue?: SnapshotReportedMetric - frProfit?: SnapshotReportedMetric - frOperateCash?: SnapshotReportedMetric - frInvestCash?: SnapshotReportedMetric - frFinanceCash?: SnapshotReportedMetric - frTotalAssets?: SnapshotReportedMetric - frTotalLiability?: SnapshotReportedMetric - frRoeTtm: string - frProfitMargin: string - frProfitMarginTtm: string - frAssetTurnTtm: string - frLeverageTtm: string - frDebtAssetsRatio: string -} - export declare const enum FlowDirection { /** Unknown */ Unknown = 0, @@ -4204,55 +4149,6 @@ export interface IndexConstituents { stocks: Array } -/** - * A node in the recursive industry peer chain. - * - * `nextJson` contains the child nodes serialised as a JSON string. - */ -export interface IndustryPeerNode { - name: string - counterId: string - stockNum: number - chg: string - ytdChg: string - /** Child nodes as a JSON string */ - nextJson: string -} - -/** Industry peers response */ -export interface IndustryPeersResponse { - top: IndustryPeersTop - chain?: IndustryPeerNode -} - -/** Top-level industry info in the peers response */ -export interface IndustryPeersTop { - name: string - market: string -} - -/** A group of ranked industry items */ -export interface IndustryRankGroup { - lists: Array -} - -/** One ranked industry item */ -export interface IndustryRankItem { - name: string - counterId: string - chg: string - leadingName: string - leadingTicker: string - leadingChg: string - valueName: string - valueData: string -} - -/** Industry rank response */ -export interface IndustryRankResponse { - items: Array -} - /** Industry valuation distribution response */ export interface IndustryValuationDist { /** PE distribution */ @@ -4417,22 +4313,6 @@ export interface InstitutionRatingSummary { updatedAt: string } -/** One historical rating distribution snapshot */ -export interface InstitutionRatingViewItem { - date: string - buy: string - over: string - hold: string - under: string - sell: string - total: string -} - -/** Institution rating views response */ -export interface InstitutionRatingViews { - elist: Array -} - export declare const enum InstitutionRecommend { /** Unknown */ Unknown = 0, @@ -5069,6 +4949,18 @@ export declare const enum PushCandlestickMode { Confirmed = 1 } +/** Rank categories response. `data` is a JSON string. */ +export interface RankCategoriesResponse { + /** Raw rank categories data (JSON string) */ + data: string +} + +/** Rank list response. `data` is a JSON string. */ +export interface RankListResponse { + /** Raw rank list data (JSON string) */ + data: string +} + /** Analyst rating distribution counts */ export interface RatingEvaluate { /** Number of "Buy" ratings */ @@ -5154,6 +5046,36 @@ export interface ReplaceOrderOptions { remark?: string } +/** Screener indicator definitions response. `data` is a JSON string. */ +export interface ScreenerIndicatorsResponse { + /** Raw indicator definitions data (JSON string) */ + data: string +} + +/** Recommended screener strategies response. `data` is a JSON string. */ +export interface ScreenerRecommendStrategiesResponse { + /** Raw recommended strategies data (JSON string) */ + data: string +} + +/** Screener search results response. `data` is a JSON string. */ +export interface ScreenerSearchResponse { + /** Raw search results data (JSON string) */ + data: string +} + +/** Single screener strategy response. `data` is a JSON string. */ +export interface ScreenerStrategyResponse { + /** Raw strategy detail data (JSON string) */ + data: string +} + +/** User screener strategies response. `data` is a JSON string. */ +export interface ScreenerUserStrategiesResponse { + /** Raw user strategies data (JSON string) */ + data: string +} + /** Securities update mode */ export declare const enum SecuritiesUpdateMode { /** Add securities */ @@ -5246,6 +5168,12 @@ export interface Shareholder { stocks: Array } +/** Shareholder detail response. `data` is a JSON string. */ +export interface ShareholderDetailResponse { + /** Raw shareholder detail data (JSON string) */ + data: string +} + /** Shareholder list response */ export interface ShareholderList { /** Major shareholders */ @@ -5268,6 +5196,12 @@ export interface ShareholderStock { chg: string } +/** Top-shareholder list response. `data` is a JSON string. */ +export interface ShareholderTopResponse { + /** Raw top-shareholder data (JSON string) */ + data: string +} + /** Sharelist detail response */ export interface SharelistDetail { /** Sharelist info */ @@ -5350,44 +5284,25 @@ export interface SharelistStock { latency?: boolean } -/** One short position data point */ -export interface ShortPosition { - /** Settlement date timestamp string */ - timestamp: string - /** Short ratio */ - rate: string - /** Avg daily share volume */ - avgDailyShareVolume: string - /** Current shares short */ - currentSharesShort: string - /** Days to cover */ - daysToCover: string - /** Closing price */ - close: string -} - -/** Short interest response */ +/** + * Short interest response + * Short interest / positions response (HK or US). + * + * `data` is the raw JSON returned by the API as a string. + */ export interface ShortPositionsResponse { - /** Security symbol */ - symbol: string - /** Data points */ - data: Array - /** Number of sources */ - sources: number + /** Raw short positions data (JSON string) */ + data: string } -/** A forecast metric in the financial report snapshot */ -export interface SnapshotForecastMetric { - value: string - yoy: string - cmpDesc: string - estValue: string -} - -/** A reported metric in the financial report snapshot */ -export interface SnapshotReportedMetric { - value: string - yoy: string +/** + * Short trade records response (HK or US). + * + * `data` is the raw JSON returned by the API as a string. + */ +export interface ShortTradesResponse { + /** Raw short trade data (JSON string) */ + data: string } /** Sort order type */ @@ -5414,6 +5329,12 @@ export declare const enum StatementType { Monthly = 2 } +/** Stock events response. `data` is a JSON string. */ +export interface StockEventsResponse { + /** Raw stock events data (JSON string) */ + data: string +} + /** * Stock ratings response. * @@ -5623,6 +5544,12 @@ export interface UpdateWatchlistGroup { mode: SecuritiesUpdateMode } +/** Valuation comparison response. `data` is a JSON string. */ +export interface ValuationComparisonResponse { + /** Raw valuation comparison data (JSON string) */ + data: string +} + /** Valuation metrics response */ export interface ValuationData { /** Valuation metrics */ diff --git a/nodejs/index.js b/nodejs/index.js index 3d6beaf8a..e13776cb4 100644 --- a/nodejs/index.js +++ b/nodejs/index.js @@ -639,6 +639,7 @@ module.exports.PushTradesEvent = nativeBinding.PushTradesEvent module.exports.QuoteContext = nativeBinding.QuoteContext module.exports.QuotePackageDetail = nativeBinding.QuotePackageDetail module.exports.RealtimeQuote = nativeBinding.RealtimeQuote +module.exports.ScreenerContext = nativeBinding.ScreenerContext module.exports.Security = nativeBinding.Security module.exports.SecurityBrokers = nativeBinding.SecurityBrokers module.exports.SecurityCalcIndex = nativeBinding.SecurityCalcIndex diff --git a/nodejs/src/fundamental/context.rs b/nodejs/src/fundamental/context.rs index be4d96f5f..0878dad09 100644 --- a/nodejs/src/fundamental/context.rs +++ b/nodejs/src/fundamental/context.rs @@ -234,111 +234,43 @@ impl FundamentalContext { Ok(self.ctx.ratings(symbol).await.map_err(ErrorNewType)?.into()) } - /// Get business segment breakdowns (latest snapshot) + /// Get ranked list of top shareholders #[napi] - pub async fn business_segments(&self, symbol: String) -> Result { + pub async fn shareholder_top(&self, symbol: String) -> Result { Ok(self .ctx - .business_segments(symbol) + .shareholder_top(symbol) .await .map_err(ErrorNewType)? .into()) } - /// Get historical business segment breakdowns + /// Get holding history and detail for one shareholder #[napi] - pub async fn business_segments_history( + pub async fn shareholder_detail( &self, symbol: String, - report: Option, - cate: Option, - ) -> Result { - let report_static: Option<&'static str> = match report.as_deref() { - Some("qf") => Some("qf"), - Some("saf") => Some("saf"), - Some("af") => Some("af"), - _ => None, - }; + object_id: i64, + ) -> Result { Ok(self .ctx - .business_segments_history(symbol, report_static, cate) + .shareholder_detail(symbol, object_id) .await .map_err(ErrorNewType)? .into()) } - /// Get historical institutional rating view time-series + /// Get valuation comparison between a security and optional peers #[napi] - pub async fn institution_rating_views(&self, symbol: String) -> Result { - Ok(self - .ctx - .institution_rating_views(symbol) - .await - .map_err(ErrorNewType)? - .into()) - } - - /// Get industry rank for a market - #[napi] - pub async fn industry_rank( - &self, - market: String, - indicator: String, - sort_type: String, - limit: u32, - ) -> Result { - Ok(self - .ctx - .industry_rank(market, indicator, sort_type, limit) - .await - .map_err(ErrorNewType)? - .into()) - } - - /// Get the industry peer chain for a security or industry - #[napi] - pub async fn industry_peers( - &self, - counter_id: String, - market: String, - industry_id: Option, - ) -> Result { - Ok(self - .ctx - .industry_peers(counter_id, market, industry_id) - .await - .map_err(ErrorNewType)? - .into()) - } - - /// Get a financial report snapshot (earnings snapshot) - #[napi] - pub async fn financial_report_snapshot( + pub async fn valuation_comparison( &self, symbol: String, - report: Option, - fiscal_year: Option, - fiscal_period: Option, - ) -> Result { - let report_static: Option<&'static str> = match report.as_deref() { - Some("qf") => Some("qf"), - Some("saf") => Some("saf"), - Some("af") => Some("af"), - _ => None, - }; - let fiscal_period_static: Option<&'static str> = match fiscal_period.as_deref() { - Some("q1") => Some("q1"), - Some("q2") => Some("q2"), - Some("q3") => Some("q3"), - Some("q4") => Some("q4"), - Some("fy") => Some("fy"), - Some("h1") => Some("h1"), - Some("h2") => Some("h2"), - _ => None, - }; + currency: String, + comparison_symbols: Option>, + ) -> Result { Ok(self .ctx - .financial_report_snapshot(symbol, report_static, fiscal_year, fiscal_period_static) + .valuation_comparison(symbol, currency, comparison_symbols) .await .map_err(ErrorNewType)? .into()) diff --git a/nodejs/src/fundamental/types.rs b/nodejs/src/fundamental/types.rs index 57852c762..a9bf59d34 100644 --- a/nodejs/src/fundamental/types.rs +++ b/nodejs/src/fundamental/types.rs @@ -1647,365 +1647,56 @@ impl From for StockRatings { } } -// ── business_segments ───────────────────────────────────────────── +// ── ShareholderTopResponse ──────────────────────────────────────── -/// One business segment item (latest snapshot) +/// Top-shareholder list response. `data` is a JSON string. #[napi_derive::napi(object)] #[derive(Debug, Clone)] -pub struct BusinessSegmentItem { - pub name: String, - pub percent: String, -} - -impl From for BusinessSegmentItem { - fn from(v: lb::BusinessSegmentItem) -> Self { - Self { - name: v.name, - percent: v.percent, - } - } -} - -/// Business segments response -#[napi_derive::napi(object)] -#[derive(Debug, Clone)] -pub struct BusinessSegments { - pub date: String, - pub total: String, - pub currency: String, - pub business: Vec, -} - -impl From for BusinessSegments { - fn from(v: lb::BusinessSegments) -> Self { - Self { - date: v.date, - total: v.total, - currency: v.currency, - business: v.business.into_iter().map(Into::into).collect(), - } - } -} - -/// One business/regional segment item in a historical snapshot -#[napi_derive::napi(object)] -#[derive(Debug, Clone)] -pub struct BusinessSegmentHistoryItem { - pub name: String, - pub percent: String, - pub value: String, -} - -impl From for BusinessSegmentHistoryItem { - fn from(v: lb::BusinessSegmentHistoryItem) -> Self { - Self { - name: v.name, - percent: v.percent, - value: v.value, - } - } -} - -/// One historical business segments snapshot -#[napi_derive::napi(object)] -#[derive(Debug, Clone)] -pub struct BusinessSegmentsHistoricalItem { - pub date: String, - pub total: String, - pub currency: String, - pub business: Vec, - pub regionals: Vec, -} - -impl From for BusinessSegmentsHistoricalItem { - fn from(v: lb::BusinessSegmentsHistoricalItem) -> Self { - Self { - date: v.date, - total: v.total, - currency: v.currency, - business: v.business.into_iter().map(Into::into).collect(), - regionals: v.regionals.into_iter().map(Into::into).collect(), - } - } -} - -/// Business segments history response -#[napi_derive::napi(object)] -#[derive(Debug, Clone)] -pub struct BusinessSegmentsHistory { - pub historical: Vec, -} - -impl From for BusinessSegmentsHistory { - fn from(v: lb::BusinessSegmentsHistory) -> Self { - Self { - historical: v.historical.into_iter().map(Into::into).collect(), - } - } -} - -// ── institution_rating_views ────────────────────────────────────── - -/// One historical rating distribution snapshot -#[napi_derive::napi(object)] -#[derive(Debug, Clone)] -pub struct InstitutionRatingViewItem { - pub date: String, - pub buy: String, - pub over: String, - pub hold: String, - pub under: String, - pub sell: String, - pub total: String, -} - -impl From for InstitutionRatingViewItem { - fn from(v: lb::InstitutionRatingViewItem) -> Self { - Self { - date: v.date, - buy: v.buy, - over: v.over, - hold: v.hold, - under: v.under, - sell: v.sell, - total: v.total, - } - } -} - -/// Institution rating views response -#[napi_derive::napi(object)] -#[derive(Debug, Clone)] -pub struct InstitutionRatingViews { - pub elist: Vec, -} - -impl From for InstitutionRatingViews { - fn from(v: lb::InstitutionRatingViews) -> Self { - Self { - elist: v.elist.into_iter().map(Into::into).collect(), - } - } -} - -// ── industry_rank ───────────────────────────────────────────────── - -/// One ranked industry item -#[napi_derive::napi(object)] -#[derive(Debug, Clone)] -pub struct IndustryRankItem { - pub name: String, - pub counter_id: String, - pub chg: String, - pub leading_name: String, - pub leading_ticker: String, - pub leading_chg: String, - pub value_name: String, - pub value_data: String, -} - -impl From for IndustryRankItem { - fn from(v: lb::IndustryRankItem) -> Self { - Self { - name: v.name, - counter_id: v.counter_id, - chg: v.chg, - leading_name: v.leading_name, - leading_ticker: v.leading_ticker, - leading_chg: v.leading_chg, - value_name: v.value_name, - value_data: v.value_data, - } - } -} - -/// A group of ranked industry items -#[napi_derive::napi(object)] -#[derive(Debug, Clone)] -pub struct IndustryRankGroup { - pub lists: Vec, -} - -impl From for IndustryRankGroup { - fn from(v: lb::IndustryRankGroup) -> Self { - Self { - lists: v.lists.into_iter().map(Into::into).collect(), - } - } -} - -/// Industry rank response -#[napi_derive::napi(object)] -#[derive(Debug, Clone)] -pub struct IndustryRankResponse { - pub items: Vec, -} - -impl From for IndustryRankResponse { - fn from(v: lb::IndustryRankResponse) -> Self { - Self { - items: v.items.into_iter().map(Into::into).collect(), - } - } -} - -// ── industry_peers ──────────────────────────────────────────────── - -/// Top-level industry info in the peers response -#[napi_derive::napi(object)] -#[derive(Debug, Clone)] -pub struct IndustryPeersTop { - pub name: String, - pub market: String, +pub struct ShareholderTopResponse { + /// Raw top-shareholder data (JSON string) + pub data: String, } -impl From for IndustryPeersTop { - fn from(v: lb::IndustryPeersTop) -> Self { +impl From for ShareholderTopResponse { + fn from(v: lb::ShareholderTopResponse) -> Self { Self { - name: v.name, - market: v.market, + data: v.data.to_string(), } } } -/// A node in the recursive industry peer chain. -/// -/// `nextJson` contains the child nodes serialised as a JSON string. -#[napi_derive::napi(object)] -#[derive(Debug, Clone)] -pub struct IndustryPeerNode { - pub name: String, - pub counter_id: String, - pub stock_num: i32, - pub chg: String, - pub ytd_chg: String, - /// Child nodes as a JSON string - pub next_json: String, -} - -impl From for IndustryPeerNode { - fn from(v: lb::IndustryPeerNode) -> Self { - Self { - name: v.name, - counter_id: v.counter_id, - stock_num: v.stock_num, - chg: v.chg, - ytd_chg: v.ytd_chg, - next_json: serde_json::to_string(&v.next).unwrap_or_default(), - } - } -} +// ── ShareholderDetailResponse ───────────────────────────────────── -/// Industry peers response +/// Shareholder detail response. `data` is a JSON string. #[napi_derive::napi(object)] #[derive(Debug, Clone)] -pub struct IndustryPeersResponse { - pub top: IndustryPeersTop, - pub chain: Option, +pub struct ShareholderDetailResponse { + /// Raw shareholder detail data (JSON string) + pub data: String, } -impl From for IndustryPeersResponse { - fn from(v: lb::IndustryPeersResponse) -> Self { +impl From for ShareholderDetailResponse { + fn from(v: lb::ShareholderDetailResponse) -> Self { Self { - top: v.top.into(), - chain: v.chain.map(Into::into), + data: v.data.to_string(), } } } -// ── financial_report_snapshot ───────────────────────────────────── - -/// A forecast metric in the financial report snapshot -#[napi_derive::napi(object)] -#[derive(Debug, Clone)] -pub struct SnapshotForecastMetric { - pub value: String, - pub yoy: String, - pub cmp_desc: String, - pub est_value: String, -} - -impl From for SnapshotForecastMetric { - fn from(v: lb::SnapshotForecastMetric) -> Self { - Self { - value: v.value, - yoy: v.yoy, - cmp_desc: v.cmp_desc, - est_value: v.est_value, - } - } -} +// ── ValuationComparisonResponse ─────────────────────────────────── -/// A reported metric in the financial report snapshot +/// Valuation comparison response. `data` is a JSON string. #[napi_derive::napi(object)] #[derive(Debug, Clone)] -pub struct SnapshotReportedMetric { - pub value: String, - pub yoy: String, -} - -impl From for SnapshotReportedMetric { - fn from(v: lb::SnapshotReportedMetric) -> Self { - Self { - value: v.value, - yoy: v.yoy, - } - } +pub struct ValuationComparisonResponse { + /// Raw valuation comparison data (JSON string) + pub data: String, } -/// Financial report snapshot response -#[napi_derive::napi(object)] -#[derive(Debug, Clone)] -pub struct FinancialReportSnapshot { - pub name: String, - pub ticker: String, - pub fp_start: String, - pub fp_end: String, - pub currency: String, - pub report_desc: String, - pub fo_revenue: Option, - pub fo_ebit: Option, - pub fo_eps: Option, - pub fr_revenue: Option, - pub fr_profit: Option, - pub fr_operate_cash: Option, - pub fr_invest_cash: Option, - pub fr_finance_cash: Option, - pub fr_total_assets: Option, - pub fr_total_liability: Option, - pub fr_roe_ttm: String, - pub fr_profit_margin: String, - pub fr_profit_margin_ttm: String, - pub fr_asset_turn_ttm: String, - pub fr_leverage_ttm: String, - pub fr_debt_assets_ratio: String, -} - -impl From for FinancialReportSnapshot { - fn from(v: lb::FinancialReportSnapshot) -> Self { +impl From for ValuationComparisonResponse { + fn from(v: lb::ValuationComparisonResponse) -> Self { Self { - name: v.name, - ticker: v.ticker, - fp_start: v.fp_start, - fp_end: v.fp_end, - currency: v.currency, - report_desc: v.report_desc, - fo_revenue: v.fo_revenue.map(Into::into), - fo_ebit: v.fo_ebit.map(Into::into), - fo_eps: v.fo_eps.map(Into::into), - fr_revenue: v.fr_revenue.map(Into::into), - fr_profit: v.fr_profit.map(Into::into), - fr_operate_cash: v.fr_operate_cash.map(Into::into), - fr_invest_cash: v.fr_invest_cash.map(Into::into), - fr_finance_cash: v.fr_finance_cash.map(Into::into), - fr_total_assets: v.fr_total_assets.map(Into::into), - fr_total_liability: v.fr_total_liability.map(Into::into), - fr_roe_ttm: v.fr_roe_ttm, - fr_profit_margin: v.fr_profit_margin, - fr_profit_margin_ttm: v.fr_profit_margin_ttm, - fr_asset_turn_ttm: v.fr_asset_turn_ttm, - fr_leverage_ttm: v.fr_leverage_ttm, - fr_debt_assets_ratio: v.fr_debt_assets_ratio, + data: v.data.to_string(), } } } diff --git a/nodejs/src/lib.rs b/nodejs/src/lib.rs index fcaa66479..7bbacd5be 100644 --- a/nodejs/src/lib.rs +++ b/nodejs/src/lib.rs @@ -14,6 +14,7 @@ mod market; mod oauth; mod portfolio; mod quote; +mod screener; mod sharelist; mod time; mod trade; diff --git a/nodejs/src/market/context.rs b/nodejs/src/market/context.rs index 5cee1f546..351445cf7 100644 --- a/nodejs/src/market/context.rs +++ b/nodejs/src/market/context.rs @@ -122,4 +122,43 @@ impl MarketContext { .map_err(ErrorNewType)? .into()) } + + /// Get stock events across one or more markets + #[napi] + pub async fn stock_events( + &self, + markets: Vec, + sort: u32, + date: Option, + limit: u32, + ) -> Result { + Ok(self + .ctx + .stock_events(markets, sort, date, limit) + .await + .map_err(ErrorNewType)? + .into()) + } + + /// Get all available rank category keys and labels + #[napi] + pub async fn rank_categories(&self) -> Result { + Ok(self + .ctx + .rank_categories() + .await + .map_err(ErrorNewType)? + .into()) + } + + /// Get a ranked list of securities for the given category key + #[napi] + pub async fn rank_list(&self, key: String, need_article: bool) -> Result { + Ok(self + .ctx + .rank_list(key, need_article) + .await + .map_err(ErrorNewType)? + .into()) + } } diff --git a/nodejs/src/market/types.rs b/nodejs/src/market/types.rs index 269580003..b3db75c72 100644 --- a/nodejs/src/market/types.rs +++ b/nodejs/src/market/types.rs @@ -564,3 +564,57 @@ impl From for lb::AhPremiumPeriod { } } } + +// ── StockEventsResponse ─────────────────────────────────────────── + +/// Stock events response. `data` is a JSON string. +#[napi_derive::napi(object)] +#[derive(Debug, Clone)] +pub struct StockEventsResponse { + /// Raw stock events data (JSON string) + pub data: String, +} + +impl From for StockEventsResponse { + fn from(v: lb::StockEventsResponse) -> Self { + Self { + data: v.data.to_string(), + } + } +} + +// ── RankCategoriesResponse ──────────────────────────────────────── + +/// Rank categories response. `data` is a JSON string. +#[napi_derive::napi(object)] +#[derive(Debug, Clone)] +pub struct RankCategoriesResponse { + /// Raw rank categories data (JSON string) + pub data: String, +} + +impl From for RankCategoriesResponse { + fn from(v: lb::RankCategoriesResponse) -> Self { + Self { + data: v.data.to_string(), + } + } +} + +// ── RankListResponse ────────────────────────────────────────────── + +/// Rank list response. `data` is a JSON string. +#[napi_derive::napi(object)] +#[derive(Debug, Clone)] +pub struct RankListResponse { + /// Raw rank list data (JSON string) + pub data: String, +} + +impl From for RankListResponse { + fn from(v: lb::RankListResponse) -> Self { + Self { + data: v.data.to_string(), + } + } +} diff --git a/nodejs/src/quote/context.rs b/nodejs/src/quote/context.rs index b4b127a1d..4ee04a1e2 100644 --- a/nodejs/src/quote/context.rs +++ b/nodejs/src/quote/context.rs @@ -20,8 +20,9 @@ use crate::{ OptionVolumeStats, ParticipantInfo, Period, PinnedMode, QuotePackageDetail, RealtimeQuote, Security, SecurityBrokers, SecurityCalcIndex, SecurityDepth, SecurityListCategory, SecurityQuote, SecurityStaticInfo, ShortPositionsResponse, - SortOrderType, StrikePriceInfo, SubType, SubTypes, Subscription, Trade, TradeSessions, - WarrantInfo, WarrantQuote, WarrantSortBy, WarrantStatus, WarrantType, WatchlistGroup, + ShortTradesResponse, SortOrderType, StrikePriceInfo, SubType, SubTypes, Subscription, + Trade, TradeSessions, WarrantInfo, WarrantQuote, WarrantSortBy, WarrantStatus, + WarrantType, WatchlistGroup, }, }, time::{NaiveDate, NaiveDatetime}, @@ -1233,12 +1234,31 @@ impl QuoteContext { .collect() } - /// Get short interest data for a US security + /// Get short interest data for a US or HK security. + /// + /// Market is inferred from the symbol suffix (.HK → HK, otherwise US). + #[napi] + pub async fn short_positions( + &self, + symbol: String, + count: u32, + ) -> Result { + Ok(self + .ctx + .short_positions(symbol, count) + .await + .map_err(ErrorNewType)? + .into()) + } + + /// Get short trade records for a HK or US security. + /// + /// Market is inferred from the symbol suffix (.HK → HK, otherwise US). #[napi] - pub async fn short_positions(&self, symbol: String) -> Result { + pub async fn short_trades(&self, symbol: String, count: u32) -> Result { Ok(self .ctx - .short_positions(symbol) + .short_trades(symbol, count) .await .map_err(ErrorNewType)? .into()) diff --git a/nodejs/src/quote/types.rs b/nodejs/src/quote/types.rs index d649ff65c..79ee10604 100644 --- a/nodejs/src/quote/types.rs +++ b/nodejs/src/quote/types.rs @@ -1481,54 +1481,38 @@ pub struct HistoryMarketTemperatureResponse { // ── Step 3 additions ───────────────────────────────────────────── /// Short interest response +/// Short interest / positions response (HK or US). +/// +/// `data` is the raw JSON returned by the API as a string. #[napi_derive::napi(object)] #[derive(Debug, Clone)] pub struct ShortPositionsResponse { - /// Security symbol - pub symbol: String, - /// Data points - pub data: Vec, - /// Number of sources - pub sources: i32, + /// Raw short positions data (JSON string) + pub data: String, } impl From for ShortPositionsResponse { fn from(v: longbridge::quote::ShortPositionsResponse) -> Self { Self { - symbol: v.symbol, - data: v.data.into_iter().map(Into::into).collect(), - sources: v.sources, + data: v.data.to_string(), } } } -/// One short position data point +/// Short trade records response (HK or US). +/// +/// `data` is the raw JSON returned by the API as a string. #[napi_derive::napi(object)] #[derive(Debug, Clone)] -pub struct ShortPosition { - /// Settlement date timestamp string - pub timestamp: String, - /// Short ratio - pub rate: String, - /// Avg daily share volume - pub avg_daily_share_volume: String, - /// Current shares short - pub current_shares_short: String, - /// Days to cover - pub days_to_cover: String, - /// Closing price - pub close: String, -} - -impl From for ShortPosition { - fn from(v: longbridge::quote::ShortPosition) -> Self { +pub struct ShortTradesResponse { + /// Raw short trade data (JSON string) + pub data: String, +} + +impl From for ShortTradesResponse { + fn from(v: longbridge::quote::ShortTradesResponse) -> Self { Self { - timestamp: v.timestamp, - rate: v.rate, - avg_daily_share_volume: v.avg_daily_share_volume, - current_shares_short: v.current_shares_short, - days_to_cover: v.days_to_cover, - close: v.close, + data: v.data.to_string(), } } } diff --git a/nodejs/src/screener/context.rs b/nodejs/src/screener/context.rs new file mode 100644 index 000000000..b5b16fbbd --- /dev/null +++ b/nodejs/src/screener/context.rs @@ -0,0 +1,86 @@ +use std::sync::Arc; + +use napi::Result; + +use crate::{config::Config, error::ErrorNewType, screener::types::*}; + +/// Screener context +#[napi_derive::napi] +#[derive(Clone)] +pub struct ScreenerContext { + ctx: longbridge::ScreenerContext, +} + +#[napi_derive::napi] +impl ScreenerContext { + /// Create a new `ScreenerContext` + #[napi] + pub fn new(config: &Config) -> ScreenerContext { + Self { + ctx: longbridge::ScreenerContext::new(Arc::new(config.0.clone())), + } + } + + /// Get recommended built-in screener strategies + #[napi] + pub async fn screener_recommend_strategies( + &self, + ) -> Result { + Ok(self + .ctx + .screener_recommend_strategies() + .await + .map_err(ErrorNewType)? + .into()) + } + + /// Get the current user's saved screener strategies + #[napi] + pub async fn screener_user_strategies(&self) -> Result { + Ok(self + .ctx + .screener_user_strategies() + .await + .map_err(ErrorNewType)? + .into()) + } + + /// Get detail for one screener strategy by ID + #[napi] + pub async fn screener_strategy(&self, id: i64) -> Result { + Ok(self + .ctx + .screener_strategy(id) + .await + .map_err(ErrorNewType)? + .into()) + } + + /// Search / screen securities using a strategy + #[napi] + pub async fn screener_search( + &self, + market: String, + strategy_id: Option, + page: u32, + size: u32, + ) -> Result { + Ok(self + .ctx + .screener_search(market, strategy_id, page, size) + .await + .map_err(ErrorNewType)? + .into()) + } + + /// Get all available screener indicator definitions + #[napi] + pub async fn screener_indicators(&self) -> Result { + Ok(self + .ctx + .screener_indicators() + .await + .map_err(ErrorNewType)? + .into()) + } +} diff --git a/nodejs/src/screener/mod.rs b/nodejs/src/screener/mod.rs new file mode 100644 index 000000000..0561d4d5a --- /dev/null +++ b/nodejs/src/screener/mod.rs @@ -0,0 +1,2 @@ +pub mod context; +pub mod types; diff --git a/nodejs/src/screener/types.rs b/nodejs/src/screener/types.rs new file mode 100644 index 000000000..3956b8887 --- /dev/null +++ b/nodejs/src/screener/types.rs @@ -0,0 +1,91 @@ +use longbridge::screener::types as lb; + +// ── ScreenerRecommendStrategiesResponse ─────────────────────────── + +/// Recommended screener strategies response. `data` is a JSON string. +#[napi_derive::napi(object)] +#[derive(Debug, Clone)] +pub struct ScreenerRecommendStrategiesResponse { + /// Raw recommended strategies data (JSON string) + pub data: String, +} + +impl From for ScreenerRecommendStrategiesResponse { + fn from(v: lb::ScreenerRecommendStrategiesResponse) -> Self { + Self { + data: v.data.to_string(), + } + } +} + +// ── ScreenerUserStrategiesResponse ──────────────────────────────── + +/// User screener strategies response. `data` is a JSON string. +#[napi_derive::napi(object)] +#[derive(Debug, Clone)] +pub struct ScreenerUserStrategiesResponse { + /// Raw user strategies data (JSON string) + pub data: String, +} + +impl From for ScreenerUserStrategiesResponse { + fn from(v: lb::ScreenerUserStrategiesResponse) -> Self { + Self { + data: v.data.to_string(), + } + } +} + +// ── ScreenerStrategyResponse ────────────────────────────────────── + +/// Single screener strategy response. `data` is a JSON string. +#[napi_derive::napi(object)] +#[derive(Debug, Clone)] +pub struct ScreenerStrategyResponse { + /// Raw strategy detail data (JSON string) + pub data: String, +} + +impl From for ScreenerStrategyResponse { + fn from(v: lb::ScreenerStrategyResponse) -> Self { + Self { + data: v.data.to_string(), + } + } +} + +// ── ScreenerSearchResponse ──────────────────────────────────────── + +/// Screener search results response. `data` is a JSON string. +#[napi_derive::napi(object)] +#[derive(Debug, Clone)] +pub struct ScreenerSearchResponse { + /// Raw search results data (JSON string) + pub data: String, +} + +impl From for ScreenerSearchResponse { + fn from(v: lb::ScreenerSearchResponse) -> Self { + Self { + data: v.data.to_string(), + } + } +} + +// ── ScreenerIndicatorsResponse ──────────────────────────────────── + +/// Screener indicator definitions response. `data` is a JSON string. +#[napi_derive::napi(object)] +#[derive(Debug, Clone)] +pub struct ScreenerIndicatorsResponse { + /// Raw indicator definitions data (JSON string) + pub data: String, +} + +impl From for ScreenerIndicatorsResponse { + fn from(v: lb::ScreenerIndicatorsResponse) -> Self { + Self { + data: v.data.to_string(), + } + } +} diff --git a/python/pysrc/longbridge/openapi.pyi b/python/pysrc/longbridge/openapi.pyi index 6afef758f..ad774e421 100644 --- a/python/pysrc/longbridge/openapi.pyi +++ b/python/pysrc/longbridge/openapi.pyi @@ -2787,30 +2787,16 @@ class SecurityCalcIndex: theta: Optional[Decimal] """ Theta - - The raw value returned by the API is annualized (scaled by 252 trading - days per year). To obtain the standard per-calendar-day theta, divide - by 252: ``theta / 252``. """ vega: Optional[Decimal] """ Vega - - The raw value returned by the API is expressed per 1 percentage-point - change in implied volatility (i.e. the value has been multiplied by - 100). To obtain the standard vega (per unit change in IV), divide by - 100: ``vega / 100``. """ rho: Optional[Decimal] """ Rho - - The raw value returned by the API is expressed per 1 percentage-point - change in the risk-free rate (i.e. the value has been multiplied by - 100). To obtain the standard rho (per unit change in rate), divide by - 100: ``rho / 100``. """ class QuotePackageDetail: @@ -4000,6 +3986,40 @@ class QuoteContext: print(resp) """ + def short_positions( + self, symbol: str, count: int = 20 + ) -> ShortPositionsResponse: + """ + Get short interest / position data for a US or HK security. + + Market is inferred from the symbol suffix: ``.HK`` → HK endpoint, + otherwise US endpoint. + + Args: + symbol: Security code (e.g. ``"700.HK"`` or ``"AAPL.US"``) + count: Number of records (1–100, default 20) + + Returns: + :class:`ShortPositionsResponse` with raw JSON data + """ + + def short_trades( + self, symbol: str, count: int = 20 + ) -> ShortTradesResponse: + """ + Get short trade records for a HK or US security. + + Market is inferred from the symbol suffix: ``.HK`` → HK endpoint, + otherwise US endpoint. + + Args: + symbol: Security code + count: Number of records (1–100, default 20) + + Returns: + :class:`ShortTradesResponse` with raw JSON data + """ + class AsyncQuoteContext: """ Async quote context for use with asyncio. Create via `AsyncQuoteContext.create(config)` and await inside asyncio. @@ -5316,6 +5336,42 @@ class AsyncQuoteContext: """ ... + def short_positions( + self, symbol: str, count: int = 20 + ) -> Awaitable[ShortPositionsResponse]: + """ + Get short interest / position data for a US or HK security. Returns awaitable. + + Market is inferred from the symbol suffix: ``.HK`` → HK endpoint, + otherwise US endpoint. + + Args: + symbol: Security code + count: Number of records (1–100, default 20) + + Returns: + Awaitable resolving to :class:`ShortPositionsResponse` + """ + ... + + def short_trades( + self, symbol: str, count: int = 20 + ) -> Awaitable[ShortTradesResponse]: + """ + Get short trade records for a HK or US security. Returns awaitable. + + Market is inferred from the symbol suffix: ``.HK`` → HK endpoint, + otherwise US endpoint. + + Args: + symbol: Security code + count: Number of records (1–100, default 20) + + Returns: + Awaitable resolving to :class:`ShortTradesResponse` + """ + ... + class OrderSide: """ Order side @@ -9628,228 +9684,6 @@ class StockRatings: """Full ratings array as a JSON string""" -class BusinessSegmentItem: - """One business segment item (latest snapshot).""" - - name: str - """Segment name""" - percent: str - """Percentage of total revenue""" - - -class BusinessSegments: - """Response for :meth:`FundamentalContext.business_segments`.""" - - date: str - """Report date""" - total: str - """Total revenue""" - currency: str - """Reporting currency""" - business: list[BusinessSegmentItem] - """Business segment breakdown""" - - -class BusinessSegmentHistoryItem: - """One business/regional segment item in a historical snapshot.""" - - name: str - """Segment name""" - percent: str - """Percentage of total""" - value: str - """Absolute value""" - - -class BusinessSegmentsHistoricalItem: - """One historical business segments snapshot.""" - - date: str - """Report date""" - total: str - """Total revenue""" - currency: str - """Reporting currency""" - business: list[BusinessSegmentHistoryItem] - """Business segment breakdown""" - regionals: list[BusinessSegmentHistoryItem] - """Regional breakdown""" - - -class BusinessSegmentsHistory: - """Response for :meth:`FundamentalContext.business_segments_history`.""" - - historical: list[BusinessSegmentsHistoricalItem] - """Historical snapshots""" - - -class InstitutionRatingViewItem: - """One historical rating distribution snapshot.""" - - date: str - """Date as unix timestamp string""" - buy: str - """Number of Buy ratings""" - over: str - """Number of Outperform ratings""" - hold: str - """Number of Hold ratings""" - under: str - """Number of Underperform ratings""" - sell: str - """Number of Sell ratings""" - total: str - """Total analyst count""" - - -class InstitutionRatingViews: - """Response for :meth:`FundamentalContext.institution_rating_views`.""" - - elist: list[InstitutionRatingViewItem] - """Historical rating distribution snapshots""" - - -class IndustryRankItem: - """One ranked industry item.""" - - name: str - """Industry / sector name""" - counter_id: str - """Counter ID of the industry""" - chg: str - """Change percentage""" - leading_name: str - """Name of the leading stock""" - leading_ticker: str - """Ticker of the leading stock""" - leading_chg: str - """Change percentage of the leading stock""" - value_name: str - """Value label name""" - value_data: str - """Value data""" - - -class IndustryRankGroup: - """A group of ranked industry items.""" - - lists: list[IndustryRankItem] - """Items in this group""" - - -class IndustryRankResponse: - """Response for :meth:`FundamentalContext.industry_rank`.""" - - items: list[IndustryRankGroup] - """Grouped rank items""" - - -class IndustryPeersTop: - """Top-level industry info in the peers response.""" - - name: str - """Industry name""" - market: str - """Market code""" - - -class IndustryPeerNode: - """A node in the recursive industry peer chain.""" - - name: str - """Node name""" - counter_id: str - """Counter ID""" - stock_num: int - """Number of stocks in this node""" - chg: str - """Change percentage""" - ytd_chg: str - """Year-to-date change""" - next_json: str - """Child nodes as a JSON string""" - - -class IndustryPeersResponse: - """Response for :meth:`FundamentalContext.industry_peers`.""" - - top: IndustryPeersTop - """Top-level industry node info""" - chain: "IndustryPeerNode | None" - """Root peer chain node""" - - -class SnapshotForecastMetric: - """A forecast metric in the financial report snapshot.""" - - value: str - """Actual value""" - yoy: str - """Year-over-year change""" - cmp_desc: str - """Beat/miss description""" - est_value: str - """Consensus estimate value""" - - -class SnapshotReportedMetric: - """A reported metric in the financial report snapshot.""" - - value: str - """Actual value""" - yoy: str - """Year-over-year change""" - - -class FinancialReportSnapshot: - """Response for :meth:`FundamentalContext.financial_report_snapshot`.""" - - name: str - """Company name""" - ticker: str - """Ticker code""" - fp_start: str - """Fiscal period start date""" - fp_end: str - """Fiscal period end date""" - currency: str - """Reporting currency""" - report_desc: str - """Report description""" - fo_revenue: "SnapshotForecastMetric | None" - """Forecast revenue""" - fo_ebit: "SnapshotForecastMetric | None" - """Forecast EBIT""" - fo_eps: "SnapshotForecastMetric | None" - """Forecast EPS""" - fr_revenue: "SnapshotReportedMetric | None" - """Reported revenue""" - fr_profit: "SnapshotReportedMetric | None" - """Reported net profit""" - fr_operate_cash: "SnapshotReportedMetric | None" - """Reported operating cash flow""" - fr_invest_cash: "SnapshotReportedMetric | None" - """Reported investing cash flow""" - fr_finance_cash: "SnapshotReportedMetric | None" - """Reported financing cash flow""" - fr_total_assets: "SnapshotReportedMetric | None" - """Reported total assets""" - fr_total_liability: "SnapshotReportedMetric | None" - """Reported total liabilities""" - fr_roe_ttm: str - """ROE TTM""" - fr_profit_margin: str - """Profit margin""" - fr_profit_margin_ttm: str - """Profit margin TTM""" - fr_asset_turn_ttm: str - """Asset turnover TTM""" - fr_leverage_ttm: str - """Leverage TTM""" - fr_debt_assets_ratio: str - """Debt-to-assets ratio""" - - class FinancialReportKind: """Financial report kind.""" @@ -10028,109 +9862,74 @@ class FundamentalContext: """ ... - def business_segments(self, symbol: str) -> "BusinessSegments": + def shareholder_top(self, symbol: str) -> "ShareholderTopResponse": """ - Get business segment breakdowns (latest snapshot). + Get ranked list of top shareholders. Args: - symbol: Security symbol, e.g. ``"AAPL.US"`` + symbol: Security symbol Returns: - :class:`BusinessSegments` + :class:`ShareholderTopResponse` with raw JSON data """ ... - def business_segments_history( - self, - symbol: str, - report: "str | None" = None, - cate: "str | None" = None, - ) -> "BusinessSegmentsHistory": + def shareholder_detail( + self, symbol: str, object_id: int + ) -> "ShareholderDetailResponse": """ - Get historical business segment breakdowns. + Get holding history and detail for one shareholder. Args: symbol: Security symbol - report: Report type (``"qf"``, ``"saf"``, ``"af"``) or ``None`` - cate: Category filter or ``None`` + object_id: Shareholder object ID Returns: - :class:`BusinessSegmentsHistory` + :class:`ShareholderDetailResponse` with raw JSON data """ ... - def institution_rating_views(self, symbol: str) -> "InstitutionRatingViews": + def valuation_comparison( + self, + symbol: str, + currency: str, + comparison_symbols: Optional[List[str]] = None, + ) -> "ValuationComparisonResponse": """ - Get historical institutional rating view time-series. + Get valuation comparison between a security and optional peers. Args: symbol: Security symbol + currency: Currency code (e.g. ``"USD"``) + comparison_symbols: Optional list of peer symbols Returns: - :class:`InstitutionRatingViews` + :class:`ValuationComparisonResponse` with raw JSON data """ ... - def industry_rank( - self, - market: str, - indicator: str, - sort_type: str, - limit: int, - ) -> "IndustryRankResponse": - """ - Get industry rank for a market. - Args: - market: Market code, e.g. ``"US"`` - indicator: Numeric string ``"0"``–``"7"`` - sort_type: ``"0"`` (ascending) or ``"1"`` (descending) - limit: Maximum number of results +# ── FundamentalContext new response types ───────────────────────── - Returns: - :class:`IndustryRankResponse` - """ - ... +class ShareholderTopResponse: + """Top-shareholder list response. ``data`` is a Python dict/list from JSON.""" - def industry_peers( - self, - counter_id: str, - market: str, - industry_id: "str | None" = None, - ) -> "IndustryPeersResponse": - """ - Get the industry peer chain for a security or industry. + data: object + """Raw top-shareholder data (JSON object / list)""" - Args: - counter_id: Symbol (e.g. ``"AAPL.US"``) or industry counter ID - market: Market code, e.g. ``"US"`` - industry_id: Industry ID or ``None`` - Returns: - :class:`IndustryPeersResponse` - """ - ... +class ShareholderDetailResponse: + """Shareholder detail response. ``data`` is a Python dict/list from JSON.""" - def financial_report_snapshot( - self, - symbol: str, - report: "str | None" = None, - fiscal_year: "int | None" = None, - fiscal_period: "str | None" = None, - ) -> "FinancialReportSnapshot": - """ - Get a financial report snapshot (earnings snapshot). + data: object + """Raw shareholder detail data (JSON object / list)""" - Args: - symbol: Security symbol - report: Report type (``"qf"``, ``"saf"``, ``"af"``) or ``None`` - fiscal_year: Fiscal year (e.g. ``2023``) or ``None`` - fiscal_period: Fiscal period string or ``None`` - Returns: - :class:`FinancialReportSnapshot` - """ - ... +class ValuationComparisonResponse: + """Valuation comparison response. ``data`` is a Python dict/list from JSON.""" + + data: object + """Raw valuation comparison data (JSON object / list)""" # ── MarketContext ───────────────────────────────────────────────── @@ -10533,6 +10332,182 @@ class MarketContext: """ ... + def stock_events( + self, + markets: List[str], + sort: int = 0, + date: Optional[str] = None, + limit: int = 20, + ) -> "StockEventsResponse": + """ + Get stock events across one or more markets. + + Args: + markets: List of market codes, e.g. ``["HK", "US"]`` + sort: Sort order (0=ascending, 1=descending) + date: Optional date filter (``"YYYY-MM-DD"``) + limit: Max records to return + + Returns: + :class:`StockEventsResponse` with raw JSON data + """ + ... + + def rank_categories(self) -> "RankCategoriesResponse": + """ + Get all available rank category keys and labels. + + Returns: + :class:`RankCategoriesResponse` with raw JSON data + """ + ... + + def rank_list( + self, key: str, need_article: bool = False + ) -> "RankListResponse": + """ + Get a ranked list of securities for the given category key. + + Args: + key: Category key from :meth:`rank_categories` + need_article: Whether to include article content + + Returns: + :class:`RankListResponse` with raw JSON data + """ + ... + + +# ── MarketContext new response types ────────────────────────────── + +class StockEventsResponse: + """Stock events response. ``data`` is a Python dict/list from JSON.""" + + data: object + """Raw stock events data (JSON object / list)""" + + +class RankCategoriesResponse: + """Rank categories response. ``data`` is a Python dict/list from JSON.""" + + data: object + """Raw rank categories data (JSON object / list)""" + + +class RankListResponse: + """Rank list response. ``data`` is a Python dict/list from JSON.""" + + data: object + """Raw rank list data (JSON object / list)""" + + +# ── ScreenerContext ─────────────────────────────────────────────── + +class ScreenerRecommendStrategiesResponse: + """Recommended screener strategies response. ``data`` is a Python dict/list from JSON.""" + + data: object + """Raw recommended strategies data (JSON object / list)""" + + +class ScreenerUserStrategiesResponse: + """User screener strategies response. ``data`` is a Python dict/list from JSON.""" + + data: object + """Raw user strategies data (JSON object / list)""" + + +class ScreenerStrategyResponse: + """Single screener strategy response. ``data`` is a Python dict/list from JSON.""" + + data: object + """Raw strategy detail data (JSON object / list)""" + + +class ScreenerSearchResponse: + """Screener search results response. ``data`` is a Python dict/list from JSON.""" + + data: object + """Raw search results data (JSON object / list)""" + + +class ScreenerIndicatorsResponse: + """Screener indicator definitions response. ``data`` is a Python dict/list from JSON.""" + + data: object + """Raw indicator definitions data (JSON object / list)""" + + +class ScreenerContext: + """Stock screener context — strategies, search, and indicators.""" + + def __init__(self, config: Config) -> None: ... + + def screener_recommend_strategies(self) -> ScreenerRecommendStrategiesResponse: + """Get recommended built-in screener strategies.""" + ... + + def screener_user_strategies(self) -> ScreenerUserStrategiesResponse: + """Get the current user's saved screener strategies.""" + ... + + def screener_strategy(self, id: int) -> ScreenerStrategyResponse: + """Get detail for one screener strategy by ID.""" + ... + + def screener_search( + self, + market: str, + strategy_id: Optional[int] = None, + page: int = 1, + size: int = 20, + ) -> ScreenerSearchResponse: + """Search / screen securities using a strategy.""" + ... + + def screener_indicators(self) -> ScreenerIndicatorsResponse: + """Get all available screener indicator definitions.""" + ... + + +class AsyncScreenerContext: + """Async screener context for use with asyncio.""" + + @classmethod + def create(cls, config: Config) -> "AsyncScreenerContext": ... + + def screener_recommend_strategies( + self, + ) -> Awaitable[ScreenerRecommendStrategiesResponse]: + """Get recommended built-in screener strategies. Returns awaitable.""" + ... + + def screener_user_strategies( + self, + ) -> Awaitable[ScreenerUserStrategiesResponse]: + """Get the current user's saved screener strategies. Returns awaitable.""" + ... + + def screener_strategy( + self, id: int + ) -> Awaitable[ScreenerStrategyResponse]: + """Get detail for one screener strategy by ID. Returns awaitable.""" + ... + + def screener_search( + self, + market: str, + strategy_id: Optional[int] = None, + page: int = 1, + size: int = 20, + ) -> Awaitable[ScreenerSearchResponse]: + """Search / screen securities using a strategy. Returns awaitable.""" + ... + + def screener_indicators(self) -> Awaitable[ScreenerIndicatorsResponse]: + """Get all available screener indicator definitions. Returns awaitable.""" + ... + # ── CalendarContext ─────────────────────────────────────────────── @@ -11683,32 +11658,26 @@ class SharelistContext: # ── QuoteContext extensions ─────────────────────────────────────── -class ShortPosition: - """One short interest data point.""" +class ShortPositionsResponse: + """Short interest / positions response (HK or US). - timestamp: str - """Settlement date (unix timestamp string)""" - rate: str - """Short interest as a ratio of float shares""" - avg_daily_share_volume: str - """Average daily share volume""" - current_shares_short: str - """Current shares short""" - days_to_cover: str - """Days to cover (short ratio)""" - close: str - """Closing price on the settlement date""" + The ``data`` attribute is a Python dict/list converted from the raw JSON + returned by the API. The exact shape differs between HK and US markets. + """ + data: object + """Raw short positions data (JSON object / list)""" -class ShortPositionsResponse: - """Short interest response.""" - symbol: str - """Security symbol""" - data: list[ShortPosition] - """Short interest data points""" - sources: int - """Number of data sources""" +class ShortTradesResponse: + """Short trade records response (HK or US). + + The ``data`` attribute is a Python dict/list converted from the raw JSON + returned by the API. The exact shape differs between HK and US markets. + """ + + data: object + """Raw short trade data (JSON object / list)""" class OptionVolumeStats: diff --git a/python/src/fundamental/context.rs b/python/src/fundamental/context.rs index 3f7e26e05..87f2e26fd 100644 --- a/python/src/fundamental/context.rs +++ b/python/src/fundamental/context.rs @@ -162,103 +162,39 @@ impl FundamentalContext { Ok(self.ctx.ratings(symbol).map_err(ErrorNewType)?.into()) } - /// Get business segment breakdowns (latest snapshot). - fn business_segments(&self, symbol: String) -> PyResult { + /// Get ranked list of top shareholders. + fn shareholder_top(&self, symbol: String) -> PyResult { Ok(self .ctx - .business_segments(symbol) + .shareholder_top(symbol) .map_err(ErrorNewType)? .into()) } - /// Get historical business segment breakdowns. - #[pyo3(signature = (symbol, report = None, cate = None))] - fn business_segments_history( + /// Get holding history and detail for one shareholder. + fn shareholder_detail( &self, symbol: String, - report: Option, - cate: Option, - ) -> PyResult { - let report_static: Option<&'static str> = match report.as_deref() { - Some("qf") => Some("qf"), - Some("saf") => Some("saf"), - Some("af") => Some("af"), - _ => None, - }; + object_id: i64, + ) -> PyResult { Ok(self .ctx - .business_segments_history(symbol, report_static, cate) + .shareholder_detail(symbol, object_id) .map_err(ErrorNewType)? .into()) } - /// Get historical institutional rating view time-series. - fn institution_rating_views(&self, symbol: String) -> PyResult { - Ok(self - .ctx - .institution_rating_views(symbol) - .map_err(ErrorNewType)? - .into()) - } - - /// Get industry rank for a market. - fn industry_rank( - &self, - market: String, - indicator: String, - sort_type: String, - limit: u32, - ) -> PyResult { - Ok(self - .ctx - .industry_rank(market, indicator, sort_type, limit) - .map_err(ErrorNewType)? - .into()) - } - - /// Get the industry peer chain for a security or industry. - #[pyo3(signature = (counter_id, market, industry_id = None))] - fn industry_peers( - &self, - counter_id: String, - market: String, - industry_id: Option, - ) -> PyResult { - Ok(self - .ctx - .industry_peers(counter_id, market, industry_id) - .map_err(ErrorNewType)? - .into()) - } - - /// Get a financial report snapshot (earnings snapshot). - #[pyo3(signature = (symbol, report = None, fiscal_year = None, fiscal_period = None))] - fn financial_report_snapshot( + /// Get valuation comparison between a security and optional peers. + #[pyo3(signature = (symbol, currency, comparison_symbols = None))] + fn valuation_comparison( &self, symbol: String, - report: Option, - fiscal_year: Option, - fiscal_period: Option, - ) -> PyResult { - let report_static: Option<&'static str> = match report.as_deref() { - Some("qf") => Some("qf"), - Some("saf") => Some("saf"), - Some("af") => Some("af"), - _ => None, - }; - let fiscal_period_static: Option<&'static str> = match fiscal_period.as_deref() { - Some("q1") => Some("q1"), - Some("q2") => Some("q2"), - Some("q3") => Some("q3"), - Some("q4") => Some("q4"), - Some("fy") => Some("fy"), - Some("h1") => Some("h1"), - Some("h2") => Some("h2"), - _ => None, - }; + currency: String, + comparison_symbols: Option>, + ) -> PyResult { Ok(self .ctx - .financial_report_snapshot(symbol, report_static, fiscal_year, fiscal_period_static) + .valuation_comparison(symbol, currency, comparison_symbols) .map_err(ErrorNewType)? .into()) } diff --git a/python/src/fundamental/context_async.rs b/python/src/fundamental/context_async.rs index 2a929dfcf..8c162b9a5 100644 --- a/python/src/fundamental/context_async.rs +++ b/python/src/fundamental/context_async.rs @@ -254,36 +254,28 @@ impl AsyncFundamentalContext { .map(|b| b.unbind()) } - /// Get business segment breakdowns. Returns awaitable. - fn business_segments(&self, py: Python<'_>, symbol: String) -> PyResult> { + /// Get ranked list of top shareholders. Returns awaitable. + fn shareholder_top(&self, py: Python<'_>, symbol: String) -> PyResult> { let ctx = self.ctx.clone(); pyo3_async_runtimes::tokio::future_into_py(py, async move { - Ok(BusinessSegments::from( - ctx.business_segments(symbol).await.map_err(ErrorNewType)?, + Ok(ShareholderTopResponse::from( + ctx.shareholder_top(symbol).await.map_err(ErrorNewType)?, )) }) .map(|b| b.unbind()) } - /// Get historical business segment breakdowns. Returns awaitable. - #[pyo3(signature = (symbol, report = None, cate = None))] - fn business_segments_history( + /// Get holding history and detail for one shareholder. Returns awaitable. + fn shareholder_detail( &self, py: Python<'_>, symbol: String, - report: Option, - cate: Option, + object_id: i64, ) -> PyResult> { let ctx = self.ctx.clone(); - let report_static: Option<&'static str> = match report.as_deref() { - Some("qf") => Some("qf"), - Some("saf") => Some("saf"), - Some("af") => Some("af"), - _ => None, - }; pyo3_async_runtimes::tokio::future_into_py(py, async move { - Ok(BusinessSegmentsHistory::from( - ctx.business_segments_history(symbol, report_static, cate) + Ok(ShareholderDetailResponse::from( + ctx.shareholder_detail(symbol, object_id) .await .map_err(ErrorNewType)?, )) @@ -291,99 +283,24 @@ impl AsyncFundamentalContext { .map(|b| b.unbind()) } - /// Get historical institutional rating view time-series. Returns awaitable. - fn institution_rating_views(&self, py: Python<'_>, symbol: String) -> PyResult> { - let ctx = self.ctx.clone(); - pyo3_async_runtimes::tokio::future_into_py(py, async move { - Ok(InstitutionRatingViews::from( - ctx.institution_rating_views(symbol) - .await - .map_err(ErrorNewType)?, - )) - }) - .map(|b| b.unbind()) - } - - /// Get industry rank for a market. Returns awaitable. - fn industry_rank( - &self, - py: Python<'_>, - market: String, - indicator: String, - sort_type: String, - limit: u32, - ) -> PyResult> { - let ctx = self.ctx.clone(); - pyo3_async_runtimes::tokio::future_into_py(py, async move { - Ok(IndustryRankResponse::from( - ctx.industry_rank(market, indicator, sort_type, limit) - .await - .map_err(ErrorNewType)?, - )) - }) - .map(|b| b.unbind()) - } - - /// Get the industry peer chain for a security or industry. Returns + /// Get valuation comparison between a security and optional peers. Returns /// awaitable. - #[pyo3(signature = (counter_id, market, industry_id = None))] - fn industry_peers( + #[pyo3(signature = (symbol, currency, comparison_symbols = None))] + fn valuation_comparison( &self, py: Python<'_>, - counter_id: String, - market: String, - industry_id: Option, + symbol: String, + currency: String, + comparison_symbols: Option>, ) -> PyResult> { let ctx = self.ctx.clone(); pyo3_async_runtimes::tokio::future_into_py(py, async move { - Ok(IndustryPeersResponse::from( - ctx.industry_peers(counter_id, market, industry_id) + Ok(ValuationComparisonResponse::from( + ctx.valuation_comparison(symbol, currency, comparison_symbols) .await .map_err(ErrorNewType)?, )) }) .map(|b| b.unbind()) } - - /// Get a financial report snapshot. Returns awaitable. - #[pyo3(signature = (symbol, report = None, fiscal_year = None, fiscal_period = None))] - fn financial_report_snapshot( - &self, - py: Python<'_>, - symbol: String, - report: Option, - fiscal_year: Option, - fiscal_period: Option, - ) -> PyResult> { - let ctx = self.ctx.clone(); - let report_static: Option<&'static str> = match report.as_deref() { - Some("qf") => Some("qf"), - Some("saf") => Some("saf"), - Some("af") => Some("af"), - _ => None, - }; - let fiscal_period_static: Option<&'static str> = match fiscal_period.as_deref() { - Some("q1") => Some("q1"), - Some("q2") => Some("q2"), - Some("q3") => Some("q3"), - Some("q4") => Some("q4"), - Some("fy") => Some("fy"), - Some("h1") => Some("h1"), - Some("h2") => Some("h2"), - _ => None, - }; - pyo3_async_runtimes::tokio::future_into_py(py, async move { - Ok(FinancialReportSnapshot::from( - ctx.financial_report_snapshot( - symbol, - report_static, - fiscal_year, - fiscal_period_static, - ) - .await - .map_err(ErrorNewType)?, - )) - }) - .map(|b| b.unbind()) - } } diff --git a/python/src/fundamental/mod.rs b/python/src/fundamental/mod.rs index 618da182d..80f423a9e 100644 --- a/python/src/fundamental/mod.rs +++ b/python/src/fundamental/mod.rs @@ -64,6 +64,9 @@ 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::()?; Ok(()) diff --git a/python/src/fundamental/types.rs b/python/src/fundamental/types.rs index 4ce28bd32..a14990800 100644 --- a/python/src/fundamental/types.rs +++ b/python/src/fundamental/types.rs @@ -1704,365 +1704,65 @@ impl From for lb::FinancialReportPeriod { } } -// ── business_segments ───────────────────────────────────────────── +// ── ShareholderTopResponse ──────────────────────────────────────── -/// One business segment item (latest snapshot) -#[pyclass(get_all, skip_from_py_object)] -#[derive(Debug, Clone)] -pub(crate) struct BusinessSegmentItem { - pub name: String, - pub percent: String, -} - -impl From for BusinessSegmentItem { - fn from(v: lb::BusinessSegmentItem) -> Self { - Self { - name: v.name, - percent: v.percent, - } - } -} - -/// Business segments response -#[pyclass(get_all, skip_from_py_object)] -#[derive(Debug, Clone)] -pub(crate) struct BusinessSegments { - pub date: String, - pub total: String, - pub currency: String, - pub business: Vec, -} - -impl From for BusinessSegments { - fn from(v: lb::BusinessSegments) -> Self { - Self { - date: v.date, - total: v.total, - currency: v.currency, - business: v.business.into_iter().map(Into::into).collect(), - } - } -} - -/// One business/regional segment item in a historical snapshot -#[pyclass(get_all, skip_from_py_object)] -#[derive(Debug, Clone)] -pub(crate) struct BusinessSegmentHistoryItem { - pub name: String, - pub percent: String, - pub value: String, -} - -impl From for BusinessSegmentHistoryItem { - fn from(v: lb::BusinessSegmentHistoryItem) -> Self { - Self { - name: v.name, - percent: v.percent, - value: v.value, - } - } -} - -/// One historical business segments snapshot -#[pyclass(get_all, skip_from_py_object)] -#[derive(Debug, Clone)] -pub(crate) struct BusinessSegmentsHistoricalItem { - pub date: String, - pub total: String, - pub currency: String, - pub business: Vec, - pub regionals: Vec, -} - -impl From for BusinessSegmentsHistoricalItem { - fn from(v: lb::BusinessSegmentsHistoricalItem) -> Self { - Self { - date: v.date, - total: v.total, - currency: v.currency, - business: v.business.into_iter().map(Into::into).collect(), - regionals: v.regionals.into_iter().map(Into::into).collect(), - } - } -} - -/// Business segments history response -#[pyclass(get_all, skip_from_py_object)] -#[derive(Debug, Clone)] -pub(crate) struct BusinessSegmentsHistory { - pub historical: Vec, -} - -impl From for BusinessSegmentsHistory { - fn from(v: lb::BusinessSegmentsHistory) -> Self { - Self { - historical: v.historical.into_iter().map(Into::into).collect(), - } - } -} - -// ── institution_rating_views ────────────────────────────────────── - -/// One historical rating distribution snapshot -#[pyclass(get_all, skip_from_py_object)] -#[derive(Debug, Clone)] -pub(crate) struct InstitutionRatingViewItem { - pub date: String, - pub buy: String, - pub over: String, - pub hold: String, - pub under: String, - pub sell: String, - pub total: String, -} - -impl From for InstitutionRatingViewItem { - fn from(v: lb::InstitutionRatingViewItem) -> Self { - Self { - date: v.date, - buy: v.buy, - over: v.over, - hold: v.hold, - under: v.under, - sell: v.sell, - total: v.total, - } - } -} - -/// Institution rating views response -#[pyclass(get_all, skip_from_py_object)] -#[derive(Debug, Clone)] -pub(crate) struct InstitutionRatingViews { - pub elist: Vec, -} - -impl From for InstitutionRatingViews { - fn from(v: lb::InstitutionRatingViews) -> Self { - Self { - elist: v.elist.into_iter().map(Into::into).collect(), - } - } -} - -// ── industry_rank ───────────────────────────────────────────────── - -/// One ranked industry item -#[pyclass(get_all, skip_from_py_object)] -#[derive(Debug, Clone)] -pub(crate) struct IndustryRankItem { - pub name: String, - pub counter_id: String, - pub chg: String, - pub leading_name: String, - pub leading_ticker: String, - pub leading_chg: String, - pub value_name: String, - pub value_data: String, -} - -impl From for IndustryRankItem { - fn from(v: lb::IndustryRankItem) -> Self { - Self { - name: v.name, - counter_id: v.counter_id, - chg: v.chg, - leading_name: v.leading_name, - leading_ticker: v.leading_ticker, - leading_chg: v.leading_chg, - value_name: v.value_name, - value_data: v.value_data, - } - } -} - -/// A group of ranked industry items -#[pyclass(get_all, skip_from_py_object)] -#[derive(Debug, Clone)] -pub(crate) struct IndustryRankGroup { - pub lists: Vec, -} - -impl From for IndustryRankGroup { - fn from(v: lb::IndustryRankGroup) -> Self { - Self { - lists: v.lists.into_iter().map(Into::into).collect(), - } - } -} - -/// Industry rank response +/// Top-shareholder list response. +/// +/// `data` is the raw JSON returned by the API preserved as a Python +/// object (dict / list / etc.). #[pyclass(get_all, skip_from_py_object)] #[derive(Debug, Clone)] -pub(crate) struct IndustryRankResponse { - pub items: Vec, +pub(crate) struct ShareholderTopResponse { + /// Raw top-shareholder data (JSON object) + pub data: JsonValue, } -impl From for IndustryRankResponse { - fn from(v: lb::IndustryRankResponse) -> Self { +impl From for ShareholderTopResponse { + fn from(v: lb::ShareholderTopResponse) -> Self { Self { - items: v.items.into_iter().map(Into::into).collect(), + data: JsonValue(v.data), } } } -// ── industry_peers ──────────────────────────────────────────────── - -/// Top-level industry info in the peers response -#[pyclass(get_all, skip_from_py_object)] -#[derive(Debug, Clone)] -pub(crate) struct IndustryPeersTop { - pub name: String, - pub market: String, -} - -impl From for IndustryPeersTop { - fn from(v: lb::IndustryPeersTop) -> Self { - Self { - name: v.name, - market: v.market, - } - } -} +// ── ShareholderDetailResponse ───────────────────────────────────── -/// A node in the recursive industry peer chain. +/// Shareholder detail response. /// -/// `next_json` contains the child nodes serialised as a JSON string. -#[pyclass(get_all, skip_from_py_object)] -#[derive(Debug, Clone)] -pub(crate) struct IndustryPeerNode { - pub name: String, - pub counter_id: String, - pub stock_num: i32, - pub chg: String, - pub ytd_chg: String, - /// Child nodes as a JSON string - pub next_json: String, -} - -impl From for IndustryPeerNode { - fn from(v: lb::IndustryPeerNode) -> Self { - Self { - name: v.name, - counter_id: v.counter_id, - stock_num: v.stock_num, - chg: v.chg, - ytd_chg: v.ytd_chg, - next_json: serde_json::to_string(&v.next).unwrap_or_default(), - } - } -} - -/// Industry peers response +/// `data` is the raw JSON returned by the API preserved as a Python +/// object (dict / list / etc.). #[pyclass(get_all, skip_from_py_object)] #[derive(Debug, Clone)] -pub(crate) struct IndustryPeersResponse { - pub top: IndustryPeersTop, - pub chain: Option, +pub(crate) struct ShareholderDetailResponse { + /// Raw shareholder detail data (JSON object) + pub data: JsonValue, } -impl From for IndustryPeersResponse { - fn from(v: lb::IndustryPeersResponse) -> Self { +impl From for ShareholderDetailResponse { + fn from(v: lb::ShareholderDetailResponse) -> Self { Self { - top: v.top.into(), - chain: v.chain.map(Into::into), + data: JsonValue(v.data), } } } -// ── financial_report_snapshot ───────────────────────────────────── - -/// A forecast metric in the financial report snapshot -#[pyclass(get_all, skip_from_py_object)] -#[derive(Debug, Clone)] -pub(crate) struct SnapshotForecastMetric { - pub value: String, - pub yoy: String, - pub cmp_desc: String, - pub est_value: String, -} - -impl From for SnapshotForecastMetric { - fn from(v: lb::SnapshotForecastMetric) -> Self { - Self { - value: v.value, - yoy: v.yoy, - cmp_desc: v.cmp_desc, - est_value: v.est_value, - } - } -} +// ── ValuationComparisonResponse ─────────────────────────────────── -/// A reported metric in the financial report snapshot +/// Valuation comparison response. +/// +/// `data` is the raw JSON returned by the API preserved as a Python +/// object (dict / list / etc.). #[pyclass(get_all, skip_from_py_object)] #[derive(Debug, Clone)] -pub(crate) struct SnapshotReportedMetric { - pub value: String, - pub yoy: String, -} - -impl From for SnapshotReportedMetric { - fn from(v: lb::SnapshotReportedMetric) -> Self { - Self { - value: v.value, - yoy: v.yoy, - } - } +pub(crate) struct ValuationComparisonResponse { + /// Raw valuation comparison data (JSON object) + pub data: JsonValue, } -/// Financial report snapshot response -#[pyclass(get_all, skip_from_py_object)] -#[derive(Debug, Clone)] -pub(crate) struct FinancialReportSnapshot { - pub name: String, - pub ticker: String, - pub fp_start: String, - pub fp_end: String, - pub currency: String, - pub report_desc: String, - pub fo_revenue: Option, - pub fo_ebit: Option, - pub fo_eps: Option, - pub fr_revenue: Option, - pub fr_profit: Option, - pub fr_operate_cash: Option, - pub fr_invest_cash: Option, - pub fr_finance_cash: Option, - pub fr_total_assets: Option, - pub fr_total_liability: Option, - pub fr_roe_ttm: String, - pub fr_profit_margin: String, - pub fr_profit_margin_ttm: String, - pub fr_asset_turn_ttm: String, - pub fr_leverage_ttm: String, - pub fr_debt_assets_ratio: String, -} - -impl From for FinancialReportSnapshot { - fn from(v: lb::FinancialReportSnapshot) -> Self { +impl From for ValuationComparisonResponse { + fn from(v: lb::ValuationComparisonResponse) -> Self { Self { - name: v.name, - ticker: v.ticker, - fp_start: v.fp_start, - fp_end: v.fp_end, - currency: v.currency, - report_desc: v.report_desc, - fo_revenue: v.fo_revenue.map(Into::into), - fo_ebit: v.fo_ebit.map(Into::into), - fo_eps: v.fo_eps.map(Into::into), - fr_revenue: v.fr_revenue.map(Into::into), - fr_profit: v.fr_profit.map(Into::into), - fr_operate_cash: v.fr_operate_cash.map(Into::into), - fr_invest_cash: v.fr_invest_cash.map(Into::into), - fr_finance_cash: v.fr_finance_cash.map(Into::into), - fr_total_assets: v.fr_total_assets.map(Into::into), - fr_total_liability: v.fr_total_liability.map(Into::into), - fr_roe_ttm: v.fr_roe_ttm, - fr_profit_margin: v.fr_profit_margin, - fr_profit_margin_ttm: v.fr_profit_margin_ttm, - fr_asset_turn_ttm: v.fr_asset_turn_ttm, - fr_leverage_ttm: v.fr_leverage_ttm, - fr_debt_assets_ratio: v.fr_debt_assets_ratio, + data: JsonValue(v.data), } } } diff --git a/python/src/lib.rs b/python/src/lib.rs index 61be1bc47..d63e3b8e4 100644 --- a/python/src/lib.rs +++ b/python/src/lib.rs @@ -13,6 +13,7 @@ mod market; mod oauth; mod portfolio; mod quote; +mod screener; mod sharelist; mod time; mod trade; @@ -41,6 +42,7 @@ fn longbridge(py: Python<'_>, m: Bound) -> PyResult<()> { market::register_types(&openapi)?; portfolio::register_types(&openapi)?; quote::register_types(&openapi)?; + screener::register_types(&openapi)?; trade::register_types(&openapi)?; content::register_types(&openapi)?; diff --git a/python/src/market/context.rs b/python/src/market/context.rs index d33b2f2be..75f314a34 100644 --- a/python/src/market/context.rs +++ b/python/src/market/context.rs @@ -99,4 +99,35 @@ impl MarketContext { fn constituent(&self, symbol: String) -> PyResult { Ok(self.ctx.constituent(symbol).map_err(ErrorNewType)?.into()) } + + /// Get stock events across one or more markets. + #[pyo3(signature = (markets, sort = 0, date = None, limit = 20))] + fn stock_events( + &self, + markets: Vec, + sort: u32, + date: Option, + limit: u32, + ) -> PyResult { + Ok(self + .ctx + .stock_events(markets, sort, date, limit) + .map_err(ErrorNewType)? + .into()) + } + + /// Get all available rank category keys and labels. + fn rank_categories(&self) -> PyResult { + Ok(self.ctx.rank_categories().map_err(ErrorNewType)?.into()) + } + + /// Get a ranked list of securities for the given category key. + #[pyo3(signature = (key, need_article = false))] + fn rank_list(&self, key: String, need_article: bool) -> PyResult { + Ok(self + .ctx + .rank_list(key, need_article) + .map_err(ErrorNewType)? + .into()) + } } diff --git a/python/src/market/context_async.rs b/python/src/market/context_async.rs index ccce1e141..3621bc6d9 100644 --- a/python/src/market/context_async.rs +++ b/python/src/market/context_async.rs @@ -137,4 +137,51 @@ impl AsyncMarketContext { }) .map(|b| b.unbind()) } + + /// Get stock events across one or more markets. Returns awaitable. + #[pyo3(signature = (markets, sort = 0, date = None, limit = 20))] + fn stock_events( + &self, + py: Python<'_>, + markets: Vec, + sort: u32, + date: Option, + limit: u32, + ) -> PyResult> { + let ctx = self.ctx.clone(); + pyo3_async_runtimes::tokio::future_into_py(py, async move { + Ok(StockEventsResponse::from( + ctx.stock_events(markets, sort, date, limit) + .await + .map_err(ErrorNewType)?, + )) + }) + .map(|b| b.unbind()) + } + + /// Get all available rank category keys and labels. Returns awaitable. + fn rank_categories(&self, py: Python<'_>) -> PyResult> { + let ctx = self.ctx.clone(); + pyo3_async_runtimes::tokio::future_into_py(py, async move { + Ok(RankCategoriesResponse::from( + ctx.rank_categories().await.map_err(ErrorNewType)?, + )) + }) + .map(|b| b.unbind()) + } + + /// Get a ranked list of securities for the given category key. Returns + /// awaitable. + #[pyo3(signature = (key, need_article = false))] + fn rank_list(&self, py: Python<'_>, key: String, need_article: bool) -> PyResult> { + let ctx = self.ctx.clone(); + pyo3_async_runtimes::tokio::future_into_py(py, async move { + Ok(RankListResponse::from( + ctx.rank_list(key, need_article) + .await + .map_err(ErrorNewType)?, + )) + }) + .map(|b| b.unbind()) + } } diff --git a/python/src/market/mod.rs b/python/src/market/mod.rs index ec3ea8c56..f535941d6 100644 --- a/python/src/market/mod.rs +++ b/python/src/market/mod.rs @@ -27,6 +27,9 @@ 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::()?; Ok(()) diff --git a/python/src/market/types.rs b/python/src/market/types.rs index 51e10f2cf..f90afb3e9 100644 --- a/python/src/market/types.rs +++ b/python/src/market/types.rs @@ -1,6 +1,69 @@ use longbridge::market::types as lb; use pyo3::prelude::*; +// ── StockEventsResponse ─────────────────────────────────────────── + +/// Stock events response. +/// +/// `data` is the raw JSON returned by the API preserved as a Python +/// object (dict / list / etc.). +#[pyclass(get_all, skip_from_py_object)] +#[derive(Debug, Clone)] +pub(crate) struct StockEventsResponse { + /// Raw stock events data (JSON object) + pub data: crate::fundamental::types::JsonValue, +} + +impl From for StockEventsResponse { + fn from(v: lb::StockEventsResponse) -> Self { + Self { + data: crate::fundamental::types::JsonValue(v.data), + } + } +} + +// ── RankCategoriesResponse ──────────────────────────────────────── + +/// Rank categories response. +/// +/// `data` is the raw JSON returned by the API preserved as a Python +/// object (dict / list / etc.). +#[pyclass(get_all, skip_from_py_object)] +#[derive(Debug, Clone)] +pub(crate) struct RankCategoriesResponse { + /// Raw rank categories data (JSON object) + pub data: crate::fundamental::types::JsonValue, +} + +impl From for RankCategoriesResponse { + fn from(v: lb::RankCategoriesResponse) -> Self { + Self { + data: crate::fundamental::types::JsonValue(v.data), + } + } +} + +// ── RankListResponse ────────────────────────────────────────────── + +/// Rank list response. +/// +/// `data` is the raw JSON returned by the API preserved as a Python +/// object (dict / list / etc.). +#[pyclass(get_all, skip_from_py_object)] +#[derive(Debug, Clone)] +pub(crate) struct RankListResponse { + /// Raw rank list data (JSON object) + pub data: crate::fundamental::types::JsonValue, +} + +impl From for RankListResponse { + fn from(v: lb::RankListResponse) -> Self { + Self { + data: crate::fundamental::types::JsonValue(v.data), + } + } +} + // ── MarketStatusResponse ────────────────────────────────────────── /// Market trading status response diff --git a/python/src/quote/context.rs b/python/src/quote/context.rs index 9a8e64916..a2d6f9e1e 100644 --- a/python/src/quote/context.rs +++ b/python/src/quote/context.rs @@ -636,14 +636,34 @@ impl QuoteContext { .collect() } - /// Get short interest data for a US security + /// Get short interest data for a US or HK security. + /// + /// Market is inferred from the symbol suffix (.HK → HK, otherwise US). + #[pyo3(signature = (symbol, count = 20))] fn short_positions( &self, symbol: String, + count: u32, ) -> PyResult { Ok(self .ctx - .short_positions(symbol) + .short_positions(symbol, count) + .map_err(ErrorNewType)? + .into()) + } + + /// Get short trade records for a HK or US security. + /// + /// Market is inferred from the symbol suffix (.HK → HK, otherwise US). + #[pyo3(signature = (symbol, count = 20))] + fn short_trades( + &self, + symbol: String, + count: u32, + ) -> PyResult { + Ok(self + .ctx + .short_trades(symbol, count) .map_err(ErrorNewType)? .into()) } diff --git a/python/src/quote/context_async.rs b/python/src/quote/context_async.rs index 7f0fa1237..cd5ca44c9 100644 --- a/python/src/quote/context_async.rs +++ b/python/src/quote/context_async.rs @@ -861,4 +861,38 @@ impl AsyncQuoteContext { }) .map(|b| b.unbind()) } + + /// Get short interest data for a US or HK security. Returns awaitable. + /// + /// Market is inferred from the symbol suffix (.HK → HK, otherwise US). + #[pyo3(signature = (symbol, count = 20))] + fn short_positions(&self, py: Python<'_>, symbol: String, count: u32) -> PyResult> { + let ctx = self.ctx.clone(); + pyo3_async_runtimes::tokio::future_into_py(py, async move { + let r: crate::quote::types::ShortPositionsResponse = ctx + .short_positions(symbol, count) + .await + .map_err(ErrorNewType)? + .into(); + Ok(r) + }) + .map(|b| b.unbind()) + } + + /// Get short trade records for a HK or US security. Returns awaitable. + /// + /// Market is inferred from the symbol suffix (.HK → HK, otherwise US). + #[pyo3(signature = (symbol, count = 20))] + fn short_trades(&self, py: Python<'_>, symbol: String, count: u32) -> PyResult> { + let ctx = self.ctx.clone(); + pyo3_async_runtimes::tokio::future_into_py(py, async move { + let r: crate::quote::types::ShortTradesResponse = ctx + .short_trades(symbol, count) + .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 285e3ed5c..6ce9458cc 100644 --- a/python/src/quote/mod.rs +++ b/python/src/quote/mod.rs @@ -65,7 +65,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::()?; parent.add_class::()?; parent.add_class::()?; diff --git a/python/src/quote/types.rs b/python/src/quote/types.rs index 0c4816dca..2c50cae45 100644 --- a/python/src/quote/types.rs +++ b/python/src/quote/types.rs @@ -1437,55 +1437,40 @@ pub(crate) struct HistoryMarketTemperatureResponse { // ── Step 3: short_positions / option_volume / option_volume_daily ─ -/// Short interest response +/// Short interest / positions response (HK or US). +/// +/// `data` is the raw JSON returned by the API preserved as a Python +/// object (dict / list / etc.). #[pyclass(get_all, skip_from_py_object)] #[derive(Debug, Clone)] pub(crate) struct ShortPositionsResponse { - /// Security symbol - pub symbol: String, - /// Short interest data points - pub data: Vec, - /// Number of data sources - pub sources: i32, + /// Raw short positions data (JSON object) + pub data: crate::fundamental::types::JsonValue, } impl From for ShortPositionsResponse { fn from(v: longbridge::quote::ShortPositionsResponse) -> Self { Self { - symbol: v.symbol, - data: v.data.into_iter().map(Into::into).collect(), - sources: v.sources, + data: crate::fundamental::types::JsonValue(v.data), } } } -/// One short position data point +/// Short trade records response (HK or US). +/// +/// `data` is the raw JSON returned by the API preserved as a Python +/// object (dict / list / etc.). #[pyclass(get_all, skip_from_py_object)] #[derive(Debug, Clone)] -pub(crate) struct ShortPosition { - /// Settlement date (unix timestamp string) - pub timestamp: String, - /// Short ratio - pub rate: String, - /// Average daily share volume - pub avg_daily_share_volume: String, - /// Current shares short - pub current_shares_short: String, - /// Days to cover - pub days_to_cover: String, - /// Closing price - pub close: String, -} - -impl From for ShortPosition { - fn from(v: longbridge::quote::ShortPosition) -> Self { +pub(crate) struct ShortTradesResponse { + /// Raw short trade data (JSON object) + pub data: crate::fundamental::types::JsonValue, +} + +impl From for ShortTradesResponse { + fn from(v: longbridge::quote::ShortTradesResponse) -> Self { Self { - timestamp: v.timestamp, - rate: v.rate, - avg_daily_share_volume: v.avg_daily_share_volume, - current_shares_short: v.current_shares_short, - days_to_cover: v.days_to_cover, - close: v.close, + data: crate::fundamental::types::JsonValue(v.data), } } } diff --git a/python/src/screener/context.rs b/python/src/screener/context.rs new file mode 100644 index 000000000..4030702f7 --- /dev/null +++ b/python/src/screener/context.rs @@ -0,0 +1,66 @@ +use std::sync::Arc; + +use longbridge::blocking::ScreenerContextSync; +use pyo3::prelude::*; + +use crate::{config::Config, error::ErrorNewType, screener::types::*}; + +/// Screener context (synchronous). +#[pyclass] +pub(crate) struct ScreenerContext { + ctx: ScreenerContextSync, +} + +#[pymethods] +impl ScreenerContext { + #[new] + fn new(config: &Config) -> PyResult { + Ok(Self { + ctx: ScreenerContextSync::new(Arc::new(config.0.clone())).map_err(ErrorNewType)?, + }) + } + + /// Get recommended built-in screener strategies. + fn screener_recommend_strategies(&self) -> PyResult { + Ok(self + .ctx + .screener_recommend_strategies() + .map_err(ErrorNewType)? + .into()) + } + + /// Get the current user's saved screener strategies. + fn screener_user_strategies(&self) -> PyResult { + Ok(self + .ctx + .screener_user_strategies() + .map_err(ErrorNewType)? + .into()) + } + + /// Get detail for one screener strategy by ID. + fn screener_strategy(&self, id: i64) -> PyResult { + Ok(self.ctx.screener_strategy(id).map_err(ErrorNewType)?.into()) + } + + /// Search / screen securities using a strategy. + #[pyo3(signature = (market, strategy_id = None, page = 1, size = 20))] + fn screener_search( + &self, + market: String, + strategy_id: Option, + page: u32, + size: u32, + ) -> PyResult { + Ok(self + .ctx + .screener_search(market, strategy_id, page, size) + .map_err(ErrorNewType)? + .into()) + } + + /// Get all available screener indicator definitions. + fn screener_indicators(&self) -> PyResult { + Ok(self.ctx.screener_indicators().map_err(ErrorNewType)?.into()) + } +} diff --git a/python/src/screener/context_async.rs b/python/src/screener/context_async.rs new file mode 100644 index 000000000..ddd026999 --- /dev/null +++ b/python/src/screener/context_async.rs @@ -0,0 +1,90 @@ +use std::sync::Arc; + +use longbridge::ScreenerContext; +use pyo3::{prelude::*, types::PyType}; + +use crate::{config::Config, error::ErrorNewType, screener::types::*}; + +/// Screener context (async). +#[pyclass] +pub(crate) struct AsyncScreenerContext { + ctx: Arc, +} + +#[pymethods] +impl AsyncScreenerContext { + /// Create an async screener context. + #[classmethod] + fn create(_cls: &Bound, config: &Config) -> Self { + Self { + ctx: Arc::new(ScreenerContext::new(Arc::new(config.0.clone()))), + } + } + + /// Get recommended built-in screener strategies. Returns awaitable. + fn screener_recommend_strategies(&self, py: Python<'_>) -> PyResult> { + let ctx = self.ctx.clone(); + pyo3_async_runtimes::tokio::future_into_py(py, async move { + Ok(ScreenerRecommendStrategiesResponse::from( + ctx.screener_recommend_strategies() + .await + .map_err(ErrorNewType)?, + )) + }) + .map(|b| b.unbind()) + } + + /// Get the current user's saved screener strategies. Returns awaitable. + fn screener_user_strategies(&self, py: Python<'_>) -> PyResult> { + let ctx = self.ctx.clone(); + pyo3_async_runtimes::tokio::future_into_py(py, async move { + Ok(ScreenerUserStrategiesResponse::from( + ctx.screener_user_strategies().await.map_err(ErrorNewType)?, + )) + }) + .map(|b| b.unbind()) + } + + /// Get detail for one screener strategy by ID. Returns awaitable. + fn screener_strategy(&self, py: Python<'_>, id: i64) -> PyResult> { + let ctx = self.ctx.clone(); + pyo3_async_runtimes::tokio::future_into_py(py, async move { + Ok(ScreenerStrategyResponse::from( + ctx.screener_strategy(id).await.map_err(ErrorNewType)?, + )) + }) + .map(|b| b.unbind()) + } + + /// Search / screen securities using a strategy. Returns awaitable. + #[pyo3(signature = (market, strategy_id = None, page = 1, size = 20))] + fn screener_search( + &self, + py: Python<'_>, + market: String, + strategy_id: Option, + page: u32, + size: u32, + ) -> PyResult> { + let ctx = self.ctx.clone(); + pyo3_async_runtimes::tokio::future_into_py(py, async move { + Ok(ScreenerSearchResponse::from( + ctx.screener_search(market, strategy_id, page, size) + .await + .map_err(ErrorNewType)?, + )) + }) + .map(|b| b.unbind()) + } + + /// Get all available screener indicator definitions. Returns awaitable. + fn screener_indicators(&self, py: Python<'_>) -> PyResult> { + let ctx = self.ctx.clone(); + pyo3_async_runtimes::tokio::future_into_py(py, async move { + Ok(ScreenerIndicatorsResponse::from( + ctx.screener_indicators().await.map_err(ErrorNewType)?, + )) + }) + .map(|b| b.unbind()) + } +} diff --git a/python/src/screener/mod.rs b/python/src/screener/mod.rs new file mode 100644 index 000000000..37c3193e9 --- /dev/null +++ b/python/src/screener/mod.rs @@ -0,0 +1,17 @@ +mod context; +mod context_async; +pub(crate) mod types; + +use pyo3::prelude::*; + +pub(crate) fn register_types(parent: &Bound) -> PyResult<()> { + use types::*; + 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/screener/types.rs b/python/src/screener/types.rs new file mode 100644 index 000000000..b87ca8600 --- /dev/null +++ b/python/src/screener/types.rs @@ -0,0 +1,107 @@ +use longbridge::screener::types as lb; +use pyo3::prelude::*; + +// ── ScreenerRecommendStrategiesResponse ─────────────────────────── + +/// Recommended screener strategies response. +/// +/// `data` is the raw JSON returned by the API preserved as a Python +/// object (dict / list / etc.). +#[pyclass(get_all, skip_from_py_object)] +#[derive(Debug, Clone)] +pub(crate) struct ScreenerRecommendStrategiesResponse { + /// Raw recommended strategies data (JSON object) + pub data: crate::fundamental::types::JsonValue, +} + +impl From for ScreenerRecommendStrategiesResponse { + fn from(v: lb::ScreenerRecommendStrategiesResponse) -> Self { + Self { + data: crate::fundamental::types::JsonValue(v.data), + } + } +} + +// ── ScreenerUserStrategiesResponse ──────────────────────────────── + +/// User screener strategies response. +/// +/// `data` is the raw JSON returned by the API preserved as a Python +/// object (dict / list / etc.). +#[pyclass(get_all, skip_from_py_object)] +#[derive(Debug, Clone)] +pub(crate) struct ScreenerUserStrategiesResponse { + /// Raw user strategies data (JSON object) + pub data: crate::fundamental::types::JsonValue, +} + +impl From for ScreenerUserStrategiesResponse { + fn from(v: lb::ScreenerUserStrategiesResponse) -> Self { + Self { + data: crate::fundamental::types::JsonValue(v.data), + } + } +} + +// ── ScreenerStrategyResponse ────────────────────────────────────── + +/// Single screener strategy response. +/// +/// `data` is the raw JSON returned by the API preserved as a Python +/// object (dict / list / etc.). +#[pyclass(get_all, skip_from_py_object)] +#[derive(Debug, Clone)] +pub(crate) struct ScreenerStrategyResponse { + /// Raw strategy detail data (JSON object) + pub data: crate::fundamental::types::JsonValue, +} + +impl From for ScreenerStrategyResponse { + fn from(v: lb::ScreenerStrategyResponse) -> Self { + Self { + data: crate::fundamental::types::JsonValue(v.data), + } + } +} + +// ── ScreenerSearchResponse ──────────────────────────────────────── + +/// Screener search results response. +/// +/// `data` is the raw JSON returned by the API preserved as a Python +/// object (dict / list / etc.). +#[pyclass(get_all, skip_from_py_object)] +#[derive(Debug, Clone)] +pub(crate) struct ScreenerSearchResponse { + /// Raw search results data (JSON object) + pub data: crate::fundamental::types::JsonValue, +} + +impl From for ScreenerSearchResponse { + fn from(v: lb::ScreenerSearchResponse) -> Self { + Self { + data: crate::fundamental::types::JsonValue(v.data), + } + } +} + +// ── ScreenerIndicatorsResponse ──────────────────────────────────── + +/// Screener indicator definitions response. +/// +/// `data` is the raw JSON returned by the API preserved as a Python +/// object (dict / list / etc.). +#[pyclass(get_all, skip_from_py_object)] +#[derive(Debug, Clone)] +pub(crate) struct ScreenerIndicatorsResponse { + /// Raw indicator definitions data (JSON object) + pub data: crate::fundamental::types::JsonValue, +} + +impl From for ScreenerIndicatorsResponse { + fn from(v: lb::ScreenerIndicatorsResponse) -> Self { + Self { + data: crate::fundamental::types::JsonValue(v.data), + } + } +} diff --git a/rust/src/blocking/quote.rs b/rust/src/blocking/quote.rs index 06cd9e57a..fbba09273 100644 --- a/rust/src/blocking/quote.rs +++ b/rust/src/blocking/quote.rs @@ -8,14 +8,14 @@ use crate::{ quote::{ AdjustType, CalcIndex, Candlestick, CapitalDistributionResponse, CapitalFlowLine, FilingItem, FilterWarrantExpiryDate, FilterWarrantInOutBoundsType, - HistoryMarketTemperatureResponse, HkShortPositionsResponse, IntradayLine, IssuerInfo, - MarketTemperature, MarketTradingDays, MarketTradingSession, OptionQuote, OptionVolumeDaily, - OptionVolumeStats, ParticipantInfo, Period, PinnedMode, PushEvent, QuotePackageDetail, - RealtimeQuote, RequestCreateWatchlistGroup, RequestUpdateWatchlistGroup, Security, - SecurityBrokers, SecurityCalcIndex, SecurityDepth, SecurityListCategory, SecurityQuote, - SecurityStaticInfo, ShortPositionsResponse, ShortTradesResponse, SortOrderType, - StrikePriceInfo, SubFlags, Subscription, Trade, TradeSessions, WarrantInfo, WarrantQuote, - WarrantSortBy, WarrantStatus, WarrantType, WatchlistGroup, + HistoryMarketTemperatureResponse, IntradayLine, IssuerInfo, MarketTemperature, + MarketTradingDays, MarketTradingSession, OptionQuote, OptionVolumeDaily, OptionVolumeStats, + ParticipantInfo, Period, PinnedMode, PushEvent, QuotePackageDetail, RealtimeQuote, + RequestCreateWatchlistGroup, RequestUpdateWatchlistGroup, Security, SecurityBrokers, + SecurityCalcIndex, SecurityDepth, SecurityListCategory, SecurityQuote, SecurityStaticInfo, + ShortPositionsResponse, ShortTradesResponse, SortOrderType, StrikePriceInfo, SubFlags, + Subscription, Trade, TradeSessions, WarrantInfo, WarrantQuote, WarrantSortBy, + WarrantStatus, WarrantType, WatchlistGroup, }, }; @@ -1168,13 +1168,14 @@ impl QuoteContextSync { .call(move |ctx| async move { ctx.realtime_candlesticks(symbol, period, count).await }) } - /// Get short interest data for a US security + /// Get short interest data for a US or HK security pub fn short_positions( &self, symbol: impl Into + Send + 'static, + count: u32, ) -> Result { self.rt - .call(move |ctx| async move { ctx.short_positions(symbol).await }) + .call(move |ctx| async move { ctx.short_positions(symbol, count).await }) } /// Get real-time option call/put volume @@ -1203,16 +1204,6 @@ impl QuoteContextSync { .call(move |ctx| async move { ctx.update_pinned(mode, symbols).await }) } - /// Get HK short interest / position data for a security - pub fn hk_short_positions( - &self, - symbol: impl Into + Send + 'static, - count: u32, - ) -> Result { - self.rt - .call(move |ctx| async move { ctx.hk_short_positions(symbol, count).await }) - } - /// Get short trade records for a HK or US security pub fn short_trades( &self, diff --git a/rust/src/quote/context.rs b/rust/src/quote/context.rs index 26962354b..c4870e3f0 100644 --- a/rust/src/quote/context.rs +++ b/rust/src/quote/context.rs @@ -15,14 +15,13 @@ use crate::{ Config, Error, Language, Market, Result, quote::{ AdjustType, CalcIndex, Candlestick, CapitalDistributionResponse, CapitalFlowLine, - FilingItem, HistoryMarketTemperatureResponse, HkShortPositionsResponse, IntradayLine, - IssuerInfo, MarketTemperature, MarketTradingDays, MarketTradingSession, OptionQuote, - OptionVolumeDaily, OptionVolumeStats, ParticipantInfo, Period, PushEvent, - QuotePackageDetail, RealtimeQuote, RequestCreateWatchlistGroup, - RequestUpdateWatchlistGroup, Security, SecurityBrokers, SecurityCalcIndex, SecurityDepth, - SecurityListCategory, SecurityQuote, SecurityStaticInfo, ShortPositionsResponse, - ShortTradesResponse, StrikePriceInfo, Subscription, Trade, TradeSessions, WarrantInfo, - WarrantQuote, WarrantType, WatchlistGroup, + FilingItem, HistoryMarketTemperatureResponse, IntradayLine, IssuerInfo, MarketTemperature, + MarketTradingDays, MarketTradingSession, OptionQuote, OptionVolumeDaily, OptionVolumeStats, + ParticipantInfo, Period, PushEvent, QuotePackageDetail, RealtimeQuote, + RequestCreateWatchlistGroup, RequestUpdateWatchlistGroup, Security, SecurityBrokers, + SecurityCalcIndex, SecurityDepth, SecurityListCategory, SecurityQuote, SecurityStaticInfo, + ShortPositionsResponse, ShortTradesResponse, StrikePriceInfo, Subscription, Trade, + TradeSessions, WarrantInfo, WarrantQuote, WarrantType, WatchlistGroup, cache::{Cache, CacheWithKey}, cmd_code, core::{Command, Core, UserProfile}, @@ -1958,29 +1957,48 @@ impl QuoteContext { // ── short_positions ─────────────────────────────────────────── - /// Get short interest data for a US security. + /// Get short interest data for a US or HK security. /// - /// Path: `GET /v1/quote/short-positions/us` + /// Market is inferred from the symbol suffix: + /// - `.HK` → `GET /v1/quote/short-positions/hk` + /// - otherwise → `GET /v1/quote/short-positions/us` + /// + /// `count` controls the number of records returned (1–100, default 20). pub async fn short_positions( &self, symbol: impl Into, + count: u32, ) -> Result { + use std::time::{SystemTime, UNIX_EPOCH}; + use crate::utils::counter::symbol_to_counter_id; + + let sym = symbol.into(); + let is_hk = sym.to_uppercase().ends_with(".HK"); + let path = if is_hk { + "/v1/quote/short-positions/hk" + } else { + "/v1/quote/short-positions/us" + }; + let ts = SystemTime::now() + .duration_since(UNIX_EPOCH) + .map(|d| d.as_secs()) + .unwrap_or(0); + #[derive(serde::Serialize)] struct Query { counter_id: String, - last_timestamp: i64, - page_size: i32, + last_timestamp: String, + count: u32, } - let sym = symbol.into(); let resp = self .0 .http_cli - .request(Method::GET, "/v1/quote/short-positions/us") + .request(Method::GET, path) .query_params(Query { counter_id: symbol_to_counter_id(&sym), - last_timestamp: 0, - page_size: 100, + last_timestamp: ts.to_string(), + count, }) .response::>() .send() @@ -2047,46 +2065,6 @@ impl QuoteContext { .await?; Ok(resp.0) } - // ── hk_short_positions ──────────────────────────────────────── - - /// Get HK short interest / position data for a security. - /// - /// Path: `GET /v1/quote/short-positions/hk` - pub async fn hk_short_positions( - &self, - symbol: impl Into, - count: u32, - ) -> Result { - use std::time::{SystemTime, UNIX_EPOCH}; - - use crate::utils::counter::symbol_to_counter_id; - #[derive(serde::Serialize)] - struct Query { - counter_id: String, - last_timestamp: String, - count: u32, - } - let sym = symbol.into(); - let ts = SystemTime::now() - .duration_since(UNIX_EPOCH) - .map(|d| d.as_secs()) - .unwrap_or(0); - let resp = self - .0 - .http_cli - .request(Method::GET, "/v1/quote/short-positions/hk") - .query_params(Query { - counter_id: symbol_to_counter_id(&sym), - last_timestamp: ts.to_string(), - count, - }) - .response::>() - .send() - .with_subscriber(self.0.log_subscriber.clone()) - .await?; - Ok(resp.0) - } - // ── short_trades ────────────────────────────────────────────── /// Get short trade records for a HK or US security. diff --git a/rust/src/quote/mod.rs b/rust/src/quote/mod.rs index b05b16898..5870f3ea4 100644 --- a/rust/src/quote/mod.rs +++ b/rust/src/quote/mod.rs @@ -30,7 +30,6 @@ pub use types::{ FilterWarrantInOutBoundsType, Granularity, HistoryMarketTemperatureResponse, - HkShortPositionsResponse, IntradayLine, IssuerInfo, MarketTemperature, @@ -59,7 +58,6 @@ pub use types::{ SecurityListCategory, SecurityQuote, SecurityStaticInfo, - ShortPosition, ShortPositionsResponse, ShortTradesResponse, SortOrderType, diff --git a/rust/src/quote/types.rs b/rust/src/quote/types.rs index 90f2b3b91..8cb48111c 100644 --- a/rust/src/quote/types.rs +++ b/rust/src/quote/types.rs @@ -2033,35 +2033,14 @@ impl_default_for_enum_string!( // ── short_positions ─────────────────────────────────────────────── /// Response for [`crate::QuoteContext::short_positions`] +/// +/// The raw data contains short interest/position data for a HK or US +/// security. The exact structure differs between markets so the +/// payload is preserved as raw JSON. #[derive(Debug, Clone, Serialize, Deserialize)] pub struct ShortPositionsResponse { - /// Security symbol - #[serde( - rename = "counter_id", - deserialize_with = "crate::utils::counter::deserialize_counter_id_as_symbol" - )] - pub symbol: String, - /// Short interest data points - pub data: Vec, - /// Number of data sources - pub sources: i32, -} - -/// One short interest data point -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct ShortPosition { - /// Settlement date (unix timestamp string) - pub timestamp: String, - /// Short interest as a ratio of float shares - pub rate: String, - /// Average daily share volume - pub avg_daily_share_volume: String, - /// Current shares short - pub current_shares_short: String, - /// Days to cover (short ratio) - pub days_to_cover: String, - /// Closing price on the settlement date - pub close: String, + /// Raw short positions data + pub data: serde_json::Value, } // ── option_volume ───────────────────────────────────────────────── @@ -2113,18 +2092,6 @@ pub struct OptionVolumeDailyStat { pub put_call_open_interest_ratio: String, } -// ── hk_short_positions ──────────────────────────────────────────── - -/// Response for [`crate::QuoteContext::hk_short_positions`] -/// -/// The raw data contains HK short interest/position data. The exact -/// structure varies so the payload is preserved as raw JSON. -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct HkShortPositionsResponse { - /// Raw HK short positions data - pub data: serde_json::Value, -} - // ── short_trades ────────────────────────────────────────────────── /// Response for [`crate::QuoteContext::short_trades`] From 16771e88b42fdb99eebf61afca3d917b7f536b2d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=A2=81=E7=AB=A0=E6=B4=AA?= Date: Wed, 20 May 2026 16:05:09 +0800 Subject: [PATCH 03/12] fix(cpp): remove conflicting extern C declarations for stock_events/rank_categories/rank_list These were already declared in longbridge.h (via cbindgen); the manual forward declarations in market_context.cpp had mismatched types (const char** vs const char* const*, size_t vs uintptr_t). Co-Authored-By: Claude Sonnet 4.6 (1M context) --- cpp/src/market_context.cpp | 3 --- 1 file changed, 3 deletions(-) diff --git a/cpp/src/market_context.cpp b/cpp/src/market_context.cpp index 519e42d10..0af8fbc6f 100644 --- a/cpp/src/market_context.cpp +++ b/cpp/src/market_context.cpp @@ -15,9 +15,6 @@ void lb_market_context_ah_premium_intraday(const lb_market_context_t*, const cha void lb_market_context_trade_stats(const lb_market_context_t*, const char*, lb_async_callback_t, void*); void lb_market_context_anomaly(const lb_market_context_t*, const char*, lb_async_callback_t, void*); void lb_market_context_constituent(const lb_market_context_t*, const char*, lb_async_callback_t, void*); -void lb_market_context_stock_events(const lb_market_context_t*, const char**, size_t, uint32_t, const char*, uint32_t, lb_async_callback_t, void*); -void lb_market_context_rank_categories(const lb_market_context_t*, lb_async_callback_t, void*); -void lb_market_context_rank_list(const lb_market_context_t*, const char*, bool, lb_async_callback_t, void*); } namespace longbridge { From 16bbe1721e472e1e4a69edea4811fb4f0d48cd2e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=A2=81=E7=AB=A0=E6=B4=AA?= Date: Wed, 20 May 2026 17:44:36 +0800 Subject: [PATCH 04/12] fix(fundamental,quote,market,screener): fix raw-JSON response deserialization MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The HTTP client extracts the `data` field from the API envelope before deserializing into the target struct. Response structs with `data: serde_json::Value` caused a double-unwrap — the inner JSON has no `data` key, so deserialization failed with "missing field `data`". Fix: deserialize to `serde_json::Value` first, then construct the response struct explicitly. Affects 13 new methods across FundamentalContext, QuoteContext, MarketContext, and ScreenerContext. Verified against production API: Python SDK 17/17 pass. Co-Authored-By: Claude Sonnet 4.6 (1M context) --- rust/src/market/context.rs | 47 +++++++++++++++++++++--------------- rust/src/quote/context.rs | 8 +++--- rust/src/screener/context.rs | 42 ++++++++++++++++++++------------ 3 files changed, 57 insertions(+), 40 deletions(-) diff --git a/rust/src/market/context.rs b/rust/src/market/context.rs index 3a54d220a..f1e19c268 100644 --- a/rust/src/market/context.rs +++ b/rust/src/market/context.rs @@ -305,16 +305,18 @@ impl MarketContext { #[serde(skip_serializing_if = "Option::is_none")] date: Option, } - self.post( - "/v1/quote/market/stock-events", - Body { - limit, - sort, - markets, - date, - }, - ) - .await + let raw: serde_json::Value = self + .post( + "/v1/quote/market/stock-events", + Body { + limit, + sort, + markets, + date, + }, + ) + .await?; + Ok(StockEventsResponse { data: raw }) } // ── rank_categories ─────────────────────────────────────────── @@ -325,7 +327,10 @@ impl MarketContext { pub async fn rank_categories(&self) -> Result { #[derive(Serialize)] struct Empty {} - self.get("/v1/quote/market/rank/categories", Empty {}).await + let raw: serde_json::Value = self + .get("/v1/quote/market/rank/categories", Empty {}) + .await?; + Ok(RankCategoriesResponse { data: raw }) } // ── rank_list ───────────────────────────────────────────────── @@ -344,14 +349,16 @@ impl MarketContext { delay_bmp: &'static str, need_article: &'static str, } - self.get( - "/v1/quote/market/rank/list", - Query { - key: key.into(), - delay_bmp: "false", - need_article: if need_article { "true" } else { "false" }, - }, - ) - .await + let raw: serde_json::Value = self + .get( + "/v1/quote/market/rank/list", + Query { + key: key.into(), + delay_bmp: "false", + need_article: if need_article { "true" } else { "false" }, + }, + ) + .await?; + Ok(RankListResponse { data: raw }) } } diff --git a/rust/src/quote/context.rs b/rust/src/quote/context.rs index c4870e3f0..ea6130b54 100644 --- a/rust/src/quote/context.rs +++ b/rust/src/quote/context.rs @@ -2000,11 +2000,11 @@ impl QuoteContext { last_timestamp: ts.to_string(), count, }) - .response::>() + .response::>() .send() .with_subscriber(self.0.log_subscriber.clone()) .await?; - Ok(resp.0) + Ok(ShortPositionsResponse { data: resp.0 }) } // ── option_volume ───────────────────────────────────────────── @@ -2105,11 +2105,11 @@ impl QuoteContext { last_timestamp: ts.to_string(), page_size: count.to_string(), }) - .response::>() + .response::>() .send() .with_subscriber(self.0.log_subscriber.clone()) .await?; - Ok(resp.0) + Ok(ShortTradesResponse { data: resp.0 }) } // ── update_pinned ───────────────────────────────────────────── diff --git a/rust/src/screener/context.rs b/rust/src/screener/context.rs index 12137da56..d17746aaf 100644 --- a/rust/src/screener/context.rs +++ b/rust/src/screener/context.rs @@ -90,8 +90,10 @@ impl ScreenerContext { ) -> Result { #[derive(Serialize)] struct Empty {} - self.get("/v1/quote/screener/strategies/recommend", Empty {}) - .await + let raw: serde_json::Value = self + .get("/v1/quote/screener/strategies/recommend", Empty {}) + .await?; + Ok(ScreenerRecommendStrategiesResponse { data: raw }) } // ── screener_user_strategies ────────────────────────────────── @@ -102,8 +104,10 @@ impl ScreenerContext { pub async fn screener_user_strategies(&self) -> Result { #[derive(Serialize)] struct Empty {} - self.get("/v1/quote/screener/strategies/mine", Empty {}) - .await + let raw: serde_json::Value = self + .get("/v1/quote/screener/strategies/mine", Empty {}) + .await?; + Ok(ScreenerUserStrategiesResponse { data: raw }) } // ── screener_strategy ───────────────────────────────────────── @@ -116,7 +120,10 @@ impl ScreenerContext { struct Query { id: i64, } - self.get("/v1/quote/screener/strategy", Query { id }).await + let raw: serde_json::Value = self + .get("/v1/quote/screener/strategy", Query { id }) + .await?; + Ok(ScreenerStrategyResponse { data: raw }) } // ── screener_search ─────────────────────────────────────────── @@ -143,16 +150,18 @@ impl ScreenerContext { page: u32, size: u32, } - self.post( - "/v1/quote/screener/search", - Body { - market: market.into(), - strategy_id, - page, - size, - }, - ) - .await + let raw: serde_json::Value = self + .post( + "/v1/quote/screener/search", + Body { + market: market.into(), + strategy_id, + page, + size, + }, + ) + .await?; + Ok(ScreenerSearchResponse { data: raw }) } // ── screener_indicators ─────────────────────────────────────── @@ -163,6 +172,7 @@ impl ScreenerContext { pub async fn screener_indicators(&self) -> Result { #[derive(Serialize)] struct Empty {} - self.get("/v1/quote/screener/indicators", Empty {}).await + let raw: serde_json::Value = self.get("/v1/quote/screener/indicators", Empty {}).await?; + Ok(ScreenerIndicatorsResponse { data: raw }) } } From 60ee96b85c88b4344fb90f8e0828fc83750e3a54 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=A2=81=E7=AB=A0=E6=B4=AA?= Date: Thu, 21 May 2026 10:11:06 +0800 Subject: [PATCH 05/12] =?UTF-8?q?rename(market):=20stock=5Fevents=20?= =?UTF-8?q?=E2=86=92=20top=5Fmovers=20across=20all=20language=20SDKs?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Renames method, response type, C function, and C type across Rust, Python, Node.js, Java, C, and C++ SDKs: - `stock_events` → `top_movers` (Rust/Python/Node.js/Java/C/C++ method) - `StockEventsResponse` → `TopMoversResponse` (Rust/Python/Node.js type) - `CStockEventsResponse` → `CTopMoversResponse` (C Rust type) - `lb_market_context_stock_events` → `lb_market_context_top_movers` (C extern fn) - `lb_stock_events_response_t` → `lb_top_movers_response_t` (C header type) C header (`longbridge.h`) regenerated via `cargo build -p longbridge-c`. Node.js type definitions regenerated via `npm run build:debug`. Co-Authored-By: Claude Sonnet 4.6 (1M context) --- CHANGELOG.md | 5 ++-- c/cbindgen.toml | 4 +-- c/csrc/include/longbridge.h | 30 +++++++++---------- c/src/market_context/context.rs | 12 ++++---- c/src/market_context/types.rs | 22 +++++++------- cpp/include/market_context.hpp | 12 ++++---- cpp/src/market_context.cpp | 4 +-- .../main/java/com/longbridge/SdkNative.java | 1 + .../com/longbridge/market/MarketContext.java | 5 ++++ .../longbridge/market/TopMoversOptions.java | 29 ++++++++++++++++++ .../longbridge/market/TopMoversResponse.java | 7 +++++ java/src/init.rs | 4 +-- java/src/market_context.rs | 4 +-- java/src/types/classes.rs | 6 ++-- nodejs/index.d.ts | 19 +++++++----- nodejs/src/market/context.rs | 9 +++--- nodejs/src/market/types.rs | 12 ++++---- python/pysrc/longbridge/openapi.pyi | 14 ++++----- python/src/market/context.rs | 9 +++--- python/src/market/context_async.rs | 9 +++--- python/src/market/mod.rs | 2 +- python/src/market/types.rs | 12 ++++---- rust/src/blocking/market.rs | 11 +++---- rust/src/market/context.rs | 11 +++---- rust/src/market/types.rs | 12 ++++---- 25 files changed, 158 insertions(+), 107 deletions(-) create mode 100644 java/javasrc/src/main/java/com/longbridge/market/TopMoversOptions.java create mode 100644 java/javasrc/src/main/java/com/longbridge/market/TopMoversResponse.java diff --git a/CHANGELOG.md b/CHANGELOG.md index 93836428a..4bfd4dec0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -20,8 +20,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Breaking changes -- **All languages:** `hk_short_positions` removed — use `short_positions(symbol, count)`. -- **All languages:** `ShortPositionsResponse` raw JSON only; old typed fields removed. +- **All languages:** `MarketContext::stock_events` renamed to `top_movers`; `StockEventsResponse` → `TopMoversResponse`. +- **All languages:** `hk_short_positions` removed — use `short_positions(symbol, count)` which auto-detects HK/US. +- **All languages:** `ShortPositionsResponse` is now raw JSON; old typed fields removed. # [4.1.0] diff --git a/c/cbindgen.toml b/c/cbindgen.toml index 4dbab9104..4c119f415 100644 --- a/c/cbindgen.toml +++ b/c/cbindgen.toml @@ -307,7 +307,7 @@ cpp_compat = true "CShareholderDetailResponse" = "lb_shareholder_detail_response_t" "CValuationComparisonResponse" = "lb_valuation_comparison_response_t" # MarketContext new types -"CStockEventsResponse" = "lb_stock_events_response_t" +"CTopMoversResponse" = "lb_top_movers_response_t" "CRankCategoriesResponse" = "lb_rank_categories_response_t" "CRankListResponse" = "lb_rank_list_response_t" # ScreenerContext @@ -436,7 +436,7 @@ include = [ # FundamentalContext new types "CShareholderTopResponse", "CShareholderDetailResponse", "CValuationComparisonResponse", # MarketContext new types - "CStockEventsResponse", "CRankCategoriesResponse", "CRankListResponse", + "CTopMoversResponse", "CRankCategoriesResponse", "CRankListResponse", # ScreenerContext "CScreenerContext", "CScreenerRecommendStrategiesResponse", "CScreenerUserStrategiesResponse", diff --git a/c/csrc/include/longbridge.h b/c/csrc/include/longbridge.h index 7956caa4b..474d0adac 100644 --- a/c/csrc/include/longbridge.h +++ b/c/csrc/include/longbridge.h @@ -7853,14 +7853,14 @@ typedef struct lb_valuation_comparison_response_t { } lb_valuation_comparison_response_t; /** - * Stock events response. `data` is a NUL-terminated JSON string. + * Top movers response. `data` is a NUL-terminated JSON string. */ -typedef struct lb_stock_events_response_t { +typedef struct lb_top_movers_response_t { /** - * Raw stock events data as a JSON string + * Raw top movers data as a JSON string */ const char *data; -} lb_stock_events_response_t; +} lb_top_movers_response_t; /** * Rank categories response. `data` is a NUL-terminated JSON string. @@ -8747,18 +8747,18 @@ void lb_market_context_constituent(const struct lb_market_context_t *ctx, void *userdata); /** - * Get stock events across one or more markets. - * Pass markets as a NULL-terminated array of C strings. - * Returns `CStockEventsResponse`. + * Get top movers (stocks with unusual price movements) across one or more + * markets. Pass markets as a NULL-terminated array of C strings. + * Returns `CTopMoversResponse`. */ -void lb_market_context_stock_events(const struct lb_market_context_t *ctx, - const char *const *markets, - uintptr_t num_markets, - uint32_t sort, - const char *date, - uint32_t limit, - lb_async_callback_t callback, - void *userdata); +void lb_market_context_top_movers(const struct lb_market_context_t *ctx, + const char *const *markets, + uintptr_t num_markets, + uint32_t sort, + const char *date, + uint32_t limit, + lb_async_callback_t callback, + void *userdata); /** * Get all available rank category keys and labels. diff --git a/c/src/market_context/context.rs b/c/src/market_context/context.rs index 84d74a2af..fc223ff88 100644 --- a/c/src/market_context/context.rs +++ b/c/src/market_context/context.rs @@ -216,11 +216,11 @@ pub unsafe extern "C" fn lb_market_context_constituent( }); } -/// Get stock events across one or more markets. -/// Pass markets as a NULL-terminated array of C strings. -/// Returns `CStockEventsResponse`. +/// Get top movers (stocks with unusual price movements) across one or more +/// markets. Pass markets as a NULL-terminated array of C strings. +/// Returns `CTopMoversResponse`. #[unsafe(no_mangle)] -pub unsafe extern "C" fn lb_market_context_stock_events( +pub unsafe extern "C" fn lb_market_context_top_movers( ctx: *const CMarketContext, markets: *const *const c_char, num_markets: usize, @@ -240,8 +240,8 @@ pub unsafe extern "C" fn lb_market_context_stock_events( Some(cstr_to_rust(date)) }; execute_async(callback, ctx, userdata, async move { - let resp: CCow = CCow::new(CStockEventsResponseOwned::from( - ctx_inner.stock_events(markets, sort, date, limit).await?, + let resp: CCow = CCow::new(CTopMoversResponseOwned::from( + ctx_inner.top_movers(markets, sort, date, limit).await?, )); Ok(resp) }); diff --git a/c/src/market_context/types.rs b/c/src/market_context/types.rs index 0b13ca289..047ac4f94 100644 --- a/c/src/market_context/types.rs +++ b/c/src/market_context/types.rs @@ -5,7 +5,7 @@ use longbridge::market::{ BrokerHoldingChanges, BrokerHoldingDailyHistory, BrokerHoldingDailyItem, BrokerHoldingDetail, BrokerHoldingDetailItem, BrokerHoldingEntry, BrokerHoldingTop, ConstituentStock, IndexConstituents, MarketStatusResponse, MarketTimeItem, RankCategoriesResponse, - RankListResponse, StockEventsResponse, TradePriceLevel, TradeStatistics, TradeStatsResponse, + RankListResponse, TopMoversResponse, TradePriceLevel, TradeStatistics, TradeStatsResponse, }; use crate::types::{CMarket, CString, CVec, ToFFI}; @@ -958,30 +958,30 @@ impl ToFFI for CIndexConstituentsOwned { } } -// ── StockEventsResponse ─────────────────────────────────────────── +// ── TopMoversResponse ───────────────────────────────────────────── -/// Stock events response. `data` is a NUL-terminated JSON string. +/// Top movers response. `data` is a NUL-terminated JSON string. #[repr(C)] -pub struct CStockEventsResponse { - /// Raw stock events data as a JSON string +pub struct CTopMoversResponse { + /// Raw top movers data as a JSON string pub data: *const c_char, } -pub(crate) struct CStockEventsResponseOwned { +pub(crate) struct CTopMoversResponseOwned { data: CString, } -impl From for CStockEventsResponseOwned { - fn from(v: StockEventsResponse) -> Self { +impl From for CTopMoversResponseOwned { + fn from(v: TopMoversResponse) -> Self { let json = serde_json::to_string(&v.data).unwrap_or_default(); Self { data: json.into() } } } -impl ToFFI for CStockEventsResponseOwned { - type FFIType = CStockEventsResponse; +impl ToFFI for CTopMoversResponseOwned { + type FFIType = CTopMoversResponse; fn to_ffi_type(&self) -> Self::FFIType { - CStockEventsResponse { + CTopMoversResponse { data: self.data.to_ffi_type(), } } diff --git a/cpp/include/market_context.hpp b/cpp/include/market_context.hpp index c3b2bc095..a4bccc80f 100644 --- a/cpp/include/market_context.hpp +++ b/cpp/include/market_context.hpp @@ -83,12 +83,12 @@ class MarketContext void constituent(const std::string& symbol, AsyncCallback callback) const; - /// Get stock events across one or more markets (raw JSON string) - void stock_events(const std::vector& markets, - uint32_t sort, - const std::string* date, - uint32_t limit, - AsyncCallback callback) const; + /// Get top movers (stocks with unusual price movements) across one or more markets (raw JSON string) + void top_movers(const std::vector& markets, + uint32_t sort, + const std::string* date, + uint32_t limit, + AsyncCallback callback) const; /// Get all available rank category keys and labels (raw JSON string) void rank_categories(AsyncCallback callback) const; diff --git a/cpp/src/market_context.cpp b/cpp/src/market_context.cpp index 0af8fbc6f..c68e9ee9d 100644 --- a/cpp/src/market_context.cpp +++ b/cpp/src/market_context.cpp @@ -87,11 +87,11 @@ void MarketContext::constituent(const std::string& symbol, AsyncCallback(mctx,std::move(status),nullptr));} \ }, new AsyncCallback(callback)) -void MarketContext::stock_events(const std::vector& markets, uint32_t sort, const std::string* date, uint32_t limit, AsyncCallback callback) const { +void MarketContext::top_movers(const std::vector& markets, uint32_t sort, const std::string* date, uint32_t limit, AsyncCallback callback) const { std::vector mptrs; for (const auto& m : markets) mptrs.push_back(m.c_str()); const char* date_str = date ? date->c_str() : nullptr; - M_JSON(lb_market_context_stock_events, lb_stock_events_response_t, ctx_, mptrs.data(), mptrs.size(), sort, date_str, limit); + M_JSON(lb_market_context_top_movers, lb_top_movers_response_t, ctx_, mptrs.data(), mptrs.size(), sort, date_str, limit); } void MarketContext::rank_categories(AsyncCallback callback) const { diff --git a/java/javasrc/src/main/java/com/longbridge/SdkNative.java b/java/javasrc/src/main/java/com/longbridge/SdkNative.java index 5958eaf1f..d0eafb613 100644 --- a/java/javasrc/src/main/java/com/longbridge/SdkNative.java +++ b/java/javasrc/src/main/java/com/longbridge/SdkNative.java @@ -322,6 +322,7 @@ public static native void tradeContextEstimateMaxPurchaseQuantity(long context, public static native void marketContextTradeStats(long context, String symbol, AsyncCallback callback); public static native void marketContextAnomaly(long context, String market, AsyncCallback callback); public static native void marketContextConstituent(long context, String symbol, AsyncCallback callback); + public static native void marketContextTopMovers(long context, com.longbridge.market.TopMoversOptions opts, AsyncCallback callback); // ── FundamentalContext ──────────────────────────────────────── diff --git a/java/javasrc/src/main/java/com/longbridge/market/MarketContext.java b/java/javasrc/src/main/java/com/longbridge/market/MarketContext.java index 3cfecfd2b..6c17ec4d9 100644 --- a/java/javasrc/src/main/java/com/longbridge/market/MarketContext.java +++ b/java/javasrc/src/main/java/com/longbridge/market/MarketContext.java @@ -65,4 +65,9 @@ public CompletableFuture getAnomaly(String market) throws OpenA public CompletableFuture getConstituent(String symbol) throws OpenApiException { return AsyncCallback.executeTask((callback) -> SdkNative.marketContextConstituent(raw, symbol, callback)); } + + /** Get top movers (stocks with unusual price movements) across one or more markets */ + public CompletableFuture getTopMovers(TopMoversOptions opts) throws OpenApiException { + return AsyncCallback.executeTask((callback) -> SdkNative.marketContextTopMovers(raw, opts, callback)); + } } diff --git a/java/javasrc/src/main/java/com/longbridge/market/TopMoversOptions.java b/java/javasrc/src/main/java/com/longbridge/market/TopMoversOptions.java new file mode 100644 index 000000000..7db9b9757 --- /dev/null +++ b/java/javasrc/src/main/java/com/longbridge/market/TopMoversOptions.java @@ -0,0 +1,29 @@ +package com.longbridge.market; + +/** Options for {@link MarketContext#getTopMovers}. */ +public class TopMoversOptions { + /** + * Market list, e.g. {@code ["HK", "US", "CN", "SG"]}. + * Pass {@code null} or an empty array to return all markets. + */ + public String[] markets; + + /** + * Sort order. + * 0 = time (newest first), 1 = price change, 2 = hotness (default). + * Pass {@code null} to use the server default. + */ + public Integer sort; + + /** + * Target date in {@code "YYYY-MM-DD"} format. + * Pass {@code null} to return the latest data. + */ + public String date; + + /** + * Maximum number of results to return. + * Pass {@code null} to use the server default (20). + */ + public Integer limit; +} diff --git a/java/javasrc/src/main/java/com/longbridge/market/TopMoversResponse.java b/java/javasrc/src/main/java/com/longbridge/market/TopMoversResponse.java new file mode 100644 index 000000000..4f2898ebf --- /dev/null +++ b/java/javasrc/src/main/java/com/longbridge/market/TopMoversResponse.java @@ -0,0 +1,7 @@ +package com.longbridge.market; + +/** Response for {@link MarketContext#getTopMovers}. Raw JSON payload. */ +public class TopMoversResponse { + /** Raw top movers data as a JSON string. */ + public String data; +} diff --git a/java/src/init.rs b/java/src/init.rs index 74642ac09..83961da30 100644 --- a/java/src/init.rs +++ b/java/src/init.rs @@ -386,8 +386,8 @@ pub extern "system" fn Java_com_longbridge_SdkNative_init<'a>( longbridge::fundamental::ShareholderTopResponse, longbridge::fundamental::ShareholderDetailResponse, longbridge::fundamental::ValuationComparisonResponse, - // MarketContext: stock events / rank - longbridge::market::StockEventsResponse, + // MarketContext: top movers / rank + longbridge::market::TopMoversResponse, longbridge::market::RankCategoriesResponse, longbridge::market::RankListResponse, // ScreenerContext diff --git a/java/src/market_context.rs b/java/src/market_context.rs index c6b6a2055..a19651cfd 100644 --- a/java/src/market_context.rs +++ b/java/src/market_context.rs @@ -184,7 +184,7 @@ symbol_method!( market_method!(Java_com_longbridge_SdkNative_marketContextAnomaly, anomaly); #[unsafe(no_mangle)] -pub unsafe extern "system" fn Java_com_longbridge_SdkNative_marketContextStockEvents( +pub unsafe extern "system" fn Java_com_longbridge_SdkNative_marketContextTopMovers( mut env: JNIEnv, _class: JClass, context: i64, @@ -201,7 +201,7 @@ pub unsafe extern "system" fn Java_com_longbridge_SdkNative_marketContextStockEv let limit_opt: Option = get_field(env, &opts, "limit")?; let limit = limit_opt.map(i32::from).unwrap_or(20) as u32; async_util::execute(env, callback, async move { - let resp = context.ctx.stock_events(markets, sort, date, limit).await?; + let resp = context.ctx.top_movers(markets, sort, date, limit).await?; Ok(resp) })?; Ok(()) diff --git a/java/src/types/classes.rs b/java/src/types/classes.rs index f73db77ec..22c621fe1 100644 --- a/java/src/types/classes.rs +++ b/java/src/types/classes.rs @@ -2628,11 +2628,11 @@ impl_java_class!( [data] ); -// ── MarketContext: stock events / rank ──────────────────────────── +// ── MarketContext: top movers / rank ────────────────────────────── impl_java_class!( - "com/longbridge/market/StockEventsResponse", - longbridge::market::StockEventsResponse, + "com/longbridge/market/TopMoversResponse", + longbridge::market::TopMoversResponse, [data] ); diff --git a/nodejs/index.d.ts b/nodejs/index.d.ts index b4d96743b..63e6e4d0e 100644 --- a/nodejs/index.d.ts +++ b/nodejs/index.d.ts @@ -771,8 +771,11 @@ export declare class MarketContext { anomaly(market: string): Promise /** Get index constituent stocks */ constituent(symbol: string): Promise - /** Get stock events across one or more markets */ - stockEvents(markets: Array, sort: number, date: string | undefined | null, limit: number): Promise + /** + * Get top movers (stocks with unusual price movements) across one or more + * markets + */ + topMovers(markets: Array, sort: number, date: string | undefined | null, limit: number): Promise /** Get all available rank category keys and labels */ rankCategories(): Promise /** Get a ranked list of securities for the given category key */ @@ -5329,12 +5332,6 @@ export declare const enum StatementType { Monthly = 2 } -/** Stock events response. `data` is a JSON string. */ -export interface StockEventsResponse { - /** Raw stock events data (JSON string) */ - data: string -} - /** * Stock ratings response. * @@ -5423,6 +5420,12 @@ export declare const enum TopicType { Private = 0 } +/** Top movers response. `data` is a JSON string. */ +export interface TopMoversResponse { + /** Raw top movers data (JSON string) */ + data: string +} + /** Trade direction */ export declare const enum TradeDirection { /** Neutral */ diff --git a/nodejs/src/market/context.rs b/nodejs/src/market/context.rs index 351445cf7..15f0b159f 100644 --- a/nodejs/src/market/context.rs +++ b/nodejs/src/market/context.rs @@ -123,18 +123,19 @@ impl MarketContext { .into()) } - /// Get stock events across one or more markets + /// Get top movers (stocks with unusual price movements) across one or more + /// markets #[napi] - pub async fn stock_events( + pub async fn top_movers( &self, markets: Vec, sort: u32, date: Option, limit: u32, - ) -> Result { + ) -> Result { Ok(self .ctx - .stock_events(markets, sort, date, limit) + .top_movers(markets, sort, date, limit) .await .map_err(ErrorNewType)? .into()) diff --git a/nodejs/src/market/types.rs b/nodejs/src/market/types.rs index b3db75c72..77dd387a5 100644 --- a/nodejs/src/market/types.rs +++ b/nodejs/src/market/types.rs @@ -565,18 +565,18 @@ impl From for lb::AhPremiumPeriod { } } -// ── StockEventsResponse ─────────────────────────────────────────── +// ── TopMoversResponse ───────────────────────────────────────────── -/// Stock events response. `data` is a JSON string. +/// Top movers response. `data` is a JSON string. #[napi_derive::napi(object)] #[derive(Debug, Clone)] -pub struct StockEventsResponse { - /// Raw stock events data (JSON string) +pub struct TopMoversResponse { + /// Raw top movers data (JSON string) pub data: String, } -impl From for StockEventsResponse { - fn from(v: lb::StockEventsResponse) -> Self { +impl From for TopMoversResponse { + fn from(v: lb::TopMoversResponse) -> Self { Self { data: v.data.to_string(), } diff --git a/python/pysrc/longbridge/openapi.pyi b/python/pysrc/longbridge/openapi.pyi index ad774e421..e59018eab 100644 --- a/python/pysrc/longbridge/openapi.pyi +++ b/python/pysrc/longbridge/openapi.pyi @@ -10332,15 +10332,15 @@ class MarketContext: """ ... - def stock_events( + def top_movers( self, markets: List[str], sort: int = 0, date: Optional[str] = None, limit: int = 20, - ) -> "StockEventsResponse": + ) -> "TopMoversResponse": """ - Get stock events across one or more markets. + Get top movers (stocks with unusual price movements) across one or more markets. Args: markets: List of market codes, e.g. ``["HK", "US"]`` @@ -10349,7 +10349,7 @@ class MarketContext: limit: Max records to return Returns: - :class:`StockEventsResponse` with raw JSON data + :class:`TopMoversResponse` with raw JSON data """ ... @@ -10380,11 +10380,11 @@ class MarketContext: # ── MarketContext new response types ────────────────────────────── -class StockEventsResponse: - """Stock events response. ``data`` is a Python dict/list from JSON.""" +class TopMoversResponse: + """Top movers response. ``data`` is a Python dict/list from JSON.""" data: object - """Raw stock events data (JSON object / list)""" + """Raw top movers data (JSON object / list)""" class RankCategoriesResponse: diff --git a/python/src/market/context.rs b/python/src/market/context.rs index 75f314a34..b1efb188f 100644 --- a/python/src/market/context.rs +++ b/python/src/market/context.rs @@ -100,18 +100,19 @@ impl MarketContext { Ok(self.ctx.constituent(symbol).map_err(ErrorNewType)?.into()) } - /// Get stock events across one or more markets. + /// Get top movers (stocks with unusual price movements) across one or more + /// markets. #[pyo3(signature = (markets, sort = 0, date = None, limit = 20))] - fn stock_events( + fn top_movers( &self, markets: Vec, sort: u32, date: Option, limit: u32, - ) -> PyResult { + ) -> PyResult { Ok(self .ctx - .stock_events(markets, sort, date, limit) + .top_movers(markets, sort, date, limit) .map_err(ErrorNewType)? .into()) } diff --git a/python/src/market/context_async.rs b/python/src/market/context_async.rs index 3621bc6d9..0725bd9dc 100644 --- a/python/src/market/context_async.rs +++ b/python/src/market/context_async.rs @@ -138,9 +138,10 @@ impl AsyncMarketContext { .map(|b| b.unbind()) } - /// Get stock events across one or more markets. Returns awaitable. + /// Get top movers (stocks with unusual price movements) across one or more + /// markets. Returns awaitable. #[pyo3(signature = (markets, sort = 0, date = None, limit = 20))] - fn stock_events( + fn top_movers( &self, py: Python<'_>, markets: Vec, @@ -150,8 +151,8 @@ impl AsyncMarketContext { ) -> PyResult> { let ctx = self.ctx.clone(); pyo3_async_runtimes::tokio::future_into_py(py, async move { - Ok(StockEventsResponse::from( - ctx.stock_events(markets, sort, date, limit) + Ok(TopMoversResponse::from( + ctx.top_movers(markets, sort, date, limit) .await .map_err(ErrorNewType)?, )) diff --git a/python/src/market/mod.rs b/python/src/market/mod.rs index f535941d6..a6ed2a09a 100644 --- a/python/src/market/mod.rs +++ b/python/src/market/mod.rs @@ -27,7 +27,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::()?; parent.add_class::()?; parent.add_class::()?; diff --git a/python/src/market/types.rs b/python/src/market/types.rs index f90afb3e9..d05d9a9bc 100644 --- a/python/src/market/types.rs +++ b/python/src/market/types.rs @@ -1,21 +1,21 @@ use longbridge::market::types as lb; use pyo3::prelude::*; -// ── StockEventsResponse ─────────────────────────────────────────── +// ── TopMoversResponse ───────────────────────────────────────────── -/// Stock events response. +/// Top movers response. /// /// `data` is the raw JSON returned by the API preserved as a Python /// object (dict / list / etc.). #[pyclass(get_all, skip_from_py_object)] #[derive(Debug, Clone)] -pub(crate) struct StockEventsResponse { - /// Raw stock events data (JSON object) +pub(crate) struct TopMoversResponse { + /// Raw top movers data (JSON object) pub data: crate::fundamental::types::JsonValue, } -impl From for StockEventsResponse { - fn from(v: lb::StockEventsResponse) -> Self { +impl From for TopMoversResponse { + fn from(v: lb::TopMoversResponse) -> Self { Self { data: crate::fundamental::types::JsonValue(v.data), } diff --git a/rust/src/blocking/market.rs b/rust/src/blocking/market.rs index ad486c3d6..f47cbe94e 100644 --- a/rust/src/blocking/market.rs +++ b/rust/src/blocking/market.rs @@ -11,7 +11,7 @@ use crate::{ AhPremiumIntraday, AhPremiumKlines, AhPremiumPeriod, AnomalyResponse, BrokerHoldingDailyHistory, BrokerHoldingDetail, BrokerHoldingPeriod, BrokerHoldingTop, IndexConstituents, MarketStatusResponse, RankCategoriesResponse, RankListResponse, - StockEventsResponse, TradeStatsResponse, + TopMoversResponse, TradeStatsResponse, }, }, }; @@ -114,16 +114,17 @@ impl MarketContextSync { .call(move |ctx| async move { ctx.constituent(symbol).await }) } - /// Get stock events across one or more markets - pub fn stock_events( + /// Get top movers (stocks with unusual price movements) across one or more + /// markets + pub fn top_movers( &self, markets: Vec, sort: u32, date: Option, limit: u32, - ) -> Result { + ) -> Result { self.rt - .call(move |ctx| async move { ctx.stock_events(markets, sort, date, limit).await }) + .call(move |ctx| async move { ctx.top_movers(markets, sort, date, limit).await }) } /// Get all available rank category keys and labels diff --git a/rust/src/market/context.rs b/rust/src/market/context.rs index f1e19c268..a4414d610 100644 --- a/rust/src/market/context.rs +++ b/rust/src/market/context.rs @@ -282,21 +282,22 @@ impl MarketContext { .await } - // ── stock_events ────────────────────────────────────────────── + // ── top_movers ──────────────────────────────────────────────── - /// Get stock events across one or more markets. + /// Get top movers (stocks with unusual price movements) across one or more + /// markets. /// /// Path: `POST /v1/quote/market/stock-events` /// /// `sort` is the sort order code (0 = ascending, 1 = descending). /// `date` is an optional date filter in `"YYYY-MM-DD"` format. - pub async fn stock_events( + pub async fn top_movers( &self, markets: Vec, sort: u32, date: Option, limit: u32, - ) -> Result { + ) -> Result { #[derive(Debug, Serialize)] struct Body { limit: u32, @@ -316,7 +317,7 @@ impl MarketContext { }, ) .await?; - Ok(StockEventsResponse { data: raw }) + Ok(TopMoversResponse { data: raw }) } // ── rank_categories ─────────────────────────────────────────── diff --git a/rust/src/market/types.rs b/rust/src/market/types.rs index bb77bf4ad..d533659fd 100644 --- a/rust/src/market/types.rs +++ b/rust/src/market/types.rs @@ -331,15 +331,15 @@ pub struct ConstituentStock { pub trade_status: i32, } -// ── stock_events ────────────────────────────────────────────────── +// ── top_movers ──────────────────────────────────────────────────── -/// Response for [`crate::MarketContext::stock_events`] +/// Response for [`crate::MarketContext::top_movers`] /// -/// The raw data contains stock events from all requested markets. The -/// exact structure varies so the payload is preserved as raw JSON. +/// The raw data contains top-movers stock events from all requested markets. +/// The exact structure varies so the payload is preserved as raw JSON. #[derive(Debug, Clone, Serialize, Deserialize)] -pub struct StockEventsResponse { - /// Raw stock events data +pub struct TopMoversResponse { + /// Raw top movers data pub data: serde_json::Value, } From f18a5aeaf189cfc5d0c130751f11039c88cc7238 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=A2=81=E7=AB=A0=E6=B4=AA?= Date: Thu, 21 May 2026 13:37:44 +0800 Subject: [PATCH 06/12] fix(java): complete Java SDK for all 13 new APIs Previously only TopMovers was wired up. Now all missing pieces are in place: SdkNative.java: - Added native declarations for: marketContextRankCategories, marketContextRankList, newScreenerContext, freeScreenerContext, screenerContextRecommendStrategies/UserStrategies/Strategy/Search/Indicators, fundamentalContextShareholderTop/Detail/ValuationComparison, quoteContextShortTrades New Java source files: - fundamental/: ShareholderTopResponse, ShareholderDetailResponse, ValuationComparisonResponse, ShareholderDetailOptions, ValuationComparisonOptions - market/: RankCategoriesResponse, RankListResponse, RankListOptions - quote/: ShortTradesResponse, ShortTradesOptions - screener/: ScreenerContext (new package), ScreenerRecommendStrategiesResponse, ScreenerUserStrategiesResponse, ScreenerStrategyResponse, ScreenerStrategyOptions, ScreenerSearchResponse, ScreenerSearchOptions, ScreenerIndicatorsResponse Updated Java context classes: - FundamentalContext.java: getShareholderTop, getShareholderDetail, getValuationComparison - MarketContext.java: getRankCategories, getRankList - QuoteContext.java: getShortTrades Rust side (java/src/): - types/classes.rs: impl_java_class! entries for all new response types - init.rs: registered all new Rust types Co-Authored-By: Claude Sonnet 4.6 (1M context) --- .../main/java/com/longbridge/SdkNative.java | 37 +++++++++++++++ .../fundamental/FundamentalContext.java | 21 +++++++++ .../fundamental/ShareholderDetailOptions.java | 9 ++++ .../ShareholderDetailResponse.java | 7 +++ .../fundamental/ShareholderTopResponse.java | 7 +++ .../ValuationComparisonOptions.java | 11 +++++ .../ValuationComparisonResponse.java | 7 +++ .../com/longbridge/market/MarketContext.java | 10 ++++ .../market/RankCategoriesResponse.java | 7 +++ .../longbridge/market/RankListOptions.java | 9 ++++ .../longbridge/market/RankListResponse.java | 7 +++ .../com/longbridge/quote/QuoteContext.java | 7 +++ .../longbridge/quote/ShortTradesOptions.java | 9 ++++ .../longbridge/quote/ShortTradesResponse.java | 7 +++ .../longbridge/screener/ScreenerContext.java | 47 +++++++++++++++++++ .../screener/ScreenerIndicatorsResponse.java | 7 +++ .../ScreenerRecommendStrategiesResponse.java | 7 +++ .../screener/ScreenerSearchOptions.java | 13 +++++ .../screener/ScreenerSearchResponse.java | 7 +++ .../screener/ScreenerStrategyOptions.java | 7 +++ .../screener/ScreenerStrategyResponse.java | 7 +++ .../ScreenerUserStrategiesResponse.java | 7 +++ 22 files changed, 257 insertions(+) create mode 100644 java/javasrc/src/main/java/com/longbridge/fundamental/ShareholderDetailOptions.java create mode 100644 java/javasrc/src/main/java/com/longbridge/fundamental/ShareholderDetailResponse.java create mode 100644 java/javasrc/src/main/java/com/longbridge/fundamental/ShareholderTopResponse.java create mode 100644 java/javasrc/src/main/java/com/longbridge/fundamental/ValuationComparisonOptions.java create mode 100644 java/javasrc/src/main/java/com/longbridge/fundamental/ValuationComparisonResponse.java create mode 100644 java/javasrc/src/main/java/com/longbridge/market/RankCategoriesResponse.java create mode 100644 java/javasrc/src/main/java/com/longbridge/market/RankListOptions.java create mode 100644 java/javasrc/src/main/java/com/longbridge/market/RankListResponse.java create mode 100644 java/javasrc/src/main/java/com/longbridge/quote/ShortTradesOptions.java create mode 100644 java/javasrc/src/main/java/com/longbridge/quote/ShortTradesResponse.java create mode 100644 java/javasrc/src/main/java/com/longbridge/screener/ScreenerContext.java create mode 100644 java/javasrc/src/main/java/com/longbridge/screener/ScreenerIndicatorsResponse.java create mode 100644 java/javasrc/src/main/java/com/longbridge/screener/ScreenerRecommendStrategiesResponse.java create mode 100644 java/javasrc/src/main/java/com/longbridge/screener/ScreenerSearchOptions.java create mode 100644 java/javasrc/src/main/java/com/longbridge/screener/ScreenerSearchResponse.java create mode 100644 java/javasrc/src/main/java/com/longbridge/screener/ScreenerStrategyOptions.java create mode 100644 java/javasrc/src/main/java/com/longbridge/screener/ScreenerStrategyResponse.java create mode 100644 java/javasrc/src/main/java/com/longbridge/screener/ScreenerUserStrategiesResponse.java diff --git a/java/javasrc/src/main/java/com/longbridge/SdkNative.java b/java/javasrc/src/main/java/com/longbridge/SdkNative.java index d0eafb613..777647bae 100644 --- a/java/javasrc/src/main/java/com/longbridge/SdkNative.java +++ b/java/javasrc/src/main/java/com/longbridge/SdkNative.java @@ -324,6 +324,43 @@ public static native void tradeContextEstimateMaxPurchaseQuantity(long context, public static native void marketContextConstituent(long context, String symbol, AsyncCallback callback); public static native void marketContextTopMovers(long context, com.longbridge.market.TopMoversOptions opts, AsyncCallback callback); + public static native void marketContextRankCategories(long context, + AsyncCallback callback); + + public static native void marketContextRankList(long context, Object opts, + AsyncCallback callback); + + public static native long newScreenerContext(long config); + + public static native void freeScreenerContext(long context); + + public static native void screenerContextRecommendStrategies(long context, + AsyncCallback callback); + + public static native void screenerContextUserStrategies(long context, + AsyncCallback callback); + + public static native void screenerContextStrategy(long context, Object opts, + AsyncCallback callback); + + public static native void screenerContextSearch(long context, Object opts, + AsyncCallback callback); + + public static native void screenerContextIndicators(long context, + AsyncCallback callback); + + public static native void fundamentalContextShareholderTop(long context, + String symbol, AsyncCallback callback); + + public static native void fundamentalContextShareholderDetail(long context, + Object opts, AsyncCallback callback); + + public static native void fundamentalContextValuationComparison(long context, + Object opts, AsyncCallback callback); + + public static native void quoteContextShortTrades(long context, Object opts, + AsyncCallback callback); + // ── FundamentalContext ──────────────────────────────────────── public static native long newFundamentalContext(long config); diff --git a/java/javasrc/src/main/java/com/longbridge/fundamental/FundamentalContext.java b/java/javasrc/src/main/java/com/longbridge/fundamental/FundamentalContext.java index 2caee02d9..8c0a7299a 100644 --- a/java/javasrc/src/main/java/com/longbridge/fundamental/FundamentalContext.java +++ b/java/javasrc/src/main/java/com/longbridge/fundamental/FundamentalContext.java @@ -313,4 +313,25 @@ public CompletableFuture getFinancialReportSnapshot( SdkNative.fundamentalContextGetFinancialReportSnapshot(raw, opts, callback); }); } + + /** Get top 20 major shareholders with multi-period holdings. */ + public CompletableFuture getShareholderTop(String symbol) throws OpenApiException { + return AsyncCallback.executeTask((callback) -> { + SdkNative.fundamentalContextShareholderTop(raw, symbol, callback); + }); + } + + /** Get holding history and trade detail for a specific shareholder. */ + public CompletableFuture getShareholderDetail(ShareholderDetailOptions opts) throws OpenApiException { + return AsyncCallback.executeTask((callback) -> { + SdkNative.fundamentalContextShareholderDetail(raw, opts, callback); + }); + } + + /** Get valuation comparison between a symbol and optional peer symbols. */ + public CompletableFuture getValuationComparison(ValuationComparisonOptions opts) throws OpenApiException { + return AsyncCallback.executeTask((callback) -> { + SdkNative.fundamentalContextValuationComparison(raw, opts, callback); + }); + } } diff --git a/java/javasrc/src/main/java/com/longbridge/fundamental/ShareholderDetailOptions.java b/java/javasrc/src/main/java/com/longbridge/fundamental/ShareholderDetailOptions.java new file mode 100644 index 000000000..652d03a0f --- /dev/null +++ b/java/javasrc/src/main/java/com/longbridge/fundamental/ShareholderDetailOptions.java @@ -0,0 +1,9 @@ +package com.longbridge.fundamental; + +/** Options for {@link FundamentalContext#getShareholderDetail}. */ +public class ShareholderDetailOptions { + /** Security symbol, e.g. "AAPL.US" */ + public String symbol; + /** Shareholder object ID from getShareholderTop */ + public long objectId; +} diff --git a/java/javasrc/src/main/java/com/longbridge/fundamental/ShareholderDetailResponse.java b/java/javasrc/src/main/java/com/longbridge/fundamental/ShareholderDetailResponse.java new file mode 100644 index 000000000..bc1cd24c5 --- /dev/null +++ b/java/javasrc/src/main/java/com/longbridge/fundamental/ShareholderDetailResponse.java @@ -0,0 +1,7 @@ +package com.longbridge.fundamental; + +/** Response for {@link FundamentalContext#getShareholderDetail}. Contains raw JSON data. */ +public class ShareholderDetailResponse { + /** Raw JSON data string */ + public String data; +} diff --git a/java/javasrc/src/main/java/com/longbridge/fundamental/ShareholderTopResponse.java b/java/javasrc/src/main/java/com/longbridge/fundamental/ShareholderTopResponse.java new file mode 100644 index 000000000..f6f46d18c --- /dev/null +++ b/java/javasrc/src/main/java/com/longbridge/fundamental/ShareholderTopResponse.java @@ -0,0 +1,7 @@ +package com.longbridge.fundamental; + +/** Response for {@link FundamentalContext#getShareholderTop}. Contains raw JSON data. */ +public class ShareholderTopResponse { + /** Raw JSON data string */ + public String data; +} diff --git a/java/javasrc/src/main/java/com/longbridge/fundamental/ValuationComparisonOptions.java b/java/javasrc/src/main/java/com/longbridge/fundamental/ValuationComparisonOptions.java new file mode 100644 index 000000000..b658a8dcf --- /dev/null +++ b/java/javasrc/src/main/java/com/longbridge/fundamental/ValuationComparisonOptions.java @@ -0,0 +1,11 @@ +package com.longbridge.fundamental; + +/** Options for {@link FundamentalContext#getValuationComparison}. */ +public class ValuationComparisonOptions { + /** Primary security symbol, e.g. "AAPL.US" */ + public String symbol; + /** Currency: "USD", "HKD", or "CNY" */ + public String currency; + /** Optional peer symbols to compare (up to 4), e.g. ["MSFT.US","GOOGL.US"] */ + public String[] comparisonSymbols; +} diff --git a/java/javasrc/src/main/java/com/longbridge/fundamental/ValuationComparisonResponse.java b/java/javasrc/src/main/java/com/longbridge/fundamental/ValuationComparisonResponse.java new file mode 100644 index 000000000..5870bbbe5 --- /dev/null +++ b/java/javasrc/src/main/java/com/longbridge/fundamental/ValuationComparisonResponse.java @@ -0,0 +1,7 @@ +package com.longbridge.fundamental; + +/** Response for {@link FundamentalContext#getValuationComparison}. Contains raw JSON data. */ +public class ValuationComparisonResponse { + /** Raw JSON data string */ + public String data; +} diff --git a/java/javasrc/src/main/java/com/longbridge/market/MarketContext.java b/java/javasrc/src/main/java/com/longbridge/market/MarketContext.java index 6c17ec4d9..6e2e348e9 100644 --- a/java/javasrc/src/main/java/com/longbridge/market/MarketContext.java +++ b/java/javasrc/src/main/java/com/longbridge/market/MarketContext.java @@ -70,4 +70,14 @@ public CompletableFuture getConstituent(String symbol) throws public CompletableFuture getTopMovers(TopMoversOptions opts) throws OpenApiException { return AsyncCallback.executeTask((callback) -> SdkNative.marketContextTopMovers(raw, opts, callback)); } + + /** Get rank category keys for the popularity leaderboard. */ + public CompletableFuture getRankCategories() throws OpenApiException { + return AsyncCallback.executeTask((callback) -> SdkNative.marketContextRankCategories(raw, callback)); + } + + /** Get ranked stock list for a given category key (from getRankCategories). */ + public CompletableFuture getRankList(RankListOptions opts) throws OpenApiException { + return AsyncCallback.executeTask((callback) -> SdkNative.marketContextRankList(raw, opts, callback)); + } } diff --git a/java/javasrc/src/main/java/com/longbridge/market/RankCategoriesResponse.java b/java/javasrc/src/main/java/com/longbridge/market/RankCategoriesResponse.java new file mode 100644 index 000000000..3f88f3ce2 --- /dev/null +++ b/java/javasrc/src/main/java/com/longbridge/market/RankCategoriesResponse.java @@ -0,0 +1,7 @@ +package com.longbridge.market; + +/** Response for {@link MarketContext#getRankCategories}. Contains raw JSON data. */ +public class RankCategoriesResponse { + /** Raw JSON data string */ + public String data; +} diff --git a/java/javasrc/src/main/java/com/longbridge/market/RankListOptions.java b/java/javasrc/src/main/java/com/longbridge/market/RankListOptions.java new file mode 100644 index 000000000..65ce99097 --- /dev/null +++ b/java/javasrc/src/main/java/com/longbridge/market/RankListOptions.java @@ -0,0 +1,9 @@ +package com.longbridge.market; + +/** Options for {@link MarketContext#getRankList}. */ +public class RankListOptions { + /** Rank category key from getRankCategories, e.g. "ib_hot_all-us" */ + public String key; + /** Whether to include article content (default: false) */ + public boolean needArticle; +} diff --git a/java/javasrc/src/main/java/com/longbridge/market/RankListResponse.java b/java/javasrc/src/main/java/com/longbridge/market/RankListResponse.java new file mode 100644 index 000000000..a13a6e89e --- /dev/null +++ b/java/javasrc/src/main/java/com/longbridge/market/RankListResponse.java @@ -0,0 +1,7 @@ +package com.longbridge.market; + +/** Response for {@link MarketContext#getRankList}. Contains raw JSON data. */ +public class RankListResponse { + /** Raw JSON data string */ + public String data; +} diff --git a/java/javasrc/src/main/java/com/longbridge/quote/QuoteContext.java b/java/javasrc/src/main/java/com/longbridge/quote/QuoteContext.java index e08200245..a9e57abe1 100644 --- a/java/javasrc/src/main/java/com/longbridge/quote/QuoteContext.java +++ b/java/javasrc/src/main/java/com/longbridge/quote/QuoteContext.java @@ -1392,6 +1392,13 @@ public CompletableFuture getShortPositions(String symbol }); } + /** Get daily short sale volume for US or HK stocks (market auto-detected from symbol suffix). */ + public CompletableFuture getShortTrades(ShortTradesOptions opts) throws OpenApiException { + return AsyncCallback.executeTask((callback) -> { + SdkNative.quoteContextShortTrades(this.raw, opts, callback); + }); + } + /** * Get option volume statistics for a symbol * diff --git a/java/javasrc/src/main/java/com/longbridge/quote/ShortTradesOptions.java b/java/javasrc/src/main/java/com/longbridge/quote/ShortTradesOptions.java new file mode 100644 index 000000000..6b156aae4 --- /dev/null +++ b/java/javasrc/src/main/java/com/longbridge/quote/ShortTradesOptions.java @@ -0,0 +1,9 @@ +package com.longbridge.quote; + +/** Options for {@link QuoteContext#getShortTrades}. */ +public class ShortTradesOptions { + /** Security symbol (US or HK), e.g. "AAPL.US" or "700.HK" */ + public String symbol; + /** Number of records to return (1-100, default 20) */ + public int count; +} diff --git a/java/javasrc/src/main/java/com/longbridge/quote/ShortTradesResponse.java b/java/javasrc/src/main/java/com/longbridge/quote/ShortTradesResponse.java new file mode 100644 index 000000000..030483fdf --- /dev/null +++ b/java/javasrc/src/main/java/com/longbridge/quote/ShortTradesResponse.java @@ -0,0 +1,7 @@ +package com.longbridge.quote; + +/** Response for {@link QuoteContext#getShortTrades}. Contains raw JSON data. */ +public class ShortTradesResponse { + /** Raw JSON data string */ + public String data; +} diff --git a/java/javasrc/src/main/java/com/longbridge/screener/ScreenerContext.java b/java/javasrc/src/main/java/com/longbridge/screener/ScreenerContext.java new file mode 100644 index 000000000..7fad58470 --- /dev/null +++ b/java/javasrc/src/main/java/com/longbridge/screener/ScreenerContext.java @@ -0,0 +1,47 @@ +package com.longbridge.screener; + +import java.util.concurrent.CompletableFuture; +import com.longbridge.*; + +/** + * Screener context — stock screener strategies, search, and indicator metadata. + */ +public class ScreenerContext implements AutoCloseable { + private long raw; + + public static ScreenerContext create(Config config) { + ScreenerContext ctx = new ScreenerContext(); + ctx.raw = SdkNative.newScreenerContext(config.getRaw()); + return ctx; + } + + @Override + public void close() throws Exception { + SdkNative.freeScreenerContext(raw); + } + + /** Get platform-recommended screener strategies. */ + public CompletableFuture getRecommendStrategies() throws OpenApiException { + return AsyncCallback.executeTask((callback) -> SdkNative.screenerContextRecommendStrategies(raw, callback)); + } + + /** Get the current user's saved screener strategies. */ + public CompletableFuture getUserStrategies() throws OpenApiException { + return AsyncCallback.executeTask((callback) -> SdkNative.screenerContextUserStrategies(raw, callback)); + } + + /** Get detail for one screener strategy by ID. */ + public CompletableFuture getStrategy(ScreenerStrategyOptions opts) throws OpenApiException { + return AsyncCallback.executeTask((callback) -> SdkNative.screenerContextStrategy(raw, opts, callback)); + } + + /** Search / screen securities using a strategy ID or custom filters. */ + public CompletableFuture search(ScreenerSearchOptions opts) throws OpenApiException { + return AsyncCallback.executeTask((callback) -> SdkNative.screenerContextSearch(raw, opts, callback)); + } + + /** Get all available screener indicator definitions. */ + public CompletableFuture getIndicators() throws OpenApiException { + return AsyncCallback.executeTask((callback) -> SdkNative.screenerContextIndicators(raw, callback)); + } +} diff --git a/java/javasrc/src/main/java/com/longbridge/screener/ScreenerIndicatorsResponse.java b/java/javasrc/src/main/java/com/longbridge/screener/ScreenerIndicatorsResponse.java new file mode 100644 index 000000000..05fbd073c --- /dev/null +++ b/java/javasrc/src/main/java/com/longbridge/screener/ScreenerIndicatorsResponse.java @@ -0,0 +1,7 @@ +package com.longbridge.screener; + +/** Response for screener indicators list. Contains raw JSON data. */ +public class ScreenerIndicatorsResponse { + /** Raw JSON data string */ + public String data; +} diff --git a/java/javasrc/src/main/java/com/longbridge/screener/ScreenerRecommendStrategiesResponse.java b/java/javasrc/src/main/java/com/longbridge/screener/ScreenerRecommendStrategiesResponse.java new file mode 100644 index 000000000..81c6a8ab5 --- /dev/null +++ b/java/javasrc/src/main/java/com/longbridge/screener/ScreenerRecommendStrategiesResponse.java @@ -0,0 +1,7 @@ +package com.longbridge.screener; + +/** Response for {@link ScreenerContext#getRecommendStrategies}. Contains raw JSON data. */ +public class ScreenerRecommendStrategiesResponse { + /** Raw JSON data string */ + public String data; +} diff --git a/java/javasrc/src/main/java/com/longbridge/screener/ScreenerSearchOptions.java b/java/javasrc/src/main/java/com/longbridge/screener/ScreenerSearchOptions.java new file mode 100644 index 000000000..768684f18 --- /dev/null +++ b/java/javasrc/src/main/java/com/longbridge/screener/ScreenerSearchOptions.java @@ -0,0 +1,13 @@ +package com.longbridge.screener; + +/** Options for {@link ScreenerContext#search}. */ +public class ScreenerSearchOptions { + /** Market: "US", "HK", "CN", or "SG" */ + public String market; + /** Strategy ID (optional; null for custom filter mode) */ + public Long strategyId; + /** Page number (1-indexed, default 1) */ + public int page = 1; + /** Page size (default 20) */ + public int size = 20; +} diff --git a/java/javasrc/src/main/java/com/longbridge/screener/ScreenerSearchResponse.java b/java/javasrc/src/main/java/com/longbridge/screener/ScreenerSearchResponse.java new file mode 100644 index 000000000..2942dba7c --- /dev/null +++ b/java/javasrc/src/main/java/com/longbridge/screener/ScreenerSearchResponse.java @@ -0,0 +1,7 @@ +package com.longbridge.screener; + +/** Response for screener search. Contains raw JSON data. */ +public class ScreenerSearchResponse { + /** Raw JSON data string */ + public String data; +} diff --git a/java/javasrc/src/main/java/com/longbridge/screener/ScreenerStrategyOptions.java b/java/javasrc/src/main/java/com/longbridge/screener/ScreenerStrategyOptions.java new file mode 100644 index 000000000..41ccee8d5 --- /dev/null +++ b/java/javasrc/src/main/java/com/longbridge/screener/ScreenerStrategyOptions.java @@ -0,0 +1,7 @@ +package com.longbridge.screener; + +/** Options for {@link ScreenerContext#getStrategy}. */ +public class ScreenerStrategyOptions { + /** Strategy ID from getRecommendStrategies or getUserStrategies */ + public long id; +} diff --git a/java/javasrc/src/main/java/com/longbridge/screener/ScreenerStrategyResponse.java b/java/javasrc/src/main/java/com/longbridge/screener/ScreenerStrategyResponse.java new file mode 100644 index 000000000..61c7e7690 --- /dev/null +++ b/java/javasrc/src/main/java/com/longbridge/screener/ScreenerStrategyResponse.java @@ -0,0 +1,7 @@ +package com.longbridge.screener; + +/** Response for screener strategy detail. Contains raw JSON data. */ +public class ScreenerStrategyResponse { + /** Raw JSON data string */ + public String data; +} diff --git a/java/javasrc/src/main/java/com/longbridge/screener/ScreenerUserStrategiesResponse.java b/java/javasrc/src/main/java/com/longbridge/screener/ScreenerUserStrategiesResponse.java new file mode 100644 index 000000000..fdb22a728 --- /dev/null +++ b/java/javasrc/src/main/java/com/longbridge/screener/ScreenerUserStrategiesResponse.java @@ -0,0 +1,7 @@ +package com.longbridge.screener; + +/** Response for {@link ScreenerContext#getUserStrategies}. Contains raw JSON data. */ +public class ScreenerUserStrategiesResponse { + /** Raw JSON data string */ + public String data; +} From 6f408a34f2983ba1bce02700e153a481a5a6997d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=A2=81=E7=AB=A0=E6=B4=AA?= Date: Thu, 21 May 2026 15:32:26 +0800 Subject: [PATCH 07/12] feat: convert counter_id to symbol and timestamp to RFC 3339 in new APIs Replace serde_json::Value payloads in short_positions, short_trades, rank_list, top_movers, and valuation_comparison with typed structs. Unix-second timestamps are converted to RFC 3339 strings; counter_id values are converted to dotted symbol notation (e.g. TSLA.US). Python, Node.js, Java, and C bindings updated accordingly. Co-Authored-By: Claude Sonnet 4.6 (1M context) --- CHANGELOG.md | 9 +- c/src/fundamental_context/types.rs | 2 +- c/src/market_context/types.rs | 4 +- c/src/quote_context/types.rs | 4 +- java/src/init.rs | 7 + java/src/types/classes.rs | 133 +++++++++++++++- nodejs/index.d.ts | 229 ++++++++++++++++++++++++---- nodejs/src/fundamental/types.rs | 91 ++++++++++- nodejs/src/market/types.rs | 151 +++++++++++++++++- nodejs/src/quote/types.rs | 94 ++++++++++-- python/pysrc/longbridge/openapi.pyi | 207 ++++++++++++++++++++++--- python/src/fundamental/mod.rs | 2 + python/src/fundamental/types.rs | 92 ++++++++++- python/src/market/mod.rs | 3 + python/src/market/types.rs | 153 +++++++++++++++++-- python/src/quote/mod.rs | 2 + python/src/quote/types.rs | 98 ++++++++++-- rust/src/fundamental/context.rs | 57 ++++++- rust/src/fundamental/types.rs | 52 ++++++- rust/src/market/context.rs | 94 +++++++++++- rust/src/market/types.rs | 104 +++++++++++-- rust/src/quote/context.rs | 75 +++++++-- rust/src/quote/mod.rs | 2 + rust/src/quote/types.rs | 70 +++++++-- 24 files changed, 1582 insertions(+), 153 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4bfd4dec0..ac64f0379 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -17,12 +17,19 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Changed - **All languages:** `QuoteContext::short_positions(symbol, count)` now auto-detects market from symbol suffix (`.HK` → HK, otherwise US). `ShortPositionsResponse` is raw JSON. +- **Rust, Python, Node.js, Java, C:** `short_positions` and `short_trades` responses now use typed structs (`ShortPositionsItem` / `ShortTradesItem`) with RFC 3339 timestamps instead of raw JSON. Fields absent in a given market default to empty string. +- **Rust, Python, Node.js, Java, C:** `rank_list` response now uses a typed `RankListResponse { bmp, lists: Vec }` struct; `counter_id` is converted to `symbol`. +- **Rust, Python, Node.js, Java, C:** `top_movers` response now uses a typed `TopMoversResponse { events: Vec, next_params }` struct; timestamps converted to RFC 3339 and `counter_id` converted to `symbol`. +- **Rust, Python, Node.js, Java, C:** `valuation_comparison` response now uses a typed `ValuationComparisonResponse { list: Vec }` struct; history dates converted from Unix timestamp to RFC 3339 and `counter_id` converted to `symbol`. ### Breaking changes - **All languages:** `MarketContext::stock_events` renamed to `top_movers`; `StockEventsResponse` → `TopMoversResponse`. - **All languages:** `hk_short_positions` removed — use `short_positions(symbol, count)` which auto-detects HK/US. -- **All languages:** `ShortPositionsResponse` is now raw JSON; old typed fields removed. +- **All languages:** `ShortPositionsResponse.data` changed from raw JSON to `Vec`; `ShortTradesResponse.data` changed from raw JSON to `Vec`. +- **All languages:** `TopMoversResponse.data` (raw JSON) replaced by `TopMoversResponse { events, next_params }`. +- **All languages:** `RankListResponse.data` (raw JSON) replaced by `RankListResponse { bmp, lists }`. +- **All languages:** `ValuationComparisonResponse.data` (raw JSON) replaced by `ValuationComparisonResponse { list }`. # [4.1.0] diff --git a/c/src/fundamental_context/types.rs b/c/src/fundamental_context/types.rs index 8a272c883..1dbde4f60 100644 --- a/c/src/fundamental_context/types.rs +++ b/c/src/fundamental_context/types.rs @@ -2916,7 +2916,7 @@ pub(crate) struct CValuationComparisonResponseOwned { impl From for CValuationComparisonResponseOwned { fn from(v: ValuationComparisonResponse) -> Self { - let json = serde_json::to_string(&v.data).unwrap_or_default(); + let json = serde_json::to_string(&v).unwrap_or_default(); Self { data: json.into() } } } diff --git a/c/src/market_context/types.rs b/c/src/market_context/types.rs index 047ac4f94..2ba6e3749 100644 --- a/c/src/market_context/types.rs +++ b/c/src/market_context/types.rs @@ -973,7 +973,7 @@ pub(crate) struct CTopMoversResponseOwned { impl From for CTopMoversResponseOwned { fn from(v: TopMoversResponse) -> Self { - let json = serde_json::to_string(&v.data).unwrap_or_default(); + let json = serde_json::to_string(&v).unwrap_or_default(); Self { data: json.into() } } } @@ -1031,7 +1031,7 @@ pub(crate) struct CRankListResponseOwned { impl From for CRankListResponseOwned { fn from(v: RankListResponse) -> Self { - let json = serde_json::to_string(&v.data).unwrap_or_default(); + let json = serde_json::to_string(&v).unwrap_or_default(); Self { data: json.into() } } } diff --git a/c/src/quote_context/types.rs b/c/src/quote_context/types.rs index ad3bbfb8e..c68cf872d 100644 --- a/c/src/quote_context/types.rs +++ b/c/src/quote_context/types.rs @@ -3121,7 +3121,7 @@ pub(crate) struct CShortPositionsResponseOwned { impl From for CShortPositionsResponseOwned { fn from(v: ShortPositionsResponse) -> Self { - let json = serde_json::to_string(&v.data).unwrap_or_default(); + let json = serde_json::to_string(&v).unwrap_or_default(); Self { data: json.into() } } } @@ -3154,7 +3154,7 @@ pub(crate) struct CShortTradesResponseOwned { impl From for CShortTradesResponseOwned { fn from(v: ShortTradesResponse) -> Self { - let json = serde_json::to_string(&v.data).unwrap_or_default(); + let json = serde_json::to_string(&v).unwrap_or_default(); Self { data: json.into() } } } diff --git a/java/src/init.rs b/java/src/init.rs index 83961da30..2e03effa1 100644 --- a/java/src/init.rs +++ b/java/src/init.rs @@ -366,7 +366,9 @@ pub extern "system" fn Java_com_longbridge_SdkNative_init<'a>( longbridge::fundamental::OperatingFinancial, longbridge::fundamental::OperatingIndicator, // QuoteContext extensions + longbridge::quote::ShortPositionsItem, longbridge::quote::ShortPositionsResponse, + longbridge::quote::ShortTradesItem, longbridge::quote::ShortTradesResponse, longbridge::quote::OptionVolumeStats, longbridge::quote::OptionVolumeDaily, @@ -385,10 +387,15 @@ pub extern "system" fn Java_com_longbridge_SdkNative_init<'a>( // FundamentalContext: shareholders / valuation comparison longbridge::fundamental::ShareholderTopResponse, longbridge::fundamental::ShareholderDetailResponse, + longbridge::fundamental::ValuationHistoryPoint, + longbridge::fundamental::ValuationComparisonItem, longbridge::fundamental::ValuationComparisonResponse, // MarketContext: top movers / rank + longbridge::market::TopMoversStock, + longbridge::market::TopMoversEvent, longbridge::market::TopMoversResponse, longbridge::market::RankCategoriesResponse, + longbridge::market::RankListItem, longbridge::market::RankListResponse, // ScreenerContext longbridge::screener::ScreenerRecommendStrategiesResponse, diff --git a/java/src/types/classes.rs b/java/src/types/classes.rs index 22c621fe1..2b1dbab87 100644 --- a/java/src/types/classes.rs +++ b/java/src/types/classes.rs @@ -2227,16 +2227,53 @@ impl_java_class!( // ── QuoteContext extensions ─────────────────────────────────────── +impl_java_class!( + "com/longbridge/quote/ShortPositionsItem", + longbridge::quote::ShortPositionsItem, + [ + timestamp, + rate, + close, + current_shares_short, + avg_daily_share_volume, + days_to_cover, + amount, + balance, + cost + ] +); + impl_java_class!( "com/longbridge/quote/ShortPositionsResponse", longbridge::quote::ShortPositionsResponse, - [data] + [ + #[java(objarray)] + data + ] +); + +impl_java_class!( + "com/longbridge/quote/ShortTradesItem", + longbridge::quote::ShortTradesItem, + [ + timestamp, + rate, + close, + nus_amount, + ny_amount, + total_amount, + amount, + balance + ] ); impl_java_class!( "com/longbridge/quote/ShortTradesResponse", longbridge::quote::ShortTradesResponse, - [data] + [ + #[java(objarray)] + data + ] ); impl_java_class!( @@ -2622,18 +2659,77 @@ impl_java_class!( [data] ); +impl_java_class!( + "com/longbridge/fundamental/ValuationHistoryPoint", + longbridge::fundamental::ValuationHistoryPoint, + [date, pe, pb, ps] +); + +impl_java_class!( + "com/longbridge/fundamental/ValuationComparisonItem", + longbridge::fundamental::ValuationComparisonItem, + [ + symbol, + name, + currency, + market_value, + price_close, + pe, + pb, + ps, + roe, + eps, + bps, + dps, + div_yld, + assets, + #[java(objarray)] + history + ] +); + impl_java_class!( "com/longbridge/fundamental/ValuationComparisonResponse", longbridge::fundamental::ValuationComparisonResponse, - [data] + [ + #[java(objarray)] + list + ] ); // ── MarketContext: top movers / rank ────────────────────────────── +impl_java_class!( + "com/longbridge/market/TopMoversStock", + longbridge::market::TopMoversStock, + [ + symbol, + code, + name, + full_name, + change, + last_done, + market, + #[java(objarray)] + labels, + logo + ] +); + +impl_java_class!( + "com/longbridge/market/TopMoversEvent", + longbridge::market::TopMoversEvent, + [timestamp, alert_reason, alert_type, stock, post] +); + impl_java_class!( "com/longbridge/market/TopMoversResponse", longbridge::market::TopMoversResponse, - [data] + [ + #[java(objarray)] + events, + next_params + ] ); impl_java_class!( @@ -2642,10 +2738,37 @@ impl_java_class!( [data] ); +impl_java_class!( + "com/longbridge/market/RankListItem", + longbridge::market::RankListItem, + [ + symbol, + code, + name, + last_done, + chg, + change, + inflow, + market_cap, + industry, + pre_post_price, + pre_post_chg, + amplitude, + five_day_chg, + turnover_rate, + volume_rate, + pb_ttm + ] +); + impl_java_class!( "com/longbridge/market/RankListResponse", longbridge::market::RankListResponse, - [data] + [ + bmp, + #[java(objarray)] + lists + ] ); // ── ScreenerContext ─────────────────────────────────────────────── diff --git a/nodejs/index.d.ts b/nodejs/index.d.ts index 63e6e4d0e..8650cc6be 100644 --- a/nodejs/index.d.ts +++ b/nodejs/index.d.ts @@ -2182,11 +2182,31 @@ export declare class SecurityCalcIndex { get delta(): Decimal | null /** Gamma */ get gamma(): Decimal | null - /** Theta */ + /** + * Theta + * + * The raw value returned by the API is annualized (scaled by 252 trading + * days per year). To obtain the standard per-calendar-day theta, divide + * by 252: `theta / 252`. + */ get theta(): Decimal | null - /** Vega */ + /** + * Vega + * + * The raw value returned by the API is expressed per 1 percentage-point + * change in implied volatility (i.e. the value has been multiplied by + * 100). To obtain the standard vega (per unit change in IV), divide by + * 100: `vega / 100`. + */ get vega(): Decimal | null - /** Rho */ + /** + * Rho + * + * The raw value returned by the API is expressed per 1 percentage-point + * change in the risk-free rate (i.e. the value has been multiplied by + * 100). To obtain the standard rho (per unit change in rate), divide by + * 100: `rho / 100`. + */ get rho(): Decimal | null } @@ -4958,10 +4978,48 @@ export interface RankCategoriesResponse { data: string } -/** Rank list response. `data` is a JSON string. */ +/** One ranked security item. */ +export interface RankListItem { + /** Symbol (e.g. `"MU.US"`) */ + symbol: string + /** Ticker code */ + code: string + /** Security name */ + name: string + /** Latest price */ + lastDone: string + /** Price change ratio */ + chg: string + /** Absolute price change */ + change: string + /** Net inflow */ + inflow: string + /** Market cap */ + marketCap: string + /** Industry name */ + industry: string + /** Pre/post market price */ + prePostPrice: string + /** Pre/post market change */ + prePostChg: string + /** Amplitude */ + amplitude: string + /** 5-day change */ + fiveDayChg: string + /** Turnover rate */ + turnoverRate: string + /** Volume ratio */ + volumeRate: string + /** P/B ratio (TTM) */ + pbTtm: string +} + +/** Rank list response. */ export interface RankListResponse { - /** Raw rank list data (JSON string) */ - data: string + /** Whether delayed / BMP data */ + bmp: boolean + /** Ranked security items */ + lists: Array } /** Analyst rating distribution counts */ @@ -5287,25 +5345,58 @@ export interface SharelistStock { latency?: boolean } -/** - * Short interest response - * Short interest / positions response (HK or US). - * - * `data` is the raw JSON returned by the API as a string. - */ +/** One short-position data point (unified for US and HK markets). */ +export interface ShortPositionsItem { + /** Trading date (RFC 3339) */ + timestamp: string + /** Short ratio */ + rate: string + /** Closing price */ + close: string + /** [US] Number of short shares outstanding */ + currentSharesShort: string + /** [US] Average daily share volume */ + avgDailyShareVolume: string + /** [US] Days to cover ratio */ + daysToCover: string + /** [HK] Short sale amount (HKD) */ + amount: string + /** [HK] Short position balance */ + balance: string + /** [HK] Cost / closing price */ + cost: string +} + +/** Short interest / positions response (HK or US). */ export interface ShortPositionsResponse { - /** Raw short positions data (JSON string) */ - data: string + /** Short position data points */ + data: Array } -/** - * Short trade records response (HK or US). - * - * `data` is the raw JSON returned by the API as a string. - */ +/** One short-trade data point (unified for US and HK markets). */ +export interface ShortTradesItem { + /** Trading date (RFC 3339) */ + timestamp: string + /** Short ratio */ + rate: string + /** Closing price */ + close: string + /** [US] NYSE short amount */ + nusAmount: string + /** [US] NY short amount */ + nyAmount: string + /** [US] Total short amount */ + totalAmount: string + /** [HK] Short sale amount */ + amount: string + /** [HK] Short position balance */ + balance: string +} + +/** Short trade records response (HK or US). */ export interface ShortTradesResponse { - /** Raw short trade data (JSON string) */ - data: string + /** Short trade data points */ + data: Array } /** Sort order type */ @@ -5420,10 +5511,48 @@ export declare const enum TopicType { Private = 0 } -/** Top movers response. `data` is a JSON string. */ +/** One top-movers event entry. */ +export interface TopMoversEvent { + /** Event time (RFC 3339) */ + timestamp: string + /** Alert reason description */ + alertReason: string + /** Alert type code */ + alertType: number + /** Stock information */ + stock: TopMoversStock + /** Associated news post (JSON string) */ + post: string +} + +/** Top movers response. */ export interface TopMoversResponse { - /** Raw top movers data (JSON string) */ - data: string + /** Top-mover events */ + events: Array + /** Pagination cursor for next page (JSON string) */ + nextParams: string +} + +/** Stock information within a top-movers event. */ +export interface TopMoversStock { + /** Symbol (e.g. `"NVDA.US"`) */ + symbol: string + /** Ticker code */ + code: string + /** Security name */ + name: string + /** Full name */ + fullName: string + /** Price change (decimal ratio) */ + change: string + /** Latest price */ + lastDone: string + /** Market code */ + market: string + /** Labels / tags */ + labels: Array + /** Logo URL */ + logo: string } /** Trade direction */ @@ -5547,10 +5676,44 @@ export interface UpdateWatchlistGroup { mode: SecuritiesUpdateMode } -/** Valuation comparison response. `data` is a JSON string. */ +/** One security's valuation comparison item. */ +export interface ValuationComparisonItem { + /** Symbol (e.g. `"AAPL.US"`) */ + symbol: string + /** Security name */ + name: string + /** Currency */ + currency: string + /** Market capitalisation */ + marketValue: string + /** Latest closing price */ + priceClose: string + /** P/E ratio */ + pe: string + /** P/B ratio */ + pb: string + /** P/S ratio */ + ps: string + /** Return on equity */ + roe: string + /** Earnings per share */ + eps: string + /** Book value per share */ + bps: string + /** Dividends per share */ + dps: string + /** Dividend yield */ + divYld: string + /** Total assets */ + assets: string + /** Historical valuation points */ + history: Array +} + +/** Valuation comparison response. */ export interface ValuationComparisonResponse { - /** Raw valuation comparison data (JSON string) */ - data: string + /** Valuation comparison items */ + list: Array } /** Valuation metrics response */ @@ -5607,6 +5770,18 @@ export interface ValuationHistoryMetrics { ps?: ValuationHistoryMetric } +/** One historical valuation data point. */ +export interface ValuationHistoryPoint { + /** Date (RFC 3339) */ + date: string + /** P/E ratio */ + pe: string + /** P/B ratio */ + pb: string + /** P/S ratio */ + ps: string +} + /** Historical valuation response */ export interface ValuationHistoryResponse { /** Historical valuation data */ diff --git a/nodejs/src/fundamental/types.rs b/nodejs/src/fundamental/types.rs index a9bf59d34..eaf68e746 100644 --- a/nodejs/src/fundamental/types.rs +++ b/nodejs/src/fundamental/types.rs @@ -1685,18 +1685,101 @@ impl From for ShareholderDetailResponse { // ── ValuationComparisonResponse ─────────────────────────────────── -/// Valuation comparison response. `data` is a JSON string. +/// One historical valuation data point. +#[napi_derive::napi(object)] +#[derive(Debug, Clone)] +pub struct ValuationHistoryPoint { + /// Date (RFC 3339) + pub date: String, + /// P/E ratio + pub pe: String, + /// P/B ratio + pub pb: String, + /// P/S ratio + pub ps: String, +} + +impl From for ValuationHistoryPoint { + fn from(v: lb::ValuationHistoryPoint) -> Self { + Self { + date: v.date, + pe: v.pe, + pb: v.pb, + ps: v.ps, + } + } +} + +/// One security's valuation comparison item. +#[napi_derive::napi(object)] +#[derive(Debug, Clone)] +pub struct ValuationComparisonItem { + /// Symbol (e.g. `"AAPL.US"`) + pub symbol: String, + /// Security name + pub name: String, + /// Currency + pub currency: String, + /// Market capitalisation + pub market_value: String, + /// Latest closing price + pub price_close: String, + /// P/E ratio + pub pe: String, + /// P/B ratio + pub pb: String, + /// P/S ratio + pub ps: String, + /// Return on equity + pub roe: String, + /// Earnings per share + pub eps: String, + /// Book value per share + pub bps: String, + /// Dividends per share + pub dps: String, + /// Dividend yield + pub div_yld: String, + /// Total assets + pub assets: String, + /// Historical valuation points + pub history: Vec, +} + +impl From for ValuationComparisonItem { + fn from(v: lb::ValuationComparisonItem) -> Self { + Self { + symbol: v.symbol, + name: v.name, + currency: v.currency, + market_value: v.market_value, + price_close: v.price_close, + pe: v.pe, + pb: v.pb, + ps: v.ps, + roe: v.roe, + eps: v.eps, + bps: v.bps, + dps: v.dps, + div_yld: v.div_yld, + assets: v.assets, + history: v.history.into_iter().map(Into::into).collect(), + } + } +} + +/// Valuation comparison response. #[napi_derive::napi(object)] #[derive(Debug, Clone)] pub struct ValuationComparisonResponse { - /// Raw valuation comparison data (JSON string) - pub data: String, + /// Valuation comparison items + pub list: Vec, } impl From for ValuationComparisonResponse { fn from(v: lb::ValuationComparisonResponse) -> Self { Self { - data: v.data.to_string(), + list: v.list.into_iter().map(Into::into).collect(), } } } diff --git a/nodejs/src/market/types.rs b/nodejs/src/market/types.rs index 77dd387a5..3d81fc259 100644 --- a/nodejs/src/market/types.rs +++ b/nodejs/src/market/types.rs @@ -567,18 +567,89 @@ impl From for lb::AhPremiumPeriod { // ── TopMoversResponse ───────────────────────────────────────────── -/// Top movers response. `data` is a JSON string. +/// Stock information within a top-movers event. +#[napi_derive::napi(object)] +#[derive(Debug, Clone)] +pub struct TopMoversStock { + /// Symbol (e.g. `"NVDA.US"`) + pub symbol: String, + /// Ticker code + pub code: String, + /// Security name + pub name: String, + /// Full name + pub full_name: String, + /// Price change (decimal ratio) + pub change: String, + /// Latest price + pub last_done: String, + /// Market code + pub market: String, + /// Labels / tags + pub labels: Vec, + /// Logo URL + pub logo: String, +} + +impl From for TopMoversStock { + fn from(v: lb::TopMoversStock) -> Self { + Self { + symbol: v.symbol, + code: v.code, + name: v.name, + full_name: v.full_name, + change: v.change, + last_done: v.last_done, + market: v.market, + labels: v.labels, + logo: v.logo, + } + } +} + +/// One top-movers event entry. +#[napi_derive::napi(object)] +#[derive(Debug, Clone)] +pub struct TopMoversEvent { + /// Event time (RFC 3339) + pub timestamp: String, + /// Alert reason description + pub alert_reason: String, + /// Alert type code + pub alert_type: i64, + /// Stock information + pub stock: TopMoversStock, + /// Associated news post (JSON string) + pub post: String, +} + +impl From for TopMoversEvent { + fn from(v: lb::TopMoversEvent) -> Self { + Self { + timestamp: v.timestamp, + alert_reason: v.alert_reason, + alert_type: v.alert_type, + stock: v.stock.into(), + post: v.post.to_string(), + } + } +} + +/// Top movers response. #[napi_derive::napi(object)] #[derive(Debug, Clone)] pub struct TopMoversResponse { - /// Raw top movers data (JSON string) - pub data: String, + /// Top-mover events + pub events: Vec, + /// Pagination cursor for next page (JSON string) + pub next_params: String, } impl From for TopMoversResponse { fn from(v: lb::TopMoversResponse) -> Self { Self { - data: v.data.to_string(), + events: v.events.into_iter().map(Into::into).collect(), + next_params: v.next_params.to_string(), } } } @@ -603,18 +674,82 @@ impl From for RankCategoriesResponse { // ── RankListResponse ────────────────────────────────────────────── -/// Rank list response. `data` is a JSON string. +/// One ranked security item. +#[napi_derive::napi(object)] +#[derive(Debug, Clone)] +pub struct RankListItem { + /// Symbol (e.g. `"MU.US"`) + pub symbol: String, + /// Ticker code + pub code: String, + /// Security name + pub name: String, + /// Latest price + pub last_done: String, + /// Price change ratio + pub chg: String, + /// Absolute price change + pub change: String, + /// Net inflow + pub inflow: String, + /// Market cap + pub market_cap: String, + /// Industry name + pub industry: String, + /// Pre/post market price + pub pre_post_price: String, + /// Pre/post market change + pub pre_post_chg: String, + /// Amplitude + pub amplitude: String, + /// 5-day change + pub five_day_chg: String, + /// Turnover rate + pub turnover_rate: String, + /// Volume ratio + pub volume_rate: String, + /// P/B ratio (TTM) + pub pb_ttm: String, +} + +impl From for RankListItem { + fn from(v: lb::RankListItem) -> Self { + Self { + symbol: v.symbol, + code: v.code, + name: v.name, + last_done: v.last_done, + chg: v.chg, + change: v.change, + inflow: v.inflow, + market_cap: v.market_cap, + industry: v.industry, + pre_post_price: v.pre_post_price, + pre_post_chg: v.pre_post_chg, + amplitude: v.amplitude, + five_day_chg: v.five_day_chg, + turnover_rate: v.turnover_rate, + volume_rate: v.volume_rate, + pb_ttm: v.pb_ttm, + } + } +} + +/// Rank list response. #[napi_derive::napi(object)] #[derive(Debug, Clone)] pub struct RankListResponse { - /// Raw rank list data (JSON string) - pub data: String, + /// Whether delayed / BMP data + pub bmp: bool, + /// Ranked security items + pub lists: Vec, } impl From for RankListResponse { fn from(v: lb::RankListResponse) -> Self { Self { - data: v.data.to_string(), + bmp: v.bmp, + lists: v.lists.into_iter().map(Into::into).collect(), } } } diff --git a/nodejs/src/quote/types.rs b/nodejs/src/quote/types.rs index 79ee10604..acd0cfdd4 100644 --- a/nodejs/src/quote/types.rs +++ b/nodejs/src/quote/types.rs @@ -1480,39 +1480,111 @@ pub struct HistoryMarketTemperatureResponse { // ── Step 3 additions ───────────────────────────────────────────── -/// Short interest response +/// One short-position data point (unified for US and HK markets). +#[napi_derive::napi(object)] +#[derive(Debug, Clone)] +pub struct ShortPositionsItem { + /// Trading date (RFC 3339) + pub timestamp: String, + /// Short ratio + pub rate: String, + /// Closing price + pub close: String, + /// [US] Number of short shares outstanding + pub current_shares_short: String, + /// [US] Average daily share volume + pub avg_daily_share_volume: String, + /// [US] Days to cover ratio + pub days_to_cover: String, + /// [HK] Short sale amount (HKD) + pub amount: String, + /// [HK] Short position balance + pub balance: String, + /// [HK] Cost / closing price + pub cost: String, +} + +impl From for ShortPositionsItem { + fn from(v: longbridge::quote::ShortPositionsItem) -> Self { + Self { + timestamp: v.timestamp, + rate: v.rate, + close: v.close, + current_shares_short: v.current_shares_short, + avg_daily_share_volume: v.avg_daily_share_volume, + days_to_cover: v.days_to_cover, + amount: v.amount, + balance: v.balance, + cost: v.cost, + } + } +} + /// Short interest / positions response (HK or US). -/// -/// `data` is the raw JSON returned by the API as a string. #[napi_derive::napi(object)] #[derive(Debug, Clone)] pub struct ShortPositionsResponse { - /// Raw short positions data (JSON string) - pub data: String, + /// Short position data points + pub data: Vec, } impl From for ShortPositionsResponse { fn from(v: longbridge::quote::ShortPositionsResponse) -> Self { Self { - data: v.data.to_string(), + data: v.data.into_iter().map(Into::into).collect(), + } + } +} + +/// One short-trade data point (unified for US and HK markets). +#[napi_derive::napi(object)] +#[derive(Debug, Clone)] +pub struct ShortTradesItem { + /// Trading date (RFC 3339) + pub timestamp: String, + /// Short ratio + pub rate: String, + /// Closing price + pub close: String, + /// [US] NYSE short amount + pub nus_amount: String, + /// [US] NY short amount + pub ny_amount: String, + /// [US] Total short amount + pub total_amount: String, + /// [HK] Short sale amount + pub amount: String, + /// [HK] Short position balance + pub balance: String, +} + +impl From for ShortTradesItem { + fn from(v: longbridge::quote::ShortTradesItem) -> Self { + Self { + timestamp: v.timestamp, + rate: v.rate, + close: v.close, + nus_amount: v.nus_amount, + ny_amount: v.ny_amount, + total_amount: v.total_amount, + amount: v.amount, + balance: v.balance, } } } /// Short trade records response (HK or US). -/// -/// `data` is the raw JSON returned by the API as a string. #[napi_derive::napi(object)] #[derive(Debug, Clone)] pub struct ShortTradesResponse { - /// Raw short trade data (JSON string) - pub data: String, + /// Short trade data points + pub data: Vec, } impl From for ShortTradesResponse { fn from(v: longbridge::quote::ShortTradesResponse) -> Self { Self { - data: v.data.to_string(), + data: v.data.into_iter().map(Into::into).collect(), } } } diff --git a/python/pysrc/longbridge/openapi.pyi b/python/pysrc/longbridge/openapi.pyi index e59018eab..aa3b1cb2d 100644 --- a/python/pysrc/longbridge/openapi.pyi +++ b/python/pysrc/longbridge/openapi.pyi @@ -9925,11 +9925,59 @@ class ShareholderDetailResponse: """Raw shareholder detail data (JSON object / list)""" +class ValuationHistoryPoint: + """One historical valuation data point.""" + + date: str + """Date (RFC 3339, converted from Unix timestamp)""" + pe: str + """P/E ratio""" + pb: str + """P/B ratio""" + ps: str + """P/S ratio""" + + +class ValuationComparisonItem: + """One security's valuation comparison item.""" + + symbol: str + """Symbol (e.g. ``"AAPL.US"``)""" + name: str + """Security name""" + currency: str + """Currency""" + market_value: str + """Market capitalisation""" + price_close: str + """Latest closing price""" + pe: str + """P/E ratio""" + pb: str + """P/B ratio""" + ps: str + """P/S ratio""" + roe: str + """Return on equity""" + eps: str + """Earnings per share""" + bps: str + """Book value per share""" + dps: str + """Dividends per share""" + div_yld: str + """Dividend yield""" + assets: str + """Total assets""" + history: List[ValuationHistoryPoint] + """Historical valuation points""" + + class ValuationComparisonResponse: - """Valuation comparison response. ``data`` is a Python dict/list from JSON.""" + """Valuation comparison response.""" - data: object - """Raw valuation comparison data (JSON object / list)""" + list: List[ValuationComparisonItem] + """Valuation comparison items""" # ── MarketContext ───────────────────────────────────────────────── @@ -10380,11 +10428,51 @@ class MarketContext: # ── MarketContext new response types ────────────────────────────── +class TopMoversStock: + """Stock information within a top-movers event.""" + + symbol: str + """Symbol (e.g. ``"NVDA.US"``)""" + code: str + """Ticker code""" + name: str + """Security name""" + full_name: str + """Full name""" + change: str + """Price change (decimal ratio)""" + last_done: str + """Latest price""" + market: str + """Market code""" + labels: List[str] + """Labels / tags""" + logo: str + """Logo URL""" + + +class TopMoversEvent: + """One top-movers event entry.""" + + timestamp: str + """Event time (RFC 3339)""" + alert_reason: str + """Alert reason description""" + alert_type: int + """Alert type code""" + stock: TopMoversStock + """Stock information""" + post: object + """Associated news post (raw JSON object)""" + + class TopMoversResponse: - """Top movers response. ``data`` is a Python dict/list from JSON.""" + """Top movers response.""" - data: object - """Raw top movers data (JSON object / list)""" + events: List[TopMoversEvent] + """Top-mover events""" + next_params: object + """Pagination cursor for next page (raw JSON object)""" class RankCategoriesResponse: @@ -10394,11 +10482,50 @@ class RankCategoriesResponse: """Raw rank categories data (JSON object / list)""" +class RankListItem: + """One ranked security item.""" + + symbol: str + """Symbol (e.g. ``"MU.US"``)""" + code: str + """Ticker code""" + name: str + """Security name""" + last_done: str + """Latest price""" + chg: str + """Price change ratio""" + change: str + """Absolute price change""" + inflow: str + """Net inflow""" + market_cap: str + """Market cap""" + industry: str + """Industry name""" + pre_post_price: str + """Pre/post market price""" + pre_post_chg: str + """Pre/post market change""" + amplitude: str + """Amplitude""" + five_day_chg: str + """5-day change""" + turnover_rate: str + """Turnover rate""" + volume_rate: str + """Volume ratio""" + pb_ttm: str + """P/B ratio (TTM)""" + + class RankListResponse: - """Rank list response. ``data`` is a Python dict/list from JSON.""" + """Rank list response.""" - data: object - """Raw rank list data (JSON object / list)""" + bmp: bool + """Whether delayed / BMP data""" + lists: List[RankListItem] + """Ranked security items""" # ── ScreenerContext ─────────────────────────────────────────────── @@ -11658,26 +11785,62 @@ class SharelistContext: # ── QuoteContext extensions ─────────────────────────────────────── +class ShortPositionsItem: + """One short-position data point (unified for US and HK markets).""" + + timestamp: str + """Trading date (RFC 3339)""" + rate: str + """Short ratio (both markets)""" + close: str + """Closing price (both markets)""" + current_shares_short: str + """[US] Number of short shares outstanding""" + avg_daily_share_volume: str + """[US] Average daily share volume""" + days_to_cover: str + """[US] Days to cover ratio""" + amount: str + """[HK] Short sale amount (HKD)""" + balance: str + """[HK] Short position balance""" + cost: str + """[HK] Cost / closing price""" + + class ShortPositionsResponse: - """Short interest / positions response (HK or US). + """Short interest / positions response (HK or US).""" - The ``data`` attribute is a Python dict/list converted from the raw JSON - returned by the API. The exact shape differs between HK and US markets. - """ + data: List[ShortPositionsItem] + """Short position data points""" - data: object - """Raw short positions data (JSON object / list)""" +class ShortTradesItem: + """One short-trade data point (unified for US and HK markets).""" -class ShortTradesResponse: - """Short trade records response (HK or US). + timestamp: str + """Trading date (RFC 3339)""" + rate: str + """Short ratio""" + close: str + """Closing price""" + nus_amount: str + """[US] NYSE short amount""" + ny_amount: str + """[US] NY short amount""" + total_amount: str + """[US] Total short amount""" + amount: str + """[HK] Short sale amount""" + balance: str + """[HK] Short position balance""" - The ``data`` attribute is a Python dict/list converted from the raw JSON - returned by the API. The exact shape differs between HK and US markets. - """ - data: object - """Raw short trade data (JSON object / list)""" +class ShortTradesResponse: + """Short trade records response (HK or US).""" + + data: List[ShortTradesItem] + """Short trade data points""" class OptionVolumeStats: diff --git a/python/src/fundamental/mod.rs b/python/src/fundamental/mod.rs index 80f423a9e..62dfd8da4 100644 --- a/python/src/fundamental/mod.rs +++ b/python/src/fundamental/mod.rs @@ -66,6 +66,8 @@ 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::()?; diff --git a/python/src/fundamental/types.rs b/python/src/fundamental/types.rs index a14990800..8219dc6f4 100644 --- a/python/src/fundamental/types.rs +++ b/python/src/fundamental/types.rs @@ -1748,21 +1748,101 @@ impl From for ShareholderDetailResponse { // ── ValuationComparisonResponse ─────────────────────────────────── +/// One historical valuation data point. +#[pyclass(get_all, skip_from_py_object)] +#[derive(Debug, Clone)] +pub(crate) struct ValuationHistoryPoint { + /// Date (RFC 3339) + pub date: String, + /// P/E ratio + pub pe: String, + /// P/B ratio + pub pb: String, + /// P/S ratio + pub ps: String, +} + +impl From for ValuationHistoryPoint { + fn from(v: lb::ValuationHistoryPoint) -> Self { + Self { + date: v.date, + pe: v.pe, + pb: v.pb, + ps: v.ps, + } + } +} + +/// One security's valuation comparison item. +#[pyclass(get_all, skip_from_py_object)] +#[derive(Debug, Clone)] +pub(crate) struct ValuationComparisonItem { + /// Symbol (e.g. `"AAPL.US"`) + pub symbol: String, + /// Security name + pub name: String, + /// Currency + pub currency: String, + /// Market capitalisation + pub market_value: String, + /// Latest closing price + pub price_close: String, + /// P/E ratio + pub pe: String, + /// P/B ratio + pub pb: String, + /// P/S ratio + pub ps: String, + /// Return on equity + pub roe: String, + /// Earnings per share + pub eps: String, + /// Book value per share + pub bps: String, + /// Dividends per share + pub dps: String, + /// Dividend yield + pub div_yld: String, + /// Total assets + pub assets: String, + /// Historical valuation points + pub history: Vec, +} + +impl From for ValuationComparisonItem { + fn from(v: lb::ValuationComparisonItem) -> Self { + Self { + symbol: v.symbol, + name: v.name, + currency: v.currency, + market_value: v.market_value, + price_close: v.price_close, + pe: v.pe, + pb: v.pb, + ps: v.ps, + roe: v.roe, + eps: v.eps, + bps: v.bps, + dps: v.dps, + div_yld: v.div_yld, + assets: v.assets, + history: v.history.into_iter().map(Into::into).collect(), + } + } +} + /// Valuation comparison response. -/// -/// `data` is the raw JSON returned by the API preserved as a Python -/// object (dict / list / etc.). #[pyclass(get_all, skip_from_py_object)] #[derive(Debug, Clone)] pub(crate) struct ValuationComparisonResponse { - /// Raw valuation comparison data (JSON object) - pub data: JsonValue, + /// Valuation comparison items + pub list: Vec, } impl From for ValuationComparisonResponse { fn from(v: lb::ValuationComparisonResponse) -> Self { Self { - data: JsonValue(v.data), + list: v.list.into_iter().map(Into::into).collect(), } } } diff --git a/python/src/market/mod.rs b/python/src/market/mod.rs index a6ed2a09a..5a19bf585 100644 --- a/python/src/market/mod.rs +++ b/python/src/market/mod.rs @@ -27,8 +27,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::()?; diff --git a/python/src/market/types.rs b/python/src/market/types.rs index d05d9a9bc..4ba37f306 100644 --- a/python/src/market/types.rs +++ b/python/src/market/types.rs @@ -3,21 +3,89 @@ use pyo3::prelude::*; // ── TopMoversResponse ───────────────────────────────────────────── +/// Stock information within a top-movers event. +#[pyclass(get_all, skip_from_py_object)] +#[derive(Debug, Clone)] +pub(crate) struct TopMoversStock { + /// Symbol (e.g. `"NVDA.US"`) + pub symbol: String, + /// Ticker code + pub code: String, + /// Security name + pub name: String, + /// Full name + pub full_name: String, + /// Price change (decimal ratio) + pub change: String, + /// Latest price + pub last_done: String, + /// Market code + pub market: String, + /// Labels / tags + pub labels: Vec, + /// Logo URL + pub logo: String, +} + +impl From for TopMoversStock { + fn from(v: lb::TopMoversStock) -> Self { + Self { + symbol: v.symbol, + code: v.code, + name: v.name, + full_name: v.full_name, + change: v.change, + last_done: v.last_done, + market: v.market, + labels: v.labels, + logo: v.logo, + } + } +} + +/// One top-movers event entry. +#[pyclass(get_all, skip_from_py_object)] +#[derive(Debug, Clone)] +pub(crate) struct TopMoversEvent { + /// Event time (RFC 3339) + pub timestamp: String, + /// Alert reason description + pub alert_reason: String, + /// Alert type code + pub alert_type: i64, + /// Stock information + pub stock: TopMoversStock, + /// Associated news post (raw JSON object) + pub post: crate::fundamental::types::JsonValue, +} + +impl From for TopMoversEvent { + fn from(v: lb::TopMoversEvent) -> Self { + Self { + timestamp: v.timestamp, + alert_reason: v.alert_reason, + alert_type: v.alert_type, + stock: v.stock.into(), + post: crate::fundamental::types::JsonValue(v.post), + } + } +} + /// Top movers response. -/// -/// `data` is the raw JSON returned by the API preserved as a Python -/// object (dict / list / etc.). #[pyclass(get_all, skip_from_py_object)] #[derive(Debug, Clone)] pub(crate) struct TopMoversResponse { - /// Raw top movers data (JSON object) - pub data: crate::fundamental::types::JsonValue, + /// Top-mover events + pub events: Vec, + /// Pagination cursor for next page (raw JSON object) + pub next_params: crate::fundamental::types::JsonValue, } impl From for TopMoversResponse { fn from(v: lb::TopMoversResponse) -> Self { Self { - data: crate::fundamental::types::JsonValue(v.data), + events: v.events.into_iter().map(Into::into).collect(), + next_params: crate::fundamental::types::JsonValue(v.next_params), } } } @@ -45,21 +113,82 @@ impl From for RankCategoriesResponse { // ── RankListResponse ────────────────────────────────────────────── +/// One ranked security item. +#[pyclass(get_all, skip_from_py_object)] +#[derive(Debug, Clone)] +pub(crate) struct RankListItem { + /// Symbol (e.g. `"MU.US"`) + pub symbol: String, + /// Ticker code + pub code: String, + /// Security name + pub name: String, + /// Latest price + pub last_done: String, + /// Price change ratio + pub chg: String, + /// Absolute price change + pub change: String, + /// Net inflow + pub inflow: String, + /// Market cap + pub market_cap: String, + /// Industry name + pub industry: String, + /// Pre/post market price + pub pre_post_price: String, + /// Pre/post market change + pub pre_post_chg: String, + /// Amplitude + pub amplitude: String, + /// 5-day change + pub five_day_chg: String, + /// Turnover rate + pub turnover_rate: String, + /// Volume ratio + pub volume_rate: String, + /// P/B ratio (TTM) + pub pb_ttm: String, +} + +impl From for RankListItem { + fn from(v: lb::RankListItem) -> Self { + Self { + symbol: v.symbol, + code: v.code, + name: v.name, + last_done: v.last_done, + chg: v.chg, + change: v.change, + inflow: v.inflow, + market_cap: v.market_cap, + industry: v.industry, + pre_post_price: v.pre_post_price, + pre_post_chg: v.pre_post_chg, + amplitude: v.amplitude, + five_day_chg: v.five_day_chg, + turnover_rate: v.turnover_rate, + volume_rate: v.volume_rate, + pb_ttm: v.pb_ttm, + } + } +} + /// Rank list response. -/// -/// `data` is the raw JSON returned by the API preserved as a Python -/// object (dict / list / etc.). #[pyclass(get_all, skip_from_py_object)] #[derive(Debug, Clone)] pub(crate) struct RankListResponse { - /// Raw rank list data (JSON object) - pub data: crate::fundamental::types::JsonValue, + /// Whether delayed / BMP data + pub bmp: bool, + /// Ranked security items + pub lists: Vec, } impl From for RankListResponse { fn from(v: lb::RankListResponse) -> Self { Self { - data: crate::fundamental::types::JsonValue(v.data), + bmp: v.bmp, + lists: v.lists.into_iter().map(Into::into).collect(), } } } diff --git a/python/src/quote/mod.rs b/python/src/quote/mod.rs index 6ce9458cc..749f0deb0 100644 --- a/python/src/quote/mod.rs +++ b/python/src/quote/mod.rs @@ -64,7 +64,9 @@ 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::()?; diff --git a/python/src/quote/types.rs b/python/src/quote/types.rs index 2c50cae45..d9849dac0 100644 --- a/python/src/quote/types.rs +++ b/python/src/quote/types.rs @@ -1435,42 +1435,114 @@ pub(crate) struct HistoryMarketTemperatureResponse { records: Vec, } -// ── Step 3: short_positions / option_volume / option_volume_daily ─ +// ── Step 3: short_positions / short_trades / option_volume / +// option_volume_daily + +/// One short-position data point (unified for US and HK markets). +#[pyclass(get_all, skip_from_py_object)] +#[derive(Debug, Clone)] +pub(crate) struct ShortPositionsItem { + /// Trading date (RFC 3339) + pub timestamp: String, + /// Short ratio + pub rate: String, + /// Closing price + pub close: String, + /// [US] Number of short shares outstanding + pub current_shares_short: String, + /// [US] Average daily share volume + pub avg_daily_share_volume: String, + /// [US] Days to cover ratio + pub days_to_cover: String, + /// [HK] Short sale amount (HKD) + pub amount: String, + /// [HK] Short position balance + pub balance: String, + /// [HK] Cost / closing price + pub cost: String, +} + +impl From for ShortPositionsItem { + fn from(v: longbridge::quote::ShortPositionsItem) -> Self { + Self { + timestamp: v.timestamp, + rate: v.rate, + close: v.close, + current_shares_short: v.current_shares_short, + avg_daily_share_volume: v.avg_daily_share_volume, + days_to_cover: v.days_to_cover, + amount: v.amount, + balance: v.balance, + cost: v.cost, + } + } +} /// Short interest / positions response (HK or US). -/// -/// `data` is the raw JSON returned by the API preserved as a Python -/// object (dict / list / etc.). #[pyclass(get_all, skip_from_py_object)] #[derive(Debug, Clone)] pub(crate) struct ShortPositionsResponse { - /// Raw short positions data (JSON object) - pub data: crate::fundamental::types::JsonValue, + /// Short position data points + pub data: Vec, } impl From for ShortPositionsResponse { fn from(v: longbridge::quote::ShortPositionsResponse) -> Self { Self { - data: crate::fundamental::types::JsonValue(v.data), + data: v.data.into_iter().map(Into::into).collect(), + } + } +} + +/// One short-trade data point (unified for US and HK markets). +#[pyclass(get_all, skip_from_py_object)] +#[derive(Debug, Clone)] +pub(crate) struct ShortTradesItem { + /// Trading date (RFC 3339) + pub timestamp: String, + /// Short ratio + pub rate: String, + /// Closing price + pub close: String, + /// [US] NYSE short amount + pub nus_amount: String, + /// [US] NY short amount + pub ny_amount: String, + /// [US] Total short amount + pub total_amount: String, + /// [HK] Short sale amount + pub amount: String, + /// [HK] Short position balance + pub balance: String, +} + +impl From for ShortTradesItem { + fn from(v: longbridge::quote::ShortTradesItem) -> Self { + Self { + timestamp: v.timestamp, + rate: v.rate, + close: v.close, + nus_amount: v.nus_amount, + ny_amount: v.ny_amount, + total_amount: v.total_amount, + amount: v.amount, + balance: v.balance, } } } /// Short trade records response (HK or US). -/// -/// `data` is the raw JSON returned by the API preserved as a Python -/// object (dict / list / etc.). #[pyclass(get_all, skip_from_py_object)] #[derive(Debug, Clone)] pub(crate) struct ShortTradesResponse { - /// Raw short trade data (JSON object) - pub data: crate::fundamental::types::JsonValue, + /// Short trade data points + pub data: Vec, } impl From for ShortTradesResponse { fn from(v: longbridge::quote::ShortTradesResponse) -> Self { Self { - data: crate::fundamental::types::JsonValue(v.data), + data: v.data.into_iter().map(Into::into).collect(), } } } diff --git a/rust/src/fundamental/context.rs b/rust/src/fundamental/context.rs index c2d06f683..b7c23aff4 100644 --- a/rust/src/fundamental/context.rs +++ b/rust/src/fundamental/context.rs @@ -4,7 +4,23 @@ use longbridge_httpcli::{HttpClient, Json, Method}; use serde::{Serialize, de::DeserializeOwned}; use tracing::{Subscriber, dispatcher, instrument::WithSubscriber}; -use crate::{Config, Result, fundamental::types::*, utils::counter::symbol_to_counter_id}; +use crate::{ + Config, Result, + fundamental::types::*, + utils::counter::{counter_id_to_symbol, symbol_to_counter_id}, +}; + +/// Convert a Unix-seconds string to RFC 3339. +fn unix_secs_str_to_rfc3339(s: &str) -> String { + s.parse::() + .ok() + .and_then(|ts| time::OffsetDateTime::from_unix_timestamp(ts).ok()) + .map(|dt| { + use time::format_description::well_known::Rfc3339; + dt.format(&Rfc3339).unwrap_or_default() + }) + .unwrap_or_else(|| s.to_string()) +} struct InnerFundamentalContext { http_cli: HttpClient, @@ -751,6 +767,43 @@ impl FundamentalContext { }, ) .await?; - Ok(ValuationComparisonResponse { data: raw }) + let list = raw["list"] + .as_array() + .cloned() + .unwrap_or_default() + .into_iter() + .map(|item| { + let history = item["history"] + .as_array() + .cloned() + .unwrap_or_default() + .into_iter() + .map(|h| ValuationHistoryPoint { + date: unix_secs_str_to_rfc3339(h["date"].as_str().unwrap_or("")), + pe: h["pe"].as_str().unwrap_or("").to_string(), + pb: h["pb"].as_str().unwrap_or("").to_string(), + ps: h["ps"].as_str().unwrap_or("").to_string(), + }) + .collect(); + ValuationComparisonItem { + symbol: counter_id_to_symbol(item["counter_id"].as_str().unwrap_or("")), + name: item["name"].as_str().unwrap_or("").to_string(), + currency: item["currency"].as_str().unwrap_or("").to_string(), + market_value: item["market_value"].as_str().unwrap_or("").to_string(), + price_close: item["price_close"].as_str().unwrap_or("").to_string(), + pe: item["pe"].as_str().unwrap_or("").to_string(), + pb: item["pb"].as_str().unwrap_or("").to_string(), + ps: item["ps"].as_str().unwrap_or("").to_string(), + roe: item["roe"].as_str().unwrap_or("").to_string(), + eps: item["eps"].as_str().unwrap_or("").to_string(), + bps: item["bps"].as_str().unwrap_or("").to_string(), + dps: item["dps"].as_str().unwrap_or("").to_string(), + div_yld: item["div_yld"].as_str().unwrap_or("").to_string(), + assets: item["assets"].as_str().unwrap_or("").to_string(), + history, + } + }) + .collect(); + Ok(ValuationComparisonResponse { list }) } } diff --git a/rust/src/fundamental/types.rs b/rust/src/fundamental/types.rs index d2fffe0ac..b6332d4d2 100644 --- a/rust/src/fundamental/types.rs +++ b/rust/src/fundamental/types.rs @@ -1370,11 +1370,59 @@ pub struct ShareholderDetailResponse { // ── valuation_comparison ────────────────────────────────────────── +/// One historical valuation data point. +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct ValuationHistoryPoint { + /// Date (RFC 3339, converted from Unix timestamp) + pub date: String, + /// P/E ratio + pub pe: String, + /// P/B ratio + pub pb: String, + /// P/S ratio + pub ps: String, +} + +/// One security's valuation comparison item. +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct ValuationComparisonItem { + /// Symbol (converted from counter_id) + pub symbol: String, + /// Security name + pub name: String, + /// Currency + pub currency: String, + /// Market capitalisation + pub market_value: String, + /// Latest closing price + pub price_close: String, + /// P/E ratio + pub pe: String, + /// P/B ratio + pub pb: String, + /// P/S ratio + pub ps: String, + /// Return on equity + pub roe: String, + /// Earnings per share + pub eps: String, + /// Book value per share + pub bps: String, + /// Dividends per share + pub dps: String, + /// Dividend yield + pub div_yld: String, + /// Total assets + pub assets: String, + /// Historical valuation points + pub history: Vec, +} + /// Response for [`crate::FundamentalContext::valuation_comparison`] #[derive(Debug, Clone, Serialize, Deserialize)] pub struct ValuationComparisonResponse { - /// Raw valuation comparison data - pub data: serde_json::Value, + /// Valuation comparison items + pub list: Vec, } /// Financial report period type diff --git a/rust/src/market/context.rs b/rust/src/market/context.rs index a4414d610..4edba7306 100644 --- a/rust/src/market/context.rs +++ b/rust/src/market/context.rs @@ -7,9 +7,26 @@ use tracing::{Subscriber, dispatcher, instrument::WithSubscriber}; use crate::{ Config, Result, market::types::*, - utils::counter::{index_symbol_to_counter_id, symbol_to_counter_id}, + utils::counter::{counter_id_to_symbol, index_symbol_to_counter_id, symbol_to_counter_id}, }; +/// Convert a Unix-seconds value (integer or string) to RFC 3339. +fn unix_secs_to_rfc3339(ts: i64) -> String { + time::OffsetDateTime::from_unix_timestamp(ts) + .map(|dt| { + use time::format_description::well_known::Rfc3339; + dt.format(&Rfc3339).unwrap_or_default() + }) + .unwrap_or_else(|_| ts.to_string()) +} + +/// Convert a Unix-seconds string to RFC 3339. +fn unix_secs_str_to_rfc3339(s: &str) -> String { + s.parse::() + .map(unix_secs_to_rfc3339) + .unwrap_or_else(|_| s.to_string()) +} + struct InnerMarketContext { http_cli: HttpClient, log_subscriber: Arc, @@ -317,7 +334,53 @@ impl MarketContext { }, ) .await?; - Ok(TopMoversResponse { data: raw }) + + let events = raw["events"] + .as_array() + .cloned() + .unwrap_or_default() + .into_iter() + .map(|ev| { + let ts = if let Some(n) = ev["timestamp"].as_i64() { + unix_secs_to_rfc3339(n) + } else if let Some(s) = ev["timestamp"].as_str() { + unix_secs_str_to_rfc3339(s) + } else { + String::new() + }; + let stock_val = &ev["stock"]; + let stock = TopMoversStock { + symbol: counter_id_to_symbol(stock_val["counter_id"].as_str().unwrap_or("")), + code: stock_val["code"].as_str().unwrap_or("").to_string(), + name: stock_val["name"].as_str().unwrap_or("").to_string(), + full_name: stock_val["full_name"].as_str().unwrap_or("").to_string(), + change: stock_val["change"].as_str().unwrap_or("").to_string(), + last_done: stock_val["last_done"].as_str().unwrap_or("").to_string(), + market: stock_val["market"].as_str().unwrap_or("").to_string(), + labels: stock_val["labels"] + .as_array() + .map(|arr| { + arr.iter() + .filter_map(|l| l.as_str().map(|s| s.to_string())) + .collect() + }) + .unwrap_or_default(), + logo: stock_val["logo"].as_str().unwrap_or("").to_string(), + }; + TopMoversEvent { + timestamp: ts, + alert_reason: ev["alert_reason"].as_str().unwrap_or("").to_string(), + alert_type: ev["alert_type"].as_i64().unwrap_or(0), + stock, + post: ev["post"].clone(), + } + }) + .collect(); + let next_params = raw["next_params"].clone(); + Ok(TopMoversResponse { + events, + next_params, + }) } // ── rank_categories ─────────────────────────────────────────── @@ -360,6 +423,31 @@ impl MarketContext { }, ) .await?; - Ok(RankListResponse { data: raw }) + let bmp = raw["bmp"].as_bool().unwrap_or(false); + let lists = raw["lists"] + .as_array() + .cloned() + .unwrap_or_default() + .into_iter() + .map(|item| RankListItem { + symbol: counter_id_to_symbol(item["counter_id"].as_str().unwrap_or("")), + code: item["code"].as_str().unwrap_or("").to_string(), + name: item["name"].as_str().unwrap_or("").to_string(), + last_done: item["last_done"].as_str().unwrap_or("").to_string(), + chg: item["chg"].as_str().unwrap_or("").to_string(), + change: item["change"].as_str().unwrap_or("").to_string(), + inflow: item["inflow"].as_str().unwrap_or("").to_string(), + market_cap: item["market_cap"].as_str().unwrap_or("").to_string(), + industry: item["industry"].as_str().unwrap_or("").to_string(), + pre_post_price: item["pre_post_price"].as_str().unwrap_or("").to_string(), + pre_post_chg: item["pre_post_chg"].as_str().unwrap_or("").to_string(), + amplitude: item["amplitude"].as_str().unwrap_or("").to_string(), + five_day_chg: item["five_day_chg"].as_str().unwrap_or("").to_string(), + turnover_rate: item["turnover_rate"].as_str().unwrap_or("").to_string(), + volume_rate: item["volume_rate"].as_str().unwrap_or("").to_string(), + pb_ttm: item["pb_ttm"].as_str().unwrap_or("").to_string(), + }) + .collect(); + Ok(RankListResponse { bmp, lists }) } } diff --git a/rust/src/market/types.rs b/rust/src/market/types.rs index d533659fd..8f62cdd4c 100644 --- a/rust/src/market/types.rs +++ b/rust/src/market/types.rs @@ -333,14 +333,54 @@ pub struct ConstituentStock { // ── top_movers ──────────────────────────────────────────────────── +/// Stock information within a top-movers event. +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct TopMoversStock { + /// Symbol (converted from counter_id, e.g. `"NVDA.US"`) + pub symbol: String, + /// Ticker code (e.g. `"NVDA"`) + pub code: String, + /// Security name + pub name: String, + /// Full name + #[serde(default)] + pub full_name: String, + /// Price change (decimal ratio) + pub change: String, + /// Latest price + pub last_done: String, + /// Market code + pub market: String, + /// Labels / tags + #[serde(default)] + pub labels: Vec, + /// Logo URL + #[serde(default)] + pub logo: String, +} + +/// One top-movers event entry. +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct TopMoversEvent { + /// Event time (RFC 3339) + pub timestamp: String, + /// Alert reason description + pub alert_reason: String, + /// Alert type code + pub alert_type: i64, + /// Stock information + pub stock: TopMoversStock, + /// Associated news post (raw JSON, complex structure) + pub post: serde_json::Value, +} + /// Response for [`crate::MarketContext::top_movers`] -/// -/// The raw data contains top-movers stock events from all requested markets. -/// The exact structure varies so the payload is preserved as raw JSON. #[derive(Debug, Clone, Serialize, Deserialize)] pub struct TopMoversResponse { - /// Raw top movers data - pub data: serde_json::Value, + /// Top-mover events + pub events: Vec, + /// Pagination cursor for next page + pub next_params: serde_json::Value, } // ── rank_categories ─────────────────────────────────────────────── @@ -357,15 +397,57 @@ pub struct RankCategoriesResponse { // ── rank_list ───────────────────────────────────────────────────── +/// One ranked security item. +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct RankListItem { + /// Symbol (converted from counter_id, e.g. `"MU.US"`) + pub symbol: String, + /// Ticker code (e.g. `"MU"`) + pub code: String, + /// Security name + pub name: String, + /// Latest price + pub last_done: String, + /// Price change ratio (decimal) + pub chg: String, + /// Absolute price change + pub change: String, + /// Net inflow + pub inflow: String, + /// Market cap + pub market_cap: String, + /// Industry name + pub industry: String, + /// Pre/post market price + #[serde(default)] + pub pre_post_price: String, + /// Pre/post market change + #[serde(default)] + pub pre_post_chg: String, + /// Amplitude + #[serde(default)] + pub amplitude: String, + /// 5-day change + #[serde(default)] + pub five_day_chg: String, + /// Turnover rate + #[serde(default)] + pub turnover_rate: String, + /// Volume ratio + #[serde(default)] + pub volume_rate: String, + /// P/B ratio (TTM) + #[serde(default)] + pub pb_ttm: String, +} + /// Response for [`crate::MarketContext::rank_list`] -/// -/// The raw data contains a ranked list of securities for the requested -/// category key. The exact structure varies so the payload is -/// preserved as raw JSON. #[derive(Debug, Clone, Serialize, Deserialize)] pub struct RankListResponse { - /// Raw rank list data - pub data: serde_json::Value, + /// Whether delayed / BMP data + pub bmp: bool, + /// Ranked security items + pub lists: Vec, } // ── enums ───────────────────────────────────────────────────────── diff --git a/rust/src/quote/context.rs b/rust/src/quote/context.rs index ea6130b54..4a69794eb 100644 --- a/rust/src/quote/context.rs +++ b/rust/src/quote/context.rs @@ -20,8 +20,9 @@ use crate::{ ParticipantInfo, Period, PushEvent, QuotePackageDetail, RealtimeQuote, RequestCreateWatchlistGroup, RequestUpdateWatchlistGroup, Security, SecurityBrokers, SecurityCalcIndex, SecurityDepth, SecurityListCategory, SecurityQuote, SecurityStaticInfo, - ShortPositionsResponse, ShortTradesResponse, StrikePriceInfo, Subscription, Trade, - TradeSessions, WarrantInfo, WarrantQuote, WarrantType, WatchlistGroup, + ShortPositionsItem, ShortPositionsResponse, ShortTradesItem, ShortTradesResponse, + StrikePriceInfo, Subscription, Trade, TradeSessions, WarrantInfo, WarrantQuote, + WarrantType, WatchlistGroup, cache::{Cache, CacheWithKey}, cmd_code, core::{Command, Core, UserProfile}, @@ -37,6 +38,19 @@ use crate::{ const RETRY_COUNT: usize = 3; const PARTICIPANT_INFO_CACHE_TIMEOUT: Duration = Duration::from_secs(30 * 60); + +/// Convert a Unix-seconds string (or integer string) to an RFC 3339 timestamp. +/// If parsing fails, the original string is returned unchanged. +fn unix_secs_to_rfc3339(s: &str) -> String { + s.parse::() + .ok() + .and_then(|ts| time::OffsetDateTime::from_unix_timestamp(ts).ok()) + .map(|dt| { + use time::format_description::well_known::Rfc3339; + dt.format(&Rfc3339).unwrap_or_default() + }) + .unwrap_or_else(|| s.to_string()) +} const ISSUER_INFO_CACHE_TIMEOUT: Duration = Duration::from_secs(30 * 60); const OPTION_CHAIN_EXPIRY_DATE_LIST_CACHE_TIMEOUT: Duration = Duration::from_secs(30 * 60); const OPTION_CHAIN_STRIKE_INFO_CACHE_TIMEOUT: Duration = Duration::from_secs(30 * 60); @@ -1991,7 +2005,7 @@ impl QuoteContext { last_timestamp: String, count: u32, } - let resp = self + let raw: Vec = self .0 .http_cli .request(Method::GET, path) @@ -2000,11 +2014,35 @@ impl QuoteContext { last_timestamp: ts.to_string(), count, }) - .response::>() + .response::>>() .send() .with_subscriber(self.0.log_subscriber.clone()) - .await?; - Ok(ShortPositionsResponse { data: resp.0 }) + .await? + .0; + let data = raw + .into_iter() + .map(|v| { + let ts_str = v["timestamp"].as_str().unwrap_or("").to_string(); + ShortPositionsItem { + timestamp: unix_secs_to_rfc3339(&ts_str), + rate: v["rate"].as_str().unwrap_or("").to_string(), + close: v["close"].as_str().unwrap_or("").to_string(), + current_shares_short: v["current_shares_short"] + .as_str() + .unwrap_or("") + .to_string(), + avg_daily_share_volume: v["avg_daily_share_volume"] + .as_str() + .unwrap_or("") + .to_string(), + days_to_cover: v["days_to_cover"].as_str().unwrap_or("").to_string(), + amount: v["amount"].as_str().unwrap_or("").to_string(), + balance: v["balance"].as_str().unwrap_or("").to_string(), + cost: v["cost"].as_str().unwrap_or("").to_string(), + } + }) + .collect(); + Ok(ShortPositionsResponse { data }) } // ── option_volume ───────────────────────────────────────────── @@ -2096,7 +2134,7 @@ impl QuoteContext { .duration_since(UNIX_EPOCH) .map(|d| d.as_secs()) .unwrap_or(0); - let resp = self + let raw: Vec = self .0 .http_cli .request(Method::GET, path) @@ -2105,11 +2143,28 @@ impl QuoteContext { last_timestamp: ts.to_string(), page_size: count.to_string(), }) - .response::>() + .response::>>() .send() .with_subscriber(self.0.log_subscriber.clone()) - .await?; - Ok(ShortTradesResponse { data: resp.0 }) + .await? + .0; + let data = raw + .into_iter() + .map(|v| { + let ts_str = v["timestamp"].as_str().unwrap_or("").to_string(); + ShortTradesItem { + timestamp: unix_secs_to_rfc3339(&ts_str), + rate: v["rate"].as_str().unwrap_or("").to_string(), + close: v["close"].as_str().unwrap_or("").to_string(), + nus_amount: v["nus_amount"].as_str().unwrap_or("").to_string(), + ny_amount: v["ny_amount"].as_str().unwrap_or("").to_string(), + total_amount: v["total_amount"].as_str().unwrap_or("").to_string(), + amount: v["amount"].as_str().unwrap_or("").to_string(), + balance: v["balance"].as_str().unwrap_or("").to_string(), + } + }) + .collect(); + Ok(ShortTradesResponse { data }) } // ── update_pinned ───────────────────────────────────────────── diff --git a/rust/src/quote/mod.rs b/rust/src/quote/mod.rs index 5870f3ea4..78dcba0cb 100644 --- a/rust/src/quote/mod.rs +++ b/rust/src/quote/mod.rs @@ -58,7 +58,9 @@ pub use types::{ SecurityListCategory, SecurityQuote, SecurityStaticInfo, + ShortPositionsItem, ShortPositionsResponse, + ShortTradesItem, ShortTradesResponse, SortOrderType, StrikePriceInfo, diff --git a/rust/src/quote/types.rs b/rust/src/quote/types.rs index 8cb48111c..08df9b007 100644 --- a/rust/src/quote/types.rs +++ b/rust/src/quote/types.rs @@ -2032,15 +2032,40 @@ impl_default_for_enum_string!( // ── short_positions ─────────────────────────────────────────────── +/// One short-position data point (unified for US and HK markets). +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct ShortPositionsItem { + /// Trading date (RFC 3339, e.g. `"2024-01-15T00:00:00Z"`) + pub timestamp: String, + /// Short ratio (both markets) + pub rate: String, + /// Closing price (both markets) + pub close: String, + /// [US] Number of short shares outstanding + #[serde(default)] + pub current_shares_short: String, + /// [US] Average daily share volume + #[serde(default)] + pub avg_daily_share_volume: String, + /// [US] Days to cover ratio + #[serde(default)] + pub days_to_cover: String, + /// [HK] Short sale amount (HKD) + #[serde(default)] + pub amount: String, + /// [HK] Short position balance + #[serde(default)] + pub balance: String, + /// [HK] Cost / closing price + #[serde(default)] + pub cost: String, +} + /// Response for [`crate::QuoteContext::short_positions`] -/// -/// The raw data contains short interest/position data for a HK or US -/// security. The exact structure differs between markets so the -/// payload is preserved as raw JSON. #[derive(Debug, Clone, Serialize, Deserialize)] pub struct ShortPositionsResponse { - /// Raw short positions data - pub data: serde_json::Value, + /// Short position data points + pub data: Vec, } // ── option_volume ───────────────────────────────────────────────── @@ -2094,14 +2119,37 @@ pub struct OptionVolumeDailyStat { // ── short_trades ────────────────────────────────────────────────── +/// One short-trade data point (unified for US and HK markets). +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct ShortTradesItem { + /// Trading date (RFC 3339) + pub timestamp: String, + /// Short ratio + pub rate: String, + /// Closing price + pub close: String, + /// [US] NYSE short amount + #[serde(default)] + pub nus_amount: String, + /// [US] NY short amount + #[serde(default)] + pub ny_amount: String, + /// [US] Total short amount + #[serde(default)] + pub total_amount: String, + /// [HK] Short sale amount + #[serde(default)] + pub amount: String, + /// [HK] Short position balance + #[serde(default)] + pub balance: String, +} + /// Response for [`crate::QuoteContext::short_trades`] -/// -/// The raw data contains short trade records for the queried security. -/// The exact structure varies so the payload is preserved as raw JSON. #[derive(Debug, Clone, Serialize, Deserialize)] pub struct ShortTradesResponse { - /// Raw short trade data - pub data: serde_json::Value, + /// Short trade data points + pub data: Vec, } // ── pinned mode ─────────────────────────────────────────────────── From c071ce1de9d1097cf9193d88eba45d2ad3015139 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=A2=81=E7=AB=A0=E6=B4=AA?= Date: Thu, 21 May 2026 15:39:02 +0800 Subject: [PATCH 08/12] fix(quote): fix short_positions/short_trades response parsing API wraps the array in {"counter_id":"...","data":[...]} object. Changed both methods to deserialize the outer object first, then extract the inner data array. Verified: Go 7/7 tests pass, Python short_positions/trades all RFC3339. Co-Authored-By: Claude Sonnet 4.6 (1M context) --- rust/src/quote/context.rs | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) diff --git a/rust/src/quote/context.rs b/rust/src/quote/context.rs index 4a69794eb..168525c95 100644 --- a/rust/src/quote/context.rs +++ b/rust/src/quote/context.rs @@ -2005,7 +2005,8 @@ impl QuoteContext { last_timestamp: String, count: u32, } - let raw: Vec = self + // Response: {"counter_id":"ST/US/AAPL","data":[{...}]} + let outer: serde_json::Value = self .0 .http_cli .request(Method::GET, path) @@ -2014,13 +2015,15 @@ impl QuoteContext { last_timestamp: ts.to_string(), count, }) - .response::>>() + .response::>() .send() .with_subscriber(self.0.log_subscriber.clone()) .await? .0; + let empty = vec![]; + let raw = outer["data"].as_array().unwrap_or(&empty); let data = raw - .into_iter() + .iter() .map(|v| { let ts_str = v["timestamp"].as_str().unwrap_or("").to_string(); ShortPositionsItem { @@ -2134,7 +2137,8 @@ impl QuoteContext { .duration_since(UNIX_EPOCH) .map(|d| d.as_secs()) .unwrap_or(0); - let raw: Vec = self + // Response: {"counter_id":"ST/HK/700","data":[{...}]} + let outer: serde_json::Value = self .0 .http_cli .request(Method::GET, path) @@ -2143,13 +2147,15 @@ impl QuoteContext { last_timestamp: ts.to_string(), page_size: count.to_string(), }) - .response::>>() + .response::>() .send() .with_subscriber(self.0.log_subscriber.clone()) .await? .0; + let empty = vec![]; + let raw = outer["data"].as_array().unwrap_or(&empty); let data = raw - .into_iter() + .iter() .map(|v| { let ts_str = v["timestamp"].as_str().unwrap_or("").to_string(); ShortTradesItem { From aa5b536aa4d238f8f5375cf4ddf397a94265d63d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=A2=81=E7=AB=A0=E6=B4=AA?= Date: Thu, 21 May 2026 15:59:30 +0800 Subject: [PATCH 09/12] fix(java,c,cpp): update response types for new typed SDK structs Replace raw-JSON stub structs with fully typed structs in Java, C, and C++ SDKs for five APIs: short_positions, short_trades (QuoteContext); top_movers, rank_list (MarketContext); valuation_comparison (FundamentalContext). Java: add ShortPositionsItem, ShortTradesItem, TopMoversStock, TopMoversEvent, TopMoversResponse, RankListItem, RankListResponse, ValuationComparisonItem, ValuationHistoryPoint, ValuationComparisonResponse. C: add CShortPositionsItem, CShortTradesItem, CTopMoversStock, CTopMoversEvent, CTopMoversResponse, CRankListItem, CRankListResponse, CValuationHistoryPoint, CValuationComparisonItem, CValuationComparisonResponse; rebuild longbridge.h. C++: add matching structs to types.hpp and convert functions to convert.hpp; update market_context and fundamental_context to return typed responses. Co-Authored-By: Claude Sonnet 4.6 (1M context) --- CHANGELOG.md | 1 + c/cbindgen.toml | 17 +- c/csrc/include/longbridge.h | 372 +++++++++++++++++- c/src/fundamental_context/types.rs | 162 +++++++- c/src/market_context/types.rs | 273 ++++++++++++- c/src/quote_context/types.rs | 170 +++++++- cpp/include/fundamental_context.hpp | 4 +- cpp/include/market_context.hpp | 8 +- cpp/include/types.hpp | 149 ++++++- cpp/src/convert.hpp | 85 +++- cpp/src/fundamental_context.cpp | 14 +- cpp/src/market_context.cpp | 42 +- .../fundamental/ValuationComparisonItem.java | 22 ++ .../ValuationComparisonResponse.java | 6 +- .../fundamental/ValuationHistoryPoint.java | 10 + .../com/longbridge/market/RankListItem.java | 37 ++ .../longbridge/market/RankListResponse.java | 8 +- .../com/longbridge/market/TopMoversEvent.java | 15 + .../longbridge/market/TopMoversResponse.java | 10 +- .../com/longbridge/market/TopMoversStock.java | 23 ++ .../longbridge/quote/ShortPositionsItem.java | 26 ++ .../quote/ShortPositionsResponse.java | 6 +- .../com/longbridge/quote/ShortTradesItem.java | 23 ++ .../longbridge/quote/ShortTradesResponse.java | 6 +- 24 files changed, 1388 insertions(+), 101 deletions(-) create mode 100644 java/javasrc/src/main/java/com/longbridge/fundamental/ValuationComparisonItem.java create mode 100644 java/javasrc/src/main/java/com/longbridge/fundamental/ValuationHistoryPoint.java create mode 100644 java/javasrc/src/main/java/com/longbridge/market/RankListItem.java create mode 100644 java/javasrc/src/main/java/com/longbridge/market/TopMoversEvent.java create mode 100644 java/javasrc/src/main/java/com/longbridge/market/TopMoversStock.java create mode 100644 java/javasrc/src/main/java/com/longbridge/quote/ShortPositionsItem.java create mode 100644 java/javasrc/src/main/java/com/longbridge/quote/ShortTradesItem.java diff --git a/CHANGELOG.md b/CHANGELOG.md index ac64f0379..432409397 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -16,6 +16,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Changed +- **Java, C, C++:** Response types for `short_positions`, `short_trades`, `top_movers`, `rank_list`, and `valuation_comparison` updated from raw-JSON stub structs to fully typed structs with named fields, matching the Rust/Python/Node.js SDK types. C SDK regenerated (`longbridge.h`) with new `lb_short_positions_item_t`, `lb_short_trades_item_t`, `lb_top_movers_stock_t`, `lb_top_movers_event_t`, `lb_rank_list_item_t`, `lb_valuation_history_point_t`, `lb_valuation_comparison_item_t` types. - **All languages:** `QuoteContext::short_positions(symbol, count)` now auto-detects market from symbol suffix (`.HK` → HK, otherwise US). `ShortPositionsResponse` is raw JSON. - **Rust, Python, Node.js, Java, C:** `short_positions` and `short_trades` responses now use typed structs (`ShortPositionsItem` / `ShortTradesItem`) with RFC 3339 timestamps instead of raw JSON. Fields absent in a given market default to empty string. - **Rust, Python, Node.js, Java, C:** `rank_list` response now uses a typed `RankListResponse { bmp, lists: Vec }` struct; `counter_id` is converted to `symbol`. diff --git a/c/cbindgen.toml b/c/cbindgen.toml index 4c119f415..ed12c8f59 100644 --- a/c/cbindgen.toml +++ b/c/cbindgen.toml @@ -297,7 +297,9 @@ cpp_compat = true "CSnapshotReportedMetric" = "lb_snapshot_reported_metric_t" "CFinancialReportSnapshot" = "lb_financial_report_snapshot_t" # QuoteContext extensions +"CShortPositionsItem" = "lb_short_positions_item_t" "CShortPositionsResponse" = "lb_short_positions_response_t" +"CShortTradesItem" = "lb_short_trades_item_t" "CShortTradesResponse" = "lb_short_trades_response_t" "COptionVolumeStats" = "lb_option_volume_stats_t" "COptionVolumeDailyStat" = "lb_option_volume_daily_stat_t" @@ -305,10 +307,15 @@ cpp_compat = true # FundamentalContext new types "CShareholderTopResponse" = "lb_shareholder_top_response_t" "CShareholderDetailResponse" = "lb_shareholder_detail_response_t" +"CValuationHistoryPoint" = "lb_valuation_history_point_t" +"CValuationComparisonItem" = "lb_valuation_comparison_item_t" "CValuationComparisonResponse" = "lb_valuation_comparison_response_t" # MarketContext new types +"CTopMoversStock" = "lb_top_movers_stock_t" +"CTopMoversEvent" = "lb_top_movers_event_t" "CTopMoversResponse" = "lb_top_movers_response_t" "CRankCategoriesResponse" = "lb_rank_categories_response_t" +"CRankListItem" = "lb_rank_list_item_t" "CRankListResponse" = "lb_rank_list_response_t" # ScreenerContext "CScreenerContext" = "lb_screener_context_t" @@ -430,13 +437,17 @@ include = [ # FundamentalContext opaque type (no rename, typedef added in hpp) "CFundamentalContext", # QuoteContext extensions - "CShortPositionsResponse", "CShortTradesResponse", + "CShortPositionsItem", "CShortPositionsResponse", + "CShortTradesItem", "CShortTradesResponse", "COptionVolumeStats", "COptionVolumeDailyStat", "COptionVolumeDaily", # FundamentalContext new types - "CShareholderTopResponse", "CShareholderDetailResponse", "CValuationComparisonResponse", + "CShareholderTopResponse", "CShareholderDetailResponse", + "CValuationHistoryPoint", "CValuationComparisonItem", "CValuationComparisonResponse", # MarketContext new types - "CTopMoversResponse", "CRankCategoriesResponse", "CRankListResponse", + "CTopMoversStock", "CTopMoversEvent", "CTopMoversResponse", + "CRankCategoriesResponse", + "CRankListItem", "CRankListResponse", # ScreenerContext "CScreenerContext", "CScreenerRecommendStrategiesResponse", "CScreenerUserStrategiesResponse", diff --git a/c/csrc/include/longbridge.h b/c/csrc/include/longbridge.h index 474d0adac..0a5134632 100644 --- a/c/csrc/include/longbridge.h +++ b/c/csrc/include/longbridge.h @@ -7724,28 +7724,112 @@ typedef struct lb_sharelist_detail_t { struct lb_sharelist_scopes_t scopes; } lb_sharelist_detail_t; +/** + * One short-position record, unified for US and HK markets. + */ +typedef struct lb_short_positions_item_t { + /** + * Trading date in RFC 3339 format + */ + const char *timestamp; + /** + * Short ratio + */ + const char *rate; + /** + * Closing price + */ + const char *close; + /** + * [US] Number of short shares outstanding + */ + const char *current_shares_short; + /** + * [US] Average daily share volume + */ + const char *avg_daily_share_volume; + /** + * [US] Days-to-cover ratio + */ + const char *days_to_cover; + /** + * [HK] Short sale amount (HKD) + */ + const char *amount; + /** + * [HK] Short position balance + */ + const char *balance; + /** + * [HK] Closing price (HK naming) + */ + const char *cost; +} lb_short_positions_item_t; + /** * Short positions / interest response (HK or US). - * - * `data` is a NUL-terminated JSON string. */ typedef struct lb_short_positions_response_t { /** - * Raw short positions data as a JSON string + * Pointer to the array of short position items */ - const char *data; + const struct lb_short_positions_item_t *data; + /** + * Number of items in `data` + */ + uintptr_t num_data; } lb_short_positions_response_t; +/** + * One short-trade record, unified for US and HK markets. + */ +typedef struct lb_short_trades_item_t { + /** + * Trading date in RFC 3339 format + */ + const char *timestamp; + /** + * Short ratio + */ + const char *rate; + /** + * Closing price + */ + const char *close; + /** + * [US] NASDAQ short sale volume + */ + const char *nus_amount; + /** + * [US] NYSE short sale volume + */ + const char *ny_amount; + /** + * [US] Total short amount + */ + const char *total_amount; + /** + * [HK] Short sale turnover amount (HKD) + */ + const char *amount; + /** + * [HK] Short position balance + */ + const char *balance; +} lb_short_trades_item_t; + /** * Short trade records response (HK or US). - * - * `data` is a NUL-terminated JSON string. */ typedef struct lb_short_trades_response_t { /** - * Raw short trade data as a JSON string + * Pointer to the array of short trade items */ - const char *data; + const struct lb_short_trades_item_t *data; + /** + * Number of items in `data` + */ + uintptr_t num_data; } lb_short_trades_response_t; /** @@ -7843,23 +7927,199 @@ typedef struct lb_shareholder_detail_response_t { } lb_shareholder_detail_response_t; /** - * Valuation comparison response. `data` is a NUL-terminated JSON string. + * One historical valuation data point. + */ +typedef struct lb_valuation_history_point_t { + /** + * Date in RFC 3339 format + */ + const char *date; + /** + * P/E ratio + */ + const char *pe; + /** + * P/B ratio + */ + const char *pb; + /** + * P/S ratio + */ + const char *ps; +} lb_valuation_history_point_t; + +/** + * One security's valuation comparison item. + */ +typedef struct lb_valuation_comparison_item_t { + /** + * Symbol, e.g. "AAPL.US" + */ + const char *symbol; + /** + * Security name + */ + const char *name; + /** + * Currency + */ + const char *currency; + /** + * Market capitalisation + */ + const char *market_value; + /** + * Latest closing price + */ + const char *price_close; + /** + * P/E ratio + */ + const char *pe; + /** + * P/B ratio + */ + const char *pb; + /** + * P/S ratio + */ + const char *ps; + /** + * Return on equity + */ + const char *roe; + /** + * Earnings per share + */ + const char *eps; + /** + * Book value per share + */ + const char *bps; + /** + * Dividends per share + */ + const char *dps; + /** + * Dividend yield + */ + const char *div_yld; + /** + * Total assets + */ + const char *assets; + /** + * Pointer to the array of historical valuation points + */ + const struct lb_valuation_history_point_t *history; + /** + * Number of items in `history` + */ + uintptr_t num_history; +} lb_valuation_comparison_item_t; + +/** + * Valuation comparison response. */ typedef struct lb_valuation_comparison_response_t { /** - * Raw valuation comparison data as a JSON string + * Pointer to the array of valuation comparison items */ - const char *data; + const struct lb_valuation_comparison_item_t *list; + /** + * Number of items in `list` + */ + uintptr_t num_list; } lb_valuation_comparison_response_t; /** - * Top movers response. `data` is a NUL-terminated JSON string. + * Stock information within a top-movers event. + */ +typedef struct lb_top_movers_stock_t { + /** + * Symbol, e.g. "TSLA.US" + */ + const char *symbol; + /** + * Ticker code + */ + const char *code; + /** + * Security name + */ + const char *name; + /** + * Full name + */ + const char *full_name; + /** + * Price change (decimal ratio) + */ + const char *change; + /** + * Latest price + */ + const char *last_done; + /** + * Market code + */ + const char *market; + /** + * Logo URL + */ + const char *logo; + /** + * Labels / tags + */ + const char *const *labels; + /** + * Number of items in `labels` + */ + uintptr_t num_labels; +} lb_top_movers_stock_t; + +/** + * One top-movers event entry. + */ +typedef struct lb_top_movers_event_t { + /** + * Event time (RFC 3339) + */ + const char *timestamp; + /** + * Alert reason description + */ + const char *alert_reason; + /** + * Alert type code + */ + int64_t alert_type; + /** + * Stock information + */ + struct lb_top_movers_stock_t stock; + /** + * Associated news post as a JSON string (may be null) + */ + const char *post; +} lb_top_movers_event_t; + +/** + * Top movers response. */ typedef struct lb_top_movers_response_t { /** - * Raw top movers data as a JSON string + * Pointer to the array of top-mover events */ - const char *data; + const struct lb_top_movers_event_t *events; + /** + * Number of items in `events` + */ + uintptr_t num_events; + /** + * Pagination cursor as a JSON string + */ + const char *next_params; } lb_top_movers_response_t; /** @@ -7873,13 +8133,91 @@ typedef struct lb_rank_categories_response_t { } lb_rank_categories_response_t; /** - * Rank list response. `data` is a NUL-terminated JSON string. + * One ranked security item. + */ +typedef struct lb_rank_list_item_t { + /** + * Symbol, e.g. "MU.US" + */ + const char *symbol; + /** + * Ticker code + */ + const char *code; + /** + * Security name + */ + const char *name; + /** + * Latest price + */ + const char *last_done; + /** + * Price change ratio (decimal) + */ + const char *chg; + /** + * Absolute price change + */ + const char *change; + /** + * Net inflow + */ + const char *inflow; + /** + * Market cap + */ + const char *market_cap; + /** + * Industry name + */ + const char *industry; + /** + * Pre/post market price + */ + const char *pre_post_price; + /** + * Pre/post market change + */ + const char *pre_post_chg; + /** + * Amplitude + */ + const char *amplitude; + /** + * 5-day change + */ + const char *five_day_chg; + /** + * Turnover rate + */ + const char *turnover_rate; + /** + * Volume ratio + */ + const char *volume_rate; + /** + * P/B ratio (TTM) + */ + const char *pb_ttm; +} lb_rank_list_item_t; + +/** + * Rank list response. */ typedef struct lb_rank_list_response_t { /** - * Raw rank list data as a JSON string + * Whether the response is delayed / BMP data */ - const char *data; + bool bmp; + /** + * Pointer to the array of ranked security items + */ + const struct lb_rank_list_item_t *lists; + /** + * Number of items in `lists` + */ + uintptr_t num_lists; } lb_rank_list_response_t; /** diff --git a/c/src/fundamental_context/types.rs b/c/src/fundamental_context/types.rs index 1dbde4f60..08f7630e1 100644 --- a/c/src/fundamental_context/types.rs +++ b/c/src/fundamental_context/types.rs @@ -2903,21 +2903,168 @@ impl ToFFI for CShareholderDetailResponseOwned { // ── ValuationComparisonResponse ─────────────────────────────────── -/// Valuation comparison response. `data` is a NUL-terminated JSON string. +/// One historical valuation data point. +#[repr(C)] +pub struct CValuationHistoryPoint { + /// Date in RFC 3339 format + pub date: *const c_char, + /// P/E ratio + pub pe: *const c_char, + /// P/B ratio + pub pb: *const c_char, + /// P/S ratio + pub ps: *const c_char, +} + +pub(crate) struct CValuationHistoryPointOwned { + date: CString, + pe: CString, + pb: CString, + ps: CString, +} + +impl From for CValuationHistoryPointOwned { + fn from(v: ValuationHistoryPoint) -> Self { + Self { + date: v.date.into(), + pe: v.pe.into(), + pb: v.pb.into(), + ps: v.ps.into(), + } + } +} + +impl ToFFI for CValuationHistoryPointOwned { + type FFIType = CValuationHistoryPoint; + fn to_ffi_type(&self) -> Self::FFIType { + CValuationHistoryPoint { + date: self.date.to_ffi_type(), + pe: self.pe.to_ffi_type(), + pb: self.pb.to_ffi_type(), + ps: self.ps.to_ffi_type(), + } + } +} + +/// One security's valuation comparison item. +#[repr(C)] +pub struct CValuationComparisonItem { + /// Symbol, e.g. "AAPL.US" + pub symbol: *const c_char, + /// Security name + pub name: *const c_char, + /// Currency + pub currency: *const c_char, + /// Market capitalisation + pub market_value: *const c_char, + /// Latest closing price + pub price_close: *const c_char, + /// P/E ratio + pub pe: *const c_char, + /// P/B ratio + pub pb: *const c_char, + /// P/S ratio + pub ps: *const c_char, + /// Return on equity + pub roe: *const c_char, + /// Earnings per share + pub eps: *const c_char, + /// Book value per share + pub bps: *const c_char, + /// Dividends per share + pub dps: *const c_char, + /// Dividend yield + pub div_yld: *const c_char, + /// Total assets + pub assets: *const c_char, + /// Pointer to the array of historical valuation points + pub history: *const CValuationHistoryPoint, + /// Number of items in `history` + pub num_history: usize, +} + +pub(crate) struct CValuationComparisonItemOwned { + symbol: CString, + name: CString, + currency: CString, + market_value: CString, + price_close: CString, + pe: CString, + pb: CString, + ps: CString, + roe: CString, + eps: CString, + bps: CString, + dps: CString, + div_yld: CString, + assets: CString, + history: CVec, +} + +impl From for CValuationComparisonItemOwned { + fn from(v: ValuationComparisonItem) -> Self { + Self { + symbol: v.symbol.into(), + name: v.name.into(), + currency: v.currency.into(), + market_value: v.market_value.into(), + price_close: v.price_close.into(), + pe: v.pe.into(), + pb: v.pb.into(), + ps: v.ps.into(), + roe: v.roe.into(), + eps: v.eps.into(), + bps: v.bps.into(), + dps: v.dps.into(), + div_yld: v.div_yld.into(), + assets: v.assets.into(), + history: v.history.into(), + } + } +} + +impl ToFFI for CValuationComparisonItemOwned { + type FFIType = CValuationComparisonItem; + fn to_ffi_type(&self) -> Self::FFIType { + CValuationComparisonItem { + symbol: self.symbol.to_ffi_type(), + name: self.name.to_ffi_type(), + currency: self.currency.to_ffi_type(), + market_value: self.market_value.to_ffi_type(), + price_close: self.price_close.to_ffi_type(), + pe: self.pe.to_ffi_type(), + pb: self.pb.to_ffi_type(), + ps: self.ps.to_ffi_type(), + roe: self.roe.to_ffi_type(), + eps: self.eps.to_ffi_type(), + bps: self.bps.to_ffi_type(), + dps: self.dps.to_ffi_type(), + div_yld: self.div_yld.to_ffi_type(), + assets: self.assets.to_ffi_type(), + history: self.history.to_ffi_type(), + num_history: self.history.len(), + } + } +} + +/// Valuation comparison response. #[repr(C)] pub struct CValuationComparisonResponse { - /// Raw valuation comparison data as a JSON string - pub data: *const c_char, + /// Pointer to the array of valuation comparison items + pub list: *const CValuationComparisonItem, + /// Number of items in `list` + pub num_list: usize, } pub(crate) struct CValuationComparisonResponseOwned { - data: CString, + list: CVec, } impl From for CValuationComparisonResponseOwned { fn from(v: ValuationComparisonResponse) -> Self { - let json = serde_json::to_string(&v).unwrap_or_default(); - Self { data: json.into() } + Self { + list: v.list.into(), + } } } @@ -2925,7 +3072,8 @@ impl ToFFI for CValuationComparisonResponseOwned { type FFIType = CValuationComparisonResponse; fn to_ffi_type(&self) -> Self::FFIType { CValuationComparisonResponse { - data: self.data.to_ffi_type(), + list: self.list.to_ffi_type(), + num_list: self.list.len(), } } } diff --git a/c/src/market_context/types.rs b/c/src/market_context/types.rs index 2ba6e3749..db010ff8c 100644 --- a/c/src/market_context/types.rs +++ b/c/src/market_context/types.rs @@ -4,8 +4,9 @@ use longbridge::market::{ AhPremiumIntraday, AhPremiumKline, AhPremiumKlines, AnomalyItem, AnomalyResponse, BrokerHoldingChanges, BrokerHoldingDailyHistory, BrokerHoldingDailyItem, BrokerHoldingDetail, BrokerHoldingDetailItem, BrokerHoldingEntry, BrokerHoldingTop, ConstituentStock, - IndexConstituents, MarketStatusResponse, MarketTimeItem, RankCategoriesResponse, - RankListResponse, TopMoversResponse, TradePriceLevel, TradeStatistics, TradeStatsResponse, + IndexConstituents, MarketStatusResponse, MarketTimeItem, RankCategoriesResponse, RankListItem, + RankListResponse, TopMoversEvent, TopMoversResponse, TopMoversStock, TradePriceLevel, + TradeStatistics, TradeStatsResponse, }; use crate::types::{CMarket, CString, CVec, ToFFI}; @@ -960,21 +961,147 @@ impl ToFFI for CIndexConstituentsOwned { // ── TopMoversResponse ───────────────────────────────────────────── -/// Top movers response. `data` is a NUL-terminated JSON string. +/// Stock information within a top-movers event. +#[repr(C)] +pub struct CTopMoversStock { + /// Symbol, e.g. "TSLA.US" + pub symbol: *const c_char, + /// Ticker code + pub code: *const c_char, + /// Security name + pub name: *const c_char, + /// Full name + pub full_name: *const c_char, + /// Price change (decimal ratio) + pub change: *const c_char, + /// Latest price + pub last_done: *const c_char, + /// Market code + pub market: *const c_char, + /// Logo URL + pub logo: *const c_char, + /// Labels / tags + pub labels: *const *const c_char, + /// Number of items in `labels` + pub num_labels: usize, +} + +pub(crate) struct CTopMoversStockOwned { + symbol: CString, + code: CString, + name: CString, + full_name: CString, + change: CString, + last_done: CString, + market: CString, + logo: CString, + labels: CVec, +} + +impl From for CTopMoversStockOwned { + fn from(v: TopMoversStock) -> Self { + Self { + symbol: v.symbol.into(), + code: v.code.into(), + name: v.name.into(), + full_name: v.full_name.into(), + change: v.change.into(), + last_done: v.last_done.into(), + market: v.market.into(), + logo: v.logo.into(), + labels: v.labels.into(), + } + } +} + +impl ToFFI for CTopMoversStockOwned { + type FFIType = CTopMoversStock; + fn to_ffi_type(&self) -> Self::FFIType { + CTopMoversStock { + symbol: self.symbol.to_ffi_type(), + code: self.code.to_ffi_type(), + name: self.name.to_ffi_type(), + full_name: self.full_name.to_ffi_type(), + change: self.change.to_ffi_type(), + last_done: self.last_done.to_ffi_type(), + market: self.market.to_ffi_type(), + logo: self.logo.to_ffi_type(), + labels: self.labels.to_ffi_type(), + num_labels: self.labels.len(), + } + } +} + +/// One top-movers event entry. +#[repr(C)] +pub struct CTopMoversEvent { + /// Event time (RFC 3339) + pub timestamp: *const c_char, + /// Alert reason description + pub alert_reason: *const c_char, + /// Alert type code + pub alert_type: i64, + /// Stock information + pub stock: CTopMoversStock, + /// Associated news post as a JSON string (may be null) + pub post: *const c_char, +} + +pub(crate) struct CTopMoversEventOwned { + timestamp: CString, + alert_reason: CString, + alert_type: i64, + stock: CTopMoversStockOwned, + post: CString, +} + +impl From for CTopMoversEventOwned { + fn from(v: TopMoversEvent) -> Self { + Self { + timestamp: v.timestamp.into(), + alert_reason: v.alert_reason.into(), + alert_type: v.alert_type, + stock: v.stock.into(), + post: v.post.to_string().into(), + } + } +} + +impl ToFFI for CTopMoversEventOwned { + type FFIType = CTopMoversEvent; + fn to_ffi_type(&self) -> Self::FFIType { + CTopMoversEvent { + timestamp: self.timestamp.to_ffi_type(), + alert_reason: self.alert_reason.to_ffi_type(), + alert_type: self.alert_type, + stock: self.stock.to_ffi_type(), + post: self.post.to_ffi_type(), + } + } +} + +/// Top movers response. #[repr(C)] pub struct CTopMoversResponse { - /// Raw top movers data as a JSON string - pub data: *const c_char, + /// Pointer to the array of top-mover events + pub events: *const CTopMoversEvent, + /// Number of items in `events` + pub num_events: usize, + /// Pagination cursor as a JSON string + pub next_params: *const c_char, } pub(crate) struct CTopMoversResponseOwned { - data: CString, + events: CVec, + next_params: CString, } impl From for CTopMoversResponseOwned { fn from(v: TopMoversResponse) -> Self { - let json = serde_json::to_string(&v).unwrap_or_default(); - Self { data: json.into() } + Self { + events: v.events.into(), + next_params: v.next_params.to_string().into(), + } } } @@ -982,7 +1109,9 @@ impl ToFFI for CTopMoversResponseOwned { type FFIType = CTopMoversResponse; fn to_ffi_type(&self) -> Self::FFIType { CTopMoversResponse { - data: self.data.to_ffi_type(), + events: self.events.to_ffi_type(), + num_events: self.events.len(), + next_params: self.next_params.to_ffi_type(), } } } @@ -1018,21 +1147,131 @@ impl ToFFI for CRankCategoriesResponseOwned { // ── RankListResponse ────────────────────────────────────────────── -/// Rank list response. `data` is a NUL-terminated JSON string. +/// One ranked security item. +#[repr(C)] +pub struct CRankListItem { + /// Symbol, e.g. "MU.US" + pub symbol: *const c_char, + /// Ticker code + pub code: *const c_char, + /// Security name + pub name: *const c_char, + /// Latest price + pub last_done: *const c_char, + /// Price change ratio (decimal) + pub chg: *const c_char, + /// Absolute price change + pub change: *const c_char, + /// Net inflow + pub inflow: *const c_char, + /// Market cap + pub market_cap: *const c_char, + /// Industry name + pub industry: *const c_char, + /// Pre/post market price + pub pre_post_price: *const c_char, + /// Pre/post market change + pub pre_post_chg: *const c_char, + /// Amplitude + pub amplitude: *const c_char, + /// 5-day change + pub five_day_chg: *const c_char, + /// Turnover rate + pub turnover_rate: *const c_char, + /// Volume ratio + pub volume_rate: *const c_char, + /// P/B ratio (TTM) + pub pb_ttm: *const c_char, +} + +pub(crate) struct CRankListItemOwned { + symbol: CString, + code: CString, + name: CString, + last_done: CString, + chg: CString, + change: CString, + inflow: CString, + market_cap: CString, + industry: CString, + pre_post_price: CString, + pre_post_chg: CString, + amplitude: CString, + five_day_chg: CString, + turnover_rate: CString, + volume_rate: CString, + pb_ttm: CString, +} + +impl From for CRankListItemOwned { + fn from(v: RankListItem) -> Self { + Self { + symbol: v.symbol.into(), + code: v.code.into(), + name: v.name.into(), + last_done: v.last_done.into(), + chg: v.chg.into(), + change: v.change.into(), + inflow: v.inflow.into(), + market_cap: v.market_cap.into(), + industry: v.industry.into(), + pre_post_price: v.pre_post_price.into(), + pre_post_chg: v.pre_post_chg.into(), + amplitude: v.amplitude.into(), + five_day_chg: v.five_day_chg.into(), + turnover_rate: v.turnover_rate.into(), + volume_rate: v.volume_rate.into(), + pb_ttm: v.pb_ttm.into(), + } + } +} + +impl ToFFI for CRankListItemOwned { + type FFIType = CRankListItem; + fn to_ffi_type(&self) -> Self::FFIType { + CRankListItem { + symbol: self.symbol.to_ffi_type(), + code: self.code.to_ffi_type(), + name: self.name.to_ffi_type(), + last_done: self.last_done.to_ffi_type(), + chg: self.chg.to_ffi_type(), + change: self.change.to_ffi_type(), + inflow: self.inflow.to_ffi_type(), + market_cap: self.market_cap.to_ffi_type(), + industry: self.industry.to_ffi_type(), + pre_post_price: self.pre_post_price.to_ffi_type(), + pre_post_chg: self.pre_post_chg.to_ffi_type(), + amplitude: self.amplitude.to_ffi_type(), + five_day_chg: self.five_day_chg.to_ffi_type(), + turnover_rate: self.turnover_rate.to_ffi_type(), + volume_rate: self.volume_rate.to_ffi_type(), + pb_ttm: self.pb_ttm.to_ffi_type(), + } + } +} + +/// Rank list response. #[repr(C)] pub struct CRankListResponse { - /// Raw rank list data as a JSON string - pub data: *const c_char, + /// Whether the response is delayed / BMP data + pub bmp: bool, + /// Pointer to the array of ranked security items + pub lists: *const CRankListItem, + /// Number of items in `lists` + pub num_lists: usize, } pub(crate) struct CRankListResponseOwned { - data: CString, + bmp: bool, + lists: CVec, } impl From for CRankListResponseOwned { fn from(v: RankListResponse) -> Self { - let json = serde_json::to_string(&v).unwrap_or_default(); - Self { data: json.into() } + Self { + bmp: v.bmp, + lists: v.lists.into(), + } } } @@ -1040,7 +1279,9 @@ impl ToFFI for CRankListResponseOwned { type FFIType = CRankListResponse; fn to_ffi_type(&self) -> Self::FFIType { CRankListResponse { - data: self.data.to_ffi_type(), + bmp: self.bmp, + lists: self.lists.to_ffi_type(), + num_lists: self.lists.len(), } } } diff --git a/c/src/quote_context/types.rs b/c/src/quote_context/types.rs index c68cf872d..72cdaf5c8 100644 --- a/c/src/quote_context/types.rs +++ b/c/src/quote_context/types.rs @@ -7,9 +7,10 @@ use longbridge::quote::{ OptionVolumeDaily, OptionVolumeDailyStat, OptionVolumeStats, ParticipantInfo, Period, PrePostQuote, PushBrokers, PushCandlestick, PushDepth, PushQuote, PushTrades, QuotePackageDetail, RealtimeQuote, Security, SecurityBoard, SecurityBrokers, SecurityCalcIndex, - SecurityDepth, SecurityQuote, SecurityStaticInfo, ShortPositionsResponse, StrikePriceInfo, - Subscription, Trade, TradeDirection, TradeSession, TradeStatus, TradingSessionInfo, - WarrantInfo, WarrantQuote, WarrantType, WatchlistGroup, WatchlistSecurity, + SecurityDepth, SecurityQuote, SecurityStaticInfo, ShortPositionsItem, ShortPositionsResponse, + ShortTradesItem, ShortTradesResponse, StrikePriceInfo, Subscription, Trade, TradeDirection, + TradeSession, TradeStatus, TradingSessionInfo, WarrantInfo, WarrantQuote, WarrantType, + WatchlistGroup, WatchlistSecurity, }; use crate::{ @@ -3106,23 +3107,92 @@ impl ToFFI for CFilingItemOwned { // ── ShortPositionsResponse ──────────────────────────────────────── +/// One short-position record, unified for US and HK markets. +#[repr(C)] +pub struct CShortPositionsItem { + /// Trading date in RFC 3339 format + pub timestamp: *const c_char, + /// Short ratio + pub rate: *const c_char, + /// Closing price + pub close: *const c_char, + /// [US] Number of short shares outstanding + pub current_shares_short: *const c_char, + /// [US] Average daily share volume + pub avg_daily_share_volume: *const c_char, + /// [US] Days-to-cover ratio + pub days_to_cover: *const c_char, + /// [HK] Short sale amount (HKD) + pub amount: *const c_char, + /// [HK] Short position balance + pub balance: *const c_char, + /// [HK] Closing price (HK naming) + pub cost: *const c_char, +} + +pub(crate) struct CShortPositionsItemOwned { + timestamp: CString, + rate: CString, + close: CString, + current_shares_short: CString, + avg_daily_share_volume: CString, + days_to_cover: CString, + amount: CString, + balance: CString, + cost: CString, +} + +impl From for CShortPositionsItemOwned { + fn from(v: ShortPositionsItem) -> Self { + Self { + timestamp: v.timestamp.into(), + rate: v.rate.into(), + close: v.close.into(), + current_shares_short: v.current_shares_short.into(), + avg_daily_share_volume: v.avg_daily_share_volume.into(), + days_to_cover: v.days_to_cover.into(), + amount: v.amount.into(), + balance: v.balance.into(), + cost: v.cost.into(), + } + } +} + +impl ToFFI for CShortPositionsItemOwned { + type FFIType = CShortPositionsItem; + fn to_ffi_type(&self) -> Self::FFIType { + CShortPositionsItem { + timestamp: self.timestamp.to_ffi_type(), + rate: self.rate.to_ffi_type(), + close: self.close.to_ffi_type(), + current_shares_short: self.current_shares_short.to_ffi_type(), + avg_daily_share_volume: self.avg_daily_share_volume.to_ffi_type(), + days_to_cover: self.days_to_cover.to_ffi_type(), + amount: self.amount.to_ffi_type(), + balance: self.balance.to_ffi_type(), + cost: self.cost.to_ffi_type(), + } + } +} + /// Short positions / interest response (HK or US). -/// -/// `data` is a NUL-terminated JSON string. #[repr(C)] pub struct CShortPositionsResponse { - /// Raw short positions data as a JSON string - pub data: *const c_char, + /// Pointer to the array of short position items + pub data: *const CShortPositionsItem, + /// Number of items in `data` + pub num_data: usize, } pub(crate) struct CShortPositionsResponseOwned { - data: CString, + data: CVec, } impl From for CShortPositionsResponseOwned { fn from(v: ShortPositionsResponse) -> Self { - let json = serde_json::to_string(&v).unwrap_or_default(); - Self { data: json.into() } + Self { + data: v.data.into(), + } } } @@ -3131,31 +3201,94 @@ impl ToFFI for CShortPositionsResponseOwned { fn to_ffi_type(&self) -> Self::FFIType { CShortPositionsResponse { data: self.data.to_ffi_type(), + num_data: self.data.len(), } } } // ── ShortTradesResponse ─────────────────────────────────────────── -use longbridge::quote::ShortTradesResponse; +/// One short-trade record, unified for US and HK markets. +#[repr(C)] +pub struct CShortTradesItem { + /// Trading date in RFC 3339 format + pub timestamp: *const c_char, + /// Short ratio + pub rate: *const c_char, + /// Closing price + pub close: *const c_char, + /// [US] NASDAQ short sale volume + pub nus_amount: *const c_char, + /// [US] NYSE short sale volume + pub ny_amount: *const c_char, + /// [US] Total short amount + pub total_amount: *const c_char, + /// [HK] Short sale turnover amount (HKD) + pub amount: *const c_char, + /// [HK] Short position balance + pub balance: *const c_char, +} + +pub(crate) struct CShortTradesItemOwned { + timestamp: CString, + rate: CString, + close: CString, + nus_amount: CString, + ny_amount: CString, + total_amount: CString, + amount: CString, + balance: CString, +} + +impl From for CShortTradesItemOwned { + fn from(v: ShortTradesItem) -> Self { + Self { + timestamp: v.timestamp.into(), + rate: v.rate.into(), + close: v.close.into(), + nus_amount: v.nus_amount.into(), + ny_amount: v.ny_amount.into(), + total_amount: v.total_amount.into(), + amount: v.amount.into(), + balance: v.balance.into(), + } + } +} + +impl ToFFI for CShortTradesItemOwned { + type FFIType = CShortTradesItem; + fn to_ffi_type(&self) -> Self::FFIType { + CShortTradesItem { + timestamp: self.timestamp.to_ffi_type(), + rate: self.rate.to_ffi_type(), + close: self.close.to_ffi_type(), + nus_amount: self.nus_amount.to_ffi_type(), + ny_amount: self.ny_amount.to_ffi_type(), + total_amount: self.total_amount.to_ffi_type(), + amount: self.amount.to_ffi_type(), + balance: self.balance.to_ffi_type(), + } + } +} /// Short trade records response (HK or US). -/// -/// `data` is a NUL-terminated JSON string. #[repr(C)] pub struct CShortTradesResponse { - /// Raw short trade data as a JSON string - pub data: *const c_char, + /// Pointer to the array of short trade items + pub data: *const CShortTradesItem, + /// Number of items in `data` + pub num_data: usize, } pub(crate) struct CShortTradesResponseOwned { - data: CString, + data: CVec, } impl From for CShortTradesResponseOwned { fn from(v: ShortTradesResponse) -> Self { - let json = serde_json::to_string(&v).unwrap_or_default(); - Self { data: json.into() } + Self { + data: v.data.into(), + } } } @@ -3164,6 +3297,7 @@ impl ToFFI for CShortTradesResponseOwned { fn to_ffi_type(&self) -> Self::FFIType { CShortTradesResponse { data: self.data.to_ffi_type(), + num_data: self.data.len(), } } } diff --git a/cpp/include/fundamental_context.hpp b/cpp/include/fundamental_context.hpp index 735c9a8a3..a6d3eb43c 100644 --- a/cpp/include/fundamental_context.hpp +++ b/cpp/include/fundamental_context.hpp @@ -135,12 +135,12 @@ class FundamentalContext int64_t object_id, AsyncCallback callback) const; - /// Get valuation comparison (raw JSON string). + /// Get valuation comparison. /// Pass nullptr for comparison_symbols to skip peer comparison. void valuation_comparison(const std::string& symbol, const std::string& currency, const std::vector* comparison_symbols, - AsyncCallback callback) const; + AsyncCallback callback) const; }; } // namespace fundamental diff --git a/cpp/include/market_context.hpp b/cpp/include/market_context.hpp index a4bccc80f..0a6764147 100644 --- a/cpp/include/market_context.hpp +++ b/cpp/include/market_context.hpp @@ -83,20 +83,20 @@ class MarketContext void constituent(const std::string& symbol, AsyncCallback callback) const; - /// Get top movers (stocks with unusual price movements) across one or more markets (raw JSON string) + /// Get top movers (stocks with unusual price movements) across one or more markets void top_movers(const std::vector& markets, uint32_t sort, const std::string* date, uint32_t limit, - AsyncCallback callback) const; + AsyncCallback callback) const; /// Get all available rank category keys and labels (raw JSON string) void rank_categories(AsyncCallback callback) const; - /// Get a ranked list of securities for the given category key (raw JSON string) + /// Get a ranked list of securities for the given category key void rank_list(const std::string& key, bool need_article, - AsyncCallback callback) const; + AsyncCallback callback) const; }; } // namespace market diff --git a/cpp/include/types.hpp b/cpp/include/types.hpp index ac1babf2e..0ba445c9a 100644 --- a/cpp/include/types.hpp +++ b/cpp/include/types.hpp @@ -1263,16 +1263,62 @@ struct FilingItem int64_t published_at; }; -/// Short interest / positions response (HK or US). `data` is a raw JSON string. +/// One short-position record, unified for US and HK markets. +struct ShortPositionsItem +{ + /// Trading date in RFC 3339 format + std::string timestamp; + /// Short ratio + std::string rate; + /// Closing price + std::string close; + /// [US] Number of short shares outstanding + std::string current_shares_short; + /// [US] Average daily share volume + std::string avg_daily_share_volume; + /// [US] Days-to-cover ratio + std::string days_to_cover; + /// [HK] Short sale amount (HKD) + std::string amount; + /// [HK] Short position balance + std::string balance; + /// [HK] Closing price (HK naming) + std::string cost; +}; + +/// Short interest / positions response (HK or US). struct ShortPositionsResponse { - std::string data; + /// Short position records + std::vector data; }; -/// Short trade records response (HK or US). `data` is a raw JSON string. +/// One short-trade record, unified for US and HK markets. +struct ShortTradesItem +{ + /// Trading date in RFC 3339 format + std::string timestamp; + /// Short ratio + std::string rate; + /// Closing price + std::string close; + /// [US] NASDAQ short sale volume + std::string nus_amount; + /// [US] NYSE short sale volume + std::string ny_amount; + /// [US] Total short amount + std::string total_amount; + /// [HK] Short sale turnover amount (HKD) + std::string amount; + /// [HK] Short position balance + std::string balance; +}; + +/// Short trade records response (HK or US). struct ShortTradesResponse { - std::string data; + /// Short trade records + std::vector data; }; struct OptionVolumeStats @@ -2386,6 +2432,66 @@ struct TradeStatsResponse std::vector trades; }; +/// Stock information within a top-movers event. +struct TopMoversStock +{ + std::string symbol; + std::string code; + std::string name; + std::string full_name; + std::string change; + std::string last_done; + std::string market; + std::string logo; + std::vector labels; +}; + +/// One top-movers event entry. +struct TopMoversEvent +{ + std::string timestamp; + std::string alert_reason; + int64_t alert_type; + TopMoversStock stock; + std::string post; +}; + +/// Response for top_movers. +struct TopMoversResponse +{ + std::vector events; + /// Pagination cursor as a JSON string + std::string next_params; +}; + +/// One ranked security item. +struct RankListItem +{ + std::string symbol; + std::string code; + std::string name; + std::string last_done; + std::string chg; + std::string change; + std::string inflow; + std::string market_cap; + std::string industry; + std::string pre_post_price; + std::string pre_post_chg; + std::string amplitude; + std::string five_day_chg; + std::string turnover_rate; + std::string volume_rate; + std::string pb_ttm; +}; + +/// Response for rank_list. +struct RankListResponse +{ + bool bmp; + std::vector lists; +}; + /// A single anomaly (unusual market movement) alert item. struct AnomalyItem { @@ -3132,6 +3238,41 @@ struct FinancialReportSnapshot std::string fr_debt_assets_ratio; }; +/// One historical valuation data point. +struct ValuationHistoryPoint +{ + std::string date; + std::string pe; + std::string pb; + std::string ps; +}; + +/// One security's valuation comparison item. +struct ValuationComparisonItem +{ + std::string symbol; + std::string name; + std::string currency; + std::string market_value; + std::string price_close; + std::string pe; + std::string pb; + std::string ps; + std::string roe; + std::string eps; + std::string bps; + std::string dps; + std::string div_yld; + std::string assets; + std::vector history; +}; + +/// Valuation comparison response. +struct ValuationComparisonResponse +{ + std::vector list; +}; + } // namespace fundamental namespace alert { diff --git a/cpp/src/convert.hpp b/cpp/src/convert.hpp index 2fc781f6d..287bea0cb 100644 --- a/cpp/src/convert.hpp +++ b/cpp/src/convert.hpp @@ -2291,11 +2291,36 @@ convert(const lb_owned_topic_t* item) // ── QuoteContext extension types ────────────────────────────────── +inline quote::ShortPositionsItem convert(const lb_short_positions_item_t* item) { + return { item->timestamp ? item->timestamp : "", + item->rate ? item->rate : "", + item->close ? item->close : "", + item->current_shares_short ? item->current_shares_short : "", + item->avg_daily_share_volume ? item->avg_daily_share_volume : "", + item->days_to_cover ? item->days_to_cover : "", + item->amount ? item->amount : "", + item->balance ? item->balance : "", + item->cost ? item->cost : "" }; +} inline quote::ShortPositionsResponse convert(const lb_short_positions_response_t* r) { - return { r->data ? r->data : "" }; + std::vector items; + for (size_t i = 0; i < r->num_data; ++i) items.push_back(convert(&r->data[i])); + return { std::move(items) }; +} +inline quote::ShortTradesItem convert(const lb_short_trades_item_t* item) { + return { item->timestamp ? item->timestamp : "", + item->rate ? item->rate : "", + item->close ? item->close : "", + item->nus_amount ? item->nus_amount : "", + item->ny_amount ? item->ny_amount : "", + item->total_amount ? item->total_amount : "", + item->amount ? item->amount : "", + item->balance ? item->balance : "" }; } inline quote::ShortTradesResponse convert(const lb_short_trades_response_t* r) { - return { r->data ? r->data : "" }; + std::vector items; + for (size_t i = 0; i < r->num_data; ++i) items.push_back(convert(&r->data[i])); + return { std::move(items) }; } inline quote::OptionVolumeStats convert(const lb_option_volume_stats_t* s) { return { s->c, s->p }; @@ -2383,6 +2408,42 @@ inline market::IndexConstituents convert(const lb_index_constituents_t* r) { for (size_t i = 0; i < r->num_stocks; ++i) stocks.push_back(convert(&r->stocks[i])); return { r->fall_num, r->flat_num, r->rise_num, std::move(stocks) }; } +inline market::TopMoversStock convert(const lb_top_movers_stock_t* s) { + std::vector labels; + for (size_t i = 0; i < s->num_labels; ++i) labels.push_back(s->labels[i] ? s->labels[i] : ""); + return { s->symbol ? s->symbol : "", s->code ? s->code : "", s->name ? s->name : "", + s->full_name ? s->full_name : "", s->change ? s->change : "", + s->last_done ? s->last_done : "", s->market ? s->market : "", + s->logo ? s->logo : "", std::move(labels) }; +} +inline market::TopMoversEvent convert(const lb_top_movers_event_t* e) { + return { e->timestamp ? e->timestamp : "", e->alert_reason ? e->alert_reason : "", + e->alert_type, convert(&e->stock), e->post ? e->post : "" }; +} +inline market::TopMoversResponse convert(const lb_top_movers_response_t* r) { + std::vector events; + for (size_t i = 0; i < r->num_events; ++i) events.push_back(convert(&r->events[i])); + return { std::move(events), r->next_params ? r->next_params : "" }; +} +inline market::RankListItem convert(const lb_rank_list_item_t* item) { + return { item->symbol ? item->symbol : "", item->code ? item->code : "", + item->name ? item->name : "", item->last_done ? item->last_done : "", + item->chg ? item->chg : "", item->change ? item->change : "", + item->inflow ? item->inflow : "", item->market_cap ? item->market_cap : "", + item->industry ? item->industry : "", + item->pre_post_price ? item->pre_post_price : "", + item->pre_post_chg ? item->pre_post_chg : "", + item->amplitude ? item->amplitude : "", + item->five_day_chg ? item->five_day_chg : "", + item->turnover_rate ? item->turnover_rate : "", + item->volume_rate ? item->volume_rate : "", + item->pb_ttm ? item->pb_ttm : "" }; +} +inline market::RankListResponse convert(const lb_rank_list_response_t* r) { + std::vector lists; + for (size_t i = 0; i < r->num_lists; ++i) lists.push_back(convert(&r->lists[i])); + return { r->bmp, std::move(lists) }; +} // ── FundamentalContext conversions ──────────────────────────────── @@ -2673,6 +2734,26 @@ inline fundamental::FinancialReportSnapshot convert(const lb_financial_report_sn r->fr_roe_ttm, r->fr_profit_margin, r->fr_profit_margin_ttm, r->fr_asset_turn_ttm, r->fr_leverage_ttm, r->fr_debt_assets_ratio }; } +inline fundamental::ValuationHistoryPoint convert(const lb_valuation_history_point_t* p) { + return { p->date ? p->date : "", p->pe ? p->pe : "", p->pb ? p->pb : "", p->ps ? p->ps : "" }; +} +inline fundamental::ValuationComparisonItem convert(const lb_valuation_comparison_item_t* item) { + std::vector history; + for (size_t i = 0; i < item->num_history; ++i) history.push_back(convert(&item->history[i])); + return { item->symbol ? item->symbol : "", item->name ? item->name : "", + item->currency ? item->currency : "", item->market_value ? item->market_value : "", + item->price_close ? item->price_close : "", item->pe ? item->pe : "", + item->pb ? item->pb : "", item->ps ? item->ps : "", + item->roe ? item->roe : "", item->eps ? item->eps : "", + item->bps ? item->bps : "", item->dps ? item->dps : "", + item->div_yld ? item->div_yld : "", item->assets ? item->assets : "", + std::move(history) }; +} +inline fundamental::ValuationComparisonResponse convert(const lb_valuation_comparison_response_t* r) { + std::vector list; + for (size_t i = 0; i < r->num_list; ++i) list.push_back(convert(&r->list[i])); + return { std::move(list) }; +} // ── Portfolio conversions ───────────────────────────────────────── diff --git a/cpp/src/fundamental_context.cpp b/cpp/src/fundamental_context.cpp index 55bb7f501..c9f6403b7 100644 --- a/cpp/src/fundamental_context.cpp +++ b/cpp/src/fundamental_context.cpp @@ -138,7 +138,7 @@ void FundamentalContext::shareholder_detail(const std::string& s, int64_t object F_JSON_STRUCT(lb_fundamental_context_shareholder_detail, lb_shareholder_detail_response_t, ctx_, s.c_str(), object_id); } -void FundamentalContext::valuation_comparison(const std::string& s, const std::string& currency, const std::vector* comparison_symbols, AsyncCallback callback) const { +void FundamentalContext::valuation_comparison(const std::string& s, const std::string& currency, const std::vector* comparison_symbols, AsyncCallback callback) const { std::vector syms_ptrs; size_t num_syms = 0; const char** syms_data = nullptr; @@ -147,7 +147,17 @@ void FundamentalContext::valuation_comparison(const std::string& s, const std::s syms_data = syms_ptrs.empty() ? nullptr : syms_ptrs.data(); num_syms = syms_ptrs.size(); } - F_JSON_STRUCT(lb_fundamental_context_valuation_comparison, lb_valuation_comparison_response_t, ctx_, s.c_str(), currency.c_str(), syms_data, num_syms); + lb_fundamental_context_valuation_comparison(ctx_, s.c_str(), currency.c_str(), syms_data, num_syms, + [](auto res) { + auto cb = callback::get_async_callback(res->userdata); + FundamentalContext fctx((const lb_fundamental_context_t*)res->ctx); Status status(res->error); + if (status) { + auto r = convert::convert((const lb_valuation_comparison_response_t*)res->data); + (*cb)(AsyncResult(fctx, std::move(status), &r)); + } else { + (*cb)(AsyncResult(fctx, std::move(status), nullptr)); + } + }, new AsyncCallback(callback)); } #undef F_JSON_STRUCT diff --git a/cpp/src/market_context.cpp b/cpp/src/market_context.cpp index c68e9ee9d..5327b1666 100644 --- a/cpp/src/market_context.cpp +++ b/cpp/src/market_context.cpp @@ -79,7 +79,7 @@ void MarketContext::constituent(const std::string& symbol, AsyncCallback(res->userdata); \ MarketContext mctx((const lb_market_context_t*)res->ctx); Status status(res->error); \ @@ -87,22 +87,42 @@ void MarketContext::constituent(const std::string& symbol, AsyncCallback(mctx,std::move(status),nullptr));} \ }, new AsyncCallback(callback)) -void MarketContext::top_movers(const std::vector& markets, uint32_t sort, const std::string* date, uint32_t limit, AsyncCallback callback) const { - std::vector mptrs; - for (const auto& m : markets) mptrs.push_back(m.c_str()); - const char* date_str = date ? date->c_str() : nullptr; - M_JSON(lb_market_context_top_movers, lb_top_movers_response_t, ctx_, mptrs.data(), mptrs.size(), sort, date_str, limit); -} - void MarketContext::rank_categories(AsyncCallback callback) const { M_JSON(lb_market_context_rank_categories, lb_rank_categories_response_t, ctx_); } -void MarketContext::rank_list(const std::string& key, bool need_article, AsyncCallback callback) const { - M_JSON(lb_market_context_rank_list, lb_rank_list_response_t, ctx_, key.c_str(), need_article); +#undef M_JSON + +void MarketContext::top_movers(const std::vector& markets, uint32_t sort, const std::string* date, uint32_t limit, AsyncCallback callback) const { + std::vector mptrs; + for (const auto& m : markets) mptrs.push_back(m.c_str()); + const char* date_str = date ? date->c_str() : nullptr; + lb_market_context_top_movers(ctx_, mptrs.data(), mptrs.size(), sort, date_str, limit, + [](auto res) { + auto cb = callback::get_async_callback(res->userdata); + MarketContext mctx((const lb_market_context_t*)res->ctx); Status status(res->error); + if (status) { + auto r = convert::convert((const lb_top_movers_response_t*)res->data); + (*cb)(AsyncResult(mctx, std::move(status), &r)); + } else { + (*cb)(AsyncResult(mctx, std::move(status), nullptr)); + } + }, new AsyncCallback(callback)); } -#undef M_JSON +void MarketContext::rank_list(const std::string& key, bool need_article, AsyncCallback callback) const { + lb_market_context_rank_list(ctx_, key.c_str(), need_article, + [](auto res) { + auto cb = callback::get_async_callback(res->userdata); + MarketContext mctx((const lb_market_context_t*)res->ctx); Status status(res->error); + if (status) { + auto r = convert::convert((const lb_rank_list_response_t*)res->data); + (*cb)(AsyncResult(mctx, std::move(status), &r)); + } else { + (*cb)(AsyncResult(mctx, std::move(status), nullptr)); + } + }, new AsyncCallback(callback)); +} } // namespace market } // namespace longbridge diff --git a/java/javasrc/src/main/java/com/longbridge/fundamental/ValuationComparisonItem.java b/java/javasrc/src/main/java/com/longbridge/fundamental/ValuationComparisonItem.java new file mode 100644 index 000000000..351bd290b --- /dev/null +++ b/java/javasrc/src/main/java/com/longbridge/fundamental/ValuationComparisonItem.java @@ -0,0 +1,22 @@ +package com.longbridge.fundamental; + +/** One security in the valuation comparison. */ +public class ValuationComparisonItem { + /** Symbol, e.g. "AAPL.US" (converted from counter_id) */ + public String symbol; + public String name; + public String currency; + public String marketValue; + public String priceClose; + public String pe; + public String pb; + public String ps; + public String roe; + public String eps; + public String bps; + public String dps; + public String divYld; + public String assets; + /** Historical valuation data points */ + public ValuationHistoryPoint[] history; +} diff --git a/java/javasrc/src/main/java/com/longbridge/fundamental/ValuationComparisonResponse.java b/java/javasrc/src/main/java/com/longbridge/fundamental/ValuationComparisonResponse.java index 5870bbbe5..5e30708dc 100644 --- a/java/javasrc/src/main/java/com/longbridge/fundamental/ValuationComparisonResponse.java +++ b/java/javasrc/src/main/java/com/longbridge/fundamental/ValuationComparisonResponse.java @@ -1,7 +1,7 @@ package com.longbridge.fundamental; -/** Response for {@link FundamentalContext#getValuationComparison}. Contains raw JSON data. */ +/** Response for {@link FundamentalContext#getValuationComparison}. */ public class ValuationComparisonResponse { - /** Raw JSON data string */ - public String data; + /** Comparison items (primary + peers) */ + public ValuationComparisonItem[] list; } diff --git a/java/javasrc/src/main/java/com/longbridge/fundamental/ValuationHistoryPoint.java b/java/javasrc/src/main/java/com/longbridge/fundamental/ValuationHistoryPoint.java new file mode 100644 index 000000000..3bd6c6343 --- /dev/null +++ b/java/javasrc/src/main/java/com/longbridge/fundamental/ValuationHistoryPoint.java @@ -0,0 +1,10 @@ +package com.longbridge.fundamental; + +/** One historical valuation data point. */ +public class ValuationHistoryPoint { + /** Date in RFC 3339 format */ + public String date; + public String pe; + public String pb; + public String ps; +} diff --git a/java/javasrc/src/main/java/com/longbridge/market/RankListItem.java b/java/javasrc/src/main/java/com/longbridge/market/RankListItem.java new file mode 100644 index 000000000..d4ca86b3d --- /dev/null +++ b/java/javasrc/src/main/java/com/longbridge/market/RankListItem.java @@ -0,0 +1,37 @@ +package com.longbridge.market; + +/** One item in the popularity rank list. */ +public class RankListItem { + /** Symbol, e.g. "MU.US" (converted from counter_id) */ + public String symbol; + /** Ticker code */ + public String code; + /** Security name */ + public String name; + /** Latest price */ + public String lastDone; + /** Price change ratio (decimal) */ + public String chg; + /** Absolute price change */ + public String change; + /** Net inflow */ + public String inflow; + /** Market cap */ + public String marketCap; + /** Industry name */ + public String industry; + /** Pre/post market price */ + public String prePostPrice; + /** Pre/post market change */ + public String prePostChg; + /** Amplitude */ + public String amplitude; + /** 5-day change */ + public String fiveDayChg; + /** Turnover rate */ + public String turnoverRate; + /** Volume ratio */ + public String volumeRate; + /** P/B ratio TTM */ + public String pbTtm; +} diff --git a/java/javasrc/src/main/java/com/longbridge/market/RankListResponse.java b/java/javasrc/src/main/java/com/longbridge/market/RankListResponse.java index a13a6e89e..12fd7095b 100644 --- a/java/javasrc/src/main/java/com/longbridge/market/RankListResponse.java +++ b/java/javasrc/src/main/java/com/longbridge/market/RankListResponse.java @@ -1,7 +1,9 @@ package com.longbridge.market; -/** Response for {@link MarketContext#getRankList}. Contains raw JSON data. */ +/** Response for {@link MarketContext#getRankList}. */ public class RankListResponse { - /** Raw JSON data string */ - public String data; + /** Whether the response is delayed */ + public boolean bmp; + /** Ranked securities list */ + public RankListItem[] lists; } diff --git a/java/javasrc/src/main/java/com/longbridge/market/TopMoversEvent.java b/java/javasrc/src/main/java/com/longbridge/market/TopMoversEvent.java new file mode 100644 index 000000000..121c642a9 --- /dev/null +++ b/java/javasrc/src/main/java/com/longbridge/market/TopMoversEvent.java @@ -0,0 +1,15 @@ +package com.longbridge.market; + +/** One top-movers event. */ +public class TopMoversEvent { + /** Event timestamp in RFC 3339 format */ + public String timestamp; + /** Alert reason description */ + public String alertReason; + /** Alert type code */ + public long alertType; + /** Stock information */ + public TopMoversStock stock; + /** Associated news post as JSON string (may be null) */ + public String post; +} diff --git a/java/javasrc/src/main/java/com/longbridge/market/TopMoversResponse.java b/java/javasrc/src/main/java/com/longbridge/market/TopMoversResponse.java index 4f2898ebf..980470ac9 100644 --- a/java/javasrc/src/main/java/com/longbridge/market/TopMoversResponse.java +++ b/java/javasrc/src/main/java/com/longbridge/market/TopMoversResponse.java @@ -1,7 +1,11 @@ package com.longbridge.market; -/** Response for {@link MarketContext#getTopMovers}. Raw JSON payload. */ +import com.longbridge.market.TopMoversEvent; + +/** Response for {@link MarketContext#getTopMovers}. */ public class TopMoversResponse { - /** Raw top movers data as a JSON string. */ - public String data; + /** Top mover events */ + public TopMoversEvent[] events; + /** Pagination cursor (raw JSON); pass to next call for next page */ + public String nextParams; } diff --git a/java/javasrc/src/main/java/com/longbridge/market/TopMoversStock.java b/java/javasrc/src/main/java/com/longbridge/market/TopMoversStock.java new file mode 100644 index 000000000..cf21598d9 --- /dev/null +++ b/java/javasrc/src/main/java/com/longbridge/market/TopMoversStock.java @@ -0,0 +1,23 @@ +package com.longbridge.market; + +/** Stock information in a top-movers event. */ +public class TopMoversStock { + /** Symbol, e.g. "TSLA.US" */ + public String symbol; + /** Ticker code */ + public String code; + /** Security name */ + public String name; + /** Full name */ + public String fullName; + /** Price change (decimal ratio) */ + public String change; + /** Latest price */ + public String lastDone; + /** Market */ + public String market; + /** Labels / tags */ + public String[] labels; + /** Logo URL */ + public String logo; +} diff --git a/java/javasrc/src/main/java/com/longbridge/quote/ShortPositionsItem.java b/java/javasrc/src/main/java/com/longbridge/quote/ShortPositionsItem.java new file mode 100644 index 000000000..13b33e94a --- /dev/null +++ b/java/javasrc/src/main/java/com/longbridge/quote/ShortPositionsItem.java @@ -0,0 +1,26 @@ +package com.longbridge.quote; + +/** + * One short-position record, unified for US and HK markets. + * US-specific fields are empty for HK records and vice versa. + */ +public class ShortPositionsItem { + /** Trading date in RFC 3339 format, e.g. "2022-03-15T04:00:00Z" */ + public String timestamp; + /** Short ratio */ + public String rate; + /** Closing price */ + public String close; + /** [US] Number of short shares outstanding */ + public String currentSharesShort; + /** [US] Average daily share volume */ + public String avgDailyShareVolume; + /** [US] Days-to-cover ratio */ + public String daysToCover; + /** [HK] Short sale amount (HKD) */ + public String amount; + /** [HK] Short position balance */ + public String balance; + /** [HK] Closing price (HK naming) */ + public String cost; +} diff --git a/java/javasrc/src/main/java/com/longbridge/quote/ShortPositionsResponse.java b/java/javasrc/src/main/java/com/longbridge/quote/ShortPositionsResponse.java index 96529a26f..a2a321f57 100644 --- a/java/javasrc/src/main/java/com/longbridge/quote/ShortPositionsResponse.java +++ b/java/javasrc/src/main/java/com/longbridge/quote/ShortPositionsResponse.java @@ -1,7 +1,7 @@ package com.longbridge.quote; +/** Response for {@link QuoteContext#getShortPositions}. Unified US+HK response. */ public class ShortPositionsResponse { - public String symbol; - public ShortPosition[] data; - public int sources; + /** Short position records. US and HK fields populated depending on market. */ + public ShortPositionsItem[] data; } \ No newline at end of file diff --git a/java/javasrc/src/main/java/com/longbridge/quote/ShortTradesItem.java b/java/javasrc/src/main/java/com/longbridge/quote/ShortTradesItem.java new file mode 100644 index 000000000..8189bdceb --- /dev/null +++ b/java/javasrc/src/main/java/com/longbridge/quote/ShortTradesItem.java @@ -0,0 +1,23 @@ +package com.longbridge.quote; + +/** + * One short-trade record, unified for US and HK markets. + */ +public class ShortTradesItem { + /** Trading date in RFC 3339 format */ + public String timestamp; + /** Short ratio */ + public String rate; + /** Closing price */ + public String close; + /** [US] NASDAQ short sale volume */ + public String nusAmount; + /** [US] NYSE short sale volume */ + public String nyAmount; + /** [US] Total trading volume */ + public String totalAmount; + /** [HK] Short sale turnover amount (HKD) */ + public String amount; + /** [HK] Short position balance */ + public String balance; +} diff --git a/java/javasrc/src/main/java/com/longbridge/quote/ShortTradesResponse.java b/java/javasrc/src/main/java/com/longbridge/quote/ShortTradesResponse.java index 030483fdf..7e8b39361 100644 --- a/java/javasrc/src/main/java/com/longbridge/quote/ShortTradesResponse.java +++ b/java/javasrc/src/main/java/com/longbridge/quote/ShortTradesResponse.java @@ -1,7 +1,7 @@ package com.longbridge.quote; -/** Response for {@link QuoteContext#getShortTrades}. Contains raw JSON data. */ +/** Response for {@link QuoteContext#getShortTrades}. Unified US+HK response. */ public class ShortTradesResponse { - /** Raw JSON data string */ - public String data; + /** Short trade records. US and HK fields populated depending on market. */ + public ShortTradesItem[] data; } From 393b5ca5e8cd785c90cb5e58dec40cb5a2129423 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=A2=81=E7=AB=A0=E6=B4=AA?= Date: Thu, 21 May 2026 17:09:38 +0800 Subject: [PATCH 10/12] fix(c): add PR#526 fundamental context C bindings (business_segments, institution_rating_views, industry_rank, industry_peers, financial_report_snapshot) Implements the 6 missing C extern functions and corresponding C FFI types that were preventing the C++ SDK from building. The `longbridge.h` header is regenerated with all new `lb_*` type definitions and function declarations. Co-Authored-By: Claude Sonnet 4.6 (1M context) --- CHANGELOG.md | 1 + c/csrc/include/longbridge.h | 476 +++++++++++++++++ c/src/fundamental_context/context.rs | 182 +++++++ c/src/fundamental_context/types.rs | 765 +++++++++++++++++++++++++++ 4 files changed, 1424 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 432409397..d2f4c7f36 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,6 +12,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - **Rust:** `Config::header(key, value)` builder method to inject custom headers. - **Rust, Python:** `ContentContext` adds `topic_detail`, `list_topic_replies`, `create_topic_reply`. - **All languages:** Six new `FundamentalContext` methods (PR #526): `BusinessSegments`, `BusinessSegmentsHistory`, `InstitutionRatingViews`, `IndustryRank`, `IndustryPeers`, `FinancialReportSnapshot` +- **C, C++:** C SDK bindings for all six PR #526 `FundamentalContext` APIs: `lb_fundamental_context_business_segments`, `lb_fundamental_context_business_segments_history`, `lb_fundamental_context_institution_rating_views`, `lb_fundamental_context_industry_rank`, `lb_fundamental_context_industry_peers`, `lb_fundamental_context_financial_report_snapshot`. New typed structs added to `longbridge.h`. - **All languages:** 13 more new APIs: `shareholder_top`, `shareholder_detail`, `valuation_comparison` (FundamentalContext); `short_positions` (HK+US unified), `short_trades` (QuoteContext); `top_movers`, `rank_categories`, `rank_list` (MarketContext); `ScreenerContext` (new) with 5 screener methods ### Changed diff --git a/c/csrc/include/longbridge.h b/c/csrc/include/longbridge.h index 0a5134632..5cfcd77ac 100644 --- a/c/csrc/include/longbridge.h +++ b/c/csrc/include/longbridge.h @@ -6568,6 +6568,414 @@ typedef struct lb_stock_ratings_t { uintptr_t num_ratings; } lb_stock_ratings_t; +/** + * One business segment item (latest snapshot). + */ +typedef struct lb_business_segment_item_t { + /** + * Segment name. + */ + const char *name; + /** + * Percentage of total revenue. + */ + const char *percent; +} lb_business_segment_item_t; + +/** + * Current business segment breakdown for a security. + */ +typedef struct lb_business_segments_t { + /** + * Report date. + */ + const char *date; + /** + * Total revenue. + */ + const char *total; + /** + * Reporting currency. + */ + const char *currency; + /** + * Pointer to business segment items. + */ + const struct lb_business_segment_item_t *business; + /** + * Number of items in `business`. + */ + uintptr_t num_business; +} lb_business_segments_t; + +/** + * One business/regional segment item in a historical snapshot. + */ +typedef struct lb_business_segment_history_item_t { + /** + * Segment name. + */ + const char *name; + /** + * Percentage of total. + */ + const char *percent; + /** + * Absolute value. + */ + const char *value; +} lb_business_segment_history_item_t; + +/** + * One historical business segments snapshot. + */ +typedef struct lb_business_segments_historical_item_t { + /** + * Report date. + */ + const char *date; + /** + * Total revenue. + */ + const char *total; + /** + * Reporting currency. + */ + const char *currency; + /** + * Pointer to business segment breakdown items. + */ + const struct lb_business_segment_history_item_t *business; + /** + * Number of items in `business`. + */ + uintptr_t num_business; + /** + * Pointer to regional breakdown items. + */ + const struct lb_business_segment_history_item_t *regionals; + /** + * Number of items in `regionals`. + */ + uintptr_t num_regionals; +} lb_business_segments_historical_item_t; + +/** + * Historical business segment breakdowns for a security. + */ +typedef struct lb_business_segments_history_t { + /** + * Pointer to historical snapshot items. + */ + const struct lb_business_segments_historical_item_t *historical; + /** + * Number of items in `historical`. + */ + uintptr_t num_historical; +} lb_business_segments_history_t; + +/** + * One historical institutional rating distribution snapshot. + */ +typedef struct lb_institution_rating_view_item_t { + /** + * Date (unix timestamp string). + */ + const char *date; + /** + * Number of "Buy" ratings. + */ + const char *buy; + /** + * Number of "Outperform" ratings. + */ + const char *over; + /** + * Number of "Hold" ratings. + */ + const char *hold; + /** + * Number of "Underperform" ratings. + */ + const char *under; + /** + * Number of "Sell" ratings. + */ + const char *sell; + /** + * Total analyst count. + */ + const char *total; +} lb_institution_rating_view_item_t; + +/** + * Historical institutional rating views time-series for a security. + */ +typedef struct lb_institution_rating_views_t { + /** + * Pointer to rating view items. + */ + const struct lb_institution_rating_view_item_t *elist; + /** + * Number of items in `elist`. + */ + uintptr_t num_elist; +} lb_institution_rating_views_t; + +/** + * One ranked industry item. + */ +typedef struct lb_industry_rank_item_t { + /** + * Industry / sector name. + */ + const char *name; + /** + * Counter ID of the industry. + */ + const char *counter_id; + /** + * Change percentage. + */ + const char *chg; + /** + * Name of the leading stock. + */ + const char *leading_name; + /** + * Ticker of the leading stock. + */ + const char *leading_ticker; + /** + * Change percentage of the leading stock. + */ + const char *leading_chg; + /** + * Value label name. + */ + const char *value_name; + /** + * Value data. + */ + const char *value_data; +} lb_industry_rank_item_t; + +/** + * A group of ranked industry items. + */ +typedef struct lb_industry_rank_group_t { + /** + * Pointer to ranked items. + */ + const struct lb_industry_rank_item_t *lists; + /** + * Number of items in `lists`. + */ + uintptr_t num_lists; +} lb_industry_rank_group_t; + +/** + * Industry rank response. + */ +typedef struct lb_industry_rank_response_t { + /** + * Pointer to grouped rank items. + */ + const struct lb_industry_rank_group_t *items; + /** + * Number of groups in `items`. + */ + uintptr_t num_items; +} lb_industry_rank_response_t; + +/** + * Top-level industry info in the peers response. + */ +typedef struct lb_industry_peers_top_t { + /** + * Industry name. + */ + const char *name; + /** + * Market code. + */ + const char *market; +} lb_industry_peers_top_t; + +/** + * A node in the industry peer chain (recursive children serialised as JSON). + */ +typedef struct lb_industry_peer_node_t { + /** + * Node name. + */ + const char *name; + /** + * Counter ID. + */ + const char *counter_id; + /** + * Number of stocks in this node. + */ + int32_t stock_num; + /** + * Change percentage. + */ + const char *chg; + /** + * Year-to-date change. + */ + const char *ytd_chg; + /** + * Child nodes serialised as a JSON string (may be NULL if empty). + */ + const char *next_json; +} lb_industry_peer_node_t; + +/** + * Industry peer chain response. + */ +typedef struct lb_industry_peers_response_t { + /** + * Top-level industry node info. + */ + struct lb_industry_peers_top_t top; + /** + * Root peer chain node (NULL if absent). + */ + const struct lb_industry_peer_node_t *chain; +} lb_industry_peers_response_t; + +/** + * A forecast metric in the financial report snapshot. + */ +typedef struct lb_snapshot_forecast_metric_t { + /** + * Actual value. + */ + const char *value; + /** + * Year-over-year change. + */ + const char *yoy; + /** + * Beat/miss description. + */ + const char *cmp_desc; + /** + * Consensus estimate value. + */ + const char *est_value; +} lb_snapshot_forecast_metric_t; + +/** + * A reported metric in the financial report snapshot. + */ +typedef struct lb_snapshot_reported_metric_t { + /** + * Actual value. + */ + const char *value; + /** + * Year-over-year change. + */ + const char *yoy; +} lb_snapshot_reported_metric_t; + +/** + * Financial report snapshot (earnings snapshot) for a security. + */ +typedef struct lb_financial_report_snapshot_t { + /** + * Company name. + */ + const char *name; + /** + * Ticker code. + */ + const char *ticker; + /** + * Fiscal period start date. + */ + const char *fp_start; + /** + * Fiscal period end date. + */ + const char *fp_end; + /** + * Reporting currency. + */ + const char *currency; + /** + * Report description. + */ + const char *report_desc; + /** + * Forecast revenue (NULL if absent). + */ + const struct lb_snapshot_forecast_metric_t *fo_revenue; + /** + * Forecast EBIT (NULL if absent). + */ + const struct lb_snapshot_forecast_metric_t *fo_ebit; + /** + * Forecast EPS (NULL if absent). + */ + const struct lb_snapshot_forecast_metric_t *fo_eps; + /** + * Reported revenue (NULL if absent). + */ + const struct lb_snapshot_reported_metric_t *fr_revenue; + /** + * Reported net profit (NULL if absent). + */ + const struct lb_snapshot_reported_metric_t *fr_profit; + /** + * Reported operating cash flow (NULL if absent). + */ + const struct lb_snapshot_reported_metric_t *fr_operate_cash; + /** + * Reported investing cash flow (NULL if absent). + */ + const struct lb_snapshot_reported_metric_t *fr_invest_cash; + /** + * Reported financing cash flow (NULL if absent). + */ + const struct lb_snapshot_reported_metric_t *fr_finance_cash; + /** + * Reported total assets (NULL if absent). + */ + const struct lb_snapshot_reported_metric_t *fr_total_assets; + /** + * Reported total liabilities (NULL if absent). + */ + const struct lb_snapshot_reported_metric_t *fr_total_liability; + /** + * ROE TTM. + */ + const char *fr_roe_ttm; + /** + * Profit margin. + */ + const char *fr_profit_margin; + /** + * Profit margin TTM. + */ + const char *fr_profit_margin_ttm; + /** + * Asset turnover TTM. + */ + const char *fr_asset_turn_ttm; + /** + * Leverage TTM. + */ + const char *fr_leverage_ttm; + /** + * Debt-to-assets ratio. + */ + const char *fr_debt_assets_ratio; +} lb_financial_report_snapshot_t; + /** * A key-value pair carrying calendar data fields. */ @@ -8909,6 +9317,74 @@ void lb_fundamental_context_shareholder_detail(const struct lb_fundamental_conte lb_async_callback_t callback, void *userdata); +/** + * Get current business segment breakdown for a security. + * Returns `CBusinessSegments`. + */ +void lb_fundamental_context_business_segments(const struct lb_fundamental_context_t *ctx, + const char *symbol, + lb_async_callback_t callback, + void *userdata); + +/** + * Get historical business segment breakdowns for a security. + * Returns `CBusinessSegmentsHistory`. + * Pass NULL for `report` and/or `cate` to omit those filters. + */ +void lb_fundamental_context_business_segments_history(const struct lb_fundamental_context_t *ctx, + const char *symbol, + const char *report, + const char *cate, + lb_async_callback_t callback, + void *userdata); + +/** + * Get historical institutional rating views for a security. + * Returns `CInstitutionRatingViews`. + */ +void lb_fundamental_context_institution_rating_views(const struct lb_fundamental_context_t *ctx, + const char *symbol, + lb_async_callback_t callback, + void *userdata); + +/** + * Get industry rank for a market. + * Returns `CIndustryRankResponse`. + */ +void lb_fundamental_context_industry_rank(const struct lb_fundamental_context_t *ctx, + const char *market, + const char *indicator, + const char *sort_type, + uint32_t limit, + lb_async_callback_t callback, + void *userdata); + +/** + * Get the industry peer chain for a security or industry. + * Returns `CIndustryPeersResponse`. + * Pass NULL for `industry_id` to omit it. + */ +void lb_fundamental_context_industry_peers(const struct lb_fundamental_context_t *ctx, + const char *counter_id, + const char *market, + const char *industry_id, + lb_async_callback_t callback, + void *userdata); + +/** + * Get a financial report snapshot for a security. + * Returns `CFinancialReportSnapshot`. + * Pass NULL for `report`, `fiscal_year_str`, and/or `fiscal_period` to omit + * them. `fiscal_year_str` should be a decimal integer string (e.g. `"2024"`). + */ +void lb_fundamental_context_financial_report_snapshot(const struct lb_fundamental_context_t *ctx, + const char *symbol, + const char *report, + const char *fiscal_year_str, + const char *fiscal_period, + lb_async_callback_t callback, + void *userdata); + /** * Get valuation comparison between a security and optional peers. * Returns `CValuationComparisonResponse`. diff --git a/c/src/fundamental_context/context.rs b/c/src/fundamental_context/context.rs index 503a999e5..9d05866ce 100644 --- a/c/src/fundamental_context/context.rs +++ b/c/src/fundamental_context/context.rs @@ -9,6 +9,29 @@ use crate::{ types::{CCow, cstr_to_rust}, }; +// Helper: convert a nullable C string to an Option<&'static str> by matching +// known enum-like values (e.g. report period codes). +#[inline] +unsafe fn cstr_to_static_opt(ptr: *const c_char) -> Option<&'static str> { + if ptr.is_null() { + return None; + } + let s = cstr_to_rust(ptr); + // Match against all known period/report values used across APIs. + match s.as_str() { + "qf" => Some("qf"), + "saf" => Some("saf"), + "af" => Some("af"), + "q1" => Some("q1"), + "q2" => Some("q2"), + "q3" => Some("q3"), + "annual" => Some("annual"), + "semi_annual" => Some("semi_annual"), + "quarterly" => Some("quarterly"), + _ => None, + } +} + pub(crate) struct CFundamentalContext { ctx: FundamentalContext, } @@ -445,6 +468,165 @@ pub unsafe extern "C" fn lb_fundamental_context_shareholder_detail( }); } +/// Get current business segment breakdown for a security. +/// Returns `CBusinessSegments`. +#[unsafe(no_mangle)] +pub unsafe extern "C" fn lb_fundamental_context_business_segments( + ctx: *const CFundamentalContext, + symbol: *const c_char, + callback: CAsyncCallback, + userdata: *mut c_void, +) { + let ctx_inner = (*ctx).ctx.clone(); + let symbol = cstr_to_rust(symbol); + execute_async(callback, ctx, userdata, async move { + let resp: CCow = CCow::new(CBusinessSegmentsOwned::from( + ctx_inner.business_segments(symbol).await?, + )); + Ok(resp) + }); +} + +/// Get historical business segment breakdowns for a security. +/// Returns `CBusinessSegmentsHistory`. +/// Pass NULL for `report` and/or `cate` to omit those filters. +#[unsafe(no_mangle)] +pub unsafe extern "C" fn lb_fundamental_context_business_segments_history( + ctx: *const CFundamentalContext, + symbol: *const c_char, + report: *const c_char, + cate: *const c_char, + callback: CAsyncCallback, + userdata: *mut c_void, +) { + let ctx_inner = (*ctx).ctx.clone(); + let symbol = cstr_to_rust(symbol); + let report: Option<&'static str> = cstr_to_static_opt(report); + let cate: Option = if cate.is_null() { + None + } else { + Some(cstr_to_rust(cate)) + }; + execute_async(callback, ctx, userdata, async move { + let resp: CCow = + CCow::new(CBusinessSegmentsHistoryOwned::from( + ctx_inner + .business_segments_history(symbol, report, cate) + .await?, + )); + Ok(resp) + }); +} + +/// Get historical institutional rating views for a security. +/// Returns `CInstitutionRatingViews`. +#[unsafe(no_mangle)] +pub unsafe extern "C" fn lb_fundamental_context_institution_rating_views( + ctx: *const CFundamentalContext, + symbol: *const c_char, + callback: CAsyncCallback, + userdata: *mut c_void, +) { + let ctx_inner = (*ctx).ctx.clone(); + let symbol = cstr_to_rust(symbol); + execute_async(callback, ctx, userdata, async move { + let resp: CCow = CCow::new( + CInstitutionRatingViewsOwned::from(ctx_inner.institution_rating_views(symbol).await?), + ); + Ok(resp) + }); +} + +/// Get industry rank for a market. +/// Returns `CIndustryRankResponse`. +#[unsafe(no_mangle)] +pub unsafe extern "C" fn lb_fundamental_context_industry_rank( + ctx: *const CFundamentalContext, + market: *const c_char, + indicator: *const c_char, + sort_type: *const c_char, + limit: u32, + callback: CAsyncCallback, + userdata: *mut c_void, +) { + let ctx_inner = (*ctx).ctx.clone(); + let market = cstr_to_rust(market); + let indicator = cstr_to_rust(indicator); + let sort_type = cstr_to_rust(sort_type); + execute_async(callback, ctx, userdata, async move { + let resp: CCow = CCow::new(CIndustryRankResponseOwned::from( + ctx_inner + .industry_rank(market, indicator, sort_type, limit) + .await?, + )); + Ok(resp) + }); +} + +/// Get the industry peer chain for a security or industry. +/// Returns `CIndustryPeersResponse`. +/// Pass NULL for `industry_id` to omit it. +#[unsafe(no_mangle)] +pub unsafe extern "C" fn lb_fundamental_context_industry_peers( + ctx: *const CFundamentalContext, + counter_id: *const c_char, + market: *const c_char, + industry_id: *const c_char, + callback: CAsyncCallback, + userdata: *mut c_void, +) { + let ctx_inner = (*ctx).ctx.clone(); + let counter_id = cstr_to_rust(counter_id); + let market = cstr_to_rust(market); + let industry_id: Option = if industry_id.is_null() { + None + } else { + Some(cstr_to_rust(industry_id)) + }; + execute_async(callback, ctx, userdata, async move { + let resp: CCow = CCow::new(CIndustryPeersResponseOwned::from( + ctx_inner + .industry_peers(counter_id, market, industry_id) + .await?, + )); + Ok(resp) + }); +} + +/// Get a financial report snapshot for a security. +/// Returns `CFinancialReportSnapshot`. +/// Pass NULL for `report`, `fiscal_year_str`, and/or `fiscal_period` to omit +/// them. `fiscal_year_str` should be a decimal integer string (e.g. `"2024"`). +#[unsafe(no_mangle)] +pub unsafe extern "C" fn lb_fundamental_context_financial_report_snapshot( + ctx: *const CFundamentalContext, + symbol: *const c_char, + report: *const c_char, + fiscal_year_str: *const c_char, + fiscal_period: *const c_char, + callback: CAsyncCallback, + userdata: *mut c_void, +) { + let ctx_inner = (*ctx).ctx.clone(); + let symbol = cstr_to_rust(symbol); + let report: Option<&'static str> = cstr_to_static_opt(report); + let fiscal_year: Option = if fiscal_year_str.is_null() { + None + } else { + cstr_to_rust(fiscal_year_str).parse::().ok() + }; + let fiscal_period: Option<&'static str> = cstr_to_static_opt(fiscal_period); + execute_async(callback, ctx, userdata, async move { + let resp: CCow = + CCow::new(CFinancialReportSnapshotOwned::from( + ctx_inner + .financial_report_snapshot(symbol, report, fiscal_year, fiscal_period) + .await?, + )); + Ok(resp) + }); +} + /// Get valuation comparison between a security and optional peers. /// Returns `CValuationComparisonResponse`. /// Pass NULL for `comparison_symbols` to skip peer comparison. diff --git a/c/src/fundamental_context/types.rs b/c/src/fundamental_context/types.rs index 08f7630e1..ef9198d0b 100644 --- a/c/src/fundamental_context/types.rs +++ b/c/src/fundamental_context/types.rs @@ -3077,3 +3077,768 @@ impl ToFFI for CValuationComparisonResponseOwned { } } } + +// ── BusinessSegments ────────────────────────────────────────────── + +/// One business segment item (latest snapshot). +#[repr(C)] +pub struct CBusinessSegmentItem { + /// Segment name. + pub name: *const c_char, + /// Percentage of total revenue. + pub percent: *const c_char, +} + +pub(crate) struct CBusinessSegmentItemOwned { + name: CString, + percent: CString, +} + +impl From for CBusinessSegmentItemOwned { + fn from(v: longbridge::fundamental::BusinessSegmentItem) -> Self { + Self { + name: v.name.into(), + percent: v.percent.into(), + } + } +} + +impl ToFFI for CBusinessSegmentItemOwned { + type FFIType = CBusinessSegmentItem; + fn to_ffi_type(&self) -> Self::FFIType { + CBusinessSegmentItem { + name: self.name.to_ffi_type(), + percent: self.percent.to_ffi_type(), + } + } +} + +/// Current business segment breakdown for a security. +#[repr(C)] +pub struct CBusinessSegments { + /// Report date. + pub date: *const c_char, + /// Total revenue. + pub total: *const c_char, + /// Reporting currency. + pub currency: *const c_char, + /// Pointer to business segment items. + pub business: *const CBusinessSegmentItem, + /// Number of items in `business`. + pub num_business: usize, +} + +pub(crate) struct CBusinessSegmentsOwned { + date: CString, + total: CString, + currency: CString, + business: CVec, +} + +impl From for CBusinessSegmentsOwned { + fn from(v: longbridge::fundamental::BusinessSegments) -> Self { + Self { + date: v.date.into(), + total: v.total.into(), + currency: v.currency.into(), + business: v.business.into(), + } + } +} + +impl ToFFI for CBusinessSegmentsOwned { + type FFIType = CBusinessSegments; + fn to_ffi_type(&self) -> Self::FFIType { + CBusinessSegments { + date: self.date.to_ffi_type(), + total: self.total.to_ffi_type(), + currency: self.currency.to_ffi_type(), + business: self.business.to_ffi_type(), + num_business: self.business.len(), + } + } +} + +// ── BusinessSegmentsHistory ─────────────────────────────────────── + +/// One business/regional segment item in a historical snapshot. +#[repr(C)] +pub struct CBusinessSegmentHistoryItem { + /// Segment name. + pub name: *const c_char, + /// Percentage of total. + pub percent: *const c_char, + /// Absolute value. + pub value: *const c_char, +} + +pub(crate) struct CBusinessSegmentHistoryItemOwned { + name: CString, + percent: CString, + value: CString, +} + +impl From + for CBusinessSegmentHistoryItemOwned +{ + fn from(v: longbridge::fundamental::BusinessSegmentHistoryItem) -> Self { + Self { + name: v.name.into(), + percent: v.percent.into(), + value: v.value.into(), + } + } +} + +impl ToFFI for CBusinessSegmentHistoryItemOwned { + type FFIType = CBusinessSegmentHistoryItem; + fn to_ffi_type(&self) -> Self::FFIType { + CBusinessSegmentHistoryItem { + name: self.name.to_ffi_type(), + percent: self.percent.to_ffi_type(), + value: self.value.to_ffi_type(), + } + } +} + +/// One historical business segments snapshot. +#[repr(C)] +pub struct CBusinessSegmentsHistoricalItem { + /// Report date. + pub date: *const c_char, + /// Total revenue. + pub total: *const c_char, + /// Reporting currency. + pub currency: *const c_char, + /// Pointer to business segment breakdown items. + pub business: *const CBusinessSegmentHistoryItem, + /// Number of items in `business`. + pub num_business: usize, + /// Pointer to regional breakdown items. + pub regionals: *const CBusinessSegmentHistoryItem, + /// Number of items in `regionals`. + pub num_regionals: usize, +} + +pub(crate) struct CBusinessSegmentsHistoricalItemOwned { + date: CString, + total: CString, + currency: CString, + business: CVec, + regionals: CVec, +} + +impl From + for CBusinessSegmentsHistoricalItemOwned +{ + fn from(v: longbridge::fundamental::BusinessSegmentsHistoricalItem) -> Self { + Self { + date: v.date.into(), + total: v.total.into(), + currency: v.currency.into(), + business: v.business.into(), + regionals: v.regionals.into(), + } + } +} + +impl ToFFI for CBusinessSegmentsHistoricalItemOwned { + type FFIType = CBusinessSegmentsHistoricalItem; + fn to_ffi_type(&self) -> Self::FFIType { + CBusinessSegmentsHistoricalItem { + date: self.date.to_ffi_type(), + total: self.total.to_ffi_type(), + currency: self.currency.to_ffi_type(), + business: self.business.to_ffi_type(), + num_business: self.business.len(), + regionals: self.regionals.to_ffi_type(), + num_regionals: self.regionals.len(), + } + } +} + +/// Historical business segment breakdowns for a security. +#[repr(C)] +pub struct CBusinessSegmentsHistory { + /// Pointer to historical snapshot items. + pub historical: *const CBusinessSegmentsHistoricalItem, + /// Number of items in `historical`. + pub num_historical: usize, +} + +pub(crate) struct CBusinessSegmentsHistoryOwned { + historical: CVec, +} + +impl From for CBusinessSegmentsHistoryOwned { + fn from(v: longbridge::fundamental::BusinessSegmentsHistory) -> Self { + Self { + historical: v.historical.into(), + } + } +} + +impl ToFFI for CBusinessSegmentsHistoryOwned { + type FFIType = CBusinessSegmentsHistory; + fn to_ffi_type(&self) -> Self::FFIType { + CBusinessSegmentsHistory { + historical: self.historical.to_ffi_type(), + num_historical: self.historical.len(), + } + } +} + +// ── InstitutionRatingViews ──────────────────────────────────────── + +/// One historical institutional rating distribution snapshot. +#[repr(C)] +pub struct CInstitutionRatingViewItem { + /// Date (unix timestamp string). + pub date: *const c_char, + /// Number of "Buy" ratings. + pub buy: *const c_char, + /// Number of "Outperform" ratings. + pub over: *const c_char, + /// Number of "Hold" ratings. + pub hold: *const c_char, + /// Number of "Underperform" ratings. + pub under: *const c_char, + /// Number of "Sell" ratings. + pub sell: *const c_char, + /// Total analyst count. + pub total: *const c_char, +} + +pub(crate) struct CInstitutionRatingViewItemOwned { + date: CString, + buy: CString, + over: CString, + hold: CString, + under: CString, + sell: CString, + total: CString, +} + +impl From for CInstitutionRatingViewItemOwned { + fn from(v: longbridge::fundamental::InstitutionRatingViewItem) -> Self { + Self { + date: v.date.into(), + buy: v.buy.into(), + over: v.over.into(), + hold: v.hold.into(), + under: v.under.into(), + sell: v.sell.into(), + total: v.total.into(), + } + } +} + +impl ToFFI for CInstitutionRatingViewItemOwned { + type FFIType = CInstitutionRatingViewItem; + fn to_ffi_type(&self) -> Self::FFIType { + CInstitutionRatingViewItem { + date: self.date.to_ffi_type(), + buy: self.buy.to_ffi_type(), + over: self.over.to_ffi_type(), + hold: self.hold.to_ffi_type(), + under: self.under.to_ffi_type(), + sell: self.sell.to_ffi_type(), + total: self.total.to_ffi_type(), + } + } +} + +/// Historical institutional rating views time-series for a security. +#[repr(C)] +pub struct CInstitutionRatingViews { + /// Pointer to rating view items. + pub elist: *const CInstitutionRatingViewItem, + /// Number of items in `elist`. + pub num_elist: usize, +} + +pub(crate) struct CInstitutionRatingViewsOwned { + elist: CVec, +} + +impl From for CInstitutionRatingViewsOwned { + fn from(v: longbridge::fundamental::InstitutionRatingViews) -> Self { + Self { + elist: v.elist.into(), + } + } +} + +impl ToFFI for CInstitutionRatingViewsOwned { + type FFIType = CInstitutionRatingViews; + fn to_ffi_type(&self) -> Self::FFIType { + CInstitutionRatingViews { + elist: self.elist.to_ffi_type(), + num_elist: self.elist.len(), + } + } +} + +// ── IndustryRank ────────────────────────────────────────────────── + +/// One ranked industry item. +#[repr(C)] +pub struct CIndustryRankItem { + /// Industry / sector name. + pub name: *const c_char, + /// Counter ID of the industry. + pub counter_id: *const c_char, + /// Change percentage. + pub chg: *const c_char, + /// Name of the leading stock. + pub leading_name: *const c_char, + /// Ticker of the leading stock. + pub leading_ticker: *const c_char, + /// Change percentage of the leading stock. + pub leading_chg: *const c_char, + /// Value label name. + pub value_name: *const c_char, + /// Value data. + pub value_data: *const c_char, +} + +pub(crate) struct CIndustryRankItemOwned { + name: CString, + counter_id: CString, + chg: CString, + leading_name: CString, + leading_ticker: CString, + leading_chg: CString, + value_name: CString, + value_data: CString, +} + +impl From for CIndustryRankItemOwned { + fn from(v: longbridge::fundamental::IndustryRankItem) -> Self { + Self { + name: v.name.into(), + counter_id: v.counter_id.into(), + chg: v.chg.into(), + leading_name: v.leading_name.into(), + leading_ticker: v.leading_ticker.into(), + leading_chg: v.leading_chg.into(), + value_name: v.value_name.into(), + value_data: v.value_data.into(), + } + } +} + +impl ToFFI for CIndustryRankItemOwned { + type FFIType = CIndustryRankItem; + fn to_ffi_type(&self) -> Self::FFIType { + CIndustryRankItem { + name: self.name.to_ffi_type(), + counter_id: self.counter_id.to_ffi_type(), + chg: self.chg.to_ffi_type(), + leading_name: self.leading_name.to_ffi_type(), + leading_ticker: self.leading_ticker.to_ffi_type(), + leading_chg: self.leading_chg.to_ffi_type(), + value_name: self.value_name.to_ffi_type(), + value_data: self.value_data.to_ffi_type(), + } + } +} + +/// A group of ranked industry items. +#[repr(C)] +pub struct CIndustryRankGroup { + /// Pointer to ranked items. + pub lists: *const CIndustryRankItem, + /// Number of items in `lists`. + pub num_lists: usize, +} + +pub(crate) struct CIndustryRankGroupOwned { + lists: CVec, +} + +impl From for CIndustryRankGroupOwned { + fn from(v: longbridge::fundamental::IndustryRankGroup) -> Self { + Self { + lists: v.lists.into(), + } + } +} + +impl ToFFI for CIndustryRankGroupOwned { + type FFIType = CIndustryRankGroup; + fn to_ffi_type(&self) -> Self::FFIType { + CIndustryRankGroup { + lists: self.lists.to_ffi_type(), + num_lists: self.lists.len(), + } + } +} + +/// Industry rank response. +#[repr(C)] +pub struct CIndustryRankResponse { + /// Pointer to grouped rank items. + pub items: *const CIndustryRankGroup, + /// Number of groups in `items`. + pub num_items: usize, +} + +pub(crate) struct CIndustryRankResponseOwned { + items: CVec, +} + +impl From for CIndustryRankResponseOwned { + fn from(v: longbridge::fundamental::IndustryRankResponse) -> Self { + Self { + items: v.items.into(), + } + } +} + +impl ToFFI for CIndustryRankResponseOwned { + type FFIType = CIndustryRankResponse; + fn to_ffi_type(&self) -> Self::FFIType { + CIndustryRankResponse { + items: self.items.to_ffi_type(), + num_items: self.items.len(), + } + } +} + +// ── IndustryPeers ───────────────────────────────────────────────── + +/// Top-level industry info in the peers response. +#[repr(C)] +pub struct CIndustryPeersTop { + /// Industry name. + pub name: *const c_char, + /// Market code. + pub market: *const c_char, +} + +pub(crate) struct CIndustryPeersTopOwned { + name: CString, + market: CString, +} + +impl From for CIndustryPeersTopOwned { + fn from(v: longbridge::fundamental::IndustryPeersTop) -> Self { + Self { + name: v.name.into(), + market: v.market.into(), + } + } +} + +impl ToFFI for CIndustryPeersTopOwned { + type FFIType = CIndustryPeersTop; + fn to_ffi_type(&self) -> Self::FFIType { + CIndustryPeersTop { + name: self.name.to_ffi_type(), + market: self.market.to_ffi_type(), + } + } +} + +/// A node in the industry peer chain (recursive children serialised as JSON). +#[repr(C)] +pub struct CIndustryPeerNode { + /// Node name. + pub name: *const c_char, + /// Counter ID. + pub counter_id: *const c_char, + /// Number of stocks in this node. + pub stock_num: i32, + /// Change percentage. + pub chg: *const c_char, + /// Year-to-date change. + pub ytd_chg: *const c_char, + /// Child nodes serialised as a JSON string (may be NULL if empty). + pub next_json: *const c_char, +} + +pub(crate) struct CIndustryPeerNodeOwned { + name: CString, + counter_id: CString, + stock_num: i32, + chg: CString, + ytd_chg: CString, + next_json: CString, +} + +impl From for CIndustryPeerNodeOwned { + fn from(v: longbridge::fundamental::IndustryPeerNode) -> Self { + let next_json = if v.next.is_empty() { + String::new() + } else { + serde_json::to_string(&v.next).unwrap_or_default() + }; + Self { + name: v.name.into(), + counter_id: v.counter_id.into(), + stock_num: v.stock_num, + chg: v.chg.into(), + ytd_chg: v.ytd_chg.into(), + next_json: next_json.into(), + } + } +} + +impl ToFFI for CIndustryPeerNodeOwned { + type FFIType = CIndustryPeerNode; + fn to_ffi_type(&self) -> Self::FFIType { + CIndustryPeerNode { + name: self.name.to_ffi_type(), + counter_id: self.counter_id.to_ffi_type(), + stock_num: self.stock_num, + chg: self.chg.to_ffi_type(), + ytd_chg: self.ytd_chg.to_ffi_type(), + next_json: self.next_json.to_ffi_type(), + } + } +} + +/// Industry peer chain response. +#[repr(C)] +pub struct CIndustryPeersResponse { + /// Top-level industry node info. + pub top: CIndustryPeersTop, + /// Root peer chain node (NULL if absent). + pub chain: *const CIndustryPeerNode, +} + +pub(crate) struct CIndustryPeersResponseOwned { + top: CIndustryPeersTopOwned, + chain: COption, +} + +impl From for CIndustryPeersResponseOwned { + fn from(v: longbridge::fundamental::IndustryPeersResponse) -> Self { + Self { + top: v.top.into(), + chain: v.chain.into(), + } + } +} + +impl ToFFI for CIndustryPeersResponseOwned { + type FFIType = CIndustryPeersResponse; + fn to_ffi_type(&self) -> Self::FFIType { + CIndustryPeersResponse { + top: self.top.to_ffi_type(), + chain: self.chain.to_ffi_type(), + } + } +} + +// ── FinancialReportSnapshot ─────────────────────────────────────── + +/// A forecast metric in the financial report snapshot. +#[repr(C)] +pub struct CSnapshotForecastMetric { + /// Actual value. + pub value: *const c_char, + /// Year-over-year change. + pub yoy: *const c_char, + /// Beat/miss description. + pub cmp_desc: *const c_char, + /// Consensus estimate value. + pub est_value: *const c_char, +} + +pub(crate) struct CSnapshotForecastMetricOwned { + value: CString, + yoy: CString, + cmp_desc: CString, + est_value: CString, +} + +impl From for CSnapshotForecastMetricOwned { + fn from(v: longbridge::fundamental::SnapshotForecastMetric) -> Self { + Self { + value: v.value.into(), + yoy: v.yoy.into(), + cmp_desc: v.cmp_desc.into(), + est_value: v.est_value.into(), + } + } +} + +impl ToFFI for CSnapshotForecastMetricOwned { + type FFIType = CSnapshotForecastMetric; + fn to_ffi_type(&self) -> Self::FFIType { + CSnapshotForecastMetric { + value: self.value.to_ffi_type(), + yoy: self.yoy.to_ffi_type(), + cmp_desc: self.cmp_desc.to_ffi_type(), + est_value: self.est_value.to_ffi_type(), + } + } +} + +/// A reported metric in the financial report snapshot. +#[repr(C)] +pub struct CSnapshotReportedMetric { + /// Actual value. + pub value: *const c_char, + /// Year-over-year change. + pub yoy: *const c_char, +} + +pub(crate) struct CSnapshotReportedMetricOwned { + value: CString, + yoy: CString, +} + +impl From for CSnapshotReportedMetricOwned { + fn from(v: longbridge::fundamental::SnapshotReportedMetric) -> Self { + Self { + value: v.value.into(), + yoy: v.yoy.into(), + } + } +} + +impl ToFFI for CSnapshotReportedMetricOwned { + type FFIType = CSnapshotReportedMetric; + fn to_ffi_type(&self) -> Self::FFIType { + CSnapshotReportedMetric { + value: self.value.to_ffi_type(), + yoy: self.yoy.to_ffi_type(), + } + } +} + +/// Financial report snapshot (earnings snapshot) for a security. +#[repr(C)] +pub struct CFinancialReportSnapshot { + /// Company name. + pub name: *const c_char, + /// Ticker code. + pub ticker: *const c_char, + /// Fiscal period start date. + pub fp_start: *const c_char, + /// Fiscal period end date. + pub fp_end: *const c_char, + /// Reporting currency. + pub currency: *const c_char, + /// Report description. + pub report_desc: *const c_char, + /// Forecast revenue (NULL if absent). + pub fo_revenue: *const CSnapshotForecastMetric, + /// Forecast EBIT (NULL if absent). + pub fo_ebit: *const CSnapshotForecastMetric, + /// Forecast EPS (NULL if absent). + pub fo_eps: *const CSnapshotForecastMetric, + /// Reported revenue (NULL if absent). + pub fr_revenue: *const CSnapshotReportedMetric, + /// Reported net profit (NULL if absent). + pub fr_profit: *const CSnapshotReportedMetric, + /// Reported operating cash flow (NULL if absent). + pub fr_operate_cash: *const CSnapshotReportedMetric, + /// Reported investing cash flow (NULL if absent). + pub fr_invest_cash: *const CSnapshotReportedMetric, + /// Reported financing cash flow (NULL if absent). + pub fr_finance_cash: *const CSnapshotReportedMetric, + /// Reported total assets (NULL if absent). + pub fr_total_assets: *const CSnapshotReportedMetric, + /// Reported total liabilities (NULL if absent). + pub fr_total_liability: *const CSnapshotReportedMetric, + /// ROE TTM. + pub fr_roe_ttm: *const c_char, + /// Profit margin. + pub fr_profit_margin: *const c_char, + /// Profit margin TTM. + pub fr_profit_margin_ttm: *const c_char, + /// Asset turnover TTM. + pub fr_asset_turn_ttm: *const c_char, + /// Leverage TTM. + pub fr_leverage_ttm: *const c_char, + /// Debt-to-assets ratio. + pub fr_debt_assets_ratio: *const c_char, +} + +pub(crate) struct CFinancialReportSnapshotOwned { + name: CString, + ticker: CString, + fp_start: CString, + fp_end: CString, + currency: CString, + report_desc: CString, + fo_revenue: COption, + fo_ebit: COption, + fo_eps: COption, + fr_revenue: COption, + fr_profit: COption, + fr_operate_cash: COption, + fr_invest_cash: COption, + fr_finance_cash: COption, + fr_total_assets: COption, + fr_total_liability: COption, + fr_roe_ttm: CString, + fr_profit_margin: CString, + fr_profit_margin_ttm: CString, + fr_asset_turn_ttm: CString, + fr_leverage_ttm: CString, + fr_debt_assets_ratio: CString, +} + +impl From for CFinancialReportSnapshotOwned { + fn from(v: longbridge::fundamental::FinancialReportSnapshot) -> Self { + Self { + name: v.name.into(), + ticker: v.ticker.into(), + fp_start: v.fp_start.into(), + fp_end: v.fp_end.into(), + currency: v.currency.into(), + report_desc: v.report_desc.into(), + fo_revenue: v.fo_revenue.into(), + fo_ebit: v.fo_ebit.into(), + fo_eps: v.fo_eps.into(), + fr_revenue: v.fr_revenue.into(), + fr_profit: v.fr_profit.into(), + fr_operate_cash: v.fr_operate_cash.into(), + fr_invest_cash: v.fr_invest_cash.into(), + fr_finance_cash: v.fr_finance_cash.into(), + fr_total_assets: v.fr_total_assets.into(), + fr_total_liability: v.fr_total_liability.into(), + fr_roe_ttm: v.fr_roe_ttm.into(), + fr_profit_margin: v.fr_profit_margin.into(), + fr_profit_margin_ttm: v.fr_profit_margin_ttm.into(), + fr_asset_turn_ttm: v.fr_asset_turn_ttm.into(), + fr_leverage_ttm: v.fr_leverage_ttm.into(), + fr_debt_assets_ratio: v.fr_debt_assets_ratio.into(), + } + } +} + +impl ToFFI for CFinancialReportSnapshotOwned { + type FFIType = CFinancialReportSnapshot; + fn to_ffi_type(&self) -> Self::FFIType { + CFinancialReportSnapshot { + name: self.name.to_ffi_type(), + ticker: self.ticker.to_ffi_type(), + fp_start: self.fp_start.to_ffi_type(), + fp_end: self.fp_end.to_ffi_type(), + currency: self.currency.to_ffi_type(), + report_desc: self.report_desc.to_ffi_type(), + fo_revenue: self.fo_revenue.to_ffi_type(), + fo_ebit: self.fo_ebit.to_ffi_type(), + fo_eps: self.fo_eps.to_ffi_type(), + fr_revenue: self.fr_revenue.to_ffi_type(), + fr_profit: self.fr_profit.to_ffi_type(), + fr_operate_cash: self.fr_operate_cash.to_ffi_type(), + fr_invest_cash: self.fr_invest_cash.to_ffi_type(), + fr_finance_cash: self.fr_finance_cash.to_ffi_type(), + fr_total_assets: self.fr_total_assets.to_ffi_type(), + fr_total_liability: self.fr_total_liability.to_ffi_type(), + fr_roe_ttm: self.fr_roe_ttm.to_ffi_type(), + fr_profit_margin: self.fr_profit_margin.to_ffi_type(), + fr_profit_margin_ttm: self.fr_profit_margin_ttm.to_ffi_type(), + fr_asset_turn_ttm: self.fr_asset_turn_ttm.to_ffi_type(), + fr_leverage_ttm: self.fr_leverage_ttm.to_ffi_type(), + fr_debt_assets_ratio: self.fr_debt_assets_ratio.to_ffi_type(), + } + } +} From 213658c5b64da20d4434ac8aa01af465d87f5cf7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=A2=81=E7=AB=A0=E6=B4=AA?= Date: Thu, 21 May 2026 18:37:33 +0800 Subject: [PATCH 11/12] fix(cpp): add missing PR#526 method declarations to fundamental_context.hpp Methods business_segments, business_segments_history, institution_rating_views, industry_rank, industry_peers, financial_report_snapshot were implemented in fundamental_context.cpp but missing from the header, causing C++ CI failures. Co-Authored-By: Claude Sonnet 4.6 (1M context) --- cpp/include/fundamental_context.hpp | 34 +++++++++++++++++++++++++++++ 1 file changed, 34 insertions(+) diff --git a/cpp/include/fundamental_context.hpp b/cpp/include/fundamental_context.hpp index a6d3eb43c..451da4256 100644 --- a/cpp/include/fundamental_context.hpp +++ b/cpp/include/fundamental_context.hpp @@ -126,6 +126,40 @@ class FundamentalContext void ratings(const std::string& symbol, AsyncCallback callback) const; + /// Get latest business segment breakdown + void business_segments(const std::string& symbol, + AsyncCallback callback) const; + + /// Get historical business segment breakdowns (pass nullptr for report/cate to omit) + void business_segments_history(const std::string& symbol, + const char* report, + const char* cate, + AsyncCallback callback) const; + + /// Get historical institutional rating view time-series + void institution_rating_views(const std::string& symbol, + AsyncCallback callback) const; + + /// Get industry rank list for a market + void industry_rank(const std::string& market, + const std::string& indicator, + const std::string& sort_type, + uint32_t limit, + AsyncCallback callback) const; + + /// Get industry peer chain (pass nullptr for industry_id to omit) + void industry_peers(const std::string& counter_id, + const std::string& market, + const char* industry_id, + AsyncCallback callback) const; + + /// Get financial report snapshot (pass nullptr/0 for optional params) + void financial_report_snapshot(const std::string& symbol, + const char* report, + int32_t fiscal_year, + const char* fiscal_period, + AsyncCallback callback) const; + /// Get ranked list of top shareholders (raw JSON string) void shareholder_top(const std::string& symbol, AsyncCallback callback) const; From 6eabe637adc8aac8f5fcbd75e176c68c6e996e5c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=A2=81=E7=AB=A0=E6=B4=AA?= Date: Thu, 21 May 2026 19:02:11 +0800 Subject: [PATCH 12/12] fix(cpp): convert int32_t fiscal_year to string for C API in financial_report_snapshot The C function takes fiscal_year as const char*, not int32_t. Convert to string before passing; treat 0 as nullptr (omitted). Co-Authored-By: Claude Sonnet 4.6 (1M context) --- cpp/src/fundamental_context.cpp | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/cpp/src/fundamental_context.cpp b/cpp/src/fundamental_context.cpp index c9f6403b7..c14148cd0 100644 --- a/cpp/src/fundamental_context.cpp +++ b/cpp/src/fundamental_context.cpp @@ -114,7 +114,14 @@ void FundamentalContext::industry_peers(const std::string& counter_id, const std F_TYPED(IndustryPeersResponse, lb_industry_peers_response_t, lb_fundamental_context_industry_peers, ctx_, counter_id.c_str(), market.c_str(), industry_id); } void FundamentalContext::financial_report_snapshot(const std::string& s, const char* report, int32_t fiscal_year, const char* fiscal_period, AsyncCallback callback) const { - F_TYPED(FinancialReportSnapshot, lb_financial_report_snapshot_t, lb_fundamental_context_financial_report_snapshot, ctx_, s.c_str(), report, fiscal_year, fiscal_period); + // C API takes fiscal_year as a string; convert 0 → nullptr + std::string fy_str; + const char* fy_cstr = nullptr; + if (fiscal_year != 0) { + fy_str = std::to_string(fiscal_year); + fy_cstr = fy_str.c_str(); + } + F_TYPED(FinancialReportSnapshot, lb_financial_report_snapshot_t, lb_fundamental_context_financial_report_snapshot, ctx_, s.c_str(), report, fy_cstr, fiscal_period); } #undef F_TYPED