From 39506104119526252d8269366e22a0e507513d98 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 1/4] 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 | 22 ++++ 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 | 84 ++++++++++++++++ rust/src/fundamental/types.rs | 38 +++++++ 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, 804 insertions(+), 15 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 13a1666f3..9ed345a5f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,28 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +# [Unreleased] + +## Added + +- **Rust:** `FundamentalContext` gains three new methods: + - `shareholder_top(symbol)` — GET `/v1/quote/shareholders/top`: ranked list of top shareholders (raw JSON). + - `shareholder_detail(symbol, object_id)` — GET `/v1/quote/shareholders/holding`: holding history and detail for one shareholder object (raw JSON). + - `valuation_comparison(symbol, currency, comparison_symbols)` — GET `/v1/quote/compare/valuation`: valuation comparison between a security and optional peers (raw JSON). +- **Rust:** `QuoteContext` gains two new methods: + - `hk_short_positions(symbol, count)` — GET `/v1/quote/short-positions/hk`: HK short interest data (raw JSON). + - `short_trades(symbol, count)` — GET `/v1/quote/short-trades/hk` or `/v1/quote/short-trades/us` (auto-detected from symbol suffix): short trade records (raw JSON). +- **Rust:** `MarketContext` gains three new methods: + - `stock_events(markets, sort, date, limit)` — POST `/v1/quote/market/stock-events`: stock events across one or more markets (raw JSON). + - `rank_categories()` — GET `/v1/quote/market/rank/categories`: all available rank category keys and labels (raw JSON). + - `rank_list(key, need_article)` — GET `/v1/quote/market/rank/list`: ranked list of securities for a category (raw JSON). +- **Rust:** New `ScreenerContext` (and `ScreenerContextSync` blocking wrapper) with five methods: + - `screener_recommend_strategies()` — GET `/v1/quote/screener/strategies/recommend`. + - `screener_user_strategies()` — GET `/v1/quote/screener/strategies/mine`. + - `screener_strategy(id)` — GET `/v1/quote/screener/strategy?id=`. + - `screener_search(market, strategy_id, page, size)` — POST `/v1/quote/screener/search`. + - `screener_indicators()` — GET `/v1/quote/screener/indicators`. + # [4.1.0] ## Breaking changes diff --git a/rust/src/blocking/fundamental.rs b/rust/src/blocking/fundamental.rs index c2e674432..81ad48e16 100644 --- a/rust/src/blocking/fundamental.rs +++ b/rust/src/blocking/fundamental.rs @@ -179,4 +179,36 @@ impl FundamentalContextSync { self.rt .call(move |ctx| async move { ctx.ratings(symbol).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 015d3c992..c15f547b3 100644 --- a/rust/src/fundamental/context.rs +++ b/rust/src/fundamental/context.rs @@ -496,4 +496,88 @@ 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, + } + self.get( + "/v1/quote/shareholders/top", + Query { + counter_id: symbol_to_counter_id(&symbol.into()), + }, + ) + .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, + } + self.get( + "/v1/quote/shareholders/holding", + Query { + counter_id: symbol_to_counter_id(&symbol.into()), + object_id: object_id.to_string(), + }, + ) + .await + } + + // ── valuation_comparison ────────────────────────────────────── + + /// Get valuation comparison between a security and optional peers. + /// + /// Path: `GET /v1/quote/compare/valuation` + /// + /// `comparison_symbols` is a list of symbols (e.g. `["MSFT.US"]`) + /// that will be serialised as a JSON array string + /// `["ST/US/MSFT"]` and sent as `comparison_counter_ids`. + 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() + }); + self.get( + "/v1/quote/compare/valuation", + Query { + counter_id: symbol_to_counter_id(&symbol.into()), + currency: currency.into(), + comparison_counter_ids, + }, + ) + .await + } } diff --git a/rust/src/fundamental/types.rs b/rust/src/fundamental/types.rs index a9c5b6af5..26b7910dd 100644 --- a/rust/src/fundamental/types.rs +++ b/rust/src/fundamental/types.rs @@ -1110,6 +1110,44 @@ pub enum FinancialReportKind { All, } +// ── shareholder_top ─────────────────────────────────────────────── + +/// Response for [`crate::FundamentalContext::shareholder_top`] +/// +/// The raw data contains a ranked list of top shareholders. The exact +/// structure varies per market so the payload is preserved as raw JSON. +#[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`] +/// +/// The raw data contains holding history and details for one shareholder +/// object. The exact structure varies per market so the payload is +/// preserved as raw JSON. +#[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`] +/// +/// The raw data contains valuation metrics for the queried security and +/// any requested comparison symbols. The exact structure varies per +/// market so the payload is preserved as raw JSON. +#[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 c4cec312d..5a7b2e67c 100644 --- a/rust/src/quote/types.rs +++ b/rust/src/quote/types.rs @@ -2099,6 +2099,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 4e7e0569270443e306621e9a97d98059fa59a9ec 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 2/4] 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 | 31 ++- c/cbindgen.toml | 27 +- c/csrc/include/longbridge.h | 283 ++++++++++++++++--- c/src/fundamental_context/context.rs | 74 +++++ c/src/fundamental_context/types.rs | 87 ++++++ 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 | 18 ++ 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 | 72 ++++- java/src/init.rs | 16 +- 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 | 161 +++++++++-- nodejs/index.js | 254 ++++++++++++++--- nodejs/src/fundamental/context.rs | 42 +++ nodejs/src/fundamental/types.rs | 54 ++++ 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 | 353 ++++++++++++++++++++++-- python/src/fundamental/context.rs | 37 +++ python/src/fundamental/context_async.rs | 50 ++++ python/src/fundamental/mod.rs | 3 + python/src/fundamental/types.rs | 63 +++++ 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, 3398 insertions(+), 451 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 9ed345a5f..03cd6c2d4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,23 +8,30 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## Added -- **Rust:** `FundamentalContext` gains three new methods: - - `shareholder_top(symbol)` — GET `/v1/quote/shareholders/top`: ranked list of top shareholders (raw JSON). - - `shareholder_detail(symbol, object_id)` — GET `/v1/quote/shareholders/holding`: holding history and detail for one shareholder object (raw JSON). - - `valuation_comparison(symbol, currency, comparison_symbols)` — GET `/v1/quote/compare/valuation`: valuation comparison between a security and optional peers (raw JSON). -- **Rust:** `QuoteContext` gains two new methods: - - `hk_short_positions(symbol, count)` — GET `/v1/quote/short-positions/hk`: HK short interest data (raw JSON). - - `short_trades(symbol, count)` — GET `/v1/quote/short-trades/hk` or `/v1/quote/short-trades/us` (auto-detected from symbol suffix): short trade records (raw JSON). -- **Rust:** `MarketContext` gains three new methods: - - `stock_events(markets, sort, date, limit)` — POST `/v1/quote/market/stock-events`: stock events across one or more markets (raw JSON). - - `rank_categories()` — GET `/v1/quote/market/rank/categories`: all available rank category keys and labels (raw JSON). - - `rank_list(key, need_article)` — GET `/v1/quote/market/rank/list`: ranked list of securities for a category (raw JSON). -- **Rust:** New `ScreenerContext` (and `ScreenerContextSync` blocking wrapper) with five methods: +- **All languages (Rust, Python, Node.js, Java, C, C++):** `FundamentalContext` gains three new methods (raw JSON responses): + - `shareholder_top(symbol)` — GET `/v1/quote/shareholders/top`: ranked list of top shareholders. + - `shareholder_detail(symbol, object_id)` — GET `/v1/quote/shareholders/holding`: holding history and detail for one shareholder object. + - `valuation_comparison(symbol, currency, comparison_symbols)` — GET `/v1/quote/compare/valuation`: valuation comparison between a security and optional peers. +- **All languages (Rust, Python, Node.js, Java, C, C++):** `MarketContext` gains three new methods (raw JSON responses): + - `stock_events(markets, sort, date, limit)` — POST `/v1/quote/market/stock-events`: stock events across one or more markets. + - `rank_categories()` — GET `/v1/quote/market/rank/categories`: all available rank category keys and labels. + - `rank_list(key, need_article)` — GET `/v1/quote/market/rank/list`: ranked list of securities for a category. +- **All languages (Rust, Python, Node.js, Java, C, C++):** New `ScreenerContext` with five methods (raw JSON responses): - `screener_recommend_strategies()` — GET `/v1/quote/screener/strategies/recommend`. - `screener_user_strategies()` — GET `/v1/quote/screener/strategies/mine`. - `screener_strategy(id)` — GET `/v1/quote/screener/strategy?id=`. - `screener_search(market, strategy_id, page, size)` — POST `/v1/quote/screener/search`. - `screener_indicators()` — GET `/v1/quote/screener/indicators`. +- **All languages (Rust, Python, Node.js, Java, C, C++):** `QuoteContext::short_trades(symbol, count)` — GET `/v1/quote/short-trades/hk` or `/v1/quote/short-trades/us` (auto-detected from symbol suffix): short trade records (raw JSON). + +## Changed + +- **All languages (Rust, Python, Node.js, Java, C, C++):** `QuoteContext::short_positions(symbol, count)` now auto-detects market from the symbol suffix (`.HK` → HK endpoint, otherwise US endpoint) and accepts a `count` parameter. `ShortPositionsResponse` is now a raw JSON response instead of a typed struct. **Breaking change** — old `ShortPosition` sub-type and `symbol`/`sources` fields are removed. + +## Breaking changes + +- **Rust / Python / Node.js / Java / C / C++:** `QuoteContext::hk_short_positions` is removed. Use `short_positions(symbol, count)` which auto-detects HK vs US from the symbol suffix. +- **Rust / Python / Node.js / Java / C / C++:** `ShortPositionsResponse` no longer contains typed `symbol`, `data: Vec`, and `sources` fields. It now has a single `data` field with the raw JSON payload. Callers must parse the JSON themselves. # [4.1.0] diff --git a/c/cbindgen.toml b/c/cbindgen.toml index e7edabefc..8ca0b8fe1 100644 --- a/c/cbindgen.toml +++ b/c/cbindgen.toml @@ -280,11 +280,26 @@ cpp_compat = true "CRatingCategory" = "lb_rating_category_t" "CStockRatings" = "lb_stock_ratings_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 = [ @@ -392,7 +407,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 0d02bd504..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; /** @@ -7723,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 + * Raw short trade data as a JSON string */ - const char *symbol; - /** - * Pointer to array of short position records - */ - 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) @@ -7848,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 @@ -8484,6 +8553,37 @@ void lb_fundamental_context_ratings(const struct lb_fundamental_context_t *ctx, lb_async_callback_t callback, void *userdata); +/** + * Get ranked list of top shareholders. Returns `CShareholderTopResponse`. + */ +void lb_fundamental_context_shareholder_top(const struct lb_fundamental_context_t *ctx, + const char *symbol, + lb_async_callback_t callback, + void *userdata); + +/** + * Get holding history and detail for one shareholder. Returns + * `CShareholderDetailResponse`. + */ +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 valuation comparison between a security and optional peers. + * Returns `CValuationComparisonResponse`. + * Pass NULL for `comparison_symbols` to skip peer comparison. + */ +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 * @@ -8646,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. * @@ -9199,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`. */ @@ -9225,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 bb6801461..503a999e5 100644 --- a/c/src/fundamental_context/context.rs +++ b/c/src/fundamental_context/context.rs @@ -405,3 +405,77 @@ pub unsafe extern "C" fn lb_fundamental_context_ratings( Ok(resp) }); } + +/// Get ranked list of top shareholders. Returns `CShareholderTopResponse`. +#[unsafe(no_mangle)] +pub unsafe extern "C" fn lb_fundamental_context_shareholder_top( + 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( + CShareholderTopResponseOwned::from(ctx_inner.shareholder_top(symbol).await?), + ); + Ok(resp) + }); +} + +/// Get holding history and detail for one shareholder. Returns +/// `CShareholderDetailResponse`. +#[unsafe(no_mangle)] +pub unsafe extern "C" fn lb_fundamental_context_shareholder_detail( + ctx: *const CFundamentalContext, + symbol: *const c_char, + object_id: i64, + 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(CShareholderDetailResponseOwned::from( + ctx_inner.shareholder_detail(symbol, object_id).await?, + )); + Ok(resp) + }); +} + +/// 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_valuation_comparison( + ctx: *const CFundamentalContext, + symbol: *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 currency = cstr_to_rust(currency); + let comparison = if comparison_symbols.is_null() || num_comparison_symbols == 0 { + None + } else { + 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(CValuationComparisonResponseOwned::from( + ctx_inner + .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 90cdb0d72..8a272c883 100644 --- a/c/src/fundamental_context/types.rs +++ b/c/src/fundamental_context/types.rs @@ -2842,3 +2842,90 @@ impl ToFFI for CStockRatingsOwned { } } } + +// ── ShareholderTopResponse ──────────────────────────────────────── + +/// Top-shareholder list response. `data` is a NUL-terminated JSON string. +#[repr(C)] +pub struct CShareholderTopResponse { + /// Raw top-shareholder data as a JSON string + pub data: *const c_char, +} + +pub(crate) struct CShareholderTopResponseOwned { + data: CString, +} + +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 CShareholderTopResponseOwned { + type FFIType = CShareholderTopResponse; + fn to_ffi_type(&self) -> Self::FFIType { + CShareholderTopResponse { + data: self.data.to_ffi_type(), + } + } +} + +// ── ShareholderDetailResponse ───────────────────────────────────── + +/// Shareholder detail response. `data` is a NUL-terminated JSON string. +#[repr(C)] +pub struct CShareholderDetailResponse { + /// Raw shareholder detail data as a JSON string + pub data: *const c_char, +} + +pub(crate) struct CShareholderDetailResponseOwned { + data: CString, +} + +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 CShareholderDetailResponseOwned { + type FFIType = CShareholderDetailResponse; + fn to_ffi_type(&self) -> Self::FFIType { + CShareholderDetailResponse { + data: self.data.to_ffi_type(), + } + } +} + +// ── ValuationComparisonResponse ─────────────────────────────────── + +/// Valuation comparison response. `data` is a NUL-terminated JSON string. +#[repr(C)] +pub struct CValuationComparisonResponse { + /// Raw valuation comparison data as a JSON string + pub data: *const c_char, +} + +pub(crate) struct CValuationComparisonResponseOwned { + data: CString, +} + +impl From for CValuationComparisonResponseOwned { + fn from(v: ValuationComparisonResponse) -> Self { + let json = serde_json::to_string(&v.data).unwrap_or_default(); + Self { data: json.into() } + } +} + +impl ToFFI for CValuationComparisonResponseOwned { + type FFIType = CValuationComparisonResponse; + fn to_ffi_type(&self) -> Self::FFIType { + 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 5cf9631cc..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; @@ -123,6 +125,22 @@ class FundamentalContext /// Get stock ratings void ratings(const std::string& symbol, 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 6785b41f7..16630f8e7 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 da14fba14..62e39e767 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 c17fadce3..3ce13bb7b 100644 --- a/cpp/src/fundamental_context.cpp +++ b/cpp/src/fundamental_context.cpp @@ -102,5 +102,37 @@ void FundamentalContext::ratings(const std::string& s, AsyncCallback(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 d4ba231e4..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 { @@ -192,3 +192,73 @@ pub unsafe extern "system" fn Java_com_longbridge_SdkNative_fundamentalContextGe Ok(()) }) } + +#[unsafe(no_mangle)] +pub unsafe extern "system" fn Java_com_longbridge_SdkNative_fundamentalContextShareholderTop( + mut env: JNIEnv, + _class: JClass, + context: i64, + symbol: JObject, + 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.shareholder_top(symbol).await?; + Ok(resp) + })?; + Ok(()) + }) +} + +#[unsafe(no_mangle)] +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.shareholder_detail(symbol, object_id).await?; + Ok(resp) + })?; + Ok(()) + }) +} + +#[unsafe(no_mangle)] +pub unsafe extern "system" fn Java_com_longbridge_SdkNative_fundamentalContextValuationComparison( + mut env: JNIEnv, + _class: JClass, + context: i64, + symbol: JObject, + currency: JObject, + comparison_symbols: JObject, + callback: JObject, +) { + jni_result(&mut env, (), |env| { + let context = &*(context as *const ContextObj); + 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 + .valuation_comparison(symbol, currency, comparison_syms) + .await?; + Ok(resp) + })?; + Ok(()) + }) +} diff --git a/java/src/init.rs b/java/src/init.rs index aa51e6d34..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,6 +382,20 @@ pub extern "system" fn Java_com_longbridge_SdkNative_init<'a>( longbridge::fundamental::RatingSubIndicatorGroup, longbridge::fundamental::RatingCategory, longbridge::fundamental::StockRatings, + // 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 84c91c0f5..2b9c85402 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!( @@ -2402,3 +2390,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 2b0b38b32..b4d96743b 100644 --- a/nodejs/index.d.ts +++ b/nodejs/index.d.ts @@ -42,10 +42,14 @@ export declare class AlertContext { * `triggerValue` is a price or percentage string depending on `condition`. */ add(symbol: string, condition: AlertCondition, triggerValue: string, frequency: AlertFrequency): Promise - /** Enable a previously disabled price alert. */ - enable(alertId: string): Promise - /** Disable a price alert without deleting it. */ - disable(alertId: string): Promise + /** + * Update a price alert. + * + * Pass the [`AlertItem`] obtained from [`list`](Self::list). Set + * `item.enabled` to `true` to re-enable or `false` to disable before + * calling this method. + */ + update(item: AlertItem): Promise /** Delete one or more price alerts by ID. */ delete(alertIds: Array): Promise } @@ -603,6 +607,12 @@ export declare class FundamentalContext { buyback(symbol: string): Promise /** Get stock ratings for a security */ ratings(symbol: string): 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 */ @@ -761,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 */ @@ -1986,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 */ @@ -2033,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 @@ -4907,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 */ @@ -4992,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 */ @@ -5084,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 */ @@ -5106,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 */ @@ -5188,30 +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 +} + +/** + * 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 */ @@ -5238,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. * @@ -5447,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 ad854d664..e13776cb4 100644 --- a/nodejs/index.js +++ b/nodejs/index.js @@ -3,9 +3,6 @@ // @ts-nocheck /* auto-generated by NAPI-RS */ -const { createRequire } = require('node:module') -require = createRequire(__filename) - const { readFileSync } = require('node:fs') let nativeBinding = null const loadErrors = [] @@ -66,7 +63,7 @@ const isMuslFromChildProcess = () => { function requireNative() { if (process.env.NAPI_RS_NATIVE_LIBRARY_PATH) { try { - nativeBinding = require(process.env.NAPI_RS_NATIVE_LIBRARY_PATH); + return require(process.env.NAPI_RS_NATIVE_LIBRARY_PATH); } catch (err) { loadErrors.push(err) } @@ -78,7 +75,12 @@ function requireNative() { loadErrors.push(e) } try { - return require('longbridge-android-arm64') + const binding = require('longbridge-android-arm64') + const bindingPackageVersion = require('longbridge-android-arm64/package.json').version + if (bindingPackageVersion !== '0.0.0' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { + throw new Error(`Native binding package version mismatch, expected 0.0.0 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) + } + return binding } catch (e) { loadErrors.push(e) } @@ -89,7 +91,12 @@ function requireNative() { loadErrors.push(e) } try { - return require('longbridge-android-arm-eabi') + const binding = require('longbridge-android-arm-eabi') + const bindingPackageVersion = require('longbridge-android-arm-eabi/package.json').version + if (bindingPackageVersion !== '0.0.0' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { + throw new Error(`Native binding package version mismatch, expected 0.0.0 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) + } + return binding } catch (e) { loadErrors.push(e) } @@ -98,16 +105,39 @@ function requireNative() { } } else if (process.platform === 'win32') { if (process.arch === 'x64') { + if (process.config?.variables?.shlib_suffix === 'dll.a' || process.config?.variables?.node_target_type === 'shared_library') { + try { + return require('./longbridge.win32-x64-gnu.node') + } catch (e) { + loadErrors.push(e) + } try { + const binding = require('longbridge-win32-x64-gnu') + const bindingPackageVersion = require('longbridge-win32-x64-gnu/package.json').version + if (bindingPackageVersion !== '0.0.0' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { + throw new Error(`Native binding package version mismatch, expected 0.0.0 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) + } + return binding + } catch (e) { + loadErrors.push(e) + } + } else { + try { return require('./longbridge.win32-x64-msvc.node') } catch (e) { loadErrors.push(e) } try { - return require('longbridge-win32-x64-msvc') + const binding = require('longbridge-win32-x64-msvc') + const bindingPackageVersion = require('longbridge-win32-x64-msvc/package.json').version + if (bindingPackageVersion !== '0.0.0' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { + throw new Error(`Native binding package version mismatch, expected 0.0.0 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) + } + return binding } catch (e) { loadErrors.push(e) } + } } else if (process.arch === 'ia32') { try { return require('./longbridge.win32-ia32-msvc.node') @@ -115,7 +145,12 @@ function requireNative() { loadErrors.push(e) } try { - return require('longbridge-win32-ia32-msvc') + const binding = require('longbridge-win32-ia32-msvc') + const bindingPackageVersion = require('longbridge-win32-ia32-msvc/package.json').version + if (bindingPackageVersion !== '0.0.0' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { + throw new Error(`Native binding package version mismatch, expected 0.0.0 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) + } + return binding } catch (e) { loadErrors.push(e) } @@ -126,7 +161,12 @@ function requireNative() { loadErrors.push(e) } try { - return require('longbridge-win32-arm64-msvc') + const binding = require('longbridge-win32-arm64-msvc') + const bindingPackageVersion = require('longbridge-win32-arm64-msvc/package.json').version + if (bindingPackageVersion !== '0.0.0' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { + throw new Error(`Native binding package version mismatch, expected 0.0.0 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) + } + return binding } catch (e) { loadErrors.push(e) } @@ -140,7 +180,12 @@ function requireNative() { loadErrors.push(e) } try { - return require('longbridge-darwin-universal') + const binding = require('longbridge-darwin-universal') + const bindingPackageVersion = require('longbridge-darwin-universal/package.json').version + if (bindingPackageVersion !== '0.0.0' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { + throw new Error(`Native binding package version mismatch, expected 0.0.0 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) + } + return binding } catch (e) { loadErrors.push(e) } @@ -151,7 +196,12 @@ function requireNative() { loadErrors.push(e) } try { - return require('longbridge-darwin-x64') + const binding = require('longbridge-darwin-x64') + const bindingPackageVersion = require('longbridge-darwin-x64/package.json').version + if (bindingPackageVersion !== '0.0.0' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { + throw new Error(`Native binding package version mismatch, expected 0.0.0 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) + } + return binding } catch (e) { loadErrors.push(e) } @@ -162,7 +212,12 @@ function requireNative() { loadErrors.push(e) } try { - return require('longbridge-darwin-arm64') + const binding = require('longbridge-darwin-arm64') + const bindingPackageVersion = require('longbridge-darwin-arm64/package.json').version + if (bindingPackageVersion !== '0.0.0' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { + throw new Error(`Native binding package version mismatch, expected 0.0.0 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) + } + return binding } catch (e) { loadErrors.push(e) } @@ -177,7 +232,12 @@ function requireNative() { loadErrors.push(e) } try { - return require('longbridge-freebsd-x64') + const binding = require('longbridge-freebsd-x64') + const bindingPackageVersion = require('longbridge-freebsd-x64/package.json').version + if (bindingPackageVersion !== '0.0.0' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { + throw new Error(`Native binding package version mismatch, expected 0.0.0 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) + } + return binding } catch (e) { loadErrors.push(e) } @@ -188,7 +248,12 @@ function requireNative() { loadErrors.push(e) } try { - return require('longbridge-freebsd-arm64') + const binding = require('longbridge-freebsd-arm64') + const bindingPackageVersion = require('longbridge-freebsd-arm64/package.json').version + if (bindingPackageVersion !== '0.0.0' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { + throw new Error(`Native binding package version mismatch, expected 0.0.0 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) + } + return binding } catch (e) { loadErrors.push(e) } @@ -204,7 +269,12 @@ function requireNative() { loadErrors.push(e) } try { - return require('longbridge-linux-x64-musl') + const binding = require('longbridge-linux-x64-musl') + const bindingPackageVersion = require('longbridge-linux-x64-musl/package.json').version + if (bindingPackageVersion !== '0.0.0' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { + throw new Error(`Native binding package version mismatch, expected 0.0.0 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) + } + return binding } catch (e) { loadErrors.push(e) } @@ -215,7 +285,12 @@ function requireNative() { loadErrors.push(e) } try { - return require('longbridge-linux-x64-gnu') + const binding = require('longbridge-linux-x64-gnu') + const bindingPackageVersion = require('longbridge-linux-x64-gnu/package.json').version + if (bindingPackageVersion !== '0.0.0' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { + throw new Error(`Native binding package version mismatch, expected 0.0.0 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) + } + return binding } catch (e) { loadErrors.push(e) } @@ -228,7 +303,12 @@ function requireNative() { loadErrors.push(e) } try { - return require('longbridge-linux-arm64-musl') + const binding = require('longbridge-linux-arm64-musl') + const bindingPackageVersion = require('longbridge-linux-arm64-musl/package.json').version + if (bindingPackageVersion !== '0.0.0' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { + throw new Error(`Native binding package version mismatch, expected 0.0.0 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) + } + return binding } catch (e) { loadErrors.push(e) } @@ -239,7 +319,12 @@ function requireNative() { loadErrors.push(e) } try { - return require('longbridge-linux-arm64-gnu') + const binding = require('longbridge-linux-arm64-gnu') + const bindingPackageVersion = require('longbridge-linux-arm64-gnu/package.json').version + if (bindingPackageVersion !== '0.0.0' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { + throw new Error(`Native binding package version mismatch, expected 0.0.0 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) + } + return binding } catch (e) { loadErrors.push(e) } @@ -252,7 +337,12 @@ function requireNative() { loadErrors.push(e) } try { - return require('longbridge-linux-arm-musleabihf') + const binding = require('longbridge-linux-arm-musleabihf') + const bindingPackageVersion = require('longbridge-linux-arm-musleabihf/package.json').version + if (bindingPackageVersion !== '0.0.0' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { + throw new Error(`Native binding package version mismatch, expected 0.0.0 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) + } + return binding } catch (e) { loadErrors.push(e) } @@ -263,7 +353,46 @@ function requireNative() { loadErrors.push(e) } try { - return require('longbridge-linux-arm-gnueabihf') + const binding = require('longbridge-linux-arm-gnueabihf') + const bindingPackageVersion = require('longbridge-linux-arm-gnueabihf/package.json').version + if (bindingPackageVersion !== '0.0.0' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { + throw new Error(`Native binding package version mismatch, expected 0.0.0 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) + } + return binding + } catch (e) { + loadErrors.push(e) + } + } + } else if (process.arch === 'loong64') { + if (isMusl()) { + try { + return require('./longbridge.linux-loong64-musl.node') + } catch (e) { + loadErrors.push(e) + } + try { + const binding = require('longbridge-linux-loong64-musl') + const bindingPackageVersion = require('longbridge-linux-loong64-musl/package.json').version + if (bindingPackageVersion !== '0.0.0' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { + throw new Error(`Native binding package version mismatch, expected 0.0.0 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) + } + return binding + } catch (e) { + loadErrors.push(e) + } + } else { + try { + return require('./longbridge.linux-loong64-gnu.node') + } catch (e) { + loadErrors.push(e) + } + try { + const binding = require('longbridge-linux-loong64-gnu') + const bindingPackageVersion = require('longbridge-linux-loong64-gnu/package.json').version + if (bindingPackageVersion !== '0.0.0' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { + throw new Error(`Native binding package version mismatch, expected 0.0.0 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) + } + return binding } catch (e) { loadErrors.push(e) } @@ -276,7 +405,12 @@ function requireNative() { loadErrors.push(e) } try { - return require('longbridge-linux-riscv64-musl') + const binding = require('longbridge-linux-riscv64-musl') + const bindingPackageVersion = require('longbridge-linux-riscv64-musl/package.json').version + if (bindingPackageVersion !== '0.0.0' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { + throw new Error(`Native binding package version mismatch, expected 0.0.0 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) + } + return binding } catch (e) { loadErrors.push(e) } @@ -287,7 +421,12 @@ function requireNative() { loadErrors.push(e) } try { - return require('longbridge-linux-riscv64-gnu') + const binding = require('longbridge-linux-riscv64-gnu') + const bindingPackageVersion = require('longbridge-linux-riscv64-gnu/package.json').version + if (bindingPackageVersion !== '0.0.0' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { + throw new Error(`Native binding package version mismatch, expected 0.0.0 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) + } + return binding } catch (e) { loadErrors.push(e) } @@ -299,7 +438,12 @@ function requireNative() { loadErrors.push(e) } try { - return require('longbridge-linux-ppc64-gnu') + const binding = require('longbridge-linux-ppc64-gnu') + const bindingPackageVersion = require('longbridge-linux-ppc64-gnu/package.json').version + if (bindingPackageVersion !== '0.0.0' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { + throw new Error(`Native binding package version mismatch, expected 0.0.0 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) + } + return binding } catch (e) { loadErrors.push(e) } @@ -310,7 +454,12 @@ function requireNative() { loadErrors.push(e) } try { - return require('longbridge-linux-s390x-gnu') + const binding = require('longbridge-linux-s390x-gnu') + const bindingPackageVersion = require('longbridge-linux-s390x-gnu/package.json').version + if (bindingPackageVersion !== '0.0.0' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { + throw new Error(`Native binding package version mismatch, expected 0.0.0 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) + } + return binding } catch (e) { loadErrors.push(e) } @@ -320,34 +469,49 @@ function requireNative() { } else if (process.platform === 'openharmony') { if (process.arch === 'arm64') { try { - return require('./longbridge.linux-arm64-ohos.node') + return require('./longbridge.openharmony-arm64.node') } catch (e) { loadErrors.push(e) } try { - return require('longbridge-linux-arm64-ohos') + const binding = require('longbridge-openharmony-arm64') + const bindingPackageVersion = require('longbridge-openharmony-arm64/package.json').version + if (bindingPackageVersion !== '0.0.0' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { + throw new Error(`Native binding package version mismatch, expected 0.0.0 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) + } + return binding } catch (e) { loadErrors.push(e) } } else if (process.arch === 'x64') { try { - return require('./longbridge.linux-x64-ohos.node') + return require('./longbridge.openharmony-x64.node') } catch (e) { loadErrors.push(e) } try { - return require('longbridge-linux-x64-ohos') + const binding = require('longbridge-openharmony-x64') + const bindingPackageVersion = require('longbridge-openharmony-x64/package.json').version + if (bindingPackageVersion !== '0.0.0' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { + throw new Error(`Native binding package version mismatch, expected 0.0.0 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) + } + return binding } catch (e) { loadErrors.push(e) } } else if (process.arch === 'arm') { try { - return require('./longbridge.linux-arm-ohos.node') + return require('./longbridge.openharmony-arm.node') } catch (e) { loadErrors.push(e) } try { - return require('longbridge-linux-arm-ohos') + const binding = require('longbridge-openharmony-arm') + const bindingPackageVersion = require('longbridge-openharmony-arm/package.json').version + if (bindingPackageVersion !== '0.0.0' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { + throw new Error(`Native binding package version mismatch, expected 0.0.0 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) + } + return binding } catch (e) { loadErrors.push(e) } @@ -362,22 +526,36 @@ function requireNative() { nativeBinding = requireNative() if (!nativeBinding || process.env.NAPI_RS_FORCE_WASI) { + let wasiBinding = null + let wasiBindingError = null try { - nativeBinding = require('./longbridge.wasi.cjs') + wasiBinding = require('./longbridge.wasi.cjs') + nativeBinding = wasiBinding } catch (err) { if (process.env.NAPI_RS_FORCE_WASI) { - loadErrors.push(err) + wasiBindingError = err } } - if (!nativeBinding) { + if (!nativeBinding || process.env.NAPI_RS_FORCE_WASI) { try { - nativeBinding = require('longbridge-wasm32-wasi') + wasiBinding = require('longbridge-wasm32-wasi') + nativeBinding = wasiBinding } catch (err) { if (process.env.NAPI_RS_FORCE_WASI) { + if (!wasiBindingError) { + wasiBindingError = err + } else { + wasiBindingError.cause = err + } loadErrors.push(err) } } } + if (process.env.NAPI_RS_FORCE_WASI === 'error' && !wasiBinding) { + const error = new Error('WASI binding not found and NAPI_RS_FORCE_WASI is set to error') + error.cause = wasiBindingError + throw error + } } if (!nativeBinding) { @@ -386,7 +564,12 @@ if (!nativeBinding) { `Cannot find native binding. ` + `npm has a bug related to optional dependencies (https://github.com/npm/cli/issues/4828). ` + 'Please try `npm i` again after removing both package-lock.json and node_modules directory.', - { cause: loadErrors } + { + cause: loadErrors.reduce((err, cur) => { + cur.cause = err + return cur + }), + }, ) } throw new Error(`Failed to load native binding`) @@ -456,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 cdf1d050f..0878dad09 100644 --- a/nodejs/src/fundamental/context.rs +++ b/nodejs/src/fundamental/context.rs @@ -233,4 +233,46 @@ impl FundamentalContext { pub async fn ratings(&self, symbol: String) -> Result { Ok(self.ctx.ratings(symbol).await.map_err(ErrorNewType)?.into()) } + + /// Get ranked list of top shareholders + #[napi] + pub async fn shareholder_top(&self, symbol: String) -> Result { + Ok(self + .ctx + .shareholder_top(symbol) + .await + .map_err(ErrorNewType)? + .into()) + } + + /// Get holding history and detail for one shareholder + #[napi] + pub async fn shareholder_detail( + &self, + symbol: String, + object_id: i64, + ) -> Result { + Ok(self + .ctx + .shareholder_detail(symbol, object_id) + .await + .map_err(ErrorNewType)? + .into()) + } + + /// Get valuation comparison between a security and optional peers + #[napi] + pub async fn valuation_comparison( + &self, + symbol: String, + currency: String, + comparison_symbols: Option>, + ) -> Result { + Ok(self + .ctx + .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 bfa374337..a9bf59d34 100644 --- a/nodejs/src/fundamental/types.rs +++ b/nodejs/src/fundamental/types.rs @@ -1646,3 +1646,57 @@ impl From for StockRatings { } } } + +// ── ShareholderTopResponse ──────────────────────────────────────── + +/// Top-shareholder list response. `data` is a JSON string. +#[napi_derive::napi(object)] +#[derive(Debug, Clone)] +pub struct ShareholderTopResponse { + /// Raw top-shareholder data (JSON string) + pub data: String, +} + +impl From for ShareholderTopResponse { + fn from(v: lb::ShareholderTopResponse) -> Self { + Self { + data: v.data.to_string(), + } + } +} + +// ── ShareholderDetailResponse ───────────────────────────────────── + +/// Shareholder detail response. `data` is a JSON string. +#[napi_derive::napi(object)] +#[derive(Debug, Clone)] +pub struct ShareholderDetailResponse { + /// Raw shareholder detail data (JSON string) + pub data: String, +} + +impl From for ShareholderDetailResponse { + fn from(v: lb::ShareholderDetailResponse) -> Self { + Self { + data: v.data.to_string(), + } + } +} + +// ── ValuationComparisonResponse ─────────────────────────────────── + +/// Valuation comparison response. `data` is a JSON string. +#[napi_derive::napi(object)] +#[derive(Debug, Clone)] +pub struct ValuationComparisonResponse { + /// Raw valuation comparison data (JSON string) + pub data: String, +} + +impl From for ValuationComparisonResponse { + fn from(v: lb::ValuationComparisonResponse) -> Self { + Self { + 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 ed2e01b24..6d9ed596d 100644 --- a/nodejs/src/quote/types.rs +++ b/nodejs/src/quote/types.rs @@ -1467,54 +1467,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 115271ebc..ad774e421 100644 --- a/python/pysrc/longbridge/openapi.pyi +++ b/python/pysrc/longbridge/openapi.pyi @@ -3986,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. @@ -5302,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 @@ -9792,6 +9862,75 @@ class FundamentalContext: """ ... + def shareholder_top(self, symbol: str) -> "ShareholderTopResponse": + """ + Get ranked list of top shareholders. + + Args: + symbol: Security symbol + + Returns: + :class:`ShareholderTopResponse` with raw JSON data + """ + ... + + def shareholder_detail( + self, symbol: str, object_id: int + ) -> "ShareholderDetailResponse": + """ + Get holding history and detail for one shareholder. + + Args: + symbol: Security symbol + object_id: Shareholder object ID + + Returns: + :class:`ShareholderDetailResponse` with raw JSON data + """ + ... + + def valuation_comparison( + self, + symbol: str, + currency: str, + comparison_symbols: Optional[List[str]] = None, + ) -> "ValuationComparisonResponse": + """ + 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:`ValuationComparisonResponse` with raw JSON data + """ + ... + + +# ── FundamentalContext new response types ───────────────────────── + +class ShareholderTopResponse: + """Top-shareholder list response. ``data`` is a Python dict/list from JSON.""" + + data: object + """Raw top-shareholder data (JSON object / list)""" + + +class ShareholderDetailResponse: + """Shareholder detail response. ``data`` is a Python dict/list from JSON.""" + + data: object + """Raw shareholder detail data (JSON object / list)""" + + +class ValuationComparisonResponse: + """Valuation comparison response. ``data`` is a Python dict/list from JSON.""" + + data: object + """Raw valuation comparison data (JSON object / list)""" + # ── MarketContext ───────────────────────────────────────────────── @@ -10193,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 ─────────────────────────────────────────────── @@ -11343,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 d4d8a8e39..87f2e26fd 100644 --- a/python/src/fundamental/context.rs +++ b/python/src/fundamental/context.rs @@ -161,4 +161,41 @@ impl FundamentalContext { fn ratings(&self, symbol: String) -> PyResult { Ok(self.ctx.ratings(symbol).map_err(ErrorNewType)?.into()) } + + /// Get ranked list of top shareholders. + fn shareholder_top(&self, symbol: String) -> PyResult { + Ok(self + .ctx + .shareholder_top(symbol) + .map_err(ErrorNewType)? + .into()) + } + + /// Get holding history and detail for one shareholder. + fn shareholder_detail( + &self, + symbol: String, + object_id: i64, + ) -> PyResult { + Ok(self + .ctx + .shareholder_detail(symbol, object_id) + .map_err(ErrorNewType)? + .into()) + } + + /// Get valuation comparison between a security and optional peers. + #[pyo3(signature = (symbol, currency, comparison_symbols = None))] + fn valuation_comparison( + &self, + symbol: String, + currency: String, + comparison_symbols: Option>, + ) -> PyResult { + Ok(self + .ctx + .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 9361bd5d3..8c162b9a5 100644 --- a/python/src/fundamental/context_async.rs +++ b/python/src/fundamental/context_async.rs @@ -253,4 +253,54 @@ impl AsyncFundamentalContext { }) .map(|b| b.unbind()) } + + /// 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(ShareholderTopResponse::from( + ctx.shareholder_top(symbol).await.map_err(ErrorNewType)?, + )) + }) + .map(|b| b.unbind()) + } + + /// Get holding history and detail for one shareholder. Returns awaitable. + fn shareholder_detail( + &self, + py: Python<'_>, + symbol: String, + object_id: i64, + ) -> PyResult> { + let ctx = self.ctx.clone(); + pyo3_async_runtimes::tokio::future_into_py(py, async move { + Ok(ShareholderDetailResponse::from( + ctx.shareholder_detail(symbol, object_id) + .await + .map_err(ErrorNewType)?, + )) + }) + .map(|b| b.unbind()) + } + + /// Get valuation comparison between a security and optional peers. Returns + /// awaitable. + #[pyo3(signature = (symbol, currency, comparison_symbols = None))] + fn valuation_comparison( + &self, + py: Python<'_>, + symbol: String, + currency: String, + comparison_symbols: Option>, + ) -> PyResult> { + let ctx = self.ctx.clone(); + pyo3_async_runtimes::tokio::future_into_py(py, async move { + Ok(ValuationComparisonResponse::from( + ctx.valuation_comparison(symbol, currency, comparison_symbols) + .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 462458662..a14990800 100644 --- a/python/src/fundamental/types.rs +++ b/python/src/fundamental/types.rs @@ -1703,3 +1703,66 @@ impl From for lb::FinancialReportPeriod { } } } + +// ── ShareholderTopResponse ──────────────────────────────────────── + +/// 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 ShareholderTopResponse { + /// Raw top-shareholder data (JSON object) + pub data: JsonValue, +} + +impl From for ShareholderTopResponse { + fn from(v: lb::ShareholderTopResponse) -> Self { + Self { + data: JsonValue(v.data), + } + } +} + +// ── ShareholderDetailResponse ───────────────────────────────────── + +/// Shareholder detail 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 ShareholderDetailResponse { + /// Raw shareholder detail data (JSON object) + pub data: JsonValue, +} + +impl From for ShareholderDetailResponse { + fn from(v: lb::ShareholderDetailResponse) -> Self { + Self { + data: JsonValue(v.data), + } + } +} + +// ── ValuationComparisonResponse ─────────────────────────────────── + +/// 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, +} + +impl From for ValuationComparisonResponse { + fn from(v: lb::ValuationComparisonResponse) -> Self { + Self { + 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 3da11132f..790b9bce0 100644 --- a/python/src/quote/types.rs +++ b/python/src/quote/types.rs @@ -1423,55 +1423,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 5a7b2e67c..4896b0a22 100644 --- a/rust/src/quote/types.rs +++ b/rust/src/quote/types.rs @@ -2019,35 +2019,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 ───────────────────────────────────────────────── @@ -2099,18 +2078,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 8889678a249cfb69692f5406b90f14df6f6a4af6 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 3/4] 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 71483a6d5b411f8daf04b0fd91f6610a07e0eb94 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 4/4] 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/fundamental/context.rs | 54 ++++++++++++++++++--------------- rust/src/market/context.rs | 47 ++++++++++++++++------------ rust/src/quote/context.rs | 8 ++--- rust/src/screener/context.rs | 42 +++++++++++++++---------- 4 files changed, 87 insertions(+), 64 deletions(-) diff --git a/rust/src/fundamental/context.rs b/rust/src/fundamental/context.rs index c15f547b3..058eb9bef 100644 --- a/rust/src/fundamental/context.rs +++ b/rust/src/fundamental/context.rs @@ -510,13 +510,15 @@ impl FundamentalContext { struct Query { counter_id: String, } - self.get( - "/v1/quote/shareholders/top", - Query { - counter_id: symbol_to_counter_id(&symbol.into()), - }, - ) - .await + 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 }) } // ── shareholder_detail ──────────────────────────────────────── @@ -534,14 +536,16 @@ impl FundamentalContext { counter_id: String, object_id: String, } - self.get( - "/v1/quote/shareholders/holding", - Query { - counter_id: symbol_to_counter_id(&symbol.into()), - object_id: object_id.to_string(), - }, - ) - .await + 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 }) } // ── valuation_comparison ────────────────────────────────────── @@ -570,14 +574,16 @@ impl FundamentalContext { let ids: Vec = syms.iter().map(|s| symbol_to_counter_id(s)).collect(); serde_json::to_string(&ids).unwrap_or_default() }); - self.get( - "/v1/quote/compare/valuation", - Query { - counter_id: symbol_to_counter_id(&symbol.into()), - currency: currency.into(), - comparison_counter_ids, - }, - ) - .await + 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/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 }) } }