diff --git a/CHANGELOG.md b/CHANGELOG.md index 2b88801a1..d2f4c7f36 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,20 +8,30 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added -- **Rust, Python, Node.js:** Document normalization formulas for `SecurityCalcIndex` Greeks fields: `theta` (divide by 252 for per-trading-day), `vega` and `rho` (divide by 100 for per-unit change). The raw API values differ from Longbridge app display values by these factors. -- **Rust:** `Config::header(key, value)` builder method to inject custom headers into every HTTP request and WebSocket upgrade request. -- **Rust, Python:** `ContentContext` adds three new methods: - - `topic_detail(topic_id)` — get detail of a single topic. - - `list_topic_replies(opts)` — list replies for a topic, with optional page/size filtering. - - `create_topic_reply(opts)` — create a reply under a topic. -- **Rust, Python:** New types `ListTopicRepliesOptions`, `CreateReplyOptions`, and `TopicReply` to support the above methods. -- **All languages (Rust/Python/Node.js/Java/C/C++):** Six new `FundamentalContext` methods: - - `BusinessSegments` — GET `/v1/quote/fundamentals/business-segments`: latest business segment breakdown. - - `BusinessSegmentsHistory` — GET `/v1/quote/fundamentals/business-segments/history`: historical business and regional segment breakdowns with optional `report` and `cate` filters. - - `InstitutionRatingViews` — GET `/v1/quote/ratings/institutional`: historical rating distribution time-series (buy/over/hold/under/sell per date). - - `IndustryRank` — GET `/v1/quote/industry/rank`: industry leaderboard; exposes `IndustryRankIndicator` and `IndustryRankSortType` enum constants. - - `IndustryPeers` — GET `/v1/quote/industries/peers`: recursive industry peer chain; accepts both symbol-style (`AAPL.US`) and raw counter IDs (`BK/US/123`). - - `FinancialReportSnapshot` — GET `/v1/quote/financials/earnings-snapshot`: earnings snapshot with forecast and reported metrics. +- **Rust, Python, Node.js:** Document normalization formulas for `SecurityCalcIndex` Greeks fields: `theta` (divide by 252 for per-trading-day), `vega` and `rho` (divide by 100 for per-unit change). +- **Rust:** `Config::header(key, value)` builder method to inject custom headers. +- **Rust, Python:** `ContentContext` adds `topic_detail`, `list_topic_replies`, `create_topic_reply`. +- **All languages:** Six new `FundamentalContext` methods (PR #526): `BusinessSegments`, `BusinessSegmentsHistory`, `InstitutionRatingViews`, `IndustryRank`, `IndustryPeers`, `FinancialReportSnapshot` +- **C, C++:** C SDK bindings for all six PR #526 `FundamentalContext` APIs: `lb_fundamental_context_business_segments`, `lb_fundamental_context_business_segments_history`, `lb_fundamental_context_institution_rating_views`, `lb_fundamental_context_industry_rank`, `lb_fundamental_context_industry_peers`, `lb_fundamental_context_financial_report_snapshot`. New typed structs added to `longbridge.h`. +- **All languages:** 13 more new APIs: `shareholder_top`, `shareholder_detail`, `valuation_comparison` (FundamentalContext); `short_positions` (HK+US unified), `short_trades` (QuoteContext); `top_movers`, `rank_categories`, `rank_list` (MarketContext); `ScreenerContext` (new) with 5 screener methods + +### Changed + +- **Java, C, C++:** Response types for `short_positions`, `short_trades`, `top_movers`, `rank_list`, and `valuation_comparison` updated from raw-JSON stub structs to fully typed structs with named fields, matching the Rust/Python/Node.js SDK types. C SDK regenerated (`longbridge.h`) with new `lb_short_positions_item_t`, `lb_short_trades_item_t`, `lb_top_movers_stock_t`, `lb_top_movers_event_t`, `lb_rank_list_item_t`, `lb_valuation_history_point_t`, `lb_valuation_comparison_item_t` types. +- **All languages:** `QuoteContext::short_positions(symbol, count)` now auto-detects market from symbol suffix (`.HK` → HK, otherwise US). `ShortPositionsResponse` is raw JSON. +- **Rust, Python, Node.js, Java, C:** `short_positions` and `short_trades` responses now use typed structs (`ShortPositionsItem` / `ShortTradesItem`) with RFC 3339 timestamps instead of raw JSON. Fields absent in a given market default to empty string. +- **Rust, Python, Node.js, Java, C:** `rank_list` response now uses a typed `RankListResponse { bmp, lists: Vec }` struct; `counter_id` is converted to `symbol`. +- **Rust, Python, Node.js, Java, C:** `top_movers` response now uses a typed `TopMoversResponse { events: Vec, next_params }` struct; timestamps converted to RFC 3339 and `counter_id` converted to `symbol`. +- **Rust, Python, Node.js, Java, C:** `valuation_comparison` response now uses a typed `ValuationComparisonResponse { list: Vec }` struct; history dates converted from Unix timestamp to RFC 3339 and `counter_id` converted to `symbol`. + +### Breaking changes + +- **All languages:** `MarketContext::stock_events` renamed to `top_movers`; `StockEventsResponse` → `TopMoversResponse`. +- **All languages:** `hk_short_positions` removed — use `short_positions(symbol, count)` which auto-detects HK/US. +- **All languages:** `ShortPositionsResponse.data` changed from raw JSON to `Vec`; `ShortTradesResponse.data` changed from raw JSON to `Vec`. +- **All languages:** `TopMoversResponse.data` (raw JSON) replaced by `TopMoversResponse { events, next_params }`. +- **All languages:** `RankListResponse.data` (raw JSON) replaced by `RankListResponse { bmp, lists }`. +- **All languages:** `ValuationComparisonResponse.data` (raw JSON) replaced by `ValuationComparisonResponse { list }`. # [4.1.0] diff --git a/c/cbindgen.toml b/c/cbindgen.toml index b4620ed58..ed12c8f59 100644 --- a/c/cbindgen.toml +++ b/c/cbindgen.toml @@ -297,11 +297,33 @@ cpp_compat = true "CSnapshotReportedMetric" = "lb_snapshot_reported_metric_t" "CFinancialReportSnapshot" = "lb_financial_report_snapshot_t" # QuoteContext extensions -"CShortPosition" = "lb_short_position_t" +"CShortPositionsItem" = "lb_short_positions_item_t" "CShortPositionsResponse" = "lb_short_positions_response_t" +"CShortTradesItem" = "lb_short_trades_item_t" +"CShortTradesResponse" = "lb_short_trades_response_t" "COptionVolumeStats" = "lb_option_volume_stats_t" "COptionVolumeDailyStat" = "lb_option_volume_daily_stat_t" "COptionVolumeDaily" = "lb_option_volume_daily_t" +# FundamentalContext new types +"CShareholderTopResponse" = "lb_shareholder_top_response_t" +"CShareholderDetailResponse" = "lb_shareholder_detail_response_t" +"CValuationHistoryPoint" = "lb_valuation_history_point_t" +"CValuationComparisonItem" = "lb_valuation_comparison_item_t" +"CValuationComparisonResponse" = "lb_valuation_comparison_response_t" +# MarketContext new types +"CTopMoversStock" = "lb_top_movers_stock_t" +"CTopMoversEvent" = "lb_top_movers_event_t" +"CTopMoversResponse" = "lb_top_movers_response_t" +"CRankCategoriesResponse" = "lb_rank_categories_response_t" +"CRankListItem" = "lb_rank_list_item_t" +"CRankListResponse" = "lb_rank_list_response_t" +# ScreenerContext +"CScreenerContext" = "lb_screener_context_t" +"CScreenerRecommendStrategiesResponse" = "lb_screener_recommend_strategies_response_t" +"CScreenerUserStrategiesResponse" = "lb_screener_user_strategies_response_t" +"CScreenerStrategyResponse" = "lb_screener_strategy_response_t" +"CScreenerSearchResponse" = "lb_screener_search_response_t" +"CScreenerIndicatorsResponse" = "lb_screener_indicators_response_t" [export] include = [ @@ -415,7 +437,19 @@ include = [ # FundamentalContext opaque type (no rename, typedef added in hpp) "CFundamentalContext", # QuoteContext extensions - "CShortPosition", "CShortPositionsResponse", + "CShortPositionsItem", "CShortPositionsResponse", + "CShortTradesItem", "CShortTradesResponse", "COptionVolumeStats", "COptionVolumeDailyStat", "COptionVolumeDaily", + # FundamentalContext new types + "CShareholderTopResponse", "CShareholderDetailResponse", + "CValuationHistoryPoint", "CValuationComparisonItem", "CValuationComparisonResponse", + # MarketContext new types + "CTopMoversStock", "CTopMoversEvent", "CTopMoversResponse", + "CRankCategoriesResponse", + "CRankListItem", "CRankListResponse", + # ScreenerContext + "CScreenerContext", + "CScreenerRecommendStrategiesResponse", "CScreenerUserStrategiesResponse", + "CScreenerStrategyResponse", "CScreenerSearchResponse", "CScreenerIndicatorsResponse", ] diff --git a/c/csrc/include/longbridge.h b/c/csrc/include/longbridge.h index 0f133cdd9..5cfcd77ac 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; /** @@ -6581,7 +6583,7 @@ typedef struct lb_business_segment_item_t { } lb_business_segment_item_t; /** - * Business segments response. + * Current business segment breakdown for a security. */ typedef struct lb_business_segments_t { /** @@ -6597,7 +6599,7 @@ typedef struct lb_business_segments_t { */ const char *currency; /** - * Pointer to the array of business segment items. + * Pointer to business segment items. */ const struct lb_business_segment_item_t *business; /** @@ -6641,7 +6643,7 @@ typedef struct lb_business_segments_historical_item_t { */ const char *currency; /** - * Pointer to the business segment items. + * Pointer to business segment breakdown items. */ const struct lb_business_segment_history_item_t *business; /** @@ -6649,7 +6651,7 @@ typedef struct lb_business_segments_historical_item_t { */ uintptr_t num_business; /** - * Pointer to the regional segment items. + * Pointer to regional breakdown items. */ const struct lb_business_segment_history_item_t *regionals; /** @@ -6659,11 +6661,11 @@ typedef struct lb_business_segments_historical_item_t { } lb_business_segments_historical_item_t; /** - * Business segments history response. + * Historical business segment breakdowns for a security. */ typedef struct lb_business_segments_history_t { /** - * Pointer to the historical snapshots. + * Pointer to historical snapshot items. */ const struct lb_business_segments_historical_item_t *historical; /** @@ -6673,31 +6675,31 @@ typedef struct lb_business_segments_history_t { } lb_business_segments_history_t; /** - * One historical rating distribution snapshot. + * One historical institutional rating distribution snapshot. */ typedef struct lb_institution_rating_view_item_t { /** - * Date as unix timestamp string. + * Date (unix timestamp string). */ const char *date; /** - * Number of Buy ratings. + * Number of "Buy" ratings. */ const char *buy; /** - * Number of Outperform ratings. + * Number of "Outperform" ratings. */ const char *over; /** - * Number of Hold ratings. + * Number of "Hold" ratings. */ const char *hold; /** - * Number of Underperform ratings. + * Number of "Underperform" ratings. */ const char *under; /** - * Number of Sell ratings. + * Number of "Sell" ratings. */ const char *sell; /** @@ -6707,11 +6709,11 @@ typedef struct lb_institution_rating_view_item_t { } lb_institution_rating_view_item_t; /** - * Institution rating views response. + * Historical institutional rating views time-series for a security. */ typedef struct lb_institution_rating_views_t { /** - * Pointer to the rating view items. + * Pointer to rating view items. */ const struct lb_institution_rating_view_item_t *elist; /** @@ -6763,7 +6765,7 @@ typedef struct lb_industry_rank_item_t { */ typedef struct lb_industry_rank_group_t { /** - * Pointer to the items in this group. + * Pointer to ranked items. */ const struct lb_industry_rank_item_t *lists; /** @@ -6777,11 +6779,11 @@ typedef struct lb_industry_rank_group_t { */ typedef struct lb_industry_rank_response_t { /** - * Pointer to the grouped rank items. + * Pointer to grouped rank items. */ const struct lb_industry_rank_group_t *items; /** - * Number of items in `items`. + * Number of groups in `items`. */ uintptr_t num_items; } lb_industry_rank_response_t; @@ -6801,9 +6803,7 @@ typedef struct lb_industry_peers_top_t { } lb_industry_peers_top_t; /** - * A node in the recursive industry peer chain. - * - * `next_json` contains the child nodes serialised as a JSON string. + * A node in the industry peer chain (recursive children serialised as JSON). */ typedef struct lb_industry_peer_node_t { /** @@ -6827,13 +6827,13 @@ typedef struct lb_industry_peer_node_t { */ const char *ytd_chg; /** - * Child nodes as a JSON string. + * Child nodes serialised as a JSON string (may be NULL if empty). */ const char *next_json; } lb_industry_peer_node_t; /** - * Industry peers response. + * Industry peer chain response. */ typedef struct lb_industry_peers_response_t { /** @@ -6841,7 +6841,7 @@ typedef struct lb_industry_peers_response_t { */ struct lb_industry_peers_top_t top; /** - * Root peer chain node, or null if absent. + * Root peer chain node (NULL if absent). */ const struct lb_industry_peer_node_t *chain; } lb_industry_peers_response_t; @@ -6883,7 +6883,7 @@ typedef struct lb_snapshot_reported_metric_t { } lb_snapshot_reported_metric_t; /** - * Financial report snapshot response. + * Financial report snapshot (earnings snapshot) for a security. */ typedef struct lb_financial_report_snapshot_t { /** @@ -6911,43 +6911,43 @@ typedef struct lb_financial_report_snapshot_t { */ const char *report_desc; /** - * Forecast revenue, or null. + * Forecast revenue (NULL if absent). */ const struct lb_snapshot_forecast_metric_t *fo_revenue; /** - * Forecast EBIT, or null. + * Forecast EBIT (NULL if absent). */ const struct lb_snapshot_forecast_metric_t *fo_ebit; /** - * Forecast EPS, or null. + * Forecast EPS (NULL if absent). */ const struct lb_snapshot_forecast_metric_t *fo_eps; /** - * Reported revenue, or null. + * Reported revenue (NULL if absent). */ const struct lb_snapshot_reported_metric_t *fr_revenue; /** - * Reported net profit, or null. + * Reported net profit (NULL if absent). */ const struct lb_snapshot_reported_metric_t *fr_profit; /** - * Reported operating cash flow, or null. + * Reported operating cash flow (NULL if absent). */ const struct lb_snapshot_reported_metric_t *fr_operate_cash; /** - * Reported investing cash flow, or null. + * Reported investing cash flow (NULL if absent). */ const struct lb_snapshot_reported_metric_t *fr_invest_cash; /** - * Reported financing cash flow, or null. + * Reported financing cash flow (NULL if absent). */ const struct lb_snapshot_reported_metric_t *fr_finance_cash; /** - * Reported total assets, or null. + * Reported total assets (NULL if absent). */ const struct lb_snapshot_reported_metric_t *fr_total_assets; /** - * Reported total liabilities, or null. + * Reported total liabilities (NULL if absent). */ const struct lb_snapshot_reported_metric_t *fr_total_liability; /** @@ -8133,56 +8133,112 @@ typedef struct lb_sharelist_detail_t { } lb_sharelist_detail_t; /** - * Short position data for a single date + * One short-position record, unified for US and HK markets. */ -typedef struct lb_short_position_t { +typedef struct lb_short_positions_item_t { /** - * Date of the short position record (formatted string) + * Trading date in RFC 3339 format */ const char *timestamp; /** - * Short interest as a percentage of shares outstanding + * Short ratio */ const char *rate; /** - * Average daily share volume + * Closing price */ - const char *avg_daily_share_volume; + const char *close; /** - * Current number of shares sold short + * [US] Number of short shares outstanding */ const char *current_shares_short; /** - * Days to cover (short interest ratio) + * [US] Average daily share volume + */ + const char *avg_daily_share_volume; + /** + * [US] Days-to-cover ratio */ const char *days_to_cover; /** - * Closing price on the record date + * [HK] Short sale amount (HKD) */ - const char *close; -} lb_short_position_t; + const char *amount; + /** + * [HK] Short position balance + */ + const char *balance; + /** + * [HK] Closing price (HK naming) + */ + const char *cost; +} lb_short_positions_item_t; /** - * Short positions response for a security + * Short positions / interest response (HK or US). */ typedef struct lb_short_positions_response_t { /** - * Security code + * Pointer to the array of short position items */ - const char *symbol; + const struct lb_short_positions_item_t *data; + /** + * Number of items in `data` + */ + uintptr_t num_data; +} lb_short_positions_response_t; + +/** + * One short-trade record, unified for US and HK markets. + */ +typedef struct lb_short_trades_item_t { /** - * Pointer to array of short position records + * Trading date in RFC 3339 format */ - const struct lb_short_position_t *data; + const char *timestamp; /** - * Number of elements in the array. + * Short ratio */ - uintptr_t num_data; + const char *rate; /** - * Bitmask indicating the data sources included in the response + * Closing price */ - int32_t sources; -} lb_short_positions_response_t; + const char *close; + /** + * [US] NASDAQ short sale volume + */ + const char *nus_amount; + /** + * [US] NYSE short sale volume + */ + const char *ny_amount; + /** + * [US] Total short amount + */ + const char *total_amount; + /** + * [HK] Short sale turnover amount (HKD) + */ + const char *amount; + /** + * [HK] Short position balance + */ + const char *balance; +} lb_short_trades_item_t; + +/** + * Short trade records response (HK or US). + */ +typedef struct lb_short_trades_response_t { + /** + * Pointer to the array of short trade items + */ + const struct lb_short_trades_item_t *data; + /** + * Number of items in `data` + */ + uintptr_t num_data; +} lb_short_trades_response_t; /** * Option volume statistics (call and put totals) @@ -8258,6 +8314,355 @@ 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; + +/** + * One historical valuation data point. + */ +typedef struct lb_valuation_history_point_t { + /** + * Date in RFC 3339 format + */ + const char *date; + /** + * P/E ratio + */ + const char *pe; + /** + * P/B ratio + */ + const char *pb; + /** + * P/S ratio + */ + const char *ps; +} lb_valuation_history_point_t; + +/** + * One security's valuation comparison item. + */ +typedef struct lb_valuation_comparison_item_t { + /** + * Symbol, e.g. "AAPL.US" + */ + const char *symbol; + /** + * Security name + */ + const char *name; + /** + * Currency + */ + const char *currency; + /** + * Market capitalisation + */ + const char *market_value; + /** + * Latest closing price + */ + const char *price_close; + /** + * P/E ratio + */ + const char *pe; + /** + * P/B ratio + */ + const char *pb; + /** + * P/S ratio + */ + const char *ps; + /** + * Return on equity + */ + const char *roe; + /** + * Earnings per share + */ + const char *eps; + /** + * Book value per share + */ + const char *bps; + /** + * Dividends per share + */ + const char *dps; + /** + * Dividend yield + */ + const char *div_yld; + /** + * Total assets + */ + const char *assets; + /** + * Pointer to the array of historical valuation points + */ + const struct lb_valuation_history_point_t *history; + /** + * Number of items in `history` + */ + uintptr_t num_history; +} lb_valuation_comparison_item_t; + +/** + * Valuation comparison response. + */ +typedef struct lb_valuation_comparison_response_t { + /** + * Pointer to the array of valuation comparison items + */ + const struct lb_valuation_comparison_item_t *list; + /** + * Number of items in `list` + */ + uintptr_t num_list; +} lb_valuation_comparison_response_t; + +/** + * Stock information within a top-movers event. + */ +typedef struct lb_top_movers_stock_t { + /** + * Symbol, e.g. "TSLA.US" + */ + const char *symbol; + /** + * Ticker code + */ + const char *code; + /** + * Security name + */ + const char *name; + /** + * Full name + */ + const char *full_name; + /** + * Price change (decimal ratio) + */ + const char *change; + /** + * Latest price + */ + const char *last_done; + /** + * Market code + */ + const char *market; + /** + * Logo URL + */ + const char *logo; + /** + * Labels / tags + */ + const char *const *labels; + /** + * Number of items in `labels` + */ + uintptr_t num_labels; +} lb_top_movers_stock_t; + +/** + * One top-movers event entry. + */ +typedef struct lb_top_movers_event_t { + /** + * Event time (RFC 3339) + */ + const char *timestamp; + /** + * Alert reason description + */ + const char *alert_reason; + /** + * Alert type code + */ + int64_t alert_type; + /** + * Stock information + */ + struct lb_top_movers_stock_t stock; + /** + * Associated news post as a JSON string (may be null) + */ + const char *post; +} lb_top_movers_event_t; + +/** + * Top movers response. + */ +typedef struct lb_top_movers_response_t { + /** + * Pointer to the array of top-mover events + */ + const struct lb_top_movers_event_t *events; + /** + * Number of items in `events` + */ + uintptr_t num_events; + /** + * Pagination cursor as a JSON string + */ + const char *next_params; +} lb_top_movers_response_t; + +/** + * 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; + +/** + * One ranked security item. + */ +typedef struct lb_rank_list_item_t { + /** + * Symbol, e.g. "MU.US" + */ + const char *symbol; + /** + * Ticker code + */ + const char *code; + /** + * Security name + */ + const char *name; + /** + * Latest price + */ + const char *last_done; + /** + * Price change ratio (decimal) + */ + const char *chg; + /** + * Absolute price change + */ + const char *change; + /** + * Net inflow + */ + const char *inflow; + /** + * Market cap + */ + const char *market_cap; + /** + * Industry name + */ + const char *industry; + /** + * Pre/post market price + */ + const char *pre_post_price; + /** + * Pre/post market change + */ + const char *pre_post_chg; + /** + * Amplitude + */ + const char *amplitude; + /** + * 5-day change + */ + const char *five_day_chg; + /** + * Turnover rate + */ + const char *turnover_rate; + /** + * Volume ratio + */ + const char *volume_rate; + /** + * P/B ratio (TTM) + */ + const char *pb_ttm; +} lb_rank_list_item_t; + +/** + * Rank list response. + */ +typedef struct lb_rank_list_response_t { + /** + * Whether the response is delayed / BMP data + */ + bool bmp; + /** + * Pointer to the array of ranked security items + */ + const struct lb_rank_list_item_t *lists; + /** + * Number of items in `lists` + */ + uintptr_t num_lists; +} lb_rank_list_response_t; + +/** + * Recommended screener strategies response. `data` is a JSON string. + */ +typedef struct lb_screener_recommend_strategies_response_t { + const char *data; +} lb_screener_recommend_strategies_response_t; + +/** + * User screener strategies response. `data` is a JSON string. + */ +typedef struct lb_screener_user_strategies_response_t { + const char *data; +} lb_screener_user_strategies_response_t; + +/** + * Single screener strategy response. `data` is a JSON string. + */ +typedef struct lb_screener_strategy_response_t { + const char *data; +} lb_screener_strategy_response_t; + +/** + * Screener search results response. `data` is a JSON string. + */ +typedef struct lb_screener_search_response_t { + const char *data; +} lb_screener_search_response_t; + +/** + * Screener indicator definitions response. `data` is a JSON string. + */ +typedef struct lb_screener_indicators_response_t { + const char *data; +} lb_screener_indicators_response_t; + #ifdef __cplusplus extern "C" { #endif // __cplusplus @@ -8895,7 +9300,26 @@ void lb_fundamental_context_ratings(const struct lb_fundamental_context_t *ctx, void *userdata); /** - * Get business segment breakdowns. Returns `CBusinessSegments`. + * 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 current business segment breakdown for a security. + * Returns `CBusinessSegments`. */ void lb_fundamental_context_business_segments(const struct lb_fundamental_context_t *ctx, const char *symbol, @@ -8903,10 +9327,9 @@ void lb_fundamental_context_business_segments(const struct lb_fundamental_contex void *userdata); /** - * Get historical business segment breakdowns. Returns - * `CBusinessSegmentsHistory`. - * - * Pass `NULL` for `report` or `cate` to omit those parameters. + * Get historical business segment breakdowns for a security. + * Returns `CBusinessSegmentsHistory`. + * Pass NULL for `report` and/or `cate` to omit those filters. */ void lb_fundamental_context_business_segments_history(const struct lb_fundamental_context_t *ctx, const char *symbol, @@ -8916,8 +9339,8 @@ void lb_fundamental_context_business_segments_history(const struct lb_fundamenta void *userdata); /** - * Get historical institutional rating views. Returns - * `CInstitutionRatingViews`. + * Get historical institutional rating views for a security. + * Returns `CInstitutionRatingViews`. */ void lb_fundamental_context_institution_rating_views(const struct lb_fundamental_context_t *ctx, const char *symbol, @@ -8925,7 +9348,8 @@ void lb_fundamental_context_institution_rating_views(const struct lb_fundamental void *userdata); /** - * Get industry rank for a market. Returns `CIndustryRankResponse`. + * Get industry rank for a market. + * Returns `CIndustryRankResponse`. */ void lb_fundamental_context_industry_rank(const struct lb_fundamental_context_t *ctx, const char *market, @@ -8936,9 +9360,9 @@ void lb_fundamental_context_industry_rank(const struct lb_fundamental_context_t void *userdata); /** - * Get industry peer chain. Returns `CIndustryPeersResponse`. - * - * Pass `NULL` for `industry_id` to omit it. + * Get the industry peer chain for a security or industry. + * Returns `CIndustryPeersResponse`. + * Pass NULL for `industry_id` to omit it. */ void lb_fundamental_context_industry_peers(const struct lb_fundamental_context_t *ctx, const char *counter_id, @@ -8948,19 +9372,32 @@ void lb_fundamental_context_industry_peers(const struct lb_fundamental_context_t void *userdata); /** - * Get financial report snapshot. Returns `CFinancialReportSnapshot`. - * - * Pass `NULL` for optional parameters to omit them. - * `fiscal_year` is ignored when 0. + * Get a financial report snapshot for a security. + * Returns `CFinancialReportSnapshot`. + * Pass NULL for `report`, `fiscal_year_str`, and/or `fiscal_period` to omit + * them. `fiscal_year_str` should be a decimal integer string (e.g. `"2024"`). */ void lb_fundamental_context_financial_report_snapshot(const struct lb_fundamental_context_t *ctx, const char *symbol, const char *report, - int32_t fiscal_year, + const char *fiscal_year_str, const char *fiscal_period, lb_async_callback_t callback, void *userdata); +/** + * Get valuation comparison between a security and optional peers. + * Returns `CValuationComparisonResponse`. + * 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 * @@ -9123,6 +9560,38 @@ void lb_market_context_constituent(const struct lb_market_context_t *ctx, lb_async_callback_t callback, void *userdata); +/** + * Get top movers (stocks with unusual price movements) across one or more + * markets. Pass markets as a NULL-terminated array of C strings. + * Returns `CTopMoversResponse`. + */ +void lb_market_context_top_movers(const struct lb_market_context_t *ctx, + const char *const *markets, + uintptr_t num_markets, + uint32_t sort, + const char *date, + uint32_t limit, + lb_async_callback_t callback, + void *userdata); + +/** + * Get all available rank category keys and labels. + * Returns `CRankCategoriesResponse`. + */ +void lb_market_context_rank_categories(const struct lb_market_context_t *ctx, + lb_async_callback_t callback, + void *userdata); + +/** + * Get a ranked list of securities for the given category key. + * Returns `CRankListResponse`. + */ +void lb_market_context_rank_list(const struct lb_market_context_t *ctx, + const char *key, + bool need_article, + lb_async_callback_t callback, + void *userdata); + /** * Asynchronously build an OAuth 2.0 client. * @@ -9676,14 +10145,25 @@ void lb_quote_context_history_market_temperature(const struct lb_quote_context_t void *userdata); /** - * Get short interest data for a US security. Returns - * `CShortPositionsResponse`. + * Get short interest data for a US or HK security. Returns + * `CShortPositionsResponse`. Market is inferred from symbol suffix. */ void lb_quote_context_short_positions(const struct lb_quote_context_t *ctx, const char *symbol, + uint32_t count, lb_async_callback_t callback, void *userdata); +/** + * Get short trade records for a HK or US security. Returns + * `CShortTradesResponse`. Market is inferred from symbol suffix. + */ +void lb_quote_context_short_trades(const struct lb_quote_context_t *ctx, + const char *symbol, + uint32_t count, + lb_async_callback_t callback, + void *userdata); + /** * Get real-time option call/put volume. Returns `COptionVolumeStats`. */ @@ -9702,6 +10182,58 @@ void lb_quote_context_option_volume_daily(const struct lb_quote_context_t *ctx, lb_async_callback_t callback, void *userdata); +const struct lb_screener_context_t *lb_screener_context_new(const struct lb_config_t *config); + +void lb_screener_context_retain(const struct lb_screener_context_t *ctx); + +void lb_screener_context_release(const struct lb_screener_context_t *ctx); + +/** + * Get recommended built-in screener strategies. + * Returns `CScreenerRecommendStrategiesResponse`. + */ +void lb_screener_context_recommend_strategies(const struct lb_screener_context_t *ctx, + lb_async_callback_t callback, + void *userdata); + +/** + * Get the current user's saved screener strategies. + * Returns `CScreenerUserStrategiesResponse`. + */ +void lb_screener_context_user_strategies(const struct lb_screener_context_t *ctx, + lb_async_callback_t callback, + void *userdata); + +/** + * Get detail for one screener strategy by ID. + * Returns `CScreenerStrategyResponse`. + */ +void lb_screener_context_strategy(const struct lb_screener_context_t *ctx, + int64_t id, + lb_async_callback_t callback, + void *userdata); + +/** + * Search / screen securities using a strategy. + * Returns `CScreenerSearchResponse`. + */ +void lb_screener_context_search(const struct lb_screener_context_t *ctx, + const char *market, + int64_t strategy_id, + bool has_strategy_id, + uint32_t page, + uint32_t size, + lb_async_callback_t callback, + void *userdata); + +/** + * Get all available screener indicator definitions. + * Returns `CScreenerIndicatorsResponse`. + */ +void lb_screener_context_indicators(const struct lb_screener_context_t *ctx, + lb_async_callback_t callback, + void *userdata); + const struct lb_sharelist_context_t *lb_sharelist_context_new(const struct lb_config_t *config); void lb_sharelist_context_retain(const struct lb_sharelist_context_t *ctx); diff --git a/c/src/fundamental_context/context.rs b/c/src/fundamental_context/context.rs index 65c69f90b..9d05866ce 100644 --- a/c/src/fundamental_context/context.rs +++ b/c/src/fundamental_context/context.rs @@ -9,6 +9,29 @@ use crate::{ types::{CCow, cstr_to_rust}, }; +// Helper: convert a nullable C string to an Option<&'static str> by matching +// known enum-like values (e.g. report period codes). +#[inline] +unsafe fn cstr_to_static_opt(ptr: *const c_char) -> Option<&'static str> { + if ptr.is_null() { + return None; + } + let s = cstr_to_rust(ptr); + // Match against all known period/report values used across APIs. + match s.as_str() { + "qf" => Some("qf"), + "saf" => Some("saf"), + "af" => Some("af"), + "q1" => Some("q1"), + "q2" => Some("q2"), + "q3" => Some("q3"), + "annual" => Some("annual"), + "semi_annual" => Some("semi_annual"), + "quarterly" => Some("quarterly"), + _ => None, + } +} + pub(crate) struct CFundamentalContext { ctx: FundamentalContext, } @@ -406,7 +429,47 @@ pub unsafe extern "C" fn lb_fundamental_context_ratings( }); } -/// Get business segment breakdowns. Returns `CBusinessSegments`. +/// Get ranked list of top shareholders. Returns `CShareholderTopResponse`. +#[unsafe(no_mangle)] +pub unsafe extern "C" fn lb_fundamental_context_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 current business segment breakdown for a security. +/// Returns `CBusinessSegments`. #[unsafe(no_mangle)] pub unsafe extern "C" fn lb_fundamental_context_business_segments( ctx: *const CFundamentalContext, @@ -424,10 +487,9 @@ pub unsafe extern "C" fn lb_fundamental_context_business_segments( }); } -/// Get historical business segment breakdowns. Returns -/// `CBusinessSegmentsHistory`. -/// -/// Pass `NULL` for `report` or `cate` to omit those parameters. +/// Get historical business segment breakdowns for a security. +/// Returns `CBusinessSegmentsHistory`. +/// Pass NULL for `report` and/or `cate` to omit those filters. #[unsafe(no_mangle)] pub unsafe extern "C" fn lb_fundamental_context_business_segments_history( ctx: *const CFundamentalContext, @@ -439,35 +501,25 @@ pub unsafe extern "C" fn lb_fundamental_context_business_segments_history( ) { let ctx_inner = (*ctx).ctx.clone(); let symbol = cstr_to_rust(symbol); - let report_str = if report.is_null() { - None - } else { - Some(cstr_to_rust(report)) - }; - let cate_opt = if cate.is_null() { + let report: Option<&'static str> = cstr_to_static_opt(report); + let cate: Option = if cate.is_null() { None } else { Some(cstr_to_rust(cate)) }; - let report_static: Option<&'static str> = match report_str.as_deref() { - Some("qf") => Some("qf"), - Some("saf") => Some("saf"), - Some("af") => Some("af"), - _ => None, - }; execute_async(callback, ctx, userdata, async move { let resp: CCow = CCow::new(CBusinessSegmentsHistoryOwned::from( ctx_inner - .business_segments_history(symbol, report_static, cate_opt) + .business_segments_history(symbol, report, cate) .await?, )); Ok(resp) }); } -/// Get historical institutional rating views. Returns -/// `CInstitutionRatingViews`. +/// Get historical institutional rating views for a security. +/// Returns `CInstitutionRatingViews`. #[unsafe(no_mangle)] pub unsafe extern "C" fn lb_fundamental_context_institution_rating_views( ctx: *const CFundamentalContext, @@ -485,7 +537,8 @@ pub unsafe extern "C" fn lb_fundamental_context_institution_rating_views( }); } -/// Get industry rank for a market. Returns `CIndustryRankResponse`. +/// Get industry rank for a market. +/// Returns `CIndustryRankResponse`. #[unsafe(no_mangle)] pub unsafe extern "C" fn lb_fundamental_context_industry_rank( ctx: *const CFundamentalContext, @@ -510,9 +563,9 @@ pub unsafe extern "C" fn lb_fundamental_context_industry_rank( }); } -/// Get industry peer chain. Returns `CIndustryPeersResponse`. -/// -/// Pass `NULL` for `industry_id` to omit it. +/// Get the industry peer chain for a security or industry. +/// Returns `CIndustryPeersResponse`. +/// Pass NULL for `industry_id` to omit it. #[unsafe(no_mangle)] pub unsafe extern "C" fn lb_fundamental_context_industry_peers( ctx: *const CFundamentalContext, @@ -525,7 +578,7 @@ pub unsafe extern "C" fn lb_fundamental_context_industry_peers( let ctx_inner = (*ctx).ctx.clone(); let counter_id = cstr_to_rust(counter_id); let market = cstr_to_rust(market); - let industry_id_opt = if industry_id.is_null() { + let industry_id: Option = if industry_id.is_null() { None } else { Some(cstr_to_rust(industry_id)) @@ -533,70 +586,76 @@ pub unsafe extern "C" fn lb_fundamental_context_industry_peers( execute_async(callback, ctx, userdata, async move { let resp: CCow = CCow::new(CIndustryPeersResponseOwned::from( ctx_inner - .industry_peers(counter_id, market, industry_id_opt) + .industry_peers(counter_id, market, industry_id) .await?, )); Ok(resp) }); } -/// Get financial report snapshot. Returns `CFinancialReportSnapshot`. -/// -/// Pass `NULL` for optional parameters to omit them. -/// `fiscal_year` is ignored when 0. +/// Get a financial report snapshot for a security. +/// Returns `CFinancialReportSnapshot`. +/// Pass NULL for `report`, `fiscal_year_str`, and/or `fiscal_period` to omit +/// them. `fiscal_year_str` should be a decimal integer string (e.g. `"2024"`). #[unsafe(no_mangle)] pub unsafe extern "C" fn lb_fundamental_context_financial_report_snapshot( ctx: *const CFundamentalContext, symbol: *const c_char, report: *const c_char, - fiscal_year: i32, + fiscal_year_str: *const c_char, fiscal_period: *const c_char, callback: CAsyncCallback, userdata: *mut c_void, ) { let ctx_inner = (*ctx).ctx.clone(); let symbol = cstr_to_rust(symbol); - let report_str = if report.is_null() { - None - } else { - Some(cstr_to_rust(report)) - }; - let fiscal_year_opt = if fiscal_year == 0 { + let report: Option<&'static str> = cstr_to_static_opt(report); + let fiscal_year: Option = if fiscal_year_str.is_null() { None } else { - Some(fiscal_year) + cstr_to_rust(fiscal_year_str).parse::().ok() }; - let fiscal_period_str = if fiscal_period.is_null() { + let fiscal_period: Option<&'static str> = cstr_to_static_opt(fiscal_period); + execute_async(callback, ctx, userdata, async move { + let resp: CCow = + CCow::new(CFinancialReportSnapshotOwned::from( + ctx_inner + .financial_report_snapshot(symbol, report, fiscal_year, fiscal_period) + .await?, + )); + Ok(resp) + }); +} + +/// Get valuation comparison between a security and optional peers. +/// Returns `CValuationComparisonResponse`. +/// Pass NULL for `comparison_symbols` to skip peer comparison. +#[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 { - Some(cstr_to_rust(fiscal_period)) - }; - let report_static: Option<&'static str> = match report_str.as_deref() { - Some("qf") => Some("qf"), - Some("saf") => Some("saf"), - Some("af") => Some("af"), - _ => None, - }; - let fiscal_period_static: Option<&'static str> = match fiscal_period_str.as_deref() { - Some("q1") => Some("q1"), - Some("q2") => Some("q2"), - Some("q3") => Some("q3"), - Some("q4") => Some("q4"), - Some("fy") => Some("fy"), - Some("h1") => Some("h1"), - Some("h2") => Some("h2"), - _ => None, + let syms: Vec = (0..num_comparison_symbols) + .map(|i| cstr_to_rust(*comparison_symbols.add(i))) + .collect(); + Some(syms) }; execute_async(callback, ctx, userdata, async move { - let resp: CCow = - CCow::new(CFinancialReportSnapshotOwned::from( + let resp: CCow = + CCow::new(CValuationComparisonResponseOwned::from( ctx_inner - .financial_report_snapshot( - symbol, - report_static, - fiscal_year_opt, - fiscal_period_static, - ) + .valuation_comparison(symbol, currency, comparison) .await?, )); Ok(resp) diff --git a/c/src/fundamental_context/types.rs b/c/src/fundamental_context/types.rs index 165e7c21a..ef9198d0b 100644 --- a/c/src/fundamental_context/types.rs +++ b/c/src/fundamental_context/types.rs @@ -2843,7 +2843,242 @@ impl ToFFI for CStockRatingsOwned { } } -// ── business_segments ───────────────────────────────────────────── +// ── 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 ─────────────────────────────────── + +/// One historical valuation data point. +#[repr(C)] +pub struct CValuationHistoryPoint { + /// Date in RFC 3339 format + pub date: *const c_char, + /// P/E ratio + pub pe: *const c_char, + /// P/B ratio + pub pb: *const c_char, + /// P/S ratio + pub ps: *const c_char, +} + +pub(crate) struct CValuationHistoryPointOwned { + date: CString, + pe: CString, + pb: CString, + ps: CString, +} + +impl From for CValuationHistoryPointOwned { + fn from(v: ValuationHistoryPoint) -> Self { + Self { + date: v.date.into(), + pe: v.pe.into(), + pb: v.pb.into(), + ps: v.ps.into(), + } + } +} + +impl ToFFI for CValuationHistoryPointOwned { + type FFIType = CValuationHistoryPoint; + fn to_ffi_type(&self) -> Self::FFIType { + CValuationHistoryPoint { + date: self.date.to_ffi_type(), + pe: self.pe.to_ffi_type(), + pb: self.pb.to_ffi_type(), + ps: self.ps.to_ffi_type(), + } + } +} + +/// One security's valuation comparison item. +#[repr(C)] +pub struct CValuationComparisonItem { + /// Symbol, e.g. "AAPL.US" + pub symbol: *const c_char, + /// Security name + pub name: *const c_char, + /// Currency + pub currency: *const c_char, + /// Market capitalisation + pub market_value: *const c_char, + /// Latest closing price + pub price_close: *const c_char, + /// P/E ratio + pub pe: *const c_char, + /// P/B ratio + pub pb: *const c_char, + /// P/S ratio + pub ps: *const c_char, + /// Return on equity + pub roe: *const c_char, + /// Earnings per share + pub eps: *const c_char, + /// Book value per share + pub bps: *const c_char, + /// Dividends per share + pub dps: *const c_char, + /// Dividend yield + pub div_yld: *const c_char, + /// Total assets + pub assets: *const c_char, + /// Pointer to the array of historical valuation points + pub history: *const CValuationHistoryPoint, + /// Number of items in `history` + pub num_history: usize, +} + +pub(crate) struct CValuationComparisonItemOwned { + symbol: CString, + name: CString, + currency: CString, + market_value: CString, + price_close: CString, + pe: CString, + pb: CString, + ps: CString, + roe: CString, + eps: CString, + bps: CString, + dps: CString, + div_yld: CString, + assets: CString, + history: CVec, +} + +impl From for CValuationComparisonItemOwned { + fn from(v: ValuationComparisonItem) -> Self { + Self { + symbol: v.symbol.into(), + name: v.name.into(), + currency: v.currency.into(), + market_value: v.market_value.into(), + price_close: v.price_close.into(), + pe: v.pe.into(), + pb: v.pb.into(), + ps: v.ps.into(), + roe: v.roe.into(), + eps: v.eps.into(), + bps: v.bps.into(), + dps: v.dps.into(), + div_yld: v.div_yld.into(), + assets: v.assets.into(), + history: v.history.into(), + } + } +} + +impl ToFFI for CValuationComparisonItemOwned { + type FFIType = CValuationComparisonItem; + fn to_ffi_type(&self) -> Self::FFIType { + CValuationComparisonItem { + symbol: self.symbol.to_ffi_type(), + name: self.name.to_ffi_type(), + currency: self.currency.to_ffi_type(), + market_value: self.market_value.to_ffi_type(), + price_close: self.price_close.to_ffi_type(), + pe: self.pe.to_ffi_type(), + pb: self.pb.to_ffi_type(), + ps: self.ps.to_ffi_type(), + roe: self.roe.to_ffi_type(), + eps: self.eps.to_ffi_type(), + bps: self.bps.to_ffi_type(), + dps: self.dps.to_ffi_type(), + div_yld: self.div_yld.to_ffi_type(), + assets: self.assets.to_ffi_type(), + history: self.history.to_ffi_type(), + num_history: self.history.len(), + } + } +} + +/// Valuation comparison response. +#[repr(C)] +pub struct CValuationComparisonResponse { + /// Pointer to the array of valuation comparison items + pub list: *const CValuationComparisonItem, + /// Number of items in `list` + pub num_list: usize, +} + +pub(crate) struct CValuationComparisonResponseOwned { + list: CVec, +} + +impl From for CValuationComparisonResponseOwned { + fn from(v: ValuationComparisonResponse) -> Self { + Self { + list: v.list.into(), + } + } +} + +impl ToFFI for CValuationComparisonResponseOwned { + type FFIType = CValuationComparisonResponse; + fn to_ffi_type(&self) -> Self::FFIType { + CValuationComparisonResponse { + list: self.list.to_ffi_type(), + num_list: self.list.len(), + } + } +} + +// ── BusinessSegments ────────────────────────────────────────────── /// One business segment item (latest snapshot). #[repr(C)] @@ -2878,7 +3113,7 @@ impl ToFFI for CBusinessSegmentItemOwned { } } -/// Business segments response. +/// Current business segment breakdown for a security. #[repr(C)] pub struct CBusinessSegments { /// Report date. @@ -2887,7 +3122,7 @@ pub struct CBusinessSegments { pub total: *const c_char, /// Reporting currency. pub currency: *const c_char, - /// Pointer to the array of business segment items. + /// Pointer to business segment items. pub business: *const CBusinessSegmentItem, /// Number of items in `business`. pub num_business: usize, @@ -2924,6 +3159,8 @@ impl ToFFI for CBusinessSegmentsOwned { } } +// ── BusinessSegmentsHistory ─────────────────────────────────────── + /// One business/regional segment item in a historical snapshot. #[repr(C)] pub struct CBusinessSegmentHistoryItem { @@ -2973,11 +3210,11 @@ pub struct CBusinessSegmentsHistoricalItem { pub total: *const c_char, /// Reporting currency. pub currency: *const c_char, - /// Pointer to the business segment items. + /// Pointer to business segment breakdown items. pub business: *const CBusinessSegmentHistoryItem, /// Number of items in `business`. pub num_business: usize, - /// Pointer to the regional segment items. + /// Pointer to regional breakdown items. pub regionals: *const CBusinessSegmentHistoryItem, /// Number of items in `regionals`. pub num_regionals: usize, @@ -3020,10 +3257,10 @@ impl ToFFI for CBusinessSegmentsHistoricalItemOwned { } } -/// Business segments history response. +/// Historical business segment breakdowns for a security. #[repr(C)] pub struct CBusinessSegmentsHistory { - /// Pointer to the historical snapshots. + /// Pointer to historical snapshot items. pub historical: *const CBusinessSegmentsHistoricalItem, /// Number of items in `historical`. pub num_historical: usize, @@ -3051,22 +3288,22 @@ impl ToFFI for CBusinessSegmentsHistoryOwned { } } -// ── institution_rating_views ────────────────────────────────────── +// ── InstitutionRatingViews ──────────────────────────────────────── -/// One historical rating distribution snapshot. +/// One historical institutional rating distribution snapshot. #[repr(C)] pub struct CInstitutionRatingViewItem { - /// Date as unix timestamp string. + /// Date (unix timestamp string). pub date: *const c_char, - /// Number of Buy ratings. + /// Number of "Buy" ratings. pub buy: *const c_char, - /// Number of Outperform ratings. + /// Number of "Outperform" ratings. pub over: *const c_char, - /// Number of Hold ratings. + /// Number of "Hold" ratings. pub hold: *const c_char, - /// Number of Underperform ratings. + /// Number of "Underperform" ratings. pub under: *const c_char, - /// Number of Sell ratings. + /// Number of "Sell" ratings. pub sell: *const c_char, /// Total analyst count. pub total: *const c_char, @@ -3111,10 +3348,10 @@ impl ToFFI for CInstitutionRatingViewItemOwned { } } -/// Institution rating views response. +/// Historical institutional rating views time-series for a security. #[repr(C)] pub struct CInstitutionRatingViews { - /// Pointer to the rating view items. + /// Pointer to rating view items. pub elist: *const CInstitutionRatingViewItem, /// Number of items in `elist`. pub num_elist: usize, @@ -3142,7 +3379,7 @@ impl ToFFI for CInstitutionRatingViewsOwned { } } -// ── industry_rank ───────────────────────────────────────────────── +// ── IndustryRank ────────────────────────────────────────────────── /// One ranked industry item. #[repr(C)] @@ -3210,7 +3447,7 @@ impl ToFFI for CIndustryRankItemOwned { /// A group of ranked industry items. #[repr(C)] pub struct CIndustryRankGroup { - /// Pointer to the items in this group. + /// Pointer to ranked items. pub lists: *const CIndustryRankItem, /// Number of items in `lists`. pub num_lists: usize, @@ -3241,9 +3478,9 @@ impl ToFFI for CIndustryRankGroupOwned { /// Industry rank response. #[repr(C)] pub struct CIndustryRankResponse { - /// Pointer to the grouped rank items. + /// Pointer to grouped rank items. pub items: *const CIndustryRankGroup, - /// Number of items in `items`. + /// Number of groups in `items`. pub num_items: usize, } @@ -3269,7 +3506,7 @@ impl ToFFI for CIndustryRankResponseOwned { } } -// ── industry_peers ──────────────────────────────────────────────── +// ── IndustryPeers ───────────────────────────────────────────────── /// Top-level industry info in the peers response. #[repr(C)] @@ -3304,9 +3541,7 @@ impl ToFFI for CIndustryPeersTopOwned { } } -/// A node in the recursive industry peer chain. -/// -/// `next_json` contains the child nodes serialised as a JSON string. +/// A node in the industry peer chain (recursive children serialised as JSON). #[repr(C)] pub struct CIndustryPeerNode { /// Node name. @@ -3319,7 +3554,7 @@ pub struct CIndustryPeerNode { pub chg: *const c_char, /// Year-to-date change. pub ytd_chg: *const c_char, - /// Child nodes as a JSON string. + /// Child nodes serialised as a JSON string (may be NULL if empty). pub next_json: *const c_char, } @@ -3334,13 +3569,18 @@ pub(crate) struct CIndustryPeerNodeOwned { impl From for CIndustryPeerNodeOwned { fn from(v: longbridge::fundamental::IndustryPeerNode) -> Self { + let next_json = if v.next.is_empty() { + String::new() + } else { + serde_json::to_string(&v.next).unwrap_or_default() + }; Self { name: v.name.into(), counter_id: v.counter_id.into(), stock_num: v.stock_num, chg: v.chg.into(), ytd_chg: v.ytd_chg.into(), - next_json: serde_json::to_string(&v.next).unwrap_or_default().into(), + next_json: next_json.into(), } } } @@ -3359,12 +3599,12 @@ impl ToFFI for CIndustryPeerNodeOwned { } } -/// Industry peers response. +/// Industry peer chain response. #[repr(C)] pub struct CIndustryPeersResponse { /// Top-level industry node info. pub top: CIndustryPeersTop, - /// Root peer chain node, or null if absent. + /// Root peer chain node (NULL if absent). pub chain: *const CIndustryPeerNode, } @@ -3392,7 +3632,7 @@ impl ToFFI for CIndustryPeersResponseOwned { } } -// ── financial_report_snapshot ───────────────────────────────────── +// ── FinancialReportSnapshot ─────────────────────────────────────── /// A forecast metric in the financial report snapshot. #[repr(C)] @@ -3470,7 +3710,7 @@ impl ToFFI for CSnapshotReportedMetricOwned { } } -/// Financial report snapshot response. +/// Financial report snapshot (earnings snapshot) for a security. #[repr(C)] pub struct CFinancialReportSnapshot { /// Company name. @@ -3485,25 +3725,25 @@ pub struct CFinancialReportSnapshot { pub currency: *const c_char, /// Report description. pub report_desc: *const c_char, - /// Forecast revenue, or null. + /// Forecast revenue (NULL if absent). pub fo_revenue: *const CSnapshotForecastMetric, - /// Forecast EBIT, or null. + /// Forecast EBIT (NULL if absent). pub fo_ebit: *const CSnapshotForecastMetric, - /// Forecast EPS, or null. + /// Forecast EPS (NULL if absent). pub fo_eps: *const CSnapshotForecastMetric, - /// Reported revenue, or null. + /// Reported revenue (NULL if absent). pub fr_revenue: *const CSnapshotReportedMetric, - /// Reported net profit, or null. + /// Reported net profit (NULL if absent). pub fr_profit: *const CSnapshotReportedMetric, - /// Reported operating cash flow, or null. + /// Reported operating cash flow (NULL if absent). pub fr_operate_cash: *const CSnapshotReportedMetric, - /// Reported investing cash flow, or null. + /// Reported investing cash flow (NULL if absent). pub fr_invest_cash: *const CSnapshotReportedMetric, - /// Reported financing cash flow, or null. + /// Reported financing cash flow (NULL if absent). pub fr_finance_cash: *const CSnapshotReportedMetric, - /// Reported total assets, or null. + /// Reported total assets (NULL if absent). pub fr_total_assets: *const CSnapshotReportedMetric, - /// Reported total liabilities, or null. + /// Reported total liabilities (NULL if absent). pub fr_total_liability: *const CSnapshotReportedMetric, /// ROE TTM. pub fr_roe_ttm: *const c_char, 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..fc223ff88 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 top movers (stocks with unusual price movements) across one or more +/// markets. Pass markets as a NULL-terminated array of C strings. +/// Returns `CTopMoversResponse`. +#[unsafe(no_mangle)] +pub unsafe extern "C" fn lb_market_context_top_movers( + 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(CTopMoversResponseOwned::from( + ctx_inner.top_movers(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..db010ff8c 100644 --- a/c/src/market_context/types.rs +++ b/c/src/market_context/types.rs @@ -4,8 +4,9 @@ use longbridge::market::{ AhPremiumIntraday, AhPremiumKline, AhPremiumKlines, AnomalyItem, AnomalyResponse, BrokerHoldingChanges, BrokerHoldingDailyHistory, BrokerHoldingDailyItem, BrokerHoldingDetail, BrokerHoldingDetailItem, BrokerHoldingEntry, BrokerHoldingTop, ConstituentStock, - IndexConstituents, MarketStatusResponse, MarketTimeItem, TradePriceLevel, TradeStatistics, - TradeStatsResponse, + IndexConstituents, MarketStatusResponse, MarketTimeItem, RankCategoriesResponse, RankListItem, + RankListResponse, TopMoversEvent, TopMoversResponse, TopMoversStock, TradePriceLevel, + TradeStatistics, TradeStatsResponse, }; use crate::types::{CMarket, CString, CVec, ToFFI}; @@ -957,3 +958,330 @@ impl ToFFI for CIndexConstituentsOwned { } } } + +// ── TopMoversResponse ───────────────────────────────────────────── + +/// Stock information within a top-movers event. +#[repr(C)] +pub struct CTopMoversStock { + /// Symbol, e.g. "TSLA.US" + pub symbol: *const c_char, + /// Ticker code + pub code: *const c_char, + /// Security name + pub name: *const c_char, + /// Full name + pub full_name: *const c_char, + /// Price change (decimal ratio) + pub change: *const c_char, + /// Latest price + pub last_done: *const c_char, + /// Market code + pub market: *const c_char, + /// Logo URL + pub logo: *const c_char, + /// Labels / tags + pub labels: *const *const c_char, + /// Number of items in `labels` + pub num_labels: usize, +} + +pub(crate) struct CTopMoversStockOwned { + symbol: CString, + code: CString, + name: CString, + full_name: CString, + change: CString, + last_done: CString, + market: CString, + logo: CString, + labels: CVec, +} + +impl From for CTopMoversStockOwned { + fn from(v: TopMoversStock) -> Self { + Self { + symbol: v.symbol.into(), + code: v.code.into(), + name: v.name.into(), + full_name: v.full_name.into(), + change: v.change.into(), + last_done: v.last_done.into(), + market: v.market.into(), + logo: v.logo.into(), + labels: v.labels.into(), + } + } +} + +impl ToFFI for CTopMoversStockOwned { + type FFIType = CTopMoversStock; + fn to_ffi_type(&self) -> Self::FFIType { + CTopMoversStock { + symbol: self.symbol.to_ffi_type(), + code: self.code.to_ffi_type(), + name: self.name.to_ffi_type(), + full_name: self.full_name.to_ffi_type(), + change: self.change.to_ffi_type(), + last_done: self.last_done.to_ffi_type(), + market: self.market.to_ffi_type(), + logo: self.logo.to_ffi_type(), + labels: self.labels.to_ffi_type(), + num_labels: self.labels.len(), + } + } +} + +/// One top-movers event entry. +#[repr(C)] +pub struct CTopMoversEvent { + /// Event time (RFC 3339) + pub timestamp: *const c_char, + /// Alert reason description + pub alert_reason: *const c_char, + /// Alert type code + pub alert_type: i64, + /// Stock information + pub stock: CTopMoversStock, + /// Associated news post as a JSON string (may be null) + pub post: *const c_char, +} + +pub(crate) struct CTopMoversEventOwned { + timestamp: CString, + alert_reason: CString, + alert_type: i64, + stock: CTopMoversStockOwned, + post: CString, +} + +impl From for CTopMoversEventOwned { + fn from(v: TopMoversEvent) -> Self { + Self { + timestamp: v.timestamp.into(), + alert_reason: v.alert_reason.into(), + alert_type: v.alert_type, + stock: v.stock.into(), + post: v.post.to_string().into(), + } + } +} + +impl ToFFI for CTopMoversEventOwned { + type FFIType = CTopMoversEvent; + fn to_ffi_type(&self) -> Self::FFIType { + CTopMoversEvent { + timestamp: self.timestamp.to_ffi_type(), + alert_reason: self.alert_reason.to_ffi_type(), + alert_type: self.alert_type, + stock: self.stock.to_ffi_type(), + post: self.post.to_ffi_type(), + } + } +} + +/// Top movers response. +#[repr(C)] +pub struct CTopMoversResponse { + /// Pointer to the array of top-mover events + pub events: *const CTopMoversEvent, + /// Number of items in `events` + pub num_events: usize, + /// Pagination cursor as a JSON string + pub next_params: *const c_char, +} + +pub(crate) struct CTopMoversResponseOwned { + events: CVec, + next_params: CString, +} + +impl From for CTopMoversResponseOwned { + fn from(v: TopMoversResponse) -> Self { + Self { + events: v.events.into(), + next_params: v.next_params.to_string().into(), + } + } +} + +impl ToFFI for CTopMoversResponseOwned { + type FFIType = CTopMoversResponse; + fn to_ffi_type(&self) -> Self::FFIType { + CTopMoversResponse { + events: self.events.to_ffi_type(), + num_events: self.events.len(), + next_params: self.next_params.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 ────────────────────────────────────────────── + +/// One ranked security item. +#[repr(C)] +pub struct CRankListItem { + /// Symbol, e.g. "MU.US" + pub symbol: *const c_char, + /// Ticker code + pub code: *const c_char, + /// Security name + pub name: *const c_char, + /// Latest price + pub last_done: *const c_char, + /// Price change ratio (decimal) + pub chg: *const c_char, + /// Absolute price change + pub change: *const c_char, + /// Net inflow + pub inflow: *const c_char, + /// Market cap + pub market_cap: *const c_char, + /// Industry name + pub industry: *const c_char, + /// Pre/post market price + pub pre_post_price: *const c_char, + /// Pre/post market change + pub pre_post_chg: *const c_char, + /// Amplitude + pub amplitude: *const c_char, + /// 5-day change + pub five_day_chg: *const c_char, + /// Turnover rate + pub turnover_rate: *const c_char, + /// Volume ratio + pub volume_rate: *const c_char, + /// P/B ratio (TTM) + pub pb_ttm: *const c_char, +} + +pub(crate) struct CRankListItemOwned { + symbol: CString, + code: CString, + name: CString, + last_done: CString, + chg: CString, + change: CString, + inflow: CString, + market_cap: CString, + industry: CString, + pre_post_price: CString, + pre_post_chg: CString, + amplitude: CString, + five_day_chg: CString, + turnover_rate: CString, + volume_rate: CString, + pb_ttm: CString, +} + +impl From for CRankListItemOwned { + fn from(v: RankListItem) -> Self { + Self { + symbol: v.symbol.into(), + code: v.code.into(), + name: v.name.into(), + last_done: v.last_done.into(), + chg: v.chg.into(), + change: v.change.into(), + inflow: v.inflow.into(), + market_cap: v.market_cap.into(), + industry: v.industry.into(), + pre_post_price: v.pre_post_price.into(), + pre_post_chg: v.pre_post_chg.into(), + amplitude: v.amplitude.into(), + five_day_chg: v.five_day_chg.into(), + turnover_rate: v.turnover_rate.into(), + volume_rate: v.volume_rate.into(), + pb_ttm: v.pb_ttm.into(), + } + } +} + +impl ToFFI for CRankListItemOwned { + type FFIType = CRankListItem; + fn to_ffi_type(&self) -> Self::FFIType { + CRankListItem { + symbol: self.symbol.to_ffi_type(), + code: self.code.to_ffi_type(), + name: self.name.to_ffi_type(), + last_done: self.last_done.to_ffi_type(), + chg: self.chg.to_ffi_type(), + change: self.change.to_ffi_type(), + inflow: self.inflow.to_ffi_type(), + market_cap: self.market_cap.to_ffi_type(), + industry: self.industry.to_ffi_type(), + pre_post_price: self.pre_post_price.to_ffi_type(), + pre_post_chg: self.pre_post_chg.to_ffi_type(), + amplitude: self.amplitude.to_ffi_type(), + five_day_chg: self.five_day_chg.to_ffi_type(), + turnover_rate: self.turnover_rate.to_ffi_type(), + volume_rate: self.volume_rate.to_ffi_type(), + pb_ttm: self.pb_ttm.to_ffi_type(), + } + } +} + +/// Rank list response. +#[repr(C)] +pub struct CRankListResponse { + /// Whether the response is delayed / BMP data + pub bmp: bool, + /// Pointer to the array of ranked security items + pub lists: *const CRankListItem, + /// Number of items in `lists` + pub num_lists: usize, +} + +pub(crate) struct CRankListResponseOwned { + bmp: bool, + lists: CVec, +} + +impl From for CRankListResponseOwned { + fn from(v: RankListResponse) -> Self { + Self { + bmp: v.bmp, + lists: v.lists.into(), + } + } +} + +impl ToFFI for CRankListResponseOwned { + type FFIType = CRankListResponse; + fn to_ffi_type(&self) -> Self::FFIType { + CRankListResponse { + bmp: self.bmp, + lists: self.lists.to_ffi_type(), + num_lists: self.lists.len(), + } + } +} 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..72cdaf5c8 100644 --- a/c/src/quote_context/types.rs +++ b/c/src/quote_context/types.rs @@ -7,9 +7,10 @@ use longbridge::quote::{ OptionVolumeDaily, OptionVolumeDailyStat, OptionVolumeStats, ParticipantInfo, Period, PrePostQuote, PushBrokers, PushCandlestick, PushDepth, PushQuote, PushTrades, QuotePackageDetail, RealtimeQuote, Security, SecurityBoard, SecurityBrokers, SecurityCalcIndex, - SecurityDepth, SecurityQuote, SecurityStaticInfo, ShortPosition, ShortPositionsResponse, - StrikePriceInfo, Subscription, Trade, TradeDirection, TradeSession, TradeStatus, - TradingSessionInfo, WarrantInfo, WarrantQuote, WarrantType, WatchlistGroup, WatchlistSecurity, + SecurityDepth, SecurityQuote, SecurityStaticInfo, ShortPositionsItem, ShortPositionsResponse, + ShortTradesItem, ShortTradesResponse, StrikePriceInfo, Subscription, Trade, TradeDirection, + TradeSession, TradeStatus, TradingSessionInfo, WarrantInfo, WarrantQuote, WarrantType, + WatchlistGroup, WatchlistSecurity, }; use crate::{ @@ -3106,84 +3107,91 @@ impl ToFFI for CFilingItemOwned { // ── ShortPositionsResponse ──────────────────────────────────────── -/// Short position data for a single date +/// One short-position record, unified for US and HK markets. #[repr(C)] -pub struct CShortPosition { - /// Date of the short position record (formatted string) +pub struct CShortPositionsItem { + /// Trading date in RFC 3339 format pub timestamp: *const c_char, - /// Short interest as a percentage of shares outstanding + /// Short ratio pub rate: *const c_char, - /// Average daily share volume - pub avg_daily_share_volume: *const c_char, - /// Current number of shares sold short + /// Closing price + pub close: *const c_char, + /// [US] Number of short shares outstanding pub current_shares_short: *const c_char, - /// Days to cover (short interest ratio) + /// [US] Average daily share volume + pub avg_daily_share_volume: *const c_char, + /// [US] Days-to-cover ratio pub days_to_cover: *const c_char, - /// Closing price on the record date - pub close: *const c_char, + /// [HK] Short sale amount (HKD) + pub amount: *const c_char, + /// [HK] Short position balance + pub balance: *const c_char, + /// [HK] Closing price (HK naming) + pub cost: *const c_char, } -pub(crate) struct CShortPositionOwned { +pub(crate) struct CShortPositionsItemOwned { timestamp: CString, rate: CString, - avg_daily_share_volume: CString, + close: CString, current_shares_short: CString, + avg_daily_share_volume: CString, days_to_cover: CString, - close: CString, + amount: CString, + balance: CString, + cost: CString, } -impl From for CShortPositionOwned { - fn from(v: ShortPosition) -> Self { +impl From for CShortPositionsItemOwned { + fn from(v: ShortPositionsItem) -> Self { Self { timestamp: v.timestamp.into(), rate: v.rate.into(), - avg_daily_share_volume: v.avg_daily_share_volume.into(), + close: v.close.into(), current_shares_short: v.current_shares_short.into(), + avg_daily_share_volume: v.avg_daily_share_volume.into(), days_to_cover: v.days_to_cover.into(), - close: v.close.into(), + amount: v.amount.into(), + balance: v.balance.into(), + cost: v.cost.into(), } } } -impl ToFFI for CShortPositionOwned { - type FFIType = CShortPosition; +impl ToFFI for CShortPositionsItemOwned { + type FFIType = CShortPositionsItem; fn to_ffi_type(&self) -> Self::FFIType { - CShortPosition { + CShortPositionsItem { timestamp: self.timestamp.to_ffi_type(), rate: self.rate.to_ffi_type(), - avg_daily_share_volume: self.avg_daily_share_volume.to_ffi_type(), + close: self.close.to_ffi_type(), current_shares_short: self.current_shares_short.to_ffi_type(), + avg_daily_share_volume: self.avg_daily_share_volume.to_ffi_type(), days_to_cover: self.days_to_cover.to_ffi_type(), - close: self.close.to_ffi_type(), + amount: self.amount.to_ffi_type(), + balance: self.balance.to_ffi_type(), + cost: self.cost.to_ffi_type(), } } } -/// Short positions response for a security +/// Short positions / interest response (HK or US). #[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. + /// Pointer to the array of short position items + pub data: *const CShortPositionsItem, + /// Number of items in `data` pub num_data: usize, - /// Bitmask indicating the data sources included in the response - pub sources: i32, } pub(crate) struct CShortPositionsResponseOwned { - symbol: CString, - data: CVec, - sources: i32, + data: CVec, } impl From for CShortPositionsResponseOwned { fn from(v: ShortPositionsResponse) -> Self { Self { - symbol: v.symbol.into(), data: v.data.into(), - sources: v.sources, } } } @@ -3192,10 +3200,104 @@ impl ToFFI for CShortPositionsResponseOwned { type FFIType = CShortPositionsResponse; fn to_ffi_type(&self) -> Self::FFIType { CShortPositionsResponse { - symbol: self.symbol.to_ffi_type(), data: self.data.to_ffi_type(), num_data: self.data.len(), - sources: self.sources, + } + } +} + +// ── ShortTradesResponse ─────────────────────────────────────────── + +/// One short-trade record, unified for US and HK markets. +#[repr(C)] +pub struct CShortTradesItem { + /// Trading date in RFC 3339 format + pub timestamp: *const c_char, + /// Short ratio + pub rate: *const c_char, + /// Closing price + pub close: *const c_char, + /// [US] NASDAQ short sale volume + pub nus_amount: *const c_char, + /// [US] NYSE short sale volume + pub ny_amount: *const c_char, + /// [US] Total short amount + pub total_amount: *const c_char, + /// [HK] Short sale turnover amount (HKD) + pub amount: *const c_char, + /// [HK] Short position balance + pub balance: *const c_char, +} + +pub(crate) struct CShortTradesItemOwned { + timestamp: CString, + rate: CString, + close: CString, + nus_amount: CString, + ny_amount: CString, + total_amount: CString, + amount: CString, + balance: CString, +} + +impl From for CShortTradesItemOwned { + fn from(v: ShortTradesItem) -> Self { + Self { + timestamp: v.timestamp.into(), + rate: v.rate.into(), + close: v.close.into(), + nus_amount: v.nus_amount.into(), + ny_amount: v.ny_amount.into(), + total_amount: v.total_amount.into(), + amount: v.amount.into(), + balance: v.balance.into(), + } + } +} + +impl ToFFI for CShortTradesItemOwned { + type FFIType = CShortTradesItem; + fn to_ffi_type(&self) -> Self::FFIType { + CShortTradesItem { + timestamp: self.timestamp.to_ffi_type(), + rate: self.rate.to_ffi_type(), + close: self.close.to_ffi_type(), + nus_amount: self.nus_amount.to_ffi_type(), + ny_amount: self.ny_amount.to_ffi_type(), + total_amount: self.total_amount.to_ffi_type(), + amount: self.amount.to_ffi_type(), + balance: self.balance.to_ffi_type(), + } + } +} + +/// Short trade records response (HK or US). +#[repr(C)] +pub struct CShortTradesResponse { + /// Pointer to the array of short trade items + pub data: *const CShortTradesItem, + /// Number of items in `data` + pub num_data: usize, +} + +pub(crate) struct CShortTradesResponseOwned { + data: CVec, +} + +impl From for CShortTradesResponseOwned { + fn from(v: ShortTradesResponse) -> Self { + Self { + data: v.data.into(), + } + } +} + +impl ToFFI for CShortTradesResponseOwned { + type FFIType = CShortTradesResponse; + fn to_ffi_type(&self) -> Self::FFIType { + CShortTradesResponse { + data: self.data.to_ffi_type(), + num_data: self.data.len(), } } } diff --git a/c/src/screener_context/context.rs b/c/src/screener_context/context.rs new file mode 100644 index 000000000..4a027ebee --- /dev/null +++ b/c/src/screener_context/context.rs @@ -0,0 +1,135 @@ +use std::{ffi::c_void, os::raw::c_char, sync::Arc}; + +use longbridge::ScreenerContext; + +use crate::{ + async_call::{CAsyncCallback, execute_async}, + config::CConfig, + screener_context::types::*, + types::{CCow, cstr_to_rust}, +}; + +pub(crate) struct CScreenerContext { + ctx: ScreenerContext, +} + +#[unsafe(no_mangle)] +pub unsafe extern "C" fn lb_screener_context_new( + config: *const CConfig, +) -> *const CScreenerContext { + let config = Arc::new((*config).0.clone()); + Arc::into_raw(Arc::new(CScreenerContext { + ctx: ScreenerContext::new(config), + })) +} + +#[unsafe(no_mangle)] +pub unsafe extern "C" fn lb_screener_context_retain(ctx: *const CScreenerContext) { + Arc::increment_strong_count(ctx); +} + +#[unsafe(no_mangle)] +pub unsafe extern "C" fn lb_screener_context_release(ctx: *const CScreenerContext) { + Arc::decrement_strong_count(ctx); +} + +/// Get recommended built-in screener strategies. +/// Returns `CScreenerRecommendStrategiesResponse`. +#[unsafe(no_mangle)] +pub unsafe extern "C" fn lb_screener_context_recommend_strategies( + ctx: *const CScreenerContext, + callback: CAsyncCallback, + userdata: *mut c_void, +) { + let ctx_inner = (*ctx).ctx.clone(); + execute_async(callback, ctx, userdata, async move { + let resp: CCow = + CCow::new(CScreenerRecommendStrategiesResponseOwned::from( + ctx_inner.screener_recommend_strategies().await?, + )); + Ok(resp) + }); +} + +/// Get the current user's saved screener strategies. +/// Returns `CScreenerUserStrategiesResponse`. +#[unsafe(no_mangle)] +pub unsafe extern "C" fn lb_screener_context_user_strategies( + ctx: *const CScreenerContext, + callback: CAsyncCallback, + userdata: *mut c_void, +) { + let ctx_inner = (*ctx).ctx.clone(); + execute_async(callback, ctx, userdata, async move { + let resp: CCow = CCow::new( + CScreenerUserStrategiesResponseOwned::from(ctx_inner.screener_user_strategies().await?), + ); + Ok(resp) + }); +} + +/// Get detail for one screener strategy by ID. +/// Returns `CScreenerStrategyResponse`. +#[unsafe(no_mangle)] +pub unsafe extern "C" fn lb_screener_context_strategy( + ctx: *const CScreenerContext, + id: i64, + callback: CAsyncCallback, + userdata: *mut c_void, +) { + let ctx_inner = (*ctx).ctx.clone(); + execute_async(callback, ctx, userdata, async move { + let resp: CCow = CCow::new( + CScreenerStrategyResponseOwned::from(ctx_inner.screener_strategy(id).await?), + ); + Ok(resp) + }); +} + +/// Search / screen securities using a strategy. +/// Returns `CScreenerSearchResponse`. +#[unsafe(no_mangle)] +pub unsafe extern "C" fn lb_screener_context_search( + ctx: *const CScreenerContext, + market: *const c_char, + strategy_id: i64, + has_strategy_id: bool, + page: u32, + size: u32, + callback: CAsyncCallback, + userdata: *mut c_void, +) { + let ctx_inner = (*ctx).ctx.clone(); + let market = cstr_to_rust(market); + let strategy_id = if has_strategy_id { + Some(strategy_id) + } else { + None + }; + execute_async(callback, ctx, userdata, async move { + let resp: CCow = + CCow::new(CScreenerSearchResponseOwned::from( + ctx_inner + .screener_search(market, strategy_id, page, size) + .await?, + )); + Ok(resp) + }); +} + +/// Get all available screener indicator definitions. +/// Returns `CScreenerIndicatorsResponse`. +#[unsafe(no_mangle)] +pub unsafe extern "C" fn lb_screener_context_indicators( + ctx: *const CScreenerContext, + callback: CAsyncCallback, + userdata: *mut c_void, +) { + let ctx_inner = (*ctx).ctx.clone(); + execute_async(callback, ctx, userdata, async move { + let resp: CCow = CCow::new( + CScreenerIndicatorsResponseOwned::from(ctx_inner.screener_indicators().await?), + ); + Ok(resp) + }); +} diff --git a/c/src/screener_context/mod.rs b/c/src/screener_context/mod.rs new file mode 100644 index 000000000..0561d4d5a --- /dev/null +++ b/c/src/screener_context/mod.rs @@ -0,0 +1,2 @@ +pub mod context; +pub mod types; diff --git a/c/src/screener_context/types.rs b/c/src/screener_context/types.rs new file mode 100644 index 000000000..72e33c9b7 --- /dev/null +++ b/c/src/screener_context/types.rs @@ -0,0 +1,153 @@ +use std::os::raw::c_char; + +use longbridge::screener::types::{ + ScreenerIndicatorsResponse, ScreenerRecommendStrategiesResponse, ScreenerSearchResponse, + ScreenerStrategyResponse, ScreenerUserStrategiesResponse, +}; + +use crate::types::{CString, ToFFI}; + +// ── ScreenerRecommendStrategiesResponse ─────────────────────────── + +/// Recommended screener strategies response. `data` is a JSON string. +#[repr(C)] +pub struct CScreenerRecommendStrategiesResponse { + pub data: *const c_char, +} + +pub(crate) struct CScreenerRecommendStrategiesResponseOwned { + data: CString, +} + +impl From for CScreenerRecommendStrategiesResponseOwned { + fn from(v: ScreenerRecommendStrategiesResponse) -> Self { + Self { + data: serde_json::to_string(&v.data).unwrap_or_default().into(), + } + } +} + +impl ToFFI for CScreenerRecommendStrategiesResponseOwned { + type FFIType = CScreenerRecommendStrategiesResponse; + fn to_ffi_type(&self) -> Self::FFIType { + CScreenerRecommendStrategiesResponse { + data: self.data.to_ffi_type(), + } + } +} + +// ── ScreenerUserStrategiesResponse ──────────────────────────────── + +/// User screener strategies response. `data` is a JSON string. +#[repr(C)] +pub struct CScreenerUserStrategiesResponse { + pub data: *const c_char, +} + +pub(crate) struct CScreenerUserStrategiesResponseOwned { + data: CString, +} + +impl From for CScreenerUserStrategiesResponseOwned { + fn from(v: ScreenerUserStrategiesResponse) -> Self { + Self { + data: serde_json::to_string(&v.data).unwrap_or_default().into(), + } + } +} + +impl ToFFI for CScreenerUserStrategiesResponseOwned { + type FFIType = CScreenerUserStrategiesResponse; + fn to_ffi_type(&self) -> Self::FFIType { + CScreenerUserStrategiesResponse { + data: self.data.to_ffi_type(), + } + } +} + +// ── ScreenerStrategyResponse ────────────────────────────────────── + +/// Single screener strategy response. `data` is a JSON string. +#[repr(C)] +pub struct CScreenerStrategyResponse { + pub data: *const c_char, +} + +pub(crate) struct CScreenerStrategyResponseOwned { + data: CString, +} + +impl From for CScreenerStrategyResponseOwned { + fn from(v: ScreenerStrategyResponse) -> Self { + Self { + data: serde_json::to_string(&v.data).unwrap_or_default().into(), + } + } +} + +impl ToFFI for CScreenerStrategyResponseOwned { + type FFIType = CScreenerStrategyResponse; + fn to_ffi_type(&self) -> Self::FFIType { + CScreenerStrategyResponse { + data: self.data.to_ffi_type(), + } + } +} + +// ── ScreenerSearchResponse ──────────────────────────────────────── + +/// Screener search results response. `data` is a JSON string. +#[repr(C)] +pub struct CScreenerSearchResponse { + pub data: *const c_char, +} + +pub(crate) struct CScreenerSearchResponseOwned { + data: CString, +} + +impl From for CScreenerSearchResponseOwned { + fn from(v: ScreenerSearchResponse) -> Self { + Self { + data: serde_json::to_string(&v.data).unwrap_or_default().into(), + } + } +} + +impl ToFFI for CScreenerSearchResponseOwned { + type FFIType = CScreenerSearchResponse; + fn to_ffi_type(&self) -> Self::FFIType { + CScreenerSearchResponse { + data: self.data.to_ffi_type(), + } + } +} + +// ── ScreenerIndicatorsResponse ──────────────────────────────────── + +/// Screener indicator definitions response. `data` is a JSON string. +#[repr(C)] +pub struct CScreenerIndicatorsResponse { + pub data: *const c_char, +} + +pub(crate) struct CScreenerIndicatorsResponseOwned { + data: CString, +} + +impl From for CScreenerIndicatorsResponseOwned { + fn from(v: ScreenerIndicatorsResponse) -> Self { + Self { + data: serde_json::to_string(&v.data).unwrap_or_default().into(), + } + } +} + +impl ToFFI for CScreenerIndicatorsResponseOwned { + type FFIType = CScreenerIndicatorsResponse; + fn to_ffi_type(&self) -> Self::FFIType { + CScreenerIndicatorsResponse { + data: self.data.to_ffi_type(), + } + } +} diff --git a/cpp/CMakeLists.txt b/cpp/CMakeLists.txt index 6832770a2..66ccf831e 100644 --- a/cpp/CMakeLists.txt +++ b/cpp/CMakeLists.txt @@ -10,6 +10,7 @@ set(SOURCES src/calendar_context.cpp src/fundamental_context.cpp src/market_context.cpp + src/screener_context.cpp src/portfolio_context.cpp src/status.cpp src/types.cpp diff --git a/cpp/include/fundamental_context.hpp b/cpp/include/fundamental_context.hpp index 6636282e9..451da4256 100644 --- a/cpp/include/fundamental_context.hpp +++ b/cpp/include/fundamental_context.hpp @@ -5,6 +5,8 @@ #include "config.hpp" #include "types.hpp" #include +#include +#include typedef struct lb_fundamental_context_t lb_fundamental_context_t; @@ -124,43 +126,55 @@ class FundamentalContext void ratings(const std::string& symbol, AsyncCallback callback) const; - /// Get business segment breakdowns (latest snapshot) + /// Get latest business segment breakdown void business_segments(const std::string& symbol, - AsyncCallback callback) const; + AsyncCallback callback) const; - /// Get historical business segment breakdowns. - /// Pass nullptr for report/cate to omit them. + /// Get historical business segment breakdowns (pass nullptr for report/cate to omit) void business_segments_history(const std::string& symbol, - const char* report, - const char* cate, - AsyncCallback callback) const; + const char* report, + const char* cate, + AsyncCallback callback) const; /// Get historical institutional rating view time-series void institution_rating_views(const std::string& symbol, - AsyncCallback callback) const; + AsyncCallback callback) const; - /// Get industry rank for a market. - /// indicator: "0"–"7"; sort_type: "0"=asc, "1"=desc + /// Get industry rank list for a market void industry_rank(const std::string& market, const std::string& indicator, const std::string& sort_type, uint32_t limit, AsyncCallback callback) const; - /// Get the industry peer chain. - /// Pass nullptr for industry_id to omit it. + /// Get industry peer chain (pass nullptr for industry_id to omit) void industry_peers(const std::string& counter_id, const std::string& market, const char* industry_id, AsyncCallback callback) const; - /// Get a financial report snapshot. - /// Pass nullptr for report/fiscal_period; pass 0 for fiscal_year to omit it. + /// Get financial report snapshot (pass nullptr/0 for optional params) void financial_report_snapshot(const std::string& symbol, - const char* report, - int32_t fiscal_year, - const char* fiscal_period, - AsyncCallback callback) const; + const char* report, + int32_t fiscal_year, + const char* fiscal_period, + AsyncCallback callback) const; + + /// Get ranked list of top shareholders (raw JSON string) + void shareholder_top(const std::string& symbol, + AsyncCallback callback) const; + + /// Get holding history and detail for one shareholder (raw JSON string) + void shareholder_detail(const std::string& symbol, + int64_t object_id, + AsyncCallback callback) const; + + /// Get valuation comparison. + /// 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..0a6764147 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 top movers (stocks with unusual price movements) across one or more markets + void top_movers(const std::vector& markets, + uint32_t sort, + const std::string* date, + uint32_t limit, + AsyncCallback callback) const; + + /// 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 + void rank_list(const std::string& key, + bool need_article, + AsyncCallback callback) const; }; } // namespace market diff --git a/cpp/include/quote_context.hpp b/cpp/include/quote_context.hpp index 1b2f5f1d7..01644f332 100644 --- a/cpp/include/quote_context.hpp +++ b/cpp/include/quote_context.hpp @@ -314,10 +314,16 @@ class QuoteContext uintptr_t count, AsyncCallback> callback) const; - /// Get short interest data for a US security + /// Get short interest data for a US or HK security (market inferred from symbol suffix) void short_positions(const std::string& symbol, + uint32_t count, AsyncCallback callback) const; + /// Get short trade records for a HK or US security (market inferred from symbol suffix) + void short_trades(const std::string& symbol, + uint32_t count, + AsyncCallback callback) const; + /// Get real-time option call/put volume void option_volume(const std::string& symbol, AsyncCallback callback) const; diff --git a/cpp/include/screener_context.hpp b/cpp/include/screener_context.hpp new file mode 100644 index 000000000..843bb0fc5 --- /dev/null +++ b/cpp/include/screener_context.hpp @@ -0,0 +1,51 @@ +#pragma once + +#include "async_result.hpp" +#include "callback.hpp" +#include "config.hpp" +#include +#include + +typedef struct lb_screener_context_t lb_screener_context_t; + +namespace longbridge { +namespace screener { + +/// Screener context — stock screener strategies, search, and indicators. +class ScreenerContext +{ +public: + ScreenerContext(); + explicit ScreenerContext(const lb_screener_context_t* ctx); + ScreenerContext(const ScreenerContext& ctx); + ScreenerContext(ScreenerContext&& ctx); + ~ScreenerContext(); + ScreenerContext& operator=(const ScreenerContext& ctx); + + static ScreenerContext create(const Config& config); + + /// Get recommended built-in screener strategies (raw JSON string) + void screener_recommend_strategies(AsyncCallback callback) const; + + /// Get the current user's saved screener strategies (raw JSON string) + void screener_user_strategies(AsyncCallback callback) const; + + /// Get detail for one screener strategy by ID (raw JSON string) + void screener_strategy(int64_t id, AsyncCallback callback) const; + + /// Search / screen securities using a strategy (raw JSON string) + void screener_search(const std::string& market, + std::optional strategy_id, + uint32_t page, + uint32_t size, + AsyncCallback callback) const; + + /// Get all available screener indicator definitions (raw JSON string) + void screener_indicators(AsyncCallback callback) const; + +private: + const lb_screener_context_t* ctx_; +}; + +} // namespace screener +} // namespace longbridge diff --git a/cpp/include/types.hpp b/cpp/include/types.hpp index 30459bd22..0ba445c9a 100644 --- a/cpp/include/types.hpp +++ b/cpp/include/types.hpp @@ -1263,21 +1263,62 @@ struct FilingItem int64_t published_at; }; -struct ShortPosition +/// One short-position record, unified for US and HK markets. +struct ShortPositionsItem { + /// Trading date in RFC 3339 format std::string timestamp; + /// Short ratio std::string rate; - std::string avg_daily_share_volume; + /// Closing price + std::string close; + /// [US] Number of short shares outstanding std::string current_shares_short; + /// [US] Average daily share volume + std::string avg_daily_share_volume; + /// [US] Days-to-cover ratio std::string days_to_cover; - std::string close; + /// [HK] Short sale amount (HKD) + std::string amount; + /// [HK] Short position balance + std::string balance; + /// [HK] Closing price (HK naming) + std::string cost; }; +/// Short interest / positions response (HK or US). struct ShortPositionsResponse { - std::string symbol; - std::vector data; - int32_t sources; + /// Short position records + std::vector data; +}; + +/// One short-trade record, unified for US and HK markets. +struct ShortTradesItem +{ + /// Trading date in RFC 3339 format + std::string timestamp; + /// Short ratio + std::string rate; + /// Closing price + std::string close; + /// [US] NASDAQ short sale volume + std::string nus_amount; + /// [US] NYSE short sale volume + std::string ny_amount; + /// [US] Total short amount + std::string total_amount; + /// [HK] Short sale turnover amount (HKD) + std::string amount; + /// [HK] Short position balance + std::string balance; +}; + +/// Short trade records response (HK or US). +struct ShortTradesResponse +{ + /// Short trade records + std::vector data; }; struct OptionVolumeStats @@ -2391,6 +2432,66 @@ struct TradeStatsResponse std::vector trades; }; +/// Stock information within a top-movers event. +struct TopMoversStock +{ + std::string symbol; + std::string code; + std::string name; + std::string full_name; + std::string change; + std::string last_done; + std::string market; + std::string logo; + std::vector labels; +}; + +/// One top-movers event entry. +struct TopMoversEvent +{ + std::string timestamp; + std::string alert_reason; + int64_t alert_type; + TopMoversStock stock; + std::string post; +}; + +/// Response for top_movers. +struct TopMoversResponse +{ + std::vector events; + /// Pagination cursor as a JSON string + std::string next_params; +}; + +/// One ranked security item. +struct RankListItem +{ + std::string symbol; + std::string code; + std::string name; + std::string last_done; + std::string chg; + std::string change; + std::string inflow; + std::string market_cap; + std::string industry; + std::string pre_post_price; + std::string pre_post_chg; + std::string amplitude; + std::string five_day_chg; + std::string turnover_rate; + std::string volume_rate; + std::string pb_ttm; +}; + +/// Response for rank_list. +struct RankListResponse +{ + bool bmp; + std::vector lists; +}; + /// A single anomaly (unusual market movement) alert item. struct AnomalyItem { @@ -3137,6 +3238,41 @@ struct FinancialReportSnapshot std::string fr_debt_assets_ratio; }; +/// One historical valuation data point. +struct ValuationHistoryPoint +{ + std::string date; + std::string pe; + std::string pb; + std::string ps; +}; + +/// One security's valuation comparison item. +struct ValuationComparisonItem +{ + std::string symbol; + std::string name; + std::string currency; + std::string market_value; + std::string price_close; + std::string pe; + std::string pb; + std::string ps; + std::string roe; + std::string eps; + std::string bps; + std::string dps; + std::string div_yld; + std::string assets; + std::vector history; +}; + +/// Valuation comparison response. +struct ValuationComparisonResponse +{ + std::vector list; +}; + } // namespace fundamental namespace alert { diff --git a/cpp/src/convert.hpp b/cpp/src/convert.hpp index 19ea00f02..287bea0cb 100644 --- a/cpp/src/convert.hpp +++ b/cpp/src/convert.hpp @@ -2291,13 +2291,36 @@ 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::ShortPositionsItem convert(const lb_short_positions_item_t* item) { + return { item->timestamp ? item->timestamp : "", + item->rate ? item->rate : "", + item->close ? item->close : "", + item->current_shares_short ? item->current_shares_short : "", + item->avg_daily_share_volume ? item->avg_daily_share_volume : "", + item->days_to_cover ? item->days_to_cover : "", + item->amount ? item->amount : "", + item->balance ? item->balance : "", + item->cost ? item->cost : "" }; } inline quote::ShortPositionsResponse convert(const lb_short_positions_response_t* r) { - 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 }; + std::vector items; + for (size_t i = 0; i < r->num_data; ++i) items.push_back(convert(&r->data[i])); + return { std::move(items) }; +} +inline quote::ShortTradesItem convert(const lb_short_trades_item_t* item) { + return { item->timestamp ? item->timestamp : "", + item->rate ? item->rate : "", + item->close ? item->close : "", + item->nus_amount ? item->nus_amount : "", + item->ny_amount ? item->ny_amount : "", + item->total_amount ? item->total_amount : "", + item->amount ? item->amount : "", + item->balance ? item->balance : "" }; +} +inline quote::ShortTradesResponse convert(const lb_short_trades_response_t* r) { + std::vector items; + for (size_t i = 0; i < r->num_data; ++i) items.push_back(convert(&r->data[i])); + return { std::move(items) }; } inline quote::OptionVolumeStats convert(const lb_option_volume_stats_t* s) { return { s->c, s->p }; @@ -2385,6 +2408,42 @@ inline market::IndexConstituents convert(const lb_index_constituents_t* r) { for (size_t i = 0; i < r->num_stocks; ++i) stocks.push_back(convert(&r->stocks[i])); return { r->fall_num, r->flat_num, r->rise_num, std::move(stocks) }; } +inline market::TopMoversStock convert(const lb_top_movers_stock_t* s) { + std::vector labels; + for (size_t i = 0; i < s->num_labels; ++i) labels.push_back(s->labels[i] ? s->labels[i] : ""); + return { s->symbol ? s->symbol : "", s->code ? s->code : "", s->name ? s->name : "", + s->full_name ? s->full_name : "", s->change ? s->change : "", + s->last_done ? s->last_done : "", s->market ? s->market : "", + s->logo ? s->logo : "", std::move(labels) }; +} +inline market::TopMoversEvent convert(const lb_top_movers_event_t* e) { + return { e->timestamp ? e->timestamp : "", e->alert_reason ? e->alert_reason : "", + e->alert_type, convert(&e->stock), e->post ? e->post : "" }; +} +inline market::TopMoversResponse convert(const lb_top_movers_response_t* r) { + std::vector events; + for (size_t i = 0; i < r->num_events; ++i) events.push_back(convert(&r->events[i])); + return { std::move(events), r->next_params ? r->next_params : "" }; +} +inline market::RankListItem convert(const lb_rank_list_item_t* item) { + return { item->symbol ? item->symbol : "", item->code ? item->code : "", + item->name ? item->name : "", item->last_done ? item->last_done : "", + item->chg ? item->chg : "", item->change ? item->change : "", + item->inflow ? item->inflow : "", item->market_cap ? item->market_cap : "", + item->industry ? item->industry : "", + item->pre_post_price ? item->pre_post_price : "", + item->pre_post_chg ? item->pre_post_chg : "", + item->amplitude ? item->amplitude : "", + item->five_day_chg ? item->five_day_chg : "", + item->turnover_rate ? item->turnover_rate : "", + item->volume_rate ? item->volume_rate : "", + item->pb_ttm ? item->pb_ttm : "" }; +} +inline market::RankListResponse convert(const lb_rank_list_response_t* r) { + std::vector lists; + for (size_t i = 0; i < r->num_lists; ++i) lists.push_back(convert(&r->lists[i])); + return { r->bmp, std::move(lists) }; +} // ── FundamentalContext conversions ──────────────────────────────── @@ -2675,6 +2734,26 @@ inline fundamental::FinancialReportSnapshot convert(const lb_financial_report_sn r->fr_roe_ttm, r->fr_profit_margin, r->fr_profit_margin_ttm, r->fr_asset_turn_ttm, r->fr_leverage_ttm, r->fr_debt_assets_ratio }; } +inline fundamental::ValuationHistoryPoint convert(const lb_valuation_history_point_t* p) { + return { p->date ? p->date : "", p->pe ? p->pe : "", p->pb ? p->pb : "", p->ps ? p->ps : "" }; +} +inline fundamental::ValuationComparisonItem convert(const lb_valuation_comparison_item_t* item) { + std::vector history; + for (size_t i = 0; i < item->num_history; ++i) history.push_back(convert(&item->history[i])); + return { item->symbol ? item->symbol : "", item->name ? item->name : "", + item->currency ? item->currency : "", item->market_value ? item->market_value : "", + item->price_close ? item->price_close : "", item->pe ? item->pe : "", + item->pb ? item->pb : "", item->ps ? item->ps : "", + item->roe ? item->roe : "", item->eps ? item->eps : "", + item->bps ? item->bps : "", item->dps ? item->dps : "", + item->div_yld ? item->div_yld : "", item->assets ? item->assets : "", + std::move(history) }; +} +inline fundamental::ValuationComparisonResponse convert(const lb_valuation_comparison_response_t* r) { + std::vector list; + for (size_t i = 0; i < r->num_list; ++i) list.push_back(convert(&r->list[i])); + return { std::move(list) }; +} // ── Portfolio conversions ───────────────────────────────────────── diff --git a/cpp/src/fundamental_context.cpp b/cpp/src/fundamental_context.cpp index 070cf686f..c14148cd0 100644 --- a/cpp/src/fundamental_context.cpp +++ b/cpp/src/fundamental_context.cpp @@ -114,11 +114,60 @@ void FundamentalContext::industry_peers(const std::string& counter_id, const std F_TYPED(IndustryPeersResponse, lb_industry_peers_response_t, lb_fundamental_context_industry_peers, ctx_, counter_id.c_str(), market.c_str(), industry_id); } void FundamentalContext::financial_report_snapshot(const std::string& s, const char* report, int32_t fiscal_year, const char* fiscal_period, AsyncCallback callback) const { - F_TYPED(FinancialReportSnapshot, lb_financial_report_snapshot_t, lb_fundamental_context_financial_report_snapshot, ctx_, s.c_str(), report, fiscal_year, fiscal_period); + // C API takes fiscal_year as a string; convert 0 → nullptr + std::string fy_str; + const char* fy_cstr = nullptr; + if (fiscal_year != 0) { + fy_str = std::to_string(fiscal_year); + fy_cstr = fy_str.c_str(); + } + F_TYPED(FinancialReportSnapshot, lb_financial_report_snapshot_t, lb_fundamental_context_financial_report_snapshot, ctx_, s.c_str(), report, fy_cstr, fiscal_period); } #undef F_TYPED #undef F_JSON +// ── New JSON-string APIs ────────────────────────────────────────── +// These return a struct with a single `data` field (JSON string). +// We extract the string and return it as std::string. +#define F_JSON_STRUCT(cfn, CType, ...) cfn(__VA_ARGS__, [](auto res) { \ + auto cb = callback::get_async_callback(res->userdata); \ + FundamentalContext fctx((const lb_fundamental_context_t*)res->ctx); Status status(res->error); \ + if(status){const CType* d=(const CType*)res->data; std::string j(d->data ? d->data : ""); (*cb)(AsyncResult(fctx,std::move(status),&j));} \ + else{(*cb)(AsyncResult(fctx,std::move(status),nullptr));} \ +}, new AsyncCallback(callback)) + +void FundamentalContext::shareholder_top(const std::string& s, AsyncCallback callback) const { + F_JSON_STRUCT(lb_fundamental_context_shareholder_top, lb_shareholder_top_response_t, ctx_, s.c_str()); +} + +void FundamentalContext::shareholder_detail(const std::string& s, int64_t object_id, AsyncCallback callback) const { + F_JSON_STRUCT(lb_fundamental_context_shareholder_detail, lb_shareholder_detail_response_t, ctx_, s.c_str(), object_id); +} + +void FundamentalContext::valuation_comparison(const std::string& s, const std::string& currency, const std::vector* comparison_symbols, AsyncCallback callback) const { + std::vector syms_ptrs; + size_t num_syms = 0; + const char** syms_data = nullptr; + if (comparison_symbols) { + for (const auto& sym : *comparison_symbols) syms_ptrs.push_back(sym.c_str()); + syms_data = syms_ptrs.empty() ? nullptr : syms_ptrs.data(); + num_syms = syms_ptrs.size(); + } + lb_fundamental_context_valuation_comparison(ctx_, s.c_str(), currency.c_str(), syms_data, num_syms, + [](auto res) { + auto cb = callback::get_async_callback(res->userdata); + FundamentalContext fctx((const lb_fundamental_context_t*)res->ctx); Status status(res->error); + if (status) { + auto r = convert::convert((const lb_valuation_comparison_response_t*)res->data); + (*cb)(AsyncResult(fctx, std::move(status), &r)); + } else { + (*cb)(AsyncResult(fctx, std::move(status), nullptr)); + } + }, new AsyncCallback(callback)); +} + +#undef F_JSON_STRUCT + } // namespace fundamental } // namespace longbridge diff --git a/cpp/src/market_context.cpp b/cpp/src/market_context.cpp index 983dc2777..5327b1666 100644 --- a/cpp/src/market_context.cpp +++ b/cpp/src/market_context.cpp @@ -79,5 +79,50 @@ 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::rank_categories(AsyncCallback callback) const { + M_JSON(lb_market_context_rank_categories, lb_rank_categories_response_t, ctx_); +} + +#undef M_JSON + +void MarketContext::top_movers(const std::vector& markets, uint32_t sort, const std::string* date, uint32_t limit, AsyncCallback callback) const { + std::vector mptrs; + for (const auto& m : markets) mptrs.push_back(m.c_str()); + const char* date_str = date ? date->c_str() : nullptr; + lb_market_context_top_movers(ctx_, mptrs.data(), mptrs.size(), sort, date_str, limit, + [](auto res) { + auto cb = callback::get_async_callback(res->userdata); + MarketContext mctx((const lb_market_context_t*)res->ctx); Status status(res->error); + if (status) { + auto r = convert::convert((const lb_top_movers_response_t*)res->data); + (*cb)(AsyncResult(mctx, std::move(status), &r)); + } else { + (*cb)(AsyncResult(mctx, std::move(status), nullptr)); + } + }, new AsyncCallback(callback)); +} + +void MarketContext::rank_list(const std::string& key, bool need_article, AsyncCallback callback) const { + lb_market_context_rank_list(ctx_, key.c_str(), need_article, + [](auto res) { + auto cb = callback::get_async_callback(res->userdata); + MarketContext mctx((const lb_market_context_t*)res->ctx); Status status(res->error); + if (status) { + auto r = convert::convert((const lb_rank_list_response_t*)res->data); + (*cb)(AsyncResult(mctx, std::move(status), &r)); + } else { + (*cb)(AsyncResult(mctx, std::move(status), nullptr)); + } + }, new AsyncCallback(callback)); +} + } // namespace market } // namespace longbridge diff --git a/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/javasrc/src/main/java/com/longbridge/SdkNative.java b/java/javasrc/src/main/java/com/longbridge/SdkNative.java index 5958eaf1f..777647bae 100644 --- a/java/javasrc/src/main/java/com/longbridge/SdkNative.java +++ b/java/javasrc/src/main/java/com/longbridge/SdkNative.java @@ -322,6 +322,44 @@ public static native void tradeContextEstimateMaxPurchaseQuantity(long context, public static native void marketContextTradeStats(long context, String symbol, AsyncCallback callback); public static native void marketContextAnomaly(long context, String market, AsyncCallback callback); public static native void marketContextConstituent(long context, String symbol, AsyncCallback callback); + public static native void marketContextTopMovers(long context, com.longbridge.market.TopMoversOptions opts, AsyncCallback callback); + + public static native void marketContextRankCategories(long context, + AsyncCallback callback); + + public static native void marketContextRankList(long context, Object opts, + AsyncCallback callback); + + public static native long newScreenerContext(long config); + + public static native void freeScreenerContext(long context); + + public static native void screenerContextRecommendStrategies(long context, + AsyncCallback callback); + + public static native void screenerContextUserStrategies(long context, + AsyncCallback callback); + + public static native void screenerContextStrategy(long context, Object opts, + AsyncCallback callback); + + public static native void screenerContextSearch(long context, Object opts, + AsyncCallback callback); + + public static native void screenerContextIndicators(long context, + AsyncCallback callback); + + public static native void fundamentalContextShareholderTop(long context, + String symbol, AsyncCallback callback); + + public static native void fundamentalContextShareholderDetail(long context, + Object opts, AsyncCallback callback); + + public static native void fundamentalContextValuationComparison(long context, + Object opts, AsyncCallback callback); + + public static native void quoteContextShortTrades(long context, Object opts, + AsyncCallback callback); // ── FundamentalContext ──────────────────────────────────────── diff --git a/java/javasrc/src/main/java/com/longbridge/fundamental/FundamentalContext.java b/java/javasrc/src/main/java/com/longbridge/fundamental/FundamentalContext.java index 2caee02d9..8c0a7299a 100644 --- a/java/javasrc/src/main/java/com/longbridge/fundamental/FundamentalContext.java +++ b/java/javasrc/src/main/java/com/longbridge/fundamental/FundamentalContext.java @@ -313,4 +313,25 @@ public CompletableFuture getFinancialReportSnapshot( SdkNative.fundamentalContextGetFinancialReportSnapshot(raw, opts, callback); }); } + + /** Get top 20 major shareholders with multi-period holdings. */ + public CompletableFuture getShareholderTop(String symbol) throws OpenApiException { + return AsyncCallback.executeTask((callback) -> { + SdkNative.fundamentalContextShareholderTop(raw, symbol, callback); + }); + } + + /** Get holding history and trade detail for a specific shareholder. */ + public CompletableFuture getShareholderDetail(ShareholderDetailOptions opts) throws OpenApiException { + return AsyncCallback.executeTask((callback) -> { + SdkNative.fundamentalContextShareholderDetail(raw, opts, callback); + }); + } + + /** Get valuation comparison between a symbol and optional peer symbols. */ + public CompletableFuture getValuationComparison(ValuationComparisonOptions opts) throws OpenApiException { + return AsyncCallback.executeTask((callback) -> { + SdkNative.fundamentalContextValuationComparison(raw, opts, callback); + }); + } } diff --git a/java/javasrc/src/main/java/com/longbridge/fundamental/ShareholderDetailOptions.java b/java/javasrc/src/main/java/com/longbridge/fundamental/ShareholderDetailOptions.java new file mode 100644 index 000000000..652d03a0f --- /dev/null +++ b/java/javasrc/src/main/java/com/longbridge/fundamental/ShareholderDetailOptions.java @@ -0,0 +1,9 @@ +package com.longbridge.fundamental; + +/** Options for {@link FundamentalContext#getShareholderDetail}. */ +public class ShareholderDetailOptions { + /** Security symbol, e.g. "AAPL.US" */ + public String symbol; + /** Shareholder object ID from getShareholderTop */ + public long objectId; +} diff --git a/java/javasrc/src/main/java/com/longbridge/fundamental/ShareholderDetailResponse.java b/java/javasrc/src/main/java/com/longbridge/fundamental/ShareholderDetailResponse.java new file mode 100644 index 000000000..bc1cd24c5 --- /dev/null +++ b/java/javasrc/src/main/java/com/longbridge/fundamental/ShareholderDetailResponse.java @@ -0,0 +1,7 @@ +package com.longbridge.fundamental; + +/** Response for {@link FundamentalContext#getShareholderDetail}. Contains raw JSON data. */ +public class ShareholderDetailResponse { + /** Raw JSON data string */ + public String data; +} diff --git a/java/javasrc/src/main/java/com/longbridge/fundamental/ShareholderTopResponse.java b/java/javasrc/src/main/java/com/longbridge/fundamental/ShareholderTopResponse.java new file mode 100644 index 000000000..f6f46d18c --- /dev/null +++ b/java/javasrc/src/main/java/com/longbridge/fundamental/ShareholderTopResponse.java @@ -0,0 +1,7 @@ +package com.longbridge.fundamental; + +/** Response for {@link FundamentalContext#getShareholderTop}. Contains raw JSON data. */ +public class ShareholderTopResponse { + /** Raw JSON data string */ + public String data; +} diff --git a/java/javasrc/src/main/java/com/longbridge/fundamental/ValuationComparisonItem.java b/java/javasrc/src/main/java/com/longbridge/fundamental/ValuationComparisonItem.java new file mode 100644 index 000000000..351bd290b --- /dev/null +++ b/java/javasrc/src/main/java/com/longbridge/fundamental/ValuationComparisonItem.java @@ -0,0 +1,22 @@ +package com.longbridge.fundamental; + +/** One security in the valuation comparison. */ +public class ValuationComparisonItem { + /** Symbol, e.g. "AAPL.US" (converted from counter_id) */ + public String symbol; + public String name; + public String currency; + public String marketValue; + public String priceClose; + public String pe; + public String pb; + public String ps; + public String roe; + public String eps; + public String bps; + public String dps; + public String divYld; + public String assets; + /** Historical valuation data points */ + public ValuationHistoryPoint[] history; +} diff --git a/java/javasrc/src/main/java/com/longbridge/fundamental/ValuationComparisonOptions.java b/java/javasrc/src/main/java/com/longbridge/fundamental/ValuationComparisonOptions.java new file mode 100644 index 000000000..b658a8dcf --- /dev/null +++ b/java/javasrc/src/main/java/com/longbridge/fundamental/ValuationComparisonOptions.java @@ -0,0 +1,11 @@ +package com.longbridge.fundamental; + +/** Options for {@link FundamentalContext#getValuationComparison}. */ +public class ValuationComparisonOptions { + /** Primary security symbol, e.g. "AAPL.US" */ + public String symbol; + /** Currency: "USD", "HKD", or "CNY" */ + public String currency; + /** Optional peer symbols to compare (up to 4), e.g. ["MSFT.US","GOOGL.US"] */ + public String[] comparisonSymbols; +} diff --git a/java/javasrc/src/main/java/com/longbridge/fundamental/ValuationComparisonResponse.java b/java/javasrc/src/main/java/com/longbridge/fundamental/ValuationComparisonResponse.java new file mode 100644 index 000000000..5e30708dc --- /dev/null +++ b/java/javasrc/src/main/java/com/longbridge/fundamental/ValuationComparisonResponse.java @@ -0,0 +1,7 @@ +package com.longbridge.fundamental; + +/** Response for {@link FundamentalContext#getValuationComparison}. */ +public class ValuationComparisonResponse { + /** Comparison items (primary + peers) */ + public ValuationComparisonItem[] list; +} diff --git a/java/javasrc/src/main/java/com/longbridge/fundamental/ValuationHistoryPoint.java b/java/javasrc/src/main/java/com/longbridge/fundamental/ValuationHistoryPoint.java new file mode 100644 index 000000000..3bd6c6343 --- /dev/null +++ b/java/javasrc/src/main/java/com/longbridge/fundamental/ValuationHistoryPoint.java @@ -0,0 +1,10 @@ +package com.longbridge.fundamental; + +/** One historical valuation data point. */ +public class ValuationHistoryPoint { + /** Date in RFC 3339 format */ + public String date; + public String pe; + public String pb; + public String ps; +} diff --git a/java/javasrc/src/main/java/com/longbridge/market/MarketContext.java b/java/javasrc/src/main/java/com/longbridge/market/MarketContext.java index 3cfecfd2b..6e2e348e9 100644 --- a/java/javasrc/src/main/java/com/longbridge/market/MarketContext.java +++ b/java/javasrc/src/main/java/com/longbridge/market/MarketContext.java @@ -65,4 +65,19 @@ public CompletableFuture getAnomaly(String market) throws OpenA public CompletableFuture getConstituent(String symbol) throws OpenApiException { return AsyncCallback.executeTask((callback) -> SdkNative.marketContextConstituent(raw, symbol, callback)); } + + /** Get top movers (stocks with unusual price movements) across one or more markets */ + public CompletableFuture getTopMovers(TopMoversOptions opts) throws OpenApiException { + return AsyncCallback.executeTask((callback) -> SdkNative.marketContextTopMovers(raw, opts, callback)); + } + + /** Get rank category keys for the popularity leaderboard. */ + public CompletableFuture getRankCategories() throws OpenApiException { + return AsyncCallback.executeTask((callback) -> SdkNative.marketContextRankCategories(raw, callback)); + } + + /** Get ranked stock list for a given category key (from getRankCategories). */ + public CompletableFuture getRankList(RankListOptions opts) throws OpenApiException { + return AsyncCallback.executeTask((callback) -> SdkNative.marketContextRankList(raw, opts, callback)); + } } diff --git a/java/javasrc/src/main/java/com/longbridge/market/RankCategoriesResponse.java b/java/javasrc/src/main/java/com/longbridge/market/RankCategoriesResponse.java new file mode 100644 index 000000000..3f88f3ce2 --- /dev/null +++ b/java/javasrc/src/main/java/com/longbridge/market/RankCategoriesResponse.java @@ -0,0 +1,7 @@ +package com.longbridge.market; + +/** Response for {@link MarketContext#getRankCategories}. Contains raw JSON data. */ +public class RankCategoriesResponse { + /** Raw JSON data string */ + public String data; +} diff --git a/java/javasrc/src/main/java/com/longbridge/market/RankListItem.java b/java/javasrc/src/main/java/com/longbridge/market/RankListItem.java new file mode 100644 index 000000000..d4ca86b3d --- /dev/null +++ b/java/javasrc/src/main/java/com/longbridge/market/RankListItem.java @@ -0,0 +1,37 @@ +package com.longbridge.market; + +/** One item in the popularity rank list. */ +public class RankListItem { + /** Symbol, e.g. "MU.US" (converted from counter_id) */ + public String symbol; + /** Ticker code */ + public String code; + /** Security name */ + public String name; + /** Latest price */ + public String lastDone; + /** Price change ratio (decimal) */ + public String chg; + /** Absolute price change */ + public String change; + /** Net inflow */ + public String inflow; + /** Market cap */ + public String marketCap; + /** Industry name */ + public String industry; + /** Pre/post market price */ + public String prePostPrice; + /** Pre/post market change */ + public String prePostChg; + /** Amplitude */ + public String amplitude; + /** 5-day change */ + public String fiveDayChg; + /** Turnover rate */ + public String turnoverRate; + /** Volume ratio */ + public String volumeRate; + /** P/B ratio TTM */ + public String pbTtm; +} diff --git a/java/javasrc/src/main/java/com/longbridge/market/RankListOptions.java b/java/javasrc/src/main/java/com/longbridge/market/RankListOptions.java new file mode 100644 index 000000000..65ce99097 --- /dev/null +++ b/java/javasrc/src/main/java/com/longbridge/market/RankListOptions.java @@ -0,0 +1,9 @@ +package com.longbridge.market; + +/** Options for {@link MarketContext#getRankList}. */ +public class RankListOptions { + /** Rank category key from getRankCategories, e.g. "ib_hot_all-us" */ + public String key; + /** Whether to include article content (default: false) */ + public boolean needArticle; +} diff --git a/java/javasrc/src/main/java/com/longbridge/market/RankListResponse.java b/java/javasrc/src/main/java/com/longbridge/market/RankListResponse.java new file mode 100644 index 000000000..12fd7095b --- /dev/null +++ b/java/javasrc/src/main/java/com/longbridge/market/RankListResponse.java @@ -0,0 +1,9 @@ +package com.longbridge.market; + +/** Response for {@link MarketContext#getRankList}. */ +public class RankListResponse { + /** Whether the response is delayed */ + public boolean bmp; + /** Ranked securities list */ + public RankListItem[] lists; +} diff --git a/java/javasrc/src/main/java/com/longbridge/market/TopMoversEvent.java b/java/javasrc/src/main/java/com/longbridge/market/TopMoversEvent.java new file mode 100644 index 000000000..121c642a9 --- /dev/null +++ b/java/javasrc/src/main/java/com/longbridge/market/TopMoversEvent.java @@ -0,0 +1,15 @@ +package com.longbridge.market; + +/** One top-movers event. */ +public class TopMoversEvent { + /** Event timestamp in RFC 3339 format */ + public String timestamp; + /** Alert reason description */ + public String alertReason; + /** Alert type code */ + public long alertType; + /** Stock information */ + public TopMoversStock stock; + /** Associated news post as JSON string (may be null) */ + public String post; +} diff --git a/java/javasrc/src/main/java/com/longbridge/market/TopMoversOptions.java b/java/javasrc/src/main/java/com/longbridge/market/TopMoversOptions.java new file mode 100644 index 000000000..7db9b9757 --- /dev/null +++ b/java/javasrc/src/main/java/com/longbridge/market/TopMoversOptions.java @@ -0,0 +1,29 @@ +package com.longbridge.market; + +/** Options for {@link MarketContext#getTopMovers}. */ +public class TopMoversOptions { + /** + * Market list, e.g. {@code ["HK", "US", "CN", "SG"]}. + * Pass {@code null} or an empty array to return all markets. + */ + public String[] markets; + + /** + * Sort order. + * 0 = time (newest first), 1 = price change, 2 = hotness (default). + * Pass {@code null} to use the server default. + */ + public Integer sort; + + /** + * Target date in {@code "YYYY-MM-DD"} format. + * Pass {@code null} to return the latest data. + */ + public String date; + + /** + * Maximum number of results to return. + * Pass {@code null} to use the server default (20). + */ + public Integer limit; +} diff --git a/java/javasrc/src/main/java/com/longbridge/market/TopMoversResponse.java b/java/javasrc/src/main/java/com/longbridge/market/TopMoversResponse.java new file mode 100644 index 000000000..980470ac9 --- /dev/null +++ b/java/javasrc/src/main/java/com/longbridge/market/TopMoversResponse.java @@ -0,0 +1,11 @@ +package com.longbridge.market; + +import com.longbridge.market.TopMoversEvent; + +/** Response for {@link MarketContext#getTopMovers}. */ +public class TopMoversResponse { + /** Top mover events */ + public TopMoversEvent[] events; + /** Pagination cursor (raw JSON); pass to next call for next page */ + public String nextParams; +} diff --git a/java/javasrc/src/main/java/com/longbridge/market/TopMoversStock.java b/java/javasrc/src/main/java/com/longbridge/market/TopMoversStock.java new file mode 100644 index 000000000..cf21598d9 --- /dev/null +++ b/java/javasrc/src/main/java/com/longbridge/market/TopMoversStock.java @@ -0,0 +1,23 @@ +package com.longbridge.market; + +/** Stock information in a top-movers event. */ +public class TopMoversStock { + /** Symbol, e.g. "TSLA.US" */ + public String symbol; + /** Ticker code */ + public String code; + /** Security name */ + public String name; + /** Full name */ + public String fullName; + /** Price change (decimal ratio) */ + public String change; + /** Latest price */ + public String lastDone; + /** Market */ + public String market; + /** Labels / tags */ + public String[] labels; + /** Logo URL */ + public String logo; +} diff --git a/java/javasrc/src/main/java/com/longbridge/quote/QuoteContext.java b/java/javasrc/src/main/java/com/longbridge/quote/QuoteContext.java index e08200245..a9e57abe1 100644 --- a/java/javasrc/src/main/java/com/longbridge/quote/QuoteContext.java +++ b/java/javasrc/src/main/java/com/longbridge/quote/QuoteContext.java @@ -1392,6 +1392,13 @@ public CompletableFuture getShortPositions(String symbol }); } + /** Get daily short sale volume for US or HK stocks (market auto-detected from symbol suffix). */ + public CompletableFuture getShortTrades(ShortTradesOptions opts) throws OpenApiException { + return AsyncCallback.executeTask((callback) -> { + SdkNative.quoteContextShortTrades(this.raw, opts, callback); + }); + } + /** * Get option volume statistics for a symbol * diff --git a/java/javasrc/src/main/java/com/longbridge/quote/ShortPositionsItem.java b/java/javasrc/src/main/java/com/longbridge/quote/ShortPositionsItem.java new file mode 100644 index 000000000..13b33e94a --- /dev/null +++ b/java/javasrc/src/main/java/com/longbridge/quote/ShortPositionsItem.java @@ -0,0 +1,26 @@ +package com.longbridge.quote; + +/** + * One short-position record, unified for US and HK markets. + * US-specific fields are empty for HK records and vice versa. + */ +public class ShortPositionsItem { + /** Trading date in RFC 3339 format, e.g. "2022-03-15T04:00:00Z" */ + public String timestamp; + /** Short ratio */ + public String rate; + /** Closing price */ + public String close; + /** [US] Number of short shares outstanding */ + public String currentSharesShort; + /** [US] Average daily share volume */ + public String avgDailyShareVolume; + /** [US] Days-to-cover ratio */ + public String daysToCover; + /** [HK] Short sale amount (HKD) */ + public String amount; + /** [HK] Short position balance */ + public String balance; + /** [HK] Closing price (HK naming) */ + public String cost; +} diff --git a/java/javasrc/src/main/java/com/longbridge/quote/ShortPositionsResponse.java b/java/javasrc/src/main/java/com/longbridge/quote/ShortPositionsResponse.java index 96529a26f..a2a321f57 100644 --- a/java/javasrc/src/main/java/com/longbridge/quote/ShortPositionsResponse.java +++ b/java/javasrc/src/main/java/com/longbridge/quote/ShortPositionsResponse.java @@ -1,7 +1,7 @@ package com.longbridge.quote; +/** Response for {@link QuoteContext#getShortPositions}. Unified US+HK response. */ public class ShortPositionsResponse { - public String symbol; - public ShortPosition[] data; - public int sources; + /** Short position records. US and HK fields populated depending on market. */ + public ShortPositionsItem[] data; } \ No newline at end of file diff --git a/java/javasrc/src/main/java/com/longbridge/quote/ShortTradesItem.java b/java/javasrc/src/main/java/com/longbridge/quote/ShortTradesItem.java new file mode 100644 index 000000000..8189bdceb --- /dev/null +++ b/java/javasrc/src/main/java/com/longbridge/quote/ShortTradesItem.java @@ -0,0 +1,23 @@ +package com.longbridge.quote; + +/** + * One short-trade record, unified for US and HK markets. + */ +public class ShortTradesItem { + /** Trading date in RFC 3339 format */ + public String timestamp; + /** Short ratio */ + public String rate; + /** Closing price */ + public String close; + /** [US] NASDAQ short sale volume */ + public String nusAmount; + /** [US] NYSE short sale volume */ + public String nyAmount; + /** [US] Total trading volume */ + public String totalAmount; + /** [HK] Short sale turnover amount (HKD) */ + public String amount; + /** [HK] Short position balance */ + public String balance; +} diff --git a/java/javasrc/src/main/java/com/longbridge/quote/ShortTradesOptions.java b/java/javasrc/src/main/java/com/longbridge/quote/ShortTradesOptions.java new file mode 100644 index 000000000..6b156aae4 --- /dev/null +++ b/java/javasrc/src/main/java/com/longbridge/quote/ShortTradesOptions.java @@ -0,0 +1,9 @@ +package com.longbridge.quote; + +/** Options for {@link QuoteContext#getShortTrades}. */ +public class ShortTradesOptions { + /** Security symbol (US or HK), e.g. "AAPL.US" or "700.HK" */ + public String symbol; + /** Number of records to return (1-100, default 20) */ + public int count; +} diff --git a/java/javasrc/src/main/java/com/longbridge/quote/ShortTradesResponse.java b/java/javasrc/src/main/java/com/longbridge/quote/ShortTradesResponse.java new file mode 100644 index 000000000..7e8b39361 --- /dev/null +++ b/java/javasrc/src/main/java/com/longbridge/quote/ShortTradesResponse.java @@ -0,0 +1,7 @@ +package com.longbridge.quote; + +/** Response for {@link QuoteContext#getShortTrades}. Unified US+HK response. */ +public class ShortTradesResponse { + /** Short trade records. US and HK fields populated depending on market. */ + public ShortTradesItem[] data; +} diff --git a/java/javasrc/src/main/java/com/longbridge/screener/ScreenerContext.java b/java/javasrc/src/main/java/com/longbridge/screener/ScreenerContext.java new file mode 100644 index 000000000..7fad58470 --- /dev/null +++ b/java/javasrc/src/main/java/com/longbridge/screener/ScreenerContext.java @@ -0,0 +1,47 @@ +package com.longbridge.screener; + +import java.util.concurrent.CompletableFuture; +import com.longbridge.*; + +/** + * Screener context — stock screener strategies, search, and indicator metadata. + */ +public class ScreenerContext implements AutoCloseable { + private long raw; + + public static ScreenerContext create(Config config) { + ScreenerContext ctx = new ScreenerContext(); + ctx.raw = SdkNative.newScreenerContext(config.getRaw()); + return ctx; + } + + @Override + public void close() throws Exception { + SdkNative.freeScreenerContext(raw); + } + + /** Get platform-recommended screener strategies. */ + public CompletableFuture getRecommendStrategies() throws OpenApiException { + return AsyncCallback.executeTask((callback) -> SdkNative.screenerContextRecommendStrategies(raw, callback)); + } + + /** Get the current user's saved screener strategies. */ + public CompletableFuture getUserStrategies() throws OpenApiException { + return AsyncCallback.executeTask((callback) -> SdkNative.screenerContextUserStrategies(raw, callback)); + } + + /** Get detail for one screener strategy by ID. */ + public CompletableFuture getStrategy(ScreenerStrategyOptions opts) throws OpenApiException { + return AsyncCallback.executeTask((callback) -> SdkNative.screenerContextStrategy(raw, opts, callback)); + } + + /** Search / screen securities using a strategy ID or custom filters. */ + public CompletableFuture search(ScreenerSearchOptions opts) throws OpenApiException { + return AsyncCallback.executeTask((callback) -> SdkNative.screenerContextSearch(raw, opts, callback)); + } + + /** Get all available screener indicator definitions. */ + public CompletableFuture getIndicators() throws OpenApiException { + return AsyncCallback.executeTask((callback) -> SdkNative.screenerContextIndicators(raw, callback)); + } +} diff --git a/java/javasrc/src/main/java/com/longbridge/screener/ScreenerIndicatorsResponse.java b/java/javasrc/src/main/java/com/longbridge/screener/ScreenerIndicatorsResponse.java new file mode 100644 index 000000000..05fbd073c --- /dev/null +++ b/java/javasrc/src/main/java/com/longbridge/screener/ScreenerIndicatorsResponse.java @@ -0,0 +1,7 @@ +package com.longbridge.screener; + +/** Response for screener indicators list. Contains raw JSON data. */ +public class ScreenerIndicatorsResponse { + /** Raw JSON data string */ + public String data; +} diff --git a/java/javasrc/src/main/java/com/longbridge/screener/ScreenerRecommendStrategiesResponse.java b/java/javasrc/src/main/java/com/longbridge/screener/ScreenerRecommendStrategiesResponse.java new file mode 100644 index 000000000..81c6a8ab5 --- /dev/null +++ b/java/javasrc/src/main/java/com/longbridge/screener/ScreenerRecommendStrategiesResponse.java @@ -0,0 +1,7 @@ +package com.longbridge.screener; + +/** Response for {@link ScreenerContext#getRecommendStrategies}. Contains raw JSON data. */ +public class ScreenerRecommendStrategiesResponse { + /** Raw JSON data string */ + public String data; +} diff --git a/java/javasrc/src/main/java/com/longbridge/screener/ScreenerSearchOptions.java b/java/javasrc/src/main/java/com/longbridge/screener/ScreenerSearchOptions.java new file mode 100644 index 000000000..768684f18 --- /dev/null +++ b/java/javasrc/src/main/java/com/longbridge/screener/ScreenerSearchOptions.java @@ -0,0 +1,13 @@ +package com.longbridge.screener; + +/** Options for {@link ScreenerContext#search}. */ +public class ScreenerSearchOptions { + /** Market: "US", "HK", "CN", or "SG" */ + public String market; + /** Strategy ID (optional; null for custom filter mode) */ + public Long strategyId; + /** Page number (1-indexed, default 1) */ + public int page = 1; + /** Page size (default 20) */ + public int size = 20; +} diff --git a/java/javasrc/src/main/java/com/longbridge/screener/ScreenerSearchResponse.java b/java/javasrc/src/main/java/com/longbridge/screener/ScreenerSearchResponse.java new file mode 100644 index 000000000..2942dba7c --- /dev/null +++ b/java/javasrc/src/main/java/com/longbridge/screener/ScreenerSearchResponse.java @@ -0,0 +1,7 @@ +package com.longbridge.screener; + +/** Response for screener search. Contains raw JSON data. */ +public class ScreenerSearchResponse { + /** Raw JSON data string */ + public String data; +} diff --git a/java/javasrc/src/main/java/com/longbridge/screener/ScreenerStrategyOptions.java b/java/javasrc/src/main/java/com/longbridge/screener/ScreenerStrategyOptions.java new file mode 100644 index 000000000..41ccee8d5 --- /dev/null +++ b/java/javasrc/src/main/java/com/longbridge/screener/ScreenerStrategyOptions.java @@ -0,0 +1,7 @@ +package com.longbridge.screener; + +/** Options for {@link ScreenerContext#getStrategy}. */ +public class ScreenerStrategyOptions { + /** Strategy ID from getRecommendStrategies or getUserStrategies */ + public long id; +} diff --git a/java/javasrc/src/main/java/com/longbridge/screener/ScreenerStrategyResponse.java b/java/javasrc/src/main/java/com/longbridge/screener/ScreenerStrategyResponse.java new file mode 100644 index 000000000..61c7e7690 --- /dev/null +++ b/java/javasrc/src/main/java/com/longbridge/screener/ScreenerStrategyResponse.java @@ -0,0 +1,7 @@ +package com.longbridge.screener; + +/** Response for screener strategy detail. Contains raw JSON data. */ +public class ScreenerStrategyResponse { + /** Raw JSON data string */ + public String data; +} diff --git a/java/javasrc/src/main/java/com/longbridge/screener/ScreenerUserStrategiesResponse.java b/java/javasrc/src/main/java/com/longbridge/screener/ScreenerUserStrategiesResponse.java new file mode 100644 index 000000000..fdb22a728 --- /dev/null +++ b/java/javasrc/src/main/java/com/longbridge/screener/ScreenerUserStrategiesResponse.java @@ -0,0 +1,7 @@ +package com.longbridge.screener; + +/** Response for {@link ScreenerContext#getUserStrategies}. Contains raw JSON data. */ +public class ScreenerUserStrategiesResponse { + /** Raw JSON data string */ + public String data; +} diff --git a/java/src/fundamental_context.rs b/java/src/fundamental_context.rs index 52caff15b..95c39a995 100644 --- a/java/src/fundamental_context.rs +++ b/java/src/fundamental_context.rs @@ -9,7 +9,7 @@ use longbridge::{Config, FundamentalContext, fundamental::types::*}; use crate::{ async_util, error::jni_result, - types::{FromJValue, get_field}, + types::{FromJValue, ObjectArray, get_field}, }; struct ContextObj { @@ -194,7 +194,7 @@ pub unsafe extern "system" fn Java_com_longbridge_SdkNative_fundamentalContextGe } #[unsafe(no_mangle)] -pub unsafe extern "system" fn Java_com_longbridge_SdkNative_fundamentalContextGetBusinessSegments( +pub unsafe extern "system" fn Java_com_longbridge_SdkNative_fundamentalContextShareholderTop( mut env: JNIEnv, _class: JClass, context: i64, @@ -205,7 +205,7 @@ pub unsafe extern "system" fn Java_com_longbridge_SdkNative_fundamentalContextGe let context = &*(context as *const ContextObj); let symbol: String = FromJValue::from_jvalue(env, symbol.into())?; async_util::execute(env, callback, async move { - let resp = context.ctx.business_segments(symbol).await?; + let resp = context.ctx.shareholder_top(symbol).await?; Ok(resp) })?; Ok(()) @@ -213,98 +213,19 @@ pub unsafe extern "system" fn Java_com_longbridge_SdkNative_fundamentalContextGe } #[unsafe(no_mangle)] -pub unsafe extern "system" fn Java_com_longbridge_SdkNative_fundamentalContextGetBusinessSegmentsHistory( - mut env: JNIEnv, - _class: JClass, - context: i64, - opts: JObject, - callback: JObject, -) { - jni_result(&mut env, (), |env| { - let context = &*(context as *const ContextObj); - let symbol: String = get_field(env, &opts, "symbol")?; - let report: Option = get_field(env, &opts, "report")?; - let cate: Option = get_field(env, &opts, "cate")?; - let report_static: Option<&'static str> = match report.as_deref() { - Some("qf") => Some("qf"), - Some("saf") => Some("saf"), - Some("af") => Some("af"), - _ => None, - }; - async_util::execute(env, callback, async move { - let resp = context - .ctx - .business_segments_history(symbol, report_static, cate) - .await?; - Ok(resp) - })?; - Ok(()) - }) -} - -#[unsafe(no_mangle)] -pub unsafe extern "system" fn Java_com_longbridge_SdkNative_fundamentalContextGetInstitutionRatingViews( +pub unsafe extern "system" fn Java_com_longbridge_SdkNative_fundamentalContextShareholderDetail( mut env: JNIEnv, _class: JClass, context: i64, symbol: JObject, + object_id: i64, callback: JObject, ) { jni_result(&mut env, (), |env| { let context = &*(context as *const ContextObj); let symbol: String = FromJValue::from_jvalue(env, symbol.into())?; async_util::execute(env, callback, async move { - let resp = context.ctx.institution_rating_views(symbol).await?; - Ok(resp) - })?; - Ok(()) - }) -} - -#[unsafe(no_mangle)] -pub unsafe extern "system" fn Java_com_longbridge_SdkNative_fundamentalContextGetIndustryRank( - mut env: JNIEnv, - _class: JClass, - context: i64, - opts: JObject, - callback: JObject, -) { - jni_result(&mut env, (), |env| { - let context = &*(context as *const ContextObj); - let market: String = get_field(env, &opts, "market")?; - let indicator: String = get_field(env, &opts, "indicator")?; - let sort_type: String = get_field(env, &opts, "sortType")?; - let limit: i32 = get_field(env, &opts, "limit")?; - let limit = limit.max(0) as u32; - async_util::execute(env, callback, async move { - let resp = context - .ctx - .industry_rank(market, indicator, sort_type, limit) - .await?; - Ok(resp) - })?; - Ok(()) - }) -} - -#[unsafe(no_mangle)] -pub unsafe extern "system" fn Java_com_longbridge_SdkNative_fundamentalContextGetIndustryPeers( - mut env: JNIEnv, - _class: JClass, - context: i64, - opts: JObject, - callback: JObject, -) { - jni_result(&mut env, (), |env| { - let context = &*(context as *const ContextObj); - let counter_id: String = get_field(env, &opts, "counterId")?; - let market: String = get_field(env, &opts, "market")?; - let industry_id: Option = get_field(env, &opts, "industryId")?; - async_util::execute(env, callback, async move { - let resp = context - .ctx - .industry_peers(counter_id, market, industry_id) - .await?; + let resp = context.ctx.shareholder_detail(symbol, object_id).await?; Ok(resp) })?; Ok(()) @@ -312,39 +233,29 @@ pub unsafe extern "system" fn Java_com_longbridge_SdkNative_fundamentalContextGe } #[unsafe(no_mangle)] -pub unsafe extern "system" fn Java_com_longbridge_SdkNative_fundamentalContextGetFinancialReportSnapshot( +pub unsafe extern "system" fn Java_com_longbridge_SdkNative_fundamentalContextValuationComparison( mut env: JNIEnv, _class: JClass, context: i64, - opts: JObject, + symbol: JObject, + currency: JObject, + comparison_symbols: JObject, callback: JObject, ) { jni_result(&mut env, (), |env| { let context = &*(context as *const ContextObj); - let symbol: String = get_field(env, &opts, "symbol")?; - let report: Option = get_field(env, &opts, "report")?; - let fiscal_year: Option = get_field(env, &opts, "fiscalYear")?; - let fiscal_period: Option = get_field(env, &opts, "fiscalPeriod")?; - let report_static: Option<&'static str> = match report.as_deref() { - Some("qf") => Some("qf"), - Some("saf") => Some("saf"), - Some("af") => Some("af"), - _ => None, - }; - let fiscal_period_static: Option<&'static str> = match fiscal_period.as_deref() { - Some("q1") => Some("q1"), - Some("q2") => Some("q2"), - Some("q3") => Some("q3"), - Some("q4") => Some("q4"), - Some("fy") => Some("fy"), - Some("h1") => Some("h1"), - Some("h2") => Some("h2"), - _ => None, + let symbol: String = FromJValue::from_jvalue(env, symbol.into())?; + let currency: String = FromJValue::from_jvalue(env, currency.into())?; + let comparison_syms: Option> = if comparison_symbols.is_null() { + None + } else { + let arr: ObjectArray = FromJValue::from_jvalue(env, comparison_symbols.into())?; + Some(arr.0) }; async_util::execute(env, callback, async move { let resp = context .ctx - .financial_report_snapshot(symbol, report_static, fiscal_year, fiscal_period_static) + .valuation_comparison(symbol, currency, comparison_syms) .await?; Ok(resp) })?; diff --git a/java/src/init.rs b/java/src/init.rs index 2fc4dbf2a..2e03effa1 100644 --- a/java/src/init.rs +++ b/java/src/init.rs @@ -366,8 +366,10 @@ pub extern "system" fn Java_com_longbridge_SdkNative_init<'a>( longbridge::fundamental::OperatingFinancial, longbridge::fundamental::OperatingIndicator, // QuoteContext extensions + longbridge::quote::ShortPositionsItem, longbridge::quote::ShortPositionsResponse, - longbridge::quote::ShortPosition, + longbridge::quote::ShortTradesItem, + longbridge::quote::ShortTradesResponse, longbridge::quote::OptionVolumeStats, longbridge::quote::OptionVolumeDaily, longbridge::quote::OptionVolumeDailyStat, @@ -382,23 +384,25 @@ pub extern "system" fn Java_com_longbridge_SdkNative_init<'a>( longbridge::fundamental::RatingSubIndicatorGroup, longbridge::fundamental::RatingCategory, longbridge::fundamental::StockRatings, - // FundamentalContext: new APIs - longbridge::fundamental::BusinessSegmentItem, - longbridge::fundamental::BusinessSegments, - longbridge::fundamental::BusinessSegmentHistoryItem, - longbridge::fundamental::BusinessSegmentsHistoricalItem, - longbridge::fundamental::BusinessSegmentsHistory, - longbridge::fundamental::InstitutionRatingViewItem, - longbridge::fundamental::InstitutionRatingViews, - longbridge::fundamental::IndustryRankItem, - longbridge::fundamental::IndustryRankGroup, - longbridge::fundamental::IndustryRankResponse, - longbridge::fundamental::IndustryPeersTop, - longbridge::fundamental::IndustryPeerNode, - longbridge::fundamental::IndustryPeersResponse, - longbridge::fundamental::SnapshotForecastMetric, - longbridge::fundamental::SnapshotReportedMetric, - longbridge::fundamental::FinancialReportSnapshot, + // FundamentalContext: shareholders / valuation comparison + longbridge::fundamental::ShareholderTopResponse, + longbridge::fundamental::ShareholderDetailResponse, + longbridge::fundamental::ValuationHistoryPoint, + longbridge::fundamental::ValuationComparisonItem, + longbridge::fundamental::ValuationComparisonResponse, + // MarketContext: top movers / rank + longbridge::market::TopMoversStock, + longbridge::market::TopMoversEvent, + longbridge::market::TopMoversResponse, + longbridge::market::RankCategoriesResponse, + longbridge::market::RankListItem, + longbridge::market::RankListResponse, + // ScreenerContext + longbridge::screener::ScreenerRecommendStrategiesResponse, + 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..a19651cfd 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_marketContextTopMovers( + 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.top_movers(markets, sort, date, limit).await?; + Ok(resp) + })?; + Ok(()) + }) +} + +#[unsafe(no_mangle)] +pub unsafe extern "system" fn Java_com_longbridge_SdkNative_marketContextRankCategories( + mut env: JNIEnv, + _class: JClass, + context: i64, + callback: JObject, +) { + jni_result(&mut env, (), |env| { + let context = &*(context as *const ContextObj); + async_util::execute(env, callback, async move { + let resp = context.ctx.rank_categories().await?; + Ok(resp) + })?; + Ok(()) + }) +} + +#[unsafe(no_mangle)] +pub unsafe extern "system" fn Java_com_longbridge_SdkNative_marketContextRankList( + mut env: JNIEnv, + _class: JClass, + context: i64, + key: JObject, + need_article: bool, + callback: JObject, +) { + jni_result(&mut env, (), |env| { + let context = &*(context as *const ContextObj); + let key: String = FromJValue::from_jvalue(env, key.into())?; + async_util::execute(env, callback, async move { + let resp = context.ctx.rank_list(key, need_article).await?; + Ok(resp) + })?; + Ok(()) + }) +} diff --git a/java/src/quote_context.rs b/java/src/quote_context.rs index eca61b6f6..167942ab7 100644 --- a/java/src/quote_context.rs +++ b/java/src/quote_context.rs @@ -1201,13 +1201,36 @@ pub unsafe extern "system" fn Java_com_longbridge_SdkNative_quoteContextShortPos _class: JClass, context: i64, symbol: JObject, + count: i32, + callback: JObject, +) { + jni_result(&mut env, (), |env| { + let context = &*(context as *const ContextObj); + let symbol: String = FromJValue::from_jvalue(env, symbol.into())?; + let count = count.max(1) as u32; + async_util::execute(env, callback, async move { + let resp = context.ctx.short_positions(symbol, count).await?; + Ok(resp) + })?; + Ok(()) + }) +} + +#[unsafe(no_mangle)] +pub unsafe extern "system" fn Java_com_longbridge_SdkNative_quoteContextShortTrades( + mut env: JNIEnv, + _class: JClass, + context: i64, + symbol: JObject, + count: i32, callback: JObject, ) { jni_result(&mut env, (), |env| { let context = &*(context as *const ContextObj); let symbol: String = FromJValue::from_jvalue(env, symbol.into())?; + let count = count.max(1) as u32; async_util::execute(env, callback, async move { - let resp = context.ctx.short_positions(symbol).await?; + let resp = context.ctx.short_trades(symbol, count).await?; Ok(resp) })?; Ok(()) diff --git a/java/src/screener_context.rs b/java/src/screener_context.rs new file mode 100644 index 000000000..c6b66330d --- /dev/null +++ b/java/src/screener_context.rs @@ -0,0 +1,133 @@ +use std::sync::Arc; + +use jni::{ + JNIEnv, + objects::{JClass, JObject}, +}; +use longbridge::{Config, ScreenerContext}; + +use crate::{ + async_util, + error::jni_result, + types::{JavaInteger, get_field}, +}; + +struct ContextObj { + ctx: ScreenerContext, +} + +#[unsafe(no_mangle)] +pub unsafe extern "system" fn Java_com_longbridge_SdkNative_newScreenerContext( + _env: JNIEnv, + _class: JClass, + config: i64, +) -> i64 { + let config = &*(config as *const Config); + let ctx = ScreenerContext::new(Arc::new(config.clone())); + Box::into_raw(Box::new(ContextObj { ctx })) as i64 +} + +#[unsafe(no_mangle)] +pub unsafe extern "system" fn Java_com_longbridge_SdkNative_freeScreenerContext( + _env: JNIEnv, + _class: JClass, + context: i64, +) { + let _ = Box::from_raw(context as *mut ContextObj); +} + +#[unsafe(no_mangle)] +pub unsafe extern "system" fn Java_com_longbridge_SdkNative_screenerContextRecommendStrategies( + mut env: JNIEnv, + _class: JClass, + context: i64, + callback: JObject, +) { + jni_result(&mut env, (), |env| { + let context = &*(context as *const ContextObj); + async_util::execute(env, callback, async move { + let resp = context.ctx.screener_recommend_strategies().await?; + Ok(resp) + })?; + Ok(()) + }) +} + +#[unsafe(no_mangle)] +pub unsafe extern "system" fn Java_com_longbridge_SdkNative_screenerContextUserStrategies( + mut env: JNIEnv, + _class: JClass, + context: i64, + callback: JObject, +) { + jni_result(&mut env, (), |env| { + let context = &*(context as *const ContextObj); + async_util::execute(env, callback, async move { + let resp = context.ctx.screener_user_strategies().await?; + Ok(resp) + })?; + Ok(()) + }) +} + +#[unsafe(no_mangle)] +pub unsafe extern "system" fn Java_com_longbridge_SdkNative_screenerContextStrategy( + mut env: JNIEnv, + _class: JClass, + context: i64, + id: i64, + callback: JObject, +) { + jni_result(&mut env, (), |env| { + let context = &*(context as *const ContextObj); + async_util::execute(env, callback, async move { + let resp = context.ctx.screener_strategy(id).await?; + Ok(resp) + })?; + Ok(()) + }) +} + +#[unsafe(no_mangle)] +pub unsafe extern "system" fn Java_com_longbridge_SdkNative_screenerContextSearch( + mut env: JNIEnv, + _class: JClass, + context: i64, + opts: JObject, + callback: JObject, +) { + jni_result(&mut env, (), |env| { + let context = &*(context as *const ContextObj); + let market: String = get_field(env, &opts, "market")?; + let strategy_id: Option = get_field(env, &opts, "strategyId")?; + let page_opt: Option = get_field(env, &opts, "page")?; + let page = page_opt.map(i32::from).unwrap_or(1) as u32; + let size_opt: Option = get_field(env, &opts, "size")?; + let size = size_opt.map(i32::from).unwrap_or(20) as u32; + async_util::execute(env, callback, async move { + let resp = context + .ctx + .screener_search(market, strategy_id, page, size) + .await?; + Ok(resp) + })?; + Ok(()) + }) +} + +#[unsafe(no_mangle)] +pub unsafe extern "system" fn Java_com_longbridge_SdkNative_screenerContextIndicators( + mut env: JNIEnv, + _class: JClass, + context: i64, + callback: JObject, +) { + jni_result(&mut env, (), |env| { + let context = &*(context as *const ContextObj); + async_util::execute(env, callback, async move { + let resp = context.ctx.screener_indicators().await?; + Ok(resp) + })?; + Ok(()) + }) +} diff --git a/java/src/types/classes.rs b/java/src/types/classes.rs index f09a01e89..2b1dbab87 100644 --- a/java/src/types/classes.rs +++ b/java/src/types/classes.rs @@ -2227,27 +2227,52 @@ impl_java_class!( // ── QuoteContext extensions ─────────────────────────────────────── +impl_java_class!( + "com/longbridge/quote/ShortPositionsItem", + longbridge::quote::ShortPositionsItem, + [ + timestamp, + rate, + close, + current_shares_short, + avg_daily_share_volume, + days_to_cover, + amount, + balance, + cost + ] +); + impl_java_class!( "com/longbridge/quote/ShortPositionsResponse", longbridge::quote::ShortPositionsResponse, [ - symbol, #[java(objarray)] - data, - sources + data ] ); impl_java_class!( - "com/longbridge/quote/ShortPosition", - longbridge::quote::ShortPosition, + "com/longbridge/quote/ShortTradesItem", + longbridge::quote::ShortTradesItem, [ timestamp, rate, - avg_daily_share_volume, - current_shares_short, - days_to_cover, - close + close, + nus_amount, + ny_amount, + total_amount, + amount, + balance + ] +); + +impl_java_class!( + "com/longbridge/quote/ShortTradesResponse", + longbridge::quote::ShortTradesResponse, + [ + #[java(objarray)] + data ] ); @@ -2619,3 +2644,161 @@ 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/ValuationHistoryPoint", + longbridge::fundamental::ValuationHistoryPoint, + [date, pe, pb, ps] +); + +impl_java_class!( + "com/longbridge/fundamental/ValuationComparisonItem", + longbridge::fundamental::ValuationComparisonItem, + [ + symbol, + name, + currency, + market_value, + price_close, + pe, + pb, + ps, + roe, + eps, + bps, + dps, + div_yld, + assets, + #[java(objarray)] + history + ] +); + +impl_java_class!( + "com/longbridge/fundamental/ValuationComparisonResponse", + longbridge::fundamental::ValuationComparisonResponse, + [ + #[java(objarray)] + list + ] +); + +// ── MarketContext: top movers / rank ────────────────────────────── + +impl_java_class!( + "com/longbridge/market/TopMoversStock", + longbridge::market::TopMoversStock, + [ + symbol, + code, + name, + full_name, + change, + last_done, + market, + #[java(objarray)] + labels, + logo + ] +); + +impl_java_class!( + "com/longbridge/market/TopMoversEvent", + longbridge::market::TopMoversEvent, + [timestamp, alert_reason, alert_type, stock, post] +); + +impl_java_class!( + "com/longbridge/market/TopMoversResponse", + longbridge::market::TopMoversResponse, + [ + #[java(objarray)] + events, + next_params + ] +); + +impl_java_class!( + "com/longbridge/market/RankCategoriesResponse", + longbridge::market::RankCategoriesResponse, + [data] +); + +impl_java_class!( + "com/longbridge/market/RankListItem", + longbridge::market::RankListItem, + [ + symbol, + code, + name, + last_done, + chg, + change, + inflow, + market_cap, + industry, + pre_post_price, + pre_post_chg, + amplitude, + five_day_chg, + turnover_rate, + volume_rate, + pb_ttm + ] +); + +impl_java_class!( + "com/longbridge/market/RankListResponse", + longbridge::market::RankListResponse, + [ + bmp, + #[java(objarray)] + lists + ] +); + +// ── ScreenerContext ─────────────────────────────────────────────── + +impl_java_class!( + "com/longbridge/screener/ScreenerRecommendStrategiesResponse", + longbridge::screener::ScreenerRecommendStrategiesResponse, + [data] +); + +impl_java_class!( + "com/longbridge/screener/ScreenerUserStrategiesResponse", + longbridge::screener::ScreenerUserStrategiesResponse, + [data] +); + +impl_java_class!( + "com/longbridge/screener/ScreenerStrategyResponse", + longbridge::screener::ScreenerStrategyResponse, + [data] +); + +impl_java_class!( + "com/longbridge/screener/ScreenerSearchResponse", + longbridge::screener::ScreenerSearchResponse, + [data] +); + +impl_java_class!( + "com/longbridge/screener/ScreenerIndicatorsResponse", + longbridge::screener::ScreenerIndicatorsResponse, + [data] +); diff --git a/nodejs/index.d.ts b/nodejs/index.d.ts index 4495ed531..8650cc6be 100644 --- a/nodejs/index.d.ts +++ b/nodejs/index.d.ts @@ -607,18 +607,12 @@ export declare class FundamentalContext { buyback(symbol: string): Promise /** Get stock ratings for a security */ ratings(symbol: string): Promise - /** Get business segment breakdowns (latest snapshot) */ - businessSegments(symbol: string): Promise - /** Get historical business segment breakdowns */ - businessSegmentsHistory(symbol: string, report?: string | undefined | null, cate?: string | undefined | null): Promise - /** Get historical institutional rating view time-series */ - institutionRatingViews(symbol: string): Promise - /** Get industry rank for a market */ - industryRank(market: string, indicator: string, sortType: string, limit: number): Promise - /** Get the industry peer chain for a security or industry */ - industryPeers(counterId: string, market: string, industryId?: string | undefined | null): Promise - /** Get a financial report snapshot (earnings snapshot) */ - financialReportSnapshot(symbol: string, report?: string | undefined | null, fiscalYear?: number | undefined | null, fiscalPeriod?: string | undefined | null): Promise + /** Get ranked list of top shareholders */ + shareholderTop(symbol: string): Promise + /** Get holding history and detail for one shareholder */ + shareholderDetail(symbol: string, objectId: number): Promise + /** Get valuation comparison between a security and optional peers */ + valuationComparison(symbol: string, currency: string, comparisonSymbols?: Array | undefined | null): Promise } /** Fund position */ @@ -777,6 +771,15 @@ export declare class MarketContext { anomaly(market: string): Promise /** Get index constituent stocks */ constituent(symbol: string): Promise + /** + * Get top movers (stocks with unusual price movements) across one or more + * markets + */ + topMovers(markets: Array, sort: number, date: string | undefined | null, limit: number): Promise + /** Get all available rank category keys and labels */ + rankCategories(): Promise + /** Get a ranked list of securities for the given category key */ + rankList(key: string, needArticle: boolean): Promise } /** Market temperature */ @@ -2002,8 +2005,18 @@ export declare class QuoteContext { * ``` */ realtimeCandlesticks(symbol: string, period: Period, count: number): Promise> - /** Get short interest data for a US security */ - shortPositions(symbol: string): Promise + /** + * Get short interest data for a US or HK security. + * + * Market is inferred from the symbol suffix (.HK → HK, otherwise US). + */ + shortPositions(symbol: string, count: number): Promise + /** + * Get short trade records for a HK or US security. + * + * Market is inferred from the symbol suffix (.HK → HK, otherwise US). + */ + shortTrades(symbol: string, count: number): Promise /** Get real-time option call/put volume */ optionVolume(symbol: string): Promise /** Get daily historical option volume */ @@ -2049,6 +2062,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 @@ -3166,41 +3195,6 @@ export interface BrokerHoldingTop { updatedAt: string } -/** One business/regional segment item in a historical snapshot */ -export interface BusinessSegmentHistoryItem { - name: string - percent: string - value: string -} - -/** One business segment item (latest snapshot) */ -export interface BusinessSegmentItem { - name: string - percent: string -} - -/** Business segments response */ -export interface BusinessSegments { - date: string - total: string - currency: string - business: Array -} - -/** One historical business segments snapshot */ -export interface BusinessSegmentsHistoricalItem { - date: string - total: string - currency: string - business: Array - regionals: Array -} - -/** Business segments history response */ -export interface BusinessSegmentsHistory { - historical: Array -} - /** Buyback data response */ export interface BuybackData { recentBuybacks?: RecentBuybacks @@ -3990,32 +3984,6 @@ export interface FinancialReports { list: any } -/** Financial report snapshot response */ -export interface FinancialReportSnapshot { - name: string - ticker: string - fpStart: string - fpEnd: string - currency: string - reportDesc: string - foRevenue?: SnapshotForecastMetric - foEbit?: SnapshotForecastMetric - foEps?: SnapshotForecastMetric - frRevenue?: SnapshotReportedMetric - frProfit?: SnapshotReportedMetric - frOperateCash?: SnapshotReportedMetric - frInvestCash?: SnapshotReportedMetric - frFinanceCash?: SnapshotReportedMetric - frTotalAssets?: SnapshotReportedMetric - frTotalLiability?: SnapshotReportedMetric - frRoeTtm: string - frProfitMargin: string - frProfitMarginTtm: string - frAssetTurnTtm: string - frLeverageTtm: string - frDebtAssetsRatio: string -} - export declare const enum FlowDirection { /** Unknown */ Unknown = 0, @@ -4204,55 +4172,6 @@ export interface IndexConstituents { stocks: Array } -/** - * A node in the recursive industry peer chain. - * - * `nextJson` contains the child nodes serialised as a JSON string. - */ -export interface IndustryPeerNode { - name: string - counterId: string - stockNum: number - chg: string - ytdChg: string - /** Child nodes as a JSON string */ - nextJson: string -} - -/** Industry peers response */ -export interface IndustryPeersResponse { - top: IndustryPeersTop - chain?: IndustryPeerNode -} - -/** Top-level industry info in the peers response */ -export interface IndustryPeersTop { - name: string - market: string -} - -/** A group of ranked industry items */ -export interface IndustryRankGroup { - lists: Array -} - -/** One ranked industry item */ -export interface IndustryRankItem { - name: string - counterId: string - chg: string - leadingName: string - leadingTicker: string - leadingChg: string - valueName: string - valueData: string -} - -/** Industry rank response */ -export interface IndustryRankResponse { - items: Array -} - /** Industry valuation distribution response */ export interface IndustryValuationDist { /** PE distribution */ @@ -4417,22 +4336,6 @@ export interface InstitutionRatingSummary { updatedAt: string } -/** One historical rating distribution snapshot */ -export interface InstitutionRatingViewItem { - date: string - buy: string - over: string - hold: string - under: string - sell: string - total: string -} - -/** Institution rating views response */ -export interface InstitutionRatingViews { - elist: Array -} - export declare const enum InstitutionRecommend { /** Unknown */ Unknown = 0, @@ -5069,6 +4972,56 @@ 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 +} + +/** One ranked security item. */ +export interface RankListItem { + /** Symbol (e.g. `"MU.US"`) */ + symbol: string + /** Ticker code */ + code: string + /** Security name */ + name: string + /** Latest price */ + lastDone: string + /** Price change ratio */ + chg: string + /** Absolute price change */ + change: string + /** Net inflow */ + inflow: string + /** Market cap */ + marketCap: string + /** Industry name */ + industry: string + /** Pre/post market price */ + prePostPrice: string + /** Pre/post market change */ + prePostChg: string + /** Amplitude */ + amplitude: string + /** 5-day change */ + fiveDayChg: string + /** Turnover rate */ + turnoverRate: string + /** Volume ratio */ + volumeRate: string + /** P/B ratio (TTM) */ + pbTtm: string +} + +/** Rank list response. */ +export interface RankListResponse { + /** Whether delayed / BMP data */ + bmp: boolean + /** Ranked security items */ + lists: Array +} + /** Analyst rating distribution counts */ export interface RatingEvaluate { /** Number of "Buy" ratings */ @@ -5154,6 +5107,36 @@ export interface ReplaceOrderOptions { remark?: string } +/** Screener indicator definitions response. `data` is a JSON string. */ +export interface ScreenerIndicatorsResponse { + /** Raw indicator definitions data (JSON string) */ + data: string +} + +/** Recommended screener strategies response. `data` is a JSON string. */ +export interface ScreenerRecommendStrategiesResponse { + /** Raw recommended strategies data (JSON string) */ + data: string +} + +/** Screener search results response. `data` is a JSON string. */ +export interface ScreenerSearchResponse { + /** Raw search results data (JSON string) */ + data: string +} + +/** Single screener strategy response. `data` is a JSON string. */ +export interface ScreenerStrategyResponse { + /** Raw strategy detail data (JSON string) */ + data: string +} + +/** User screener strategies response. `data` is a JSON string. */ +export interface ScreenerUserStrategiesResponse { + /** Raw user strategies data (JSON string) */ + data: string +} + /** Securities update mode */ export declare const enum SecuritiesUpdateMode { /** Add securities */ @@ -5246,6 +5229,12 @@ export interface Shareholder { stocks: Array } +/** Shareholder detail response. `data` is a JSON string. */ +export interface ShareholderDetailResponse { + /** Raw shareholder detail data (JSON string) */ + data: string +} + /** Shareholder list response */ export interface ShareholderList { /** Major shareholders */ @@ -5268,6 +5257,12 @@ export interface ShareholderStock { chg: string } +/** Top-shareholder list response. `data` is a JSON string. */ +export interface ShareholderTopResponse { + /** Raw top-shareholder data (JSON string) */ + data: string +} + /** Sharelist detail response */ export interface SharelistDetail { /** Sharelist info */ @@ -5350,44 +5345,58 @@ export interface SharelistStock { latency?: boolean } -/** One short position data point */ -export interface ShortPosition { - /** Settlement date timestamp string */ +/** One short-position data point (unified for US and HK markets). */ +export interface ShortPositionsItem { + /** Trading date (RFC 3339) */ timestamp: string /** Short ratio */ rate: string - /** Avg daily share volume */ - avgDailyShareVolume: string - /** Current shares short */ - currentSharesShort: string - /** Days to cover */ - daysToCover: string /** Closing price */ close: string + /** [US] Number of short shares outstanding */ + currentSharesShort: string + /** [US] Average daily share volume */ + avgDailyShareVolume: string + /** [US] Days to cover ratio */ + daysToCover: string + /** [HK] Short sale amount (HKD) */ + amount: string + /** [HK] Short position balance */ + balance: string + /** [HK] Cost / closing price */ + cost: string } -/** Short interest response */ +/** Short interest / positions response (HK or US). */ export interface ShortPositionsResponse { - /** Security symbol */ - symbol: string - /** Data points */ - data: Array - /** Number of sources */ - sources: number + /** Short position data points */ + data: Array } -/** A forecast metric in the financial report snapshot */ -export interface SnapshotForecastMetric { - value: string - yoy: string - cmpDesc: string - estValue: string +/** One short-trade data point (unified for US and HK markets). */ +export interface ShortTradesItem { + /** Trading date (RFC 3339) */ + timestamp: string + /** Short ratio */ + rate: string + /** Closing price */ + close: string + /** [US] NYSE short amount */ + nusAmount: string + /** [US] NY short amount */ + nyAmount: string + /** [US] Total short amount */ + totalAmount: string + /** [HK] Short sale amount */ + amount: string + /** [HK] Short position balance */ + balance: string } -/** A reported metric in the financial report snapshot */ -export interface SnapshotReportedMetric { - value: string - yoy: string +/** Short trade records response (HK or US). */ +export interface ShortTradesResponse { + /** Short trade data points */ + data: Array } /** Sort order type */ @@ -5502,6 +5511,50 @@ export declare const enum TopicType { Private = 0 } +/** One top-movers event entry. */ +export interface TopMoversEvent { + /** Event time (RFC 3339) */ + timestamp: string + /** Alert reason description */ + alertReason: string + /** Alert type code */ + alertType: number + /** Stock information */ + stock: TopMoversStock + /** Associated news post (JSON string) */ + post: string +} + +/** Top movers response. */ +export interface TopMoversResponse { + /** Top-mover events */ + events: Array + /** Pagination cursor for next page (JSON string) */ + nextParams: string +} + +/** Stock information within a top-movers event. */ +export interface TopMoversStock { + /** Symbol (e.g. `"NVDA.US"`) */ + symbol: string + /** Ticker code */ + code: string + /** Security name */ + name: string + /** Full name */ + fullName: string + /** Price change (decimal ratio) */ + change: string + /** Latest price */ + lastDone: string + /** Market code */ + market: string + /** Labels / tags */ + labels: Array + /** Logo URL */ + logo: string +} + /** Trade direction */ export declare const enum TradeDirection { /** Neutral */ @@ -5623,6 +5676,46 @@ export interface UpdateWatchlistGroup { mode: SecuritiesUpdateMode } +/** One security's valuation comparison item. */ +export interface ValuationComparisonItem { + /** Symbol (e.g. `"AAPL.US"`) */ + symbol: string + /** Security name */ + name: string + /** Currency */ + currency: string + /** Market capitalisation */ + marketValue: string + /** Latest closing price */ + priceClose: string + /** P/E ratio */ + pe: string + /** P/B ratio */ + pb: string + /** P/S ratio */ + ps: string + /** Return on equity */ + roe: string + /** Earnings per share */ + eps: string + /** Book value per share */ + bps: string + /** Dividends per share */ + dps: string + /** Dividend yield */ + divYld: string + /** Total assets */ + assets: string + /** Historical valuation points */ + history: Array +} + +/** Valuation comparison response. */ +export interface ValuationComparisonResponse { + /** Valuation comparison items */ + list: Array +} + /** Valuation metrics response */ export interface ValuationData { /** Valuation metrics */ @@ -5677,6 +5770,18 @@ export interface ValuationHistoryMetrics { ps?: ValuationHistoryMetric } +/** One historical valuation data point. */ +export interface ValuationHistoryPoint { + /** Date (RFC 3339) */ + date: string + /** P/E ratio */ + pe: string + /** P/B ratio */ + pb: string + /** P/S ratio */ + ps: string +} + /** Historical valuation response */ export interface ValuationHistoryResponse { /** Historical valuation data */ diff --git a/nodejs/index.js b/nodejs/index.js index 3d6beaf8a..e13776cb4 100644 --- a/nodejs/index.js +++ b/nodejs/index.js @@ -639,6 +639,7 @@ module.exports.PushTradesEvent = nativeBinding.PushTradesEvent module.exports.QuoteContext = nativeBinding.QuoteContext module.exports.QuotePackageDetail = nativeBinding.QuotePackageDetail module.exports.RealtimeQuote = nativeBinding.RealtimeQuote +module.exports.ScreenerContext = nativeBinding.ScreenerContext module.exports.Security = nativeBinding.Security module.exports.SecurityBrokers = nativeBinding.SecurityBrokers module.exports.SecurityCalcIndex = nativeBinding.SecurityCalcIndex diff --git a/nodejs/src/fundamental/context.rs b/nodejs/src/fundamental/context.rs index be4d96f5f..0878dad09 100644 --- a/nodejs/src/fundamental/context.rs +++ b/nodejs/src/fundamental/context.rs @@ -234,111 +234,43 @@ impl FundamentalContext { Ok(self.ctx.ratings(symbol).await.map_err(ErrorNewType)?.into()) } - /// Get business segment breakdowns (latest snapshot) + /// Get ranked list of top shareholders #[napi] - pub async fn business_segments(&self, symbol: String) -> Result { + pub async fn shareholder_top(&self, symbol: String) -> Result { Ok(self .ctx - .business_segments(symbol) + .shareholder_top(symbol) .await .map_err(ErrorNewType)? .into()) } - /// Get historical business segment breakdowns + /// Get holding history and detail for one shareholder #[napi] - pub async fn business_segments_history( + pub async fn shareholder_detail( &self, symbol: String, - report: Option, - cate: Option, - ) -> Result { - let report_static: Option<&'static str> = match report.as_deref() { - Some("qf") => Some("qf"), - Some("saf") => Some("saf"), - Some("af") => Some("af"), - _ => None, - }; + object_id: i64, + ) -> Result { Ok(self .ctx - .business_segments_history(symbol, report_static, cate) + .shareholder_detail(symbol, object_id) .await .map_err(ErrorNewType)? .into()) } - /// Get historical institutional rating view time-series + /// Get valuation comparison between a security and optional peers #[napi] - pub async fn institution_rating_views(&self, symbol: String) -> Result { - Ok(self - .ctx - .institution_rating_views(symbol) - .await - .map_err(ErrorNewType)? - .into()) - } - - /// Get industry rank for a market - #[napi] - pub async fn industry_rank( - &self, - market: String, - indicator: String, - sort_type: String, - limit: u32, - ) -> Result { - Ok(self - .ctx - .industry_rank(market, indicator, sort_type, limit) - .await - .map_err(ErrorNewType)? - .into()) - } - - /// Get the industry peer chain for a security or industry - #[napi] - pub async fn industry_peers( - &self, - counter_id: String, - market: String, - industry_id: Option, - ) -> Result { - Ok(self - .ctx - .industry_peers(counter_id, market, industry_id) - .await - .map_err(ErrorNewType)? - .into()) - } - - /// Get a financial report snapshot (earnings snapshot) - #[napi] - pub async fn financial_report_snapshot( + pub async fn valuation_comparison( &self, symbol: String, - report: Option, - fiscal_year: Option, - fiscal_period: Option, - ) -> Result { - let report_static: Option<&'static str> = match report.as_deref() { - Some("qf") => Some("qf"), - Some("saf") => Some("saf"), - Some("af") => Some("af"), - _ => None, - }; - let fiscal_period_static: Option<&'static str> = match fiscal_period.as_deref() { - Some("q1") => Some("q1"), - Some("q2") => Some("q2"), - Some("q3") => Some("q3"), - Some("q4") => Some("q4"), - Some("fy") => Some("fy"), - Some("h1") => Some("h1"), - Some("h2") => Some("h2"), - _ => None, - }; + currency: String, + comparison_symbols: Option>, + ) -> Result { Ok(self .ctx - .financial_report_snapshot(symbol, report_static, fiscal_year, fiscal_period_static) + .valuation_comparison(symbol, currency, comparison_symbols) .await .map_err(ErrorNewType)? .into()) diff --git a/nodejs/src/fundamental/types.rs b/nodejs/src/fundamental/types.rs index 57852c762..eaf68e746 100644 --- a/nodejs/src/fundamental/types.rs +++ b/nodejs/src/fundamental/types.rs @@ -1647,365 +1647,139 @@ impl From for StockRatings { } } -// ── business_segments ───────────────────────────────────────────── +// ── ShareholderTopResponse ──────────────────────────────────────── -/// One business segment item (latest snapshot) +/// Top-shareholder list response. `data` is a JSON string. #[napi_derive::napi(object)] #[derive(Debug, Clone)] -pub struct BusinessSegmentItem { - pub name: String, - pub percent: String, +pub struct ShareholderTopResponse { + /// Raw top-shareholder data (JSON string) + pub data: String, } -impl From for BusinessSegmentItem { - fn from(v: lb::BusinessSegmentItem) -> Self { +impl From for ShareholderTopResponse { + fn from(v: lb::ShareholderTopResponse) -> Self { Self { - name: v.name, - percent: v.percent, + data: v.data.to_string(), } } } -/// Business segments response -#[napi_derive::napi(object)] -#[derive(Debug, Clone)] -pub struct BusinessSegments { - pub date: String, - pub total: String, - pub currency: String, - pub business: Vec, -} +// ── ShareholderDetailResponse ───────────────────────────────────── -impl From for BusinessSegments { - fn from(v: lb::BusinessSegments) -> Self { - Self { - date: v.date, - total: v.total, - currency: v.currency, - business: v.business.into_iter().map(Into::into).collect(), - } - } -} - -/// One business/regional segment item in a historical snapshot +/// Shareholder detail response. `data` is a JSON string. #[napi_derive::napi(object)] #[derive(Debug, Clone)] -pub struct BusinessSegmentHistoryItem { - pub name: String, - pub percent: String, - pub value: String, +pub struct ShareholderDetailResponse { + /// Raw shareholder detail data (JSON string) + pub data: String, } -impl From for BusinessSegmentHistoryItem { - fn from(v: lb::BusinessSegmentHistoryItem) -> Self { +impl From for ShareholderDetailResponse { + fn from(v: lb::ShareholderDetailResponse) -> Self { Self { - name: v.name, - percent: v.percent, - value: v.value, + data: v.data.to_string(), } } } -/// One historical business segments snapshot -#[napi_derive::napi(object)] -#[derive(Debug, Clone)] -pub struct BusinessSegmentsHistoricalItem { - pub date: String, - pub total: String, - pub currency: String, - pub business: Vec, - pub regionals: Vec, -} - -impl From for BusinessSegmentsHistoricalItem { - fn from(v: lb::BusinessSegmentsHistoricalItem) -> Self { - Self { - date: v.date, - total: v.total, - currency: v.currency, - business: v.business.into_iter().map(Into::into).collect(), - regionals: v.regionals.into_iter().map(Into::into).collect(), - } - } -} +// ── ValuationComparisonResponse ─────────────────────────────────── -/// Business segments history response +/// One historical valuation data point. #[napi_derive::napi(object)] #[derive(Debug, Clone)] -pub struct BusinessSegmentsHistory { - pub historical: Vec, -} - -impl From for BusinessSegmentsHistory { - fn from(v: lb::BusinessSegmentsHistory) -> Self { - Self { - historical: v.historical.into_iter().map(Into::into).collect(), - } - } -} - -// ── institution_rating_views ────────────────────────────────────── - -/// One historical rating distribution snapshot -#[napi_derive::napi(object)] -#[derive(Debug, Clone)] -pub struct InstitutionRatingViewItem { +pub struct ValuationHistoryPoint { + /// Date (RFC 3339) pub date: String, - pub buy: String, - pub over: String, - pub hold: String, - pub under: String, - pub sell: String, - pub total: String, + /// P/E ratio + pub pe: String, + /// P/B ratio + pub pb: String, + /// P/S ratio + pub ps: String, } -impl From for InstitutionRatingViewItem { - fn from(v: lb::InstitutionRatingViewItem) -> Self { +impl From for ValuationHistoryPoint { + fn from(v: lb::ValuationHistoryPoint) -> Self { Self { date: v.date, - buy: v.buy, - over: v.over, - hold: v.hold, - under: v.under, - sell: v.sell, - total: v.total, - } - } -} - -/// Institution rating views response -#[napi_derive::napi(object)] -#[derive(Debug, Clone)] -pub struct InstitutionRatingViews { - pub elist: Vec, -} - -impl From for InstitutionRatingViews { - fn from(v: lb::InstitutionRatingViews) -> Self { - Self { - elist: v.elist.into_iter().map(Into::into).collect(), - } - } -} - -// ── industry_rank ───────────────────────────────────────────────── - -/// One ranked industry item -#[napi_derive::napi(object)] -#[derive(Debug, Clone)] -pub struct IndustryRankItem { - pub name: String, - pub counter_id: String, - pub chg: String, - pub leading_name: String, - pub leading_ticker: String, - pub leading_chg: String, - pub value_name: String, - pub value_data: String, -} - -impl From for IndustryRankItem { - fn from(v: lb::IndustryRankItem) -> Self { - Self { - name: v.name, - counter_id: v.counter_id, - chg: v.chg, - leading_name: v.leading_name, - leading_ticker: v.leading_ticker, - leading_chg: v.leading_chg, - value_name: v.value_name, - value_data: v.value_data, - } - } -} - -/// A group of ranked industry items -#[napi_derive::napi(object)] -#[derive(Debug, Clone)] -pub struct IndustryRankGroup { - pub lists: Vec, -} - -impl From for IndustryRankGroup { - fn from(v: lb::IndustryRankGroup) -> Self { - Self { - lists: v.lists.into_iter().map(Into::into).collect(), - } - } -} - -/// Industry rank response -#[napi_derive::napi(object)] -#[derive(Debug, Clone)] -pub struct IndustryRankResponse { - pub items: Vec, -} - -impl From for IndustryRankResponse { - fn from(v: lb::IndustryRankResponse) -> Self { - Self { - items: v.items.into_iter().map(Into::into).collect(), - } - } -} - -// ── industry_peers ──────────────────────────────────────────────── - -/// Top-level industry info in the peers response -#[napi_derive::napi(object)] -#[derive(Debug, Clone)] -pub struct IndustryPeersTop { - pub name: String, - pub market: String, -} - -impl From for IndustryPeersTop { - fn from(v: lb::IndustryPeersTop) -> Self { - Self { - name: v.name, - market: v.market, + pe: v.pe, + pb: v.pb, + ps: v.ps, } } } -/// A node in the recursive industry peer chain. -/// -/// `nextJson` contains the child nodes serialised as a JSON string. +/// One security's valuation comparison item. #[napi_derive::napi(object)] #[derive(Debug, Clone)] -pub struct IndustryPeerNode { +pub struct ValuationComparisonItem { + /// Symbol (e.g. `"AAPL.US"`) + pub symbol: String, + /// Security name pub name: String, - pub counter_id: String, - pub stock_num: i32, - pub chg: String, - pub ytd_chg: String, - /// Child nodes as a JSON string - pub next_json: String, + /// Currency + pub currency: String, + /// Market capitalisation + pub market_value: String, + /// Latest closing price + pub price_close: String, + /// P/E ratio + pub pe: String, + /// P/B ratio + pub pb: String, + /// P/S ratio + pub ps: String, + /// Return on equity + pub roe: String, + /// Earnings per share + pub eps: String, + /// Book value per share + pub bps: String, + /// Dividends per share + pub dps: String, + /// Dividend yield + pub div_yld: String, + /// Total assets + pub assets: String, + /// Historical valuation points + pub history: Vec, } -impl From for IndustryPeerNode { - fn from(v: lb::IndustryPeerNode) -> Self { +impl From for ValuationComparisonItem { + fn from(v: lb::ValuationComparisonItem) -> Self { Self { + symbol: v.symbol, name: v.name, - counter_id: v.counter_id, - stock_num: v.stock_num, - chg: v.chg, - ytd_chg: v.ytd_chg, - next_json: serde_json::to_string(&v.next).unwrap_or_default(), - } - } -} - -/// Industry peers response -#[napi_derive::napi(object)] -#[derive(Debug, Clone)] -pub struct IndustryPeersResponse { - pub top: IndustryPeersTop, - pub chain: Option, -} - -impl From for IndustryPeersResponse { - fn from(v: lb::IndustryPeersResponse) -> Self { - Self { - top: v.top.into(), - chain: v.chain.map(Into::into), - } - } -} - -// ── financial_report_snapshot ───────────────────────────────────── - -/// A forecast metric in the financial report snapshot -#[napi_derive::napi(object)] -#[derive(Debug, Clone)] -pub struct SnapshotForecastMetric { - pub value: String, - pub yoy: String, - pub cmp_desc: String, - pub est_value: String, -} - -impl From for SnapshotForecastMetric { - fn from(v: lb::SnapshotForecastMetric) -> Self { - Self { - value: v.value, - yoy: v.yoy, - cmp_desc: v.cmp_desc, - est_value: v.est_value, + currency: v.currency, + market_value: v.market_value, + price_close: v.price_close, + pe: v.pe, + pb: v.pb, + ps: v.ps, + roe: v.roe, + eps: v.eps, + bps: v.bps, + dps: v.dps, + div_yld: v.div_yld, + assets: v.assets, + history: v.history.into_iter().map(Into::into).collect(), } } } -/// A reported metric in the financial report snapshot +/// Valuation comparison response. #[napi_derive::napi(object)] #[derive(Debug, Clone)] -pub struct SnapshotReportedMetric { - pub value: String, - pub yoy: String, -} - -impl From for SnapshotReportedMetric { - fn from(v: lb::SnapshotReportedMetric) -> Self { - Self { - value: v.value, - yoy: v.yoy, - } - } +pub struct ValuationComparisonResponse { + /// Valuation comparison items + pub list: Vec, } -/// Financial report snapshot response -#[napi_derive::napi(object)] -#[derive(Debug, Clone)] -pub struct FinancialReportSnapshot { - pub name: String, - pub ticker: String, - pub fp_start: String, - pub fp_end: String, - pub currency: String, - pub report_desc: String, - pub fo_revenue: Option, - pub fo_ebit: Option, - pub fo_eps: Option, - pub fr_revenue: Option, - pub fr_profit: Option, - pub fr_operate_cash: Option, - pub fr_invest_cash: Option, - pub fr_finance_cash: Option, - pub fr_total_assets: Option, - pub fr_total_liability: Option, - pub fr_roe_ttm: String, - pub fr_profit_margin: String, - pub fr_profit_margin_ttm: String, - pub fr_asset_turn_ttm: String, - pub fr_leverage_ttm: String, - pub fr_debt_assets_ratio: String, -} - -impl From for FinancialReportSnapshot { - fn from(v: lb::FinancialReportSnapshot) -> Self { +impl From for ValuationComparisonResponse { + fn from(v: lb::ValuationComparisonResponse) -> Self { Self { - name: v.name, - ticker: v.ticker, - fp_start: v.fp_start, - fp_end: v.fp_end, - currency: v.currency, - report_desc: v.report_desc, - fo_revenue: v.fo_revenue.map(Into::into), - fo_ebit: v.fo_ebit.map(Into::into), - fo_eps: v.fo_eps.map(Into::into), - fr_revenue: v.fr_revenue.map(Into::into), - fr_profit: v.fr_profit.map(Into::into), - fr_operate_cash: v.fr_operate_cash.map(Into::into), - fr_invest_cash: v.fr_invest_cash.map(Into::into), - fr_finance_cash: v.fr_finance_cash.map(Into::into), - fr_total_assets: v.fr_total_assets.map(Into::into), - fr_total_liability: v.fr_total_liability.map(Into::into), - fr_roe_ttm: v.fr_roe_ttm, - fr_profit_margin: v.fr_profit_margin, - fr_profit_margin_ttm: v.fr_profit_margin_ttm, - fr_asset_turn_ttm: v.fr_asset_turn_ttm, - fr_leverage_ttm: v.fr_leverage_ttm, - fr_debt_assets_ratio: v.fr_debt_assets_ratio, + list: v.list.into_iter().map(Into::into).collect(), } } } 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..15f0b159f 100644 --- a/nodejs/src/market/context.rs +++ b/nodejs/src/market/context.rs @@ -122,4 +122,44 @@ impl MarketContext { .map_err(ErrorNewType)? .into()) } + + /// Get top movers (stocks with unusual price movements) across one or more + /// markets + #[napi] + pub async fn top_movers( + &self, + markets: Vec, + sort: u32, + date: Option, + limit: u32, + ) -> Result { + Ok(self + .ctx + .top_movers(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..3d81fc259 100644 --- a/nodejs/src/market/types.rs +++ b/nodejs/src/market/types.rs @@ -564,3 +564,192 @@ impl From for lb::AhPremiumPeriod { } } } + +// ── TopMoversResponse ───────────────────────────────────────────── + +/// Stock information within a top-movers event. +#[napi_derive::napi(object)] +#[derive(Debug, Clone)] +pub struct TopMoversStock { + /// Symbol (e.g. `"NVDA.US"`) + pub symbol: String, + /// Ticker code + pub code: String, + /// Security name + pub name: String, + /// Full name + pub full_name: String, + /// Price change (decimal ratio) + pub change: String, + /// Latest price + pub last_done: String, + /// Market code + pub market: String, + /// Labels / tags + pub labels: Vec, + /// Logo URL + pub logo: String, +} + +impl From for TopMoversStock { + fn from(v: lb::TopMoversStock) -> Self { + Self { + symbol: v.symbol, + code: v.code, + name: v.name, + full_name: v.full_name, + change: v.change, + last_done: v.last_done, + market: v.market, + labels: v.labels, + logo: v.logo, + } + } +} + +/// One top-movers event entry. +#[napi_derive::napi(object)] +#[derive(Debug, Clone)] +pub struct TopMoversEvent { + /// Event time (RFC 3339) + pub timestamp: String, + /// Alert reason description + pub alert_reason: String, + /// Alert type code + pub alert_type: i64, + /// Stock information + pub stock: TopMoversStock, + /// Associated news post (JSON string) + pub post: String, +} + +impl From for TopMoversEvent { + fn from(v: lb::TopMoversEvent) -> Self { + Self { + timestamp: v.timestamp, + alert_reason: v.alert_reason, + alert_type: v.alert_type, + stock: v.stock.into(), + post: v.post.to_string(), + } + } +} + +/// Top movers response. +#[napi_derive::napi(object)] +#[derive(Debug, Clone)] +pub struct TopMoversResponse { + /// Top-mover events + pub events: Vec, + /// Pagination cursor for next page (JSON string) + pub next_params: String, +} + +impl From for TopMoversResponse { + fn from(v: lb::TopMoversResponse) -> Self { + Self { + events: v.events.into_iter().map(Into::into).collect(), + next_params: v.next_params.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 ────────────────────────────────────────────── + +/// One ranked security item. +#[napi_derive::napi(object)] +#[derive(Debug, Clone)] +pub struct RankListItem { + /// Symbol (e.g. `"MU.US"`) + pub symbol: String, + /// Ticker code + pub code: String, + /// Security name + pub name: String, + /// Latest price + pub last_done: String, + /// Price change ratio + pub chg: String, + /// Absolute price change + pub change: String, + /// Net inflow + pub inflow: String, + /// Market cap + pub market_cap: String, + /// Industry name + pub industry: String, + /// Pre/post market price + pub pre_post_price: String, + /// Pre/post market change + pub pre_post_chg: String, + /// Amplitude + pub amplitude: String, + /// 5-day change + pub five_day_chg: String, + /// Turnover rate + pub turnover_rate: String, + /// Volume ratio + pub volume_rate: String, + /// P/B ratio (TTM) + pub pb_ttm: String, +} + +impl From for RankListItem { + fn from(v: lb::RankListItem) -> Self { + Self { + symbol: v.symbol, + code: v.code, + name: v.name, + last_done: v.last_done, + chg: v.chg, + change: v.change, + inflow: v.inflow, + market_cap: v.market_cap, + industry: v.industry, + pre_post_price: v.pre_post_price, + pre_post_chg: v.pre_post_chg, + amplitude: v.amplitude, + five_day_chg: v.five_day_chg, + turnover_rate: v.turnover_rate, + volume_rate: v.volume_rate, + pb_ttm: v.pb_ttm, + } + } +} + +/// Rank list response. +#[napi_derive::napi(object)] +#[derive(Debug, Clone)] +pub struct RankListResponse { + /// Whether delayed / BMP data + pub bmp: bool, + /// Ranked security items + pub lists: Vec, +} + +impl From for RankListResponse { + fn from(v: lb::RankListResponse) -> Self { + Self { + bmp: v.bmp, + lists: v.lists.into_iter().map(Into::into).collect(), + } + } +} diff --git a/nodejs/src/quote/context.rs b/nodejs/src/quote/context.rs index b4b127a1d..4ee04a1e2 100644 --- a/nodejs/src/quote/context.rs +++ b/nodejs/src/quote/context.rs @@ -20,8 +20,9 @@ use crate::{ OptionVolumeStats, ParticipantInfo, Period, PinnedMode, QuotePackageDetail, RealtimeQuote, Security, SecurityBrokers, SecurityCalcIndex, SecurityDepth, SecurityListCategory, SecurityQuote, SecurityStaticInfo, ShortPositionsResponse, - SortOrderType, StrikePriceInfo, SubType, SubTypes, Subscription, Trade, TradeSessions, - WarrantInfo, WarrantQuote, WarrantSortBy, WarrantStatus, WarrantType, WatchlistGroup, + ShortTradesResponse, SortOrderType, StrikePriceInfo, SubType, SubTypes, Subscription, + Trade, TradeSessions, WarrantInfo, WarrantQuote, WarrantSortBy, WarrantStatus, + WarrantType, WatchlistGroup, }, }, time::{NaiveDate, NaiveDatetime}, @@ -1233,12 +1234,31 @@ impl QuoteContext { .collect() } - /// Get short interest data for a US security + /// Get short interest data for a US or HK security. + /// + /// Market is inferred from the symbol suffix (.HK → HK, otherwise US). + #[napi] + pub async fn short_positions( + &self, + symbol: String, + count: u32, + ) -> Result { + Ok(self + .ctx + .short_positions(symbol, count) + .await + .map_err(ErrorNewType)? + .into()) + } + + /// Get short trade records for a HK or US security. + /// + /// Market is inferred from the symbol suffix (.HK → HK, otherwise US). #[napi] - pub async fn short_positions(&self, symbol: String) -> Result { + pub async fn short_trades(&self, symbol: String, count: u32) -> Result { Ok(self .ctx - .short_positions(symbol) + .short_trades(symbol, count) .await .map_err(ErrorNewType)? .into()) diff --git a/nodejs/src/quote/types.rs b/nodejs/src/quote/types.rs index d649ff65c..acd0cfdd4 100644 --- a/nodejs/src/quote/types.rs +++ b/nodejs/src/quote/types.rs @@ -1480,55 +1480,111 @@ pub struct HistoryMarketTemperatureResponse { // ── Step 3 additions ───────────────────────────────────────────── -/// Short interest response +/// One short-position data point (unified for US and HK markets). +#[napi_derive::napi(object)] +#[derive(Debug, Clone)] +pub struct ShortPositionsItem { + /// Trading date (RFC 3339) + pub timestamp: String, + /// Short ratio + pub rate: String, + /// Closing price + pub close: String, + /// [US] Number of short shares outstanding + pub current_shares_short: String, + /// [US] Average daily share volume + pub avg_daily_share_volume: String, + /// [US] Days to cover ratio + pub days_to_cover: String, + /// [HK] Short sale amount (HKD) + pub amount: String, + /// [HK] Short position balance + pub balance: String, + /// [HK] Cost / closing price + pub cost: String, +} + +impl From for ShortPositionsItem { + fn from(v: longbridge::quote::ShortPositionsItem) -> Self { + Self { + timestamp: v.timestamp, + rate: v.rate, + close: v.close, + current_shares_short: v.current_shares_short, + avg_daily_share_volume: v.avg_daily_share_volume, + days_to_cover: v.days_to_cover, + amount: v.amount, + balance: v.balance, + cost: v.cost, + } + } +} + +/// Short interest / positions response (HK or US). #[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, + /// Short position data points + pub data: Vec, } 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, } } } -/// One short position data point +/// One short-trade data point (unified for US and HK markets). #[napi_derive::napi(object)] #[derive(Debug, Clone)] -pub struct ShortPosition { - /// Settlement date timestamp string +pub struct ShortTradesItem { + /// Trading date (RFC 3339) 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 { + /// [US] NYSE short amount + pub nus_amount: String, + /// [US] NY short amount + pub ny_amount: String, + /// [US] Total short amount + pub total_amount: String, + /// [HK] Short sale amount + pub amount: String, + /// [HK] Short position balance + pub balance: String, +} + +impl From for ShortTradesItem { + fn from(v: longbridge::quote::ShortTradesItem) -> Self { Self { timestamp: v.timestamp, rate: v.rate, - 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, + nus_amount: v.nus_amount, + ny_amount: v.ny_amount, + total_amount: v.total_amount, + amount: v.amount, + balance: v.balance, + } + } +} + +/// Short trade records response (HK or US). +#[napi_derive::napi(object)] +#[derive(Debug, Clone)] +pub struct ShortTradesResponse { + /// Short trade data points + pub data: Vec, +} + +impl From for ShortTradesResponse { + fn from(v: longbridge::quote::ShortTradesResponse) -> Self { + Self { + data: v.data.into_iter().map(Into::into).collect(), } } } diff --git a/nodejs/src/screener/context.rs b/nodejs/src/screener/context.rs new file mode 100644 index 000000000..b5b16fbbd --- /dev/null +++ b/nodejs/src/screener/context.rs @@ -0,0 +1,86 @@ +use std::sync::Arc; + +use napi::Result; + +use crate::{config::Config, error::ErrorNewType, screener::types::*}; + +/// Screener context +#[napi_derive::napi] +#[derive(Clone)] +pub struct ScreenerContext { + ctx: longbridge::ScreenerContext, +} + +#[napi_derive::napi] +impl ScreenerContext { + /// Create a new `ScreenerContext` + #[napi] + pub fn new(config: &Config) -> ScreenerContext { + Self { + ctx: longbridge::ScreenerContext::new(Arc::new(config.0.clone())), + } + } + + /// Get recommended built-in screener strategies + #[napi] + pub async fn screener_recommend_strategies( + &self, + ) -> Result { + Ok(self + .ctx + .screener_recommend_strategies() + .await + .map_err(ErrorNewType)? + .into()) + } + + /// Get the current user's saved screener strategies + #[napi] + pub async fn screener_user_strategies(&self) -> Result { + Ok(self + .ctx + .screener_user_strategies() + .await + .map_err(ErrorNewType)? + .into()) + } + + /// Get detail for one screener strategy by ID + #[napi] + pub async fn screener_strategy(&self, id: i64) -> Result { + Ok(self + .ctx + .screener_strategy(id) + .await + .map_err(ErrorNewType)? + .into()) + } + + /// Search / screen securities using a strategy + #[napi] + pub async fn screener_search( + &self, + market: String, + strategy_id: Option, + page: u32, + size: u32, + ) -> Result { + Ok(self + .ctx + .screener_search(market, strategy_id, page, size) + .await + .map_err(ErrorNewType)? + .into()) + } + + /// Get all available screener indicator definitions + #[napi] + pub async fn screener_indicators(&self) -> Result { + Ok(self + .ctx + .screener_indicators() + .await + .map_err(ErrorNewType)? + .into()) + } +} diff --git a/nodejs/src/screener/mod.rs b/nodejs/src/screener/mod.rs new file mode 100644 index 000000000..0561d4d5a --- /dev/null +++ b/nodejs/src/screener/mod.rs @@ -0,0 +1,2 @@ +pub mod context; +pub mod types; diff --git a/nodejs/src/screener/types.rs b/nodejs/src/screener/types.rs new file mode 100644 index 000000000..3956b8887 --- /dev/null +++ b/nodejs/src/screener/types.rs @@ -0,0 +1,91 @@ +use longbridge::screener::types as lb; + +// ── ScreenerRecommendStrategiesResponse ─────────────────────────── + +/// Recommended screener strategies response. `data` is a JSON string. +#[napi_derive::napi(object)] +#[derive(Debug, Clone)] +pub struct ScreenerRecommendStrategiesResponse { + /// Raw recommended strategies data (JSON string) + pub data: String, +} + +impl From for ScreenerRecommendStrategiesResponse { + fn from(v: lb::ScreenerRecommendStrategiesResponse) -> Self { + Self { + data: v.data.to_string(), + } + } +} + +// ── ScreenerUserStrategiesResponse ──────────────────────────────── + +/// User screener strategies response. `data` is a JSON string. +#[napi_derive::napi(object)] +#[derive(Debug, Clone)] +pub struct ScreenerUserStrategiesResponse { + /// Raw user strategies data (JSON string) + pub data: String, +} + +impl From for ScreenerUserStrategiesResponse { + fn from(v: lb::ScreenerUserStrategiesResponse) -> Self { + Self { + data: v.data.to_string(), + } + } +} + +// ── ScreenerStrategyResponse ────────────────────────────────────── + +/// Single screener strategy response. `data` is a JSON string. +#[napi_derive::napi(object)] +#[derive(Debug, Clone)] +pub struct ScreenerStrategyResponse { + /// Raw strategy detail data (JSON string) + pub data: String, +} + +impl From for ScreenerStrategyResponse { + fn from(v: lb::ScreenerStrategyResponse) -> Self { + Self { + data: v.data.to_string(), + } + } +} + +// ── ScreenerSearchResponse ──────────────────────────────────────── + +/// Screener search results response. `data` is a JSON string. +#[napi_derive::napi(object)] +#[derive(Debug, Clone)] +pub struct ScreenerSearchResponse { + /// Raw search results data (JSON string) + pub data: String, +} + +impl From for ScreenerSearchResponse { + fn from(v: lb::ScreenerSearchResponse) -> Self { + Self { + data: v.data.to_string(), + } + } +} + +// ── ScreenerIndicatorsResponse ──────────────────────────────────── + +/// Screener indicator definitions response. `data` is a JSON string. +#[napi_derive::napi(object)] +#[derive(Debug, Clone)] +pub struct ScreenerIndicatorsResponse { + /// Raw indicator definitions data (JSON string) + pub data: String, +} + +impl From for ScreenerIndicatorsResponse { + fn from(v: lb::ScreenerIndicatorsResponse) -> Self { + Self { + data: v.data.to_string(), + } + } +} diff --git a/python/pysrc/longbridge/openapi.pyi b/python/pysrc/longbridge/openapi.pyi index 6afef758f..aa3b1cb2d 100644 --- a/python/pysrc/longbridge/openapi.pyi +++ b/python/pysrc/longbridge/openapi.pyi @@ -2787,30 +2787,16 @@ class SecurityCalcIndex: theta: Optional[Decimal] """ Theta - - The raw value returned by the API is annualized (scaled by 252 trading - days per year). To obtain the standard per-calendar-day theta, divide - by 252: ``theta / 252``. """ vega: Optional[Decimal] """ Vega - - The raw value returned by the API is expressed per 1 percentage-point - change in implied volatility (i.e. the value has been multiplied by - 100). To obtain the standard vega (per unit change in IV), divide by - 100: ``vega / 100``. """ rho: Optional[Decimal] """ Rho - - The raw value returned by the API is expressed per 1 percentage-point - change in the risk-free rate (i.e. the value has been multiplied by - 100). To obtain the standard rho (per unit change in rate), divide by - 100: ``rho / 100``. """ class QuotePackageDetail: @@ -4000,6 +3986,40 @@ class QuoteContext: print(resp) """ + def short_positions( + self, symbol: str, count: int = 20 + ) -> ShortPositionsResponse: + """ + Get short interest / position data for a US or HK security. + + Market is inferred from the symbol suffix: ``.HK`` → HK endpoint, + otherwise US endpoint. + + Args: + symbol: Security code (e.g. ``"700.HK"`` or ``"AAPL.US"``) + count: Number of records (1–100, default 20) + + Returns: + :class:`ShortPositionsResponse` with raw JSON data + """ + + def short_trades( + self, symbol: str, count: int = 20 + ) -> ShortTradesResponse: + """ + Get short trade records for a HK or US security. + + Market is inferred from the symbol suffix: ``.HK`` → HK endpoint, + otherwise US endpoint. + + Args: + symbol: Security code + count: Number of records (1–100, default 20) + + Returns: + :class:`ShortTradesResponse` with raw JSON data + """ + class AsyncQuoteContext: """ Async quote context for use with asyncio. Create via `AsyncQuoteContext.create(config)` and await inside asyncio. @@ -5316,6 +5336,42 @@ class AsyncQuoteContext: """ ... + def short_positions( + self, symbol: str, count: int = 20 + ) -> Awaitable[ShortPositionsResponse]: + """ + Get short interest / position data for a US or HK security. Returns awaitable. + + Market is inferred from the symbol suffix: ``.HK`` → HK endpoint, + otherwise US endpoint. + + Args: + symbol: Security code + count: Number of records (1–100, default 20) + + Returns: + Awaitable resolving to :class:`ShortPositionsResponse` + """ + ... + + def short_trades( + self, symbol: str, count: int = 20 + ) -> Awaitable[ShortTradesResponse]: + """ + Get short trade records for a HK or US security. Returns awaitable. + + Market is inferred from the symbol suffix: ``.HK`` → HK endpoint, + otherwise US endpoint. + + Args: + symbol: Security code + count: Number of records (1–100, default 20) + + Returns: + Awaitable resolving to :class:`ShortTradesResponse` + """ + ... + class OrderSide: """ Order side @@ -9628,228 +9684,6 @@ class StockRatings: """Full ratings array as a JSON string""" -class BusinessSegmentItem: - """One business segment item (latest snapshot).""" - - name: str - """Segment name""" - percent: str - """Percentage of total revenue""" - - -class BusinessSegments: - """Response for :meth:`FundamentalContext.business_segments`.""" - - date: str - """Report date""" - total: str - """Total revenue""" - currency: str - """Reporting currency""" - business: list[BusinessSegmentItem] - """Business segment breakdown""" - - -class BusinessSegmentHistoryItem: - """One business/regional segment item in a historical snapshot.""" - - name: str - """Segment name""" - percent: str - """Percentage of total""" - value: str - """Absolute value""" - - -class BusinessSegmentsHistoricalItem: - """One historical business segments snapshot.""" - - date: str - """Report date""" - total: str - """Total revenue""" - currency: str - """Reporting currency""" - business: list[BusinessSegmentHistoryItem] - """Business segment breakdown""" - regionals: list[BusinessSegmentHistoryItem] - """Regional breakdown""" - - -class BusinessSegmentsHistory: - """Response for :meth:`FundamentalContext.business_segments_history`.""" - - historical: list[BusinessSegmentsHistoricalItem] - """Historical snapshots""" - - -class InstitutionRatingViewItem: - """One historical rating distribution snapshot.""" - - date: str - """Date as unix timestamp string""" - buy: str - """Number of Buy ratings""" - over: str - """Number of Outperform ratings""" - hold: str - """Number of Hold ratings""" - under: str - """Number of Underperform ratings""" - sell: str - """Number of Sell ratings""" - total: str - """Total analyst count""" - - -class InstitutionRatingViews: - """Response for :meth:`FundamentalContext.institution_rating_views`.""" - - elist: list[InstitutionRatingViewItem] - """Historical rating distribution snapshots""" - - -class IndustryRankItem: - """One ranked industry item.""" - - name: str - """Industry / sector name""" - counter_id: str - """Counter ID of the industry""" - chg: str - """Change percentage""" - leading_name: str - """Name of the leading stock""" - leading_ticker: str - """Ticker of the leading stock""" - leading_chg: str - """Change percentage of the leading stock""" - value_name: str - """Value label name""" - value_data: str - """Value data""" - - -class IndustryRankGroup: - """A group of ranked industry items.""" - - lists: list[IndustryRankItem] - """Items in this group""" - - -class IndustryRankResponse: - """Response for :meth:`FundamentalContext.industry_rank`.""" - - items: list[IndustryRankGroup] - """Grouped rank items""" - - -class IndustryPeersTop: - """Top-level industry info in the peers response.""" - - name: str - """Industry name""" - market: str - """Market code""" - - -class IndustryPeerNode: - """A node in the recursive industry peer chain.""" - - name: str - """Node name""" - counter_id: str - """Counter ID""" - stock_num: int - """Number of stocks in this node""" - chg: str - """Change percentage""" - ytd_chg: str - """Year-to-date change""" - next_json: str - """Child nodes as a JSON string""" - - -class IndustryPeersResponse: - """Response for :meth:`FundamentalContext.industry_peers`.""" - - top: IndustryPeersTop - """Top-level industry node info""" - chain: "IndustryPeerNode | None" - """Root peer chain node""" - - -class SnapshotForecastMetric: - """A forecast metric in the financial report snapshot.""" - - value: str - """Actual value""" - yoy: str - """Year-over-year change""" - cmp_desc: str - """Beat/miss description""" - est_value: str - """Consensus estimate value""" - - -class SnapshotReportedMetric: - """A reported metric in the financial report snapshot.""" - - value: str - """Actual value""" - yoy: str - """Year-over-year change""" - - -class FinancialReportSnapshot: - """Response for :meth:`FundamentalContext.financial_report_snapshot`.""" - - name: str - """Company name""" - ticker: str - """Ticker code""" - fp_start: str - """Fiscal period start date""" - fp_end: str - """Fiscal period end date""" - currency: str - """Reporting currency""" - report_desc: str - """Report description""" - fo_revenue: "SnapshotForecastMetric | None" - """Forecast revenue""" - fo_ebit: "SnapshotForecastMetric | None" - """Forecast EBIT""" - fo_eps: "SnapshotForecastMetric | None" - """Forecast EPS""" - fr_revenue: "SnapshotReportedMetric | None" - """Reported revenue""" - fr_profit: "SnapshotReportedMetric | None" - """Reported net profit""" - fr_operate_cash: "SnapshotReportedMetric | None" - """Reported operating cash flow""" - fr_invest_cash: "SnapshotReportedMetric | None" - """Reported investing cash flow""" - fr_finance_cash: "SnapshotReportedMetric | None" - """Reported financing cash flow""" - fr_total_assets: "SnapshotReportedMetric | None" - """Reported total assets""" - fr_total_liability: "SnapshotReportedMetric | None" - """Reported total liabilities""" - fr_roe_ttm: str - """ROE TTM""" - fr_profit_margin: str - """Profit margin""" - fr_profit_margin_ttm: str - """Profit margin TTM""" - fr_asset_turn_ttm: str - """Asset turnover TTM""" - fr_leverage_ttm: str - """Leverage TTM""" - fr_debt_assets_ratio: str - """Debt-to-assets ratio""" - - class FinancialReportKind: """Financial report kind.""" @@ -10028,109 +9862,122 @@ class FundamentalContext: """ ... - def business_segments(self, symbol: str) -> "BusinessSegments": + def shareholder_top(self, symbol: str) -> "ShareholderTopResponse": """ - Get business segment breakdowns (latest snapshot). + Get ranked list of top shareholders. Args: - symbol: Security symbol, e.g. ``"AAPL.US"`` + symbol: Security symbol Returns: - :class:`BusinessSegments` + :class:`ShareholderTopResponse` with raw JSON data """ ... - def business_segments_history( - self, - symbol: str, - report: "str | None" = None, - cate: "str | None" = None, - ) -> "BusinessSegmentsHistory": + def shareholder_detail( + self, symbol: str, object_id: int + ) -> "ShareholderDetailResponse": """ - Get historical business segment breakdowns. + Get holding history and detail for one shareholder. Args: symbol: Security symbol - report: Report type (``"qf"``, ``"saf"``, ``"af"``) or ``None`` - cate: Category filter or ``None`` + object_id: Shareholder object ID Returns: - :class:`BusinessSegmentsHistory` + :class:`ShareholderDetailResponse` with raw JSON data """ ... - def institution_rating_views(self, symbol: str) -> "InstitutionRatingViews": + def valuation_comparison( + self, + symbol: str, + currency: str, + comparison_symbols: Optional[List[str]] = None, + ) -> "ValuationComparisonResponse": """ - Get historical institutional rating view time-series. + Get valuation comparison between a security and optional peers. Args: symbol: Security symbol + currency: Currency code (e.g. ``"USD"``) + comparison_symbols: Optional list of peer symbols Returns: - :class:`InstitutionRatingViews` + :class:`ValuationComparisonResponse` with raw JSON data """ ... - def industry_rank( - self, - market: str, - indicator: str, - sort_type: str, - limit: int, - ) -> "IndustryRankResponse": - """ - Get industry rank for a market. - Args: - market: Market code, e.g. ``"US"`` - indicator: Numeric string ``"0"``–``"7"`` - sort_type: ``"0"`` (ascending) or ``"1"`` (descending) - limit: Maximum number of results +# ── FundamentalContext new response types ───────────────────────── - Returns: - :class:`IndustryRankResponse` - """ - ... +class ShareholderTopResponse: + """Top-shareholder list response. ``data`` is a Python dict/list from JSON.""" - def industry_peers( - self, - counter_id: str, - market: str, - industry_id: "str | None" = None, - ) -> "IndustryPeersResponse": - """ - Get the industry peer chain for a security or industry. + data: object + """Raw top-shareholder data (JSON object / list)""" - Args: - counter_id: Symbol (e.g. ``"AAPL.US"``) or industry counter ID - market: Market code, e.g. ``"US"`` - industry_id: Industry ID or ``None`` - Returns: - :class:`IndustryPeersResponse` - """ - ... +class ShareholderDetailResponse: + """Shareholder detail response. ``data`` is a Python dict/list from JSON.""" - def financial_report_snapshot( - self, - symbol: str, - report: "str | None" = None, - fiscal_year: "int | None" = None, - fiscal_period: "str | None" = None, - ) -> "FinancialReportSnapshot": - """ - Get a financial report snapshot (earnings snapshot). + data: object + """Raw shareholder detail data (JSON object / list)""" - Args: - symbol: Security symbol - report: Report type (``"qf"``, ``"saf"``, ``"af"``) or ``None`` - fiscal_year: Fiscal year (e.g. ``2023``) or ``None`` - fiscal_period: Fiscal period string or ``None`` - Returns: - :class:`FinancialReportSnapshot` - """ - ... +class ValuationHistoryPoint: + """One historical valuation data point.""" + + date: str + """Date (RFC 3339, converted from Unix timestamp)""" + pe: str + """P/E ratio""" + pb: str + """P/B ratio""" + ps: str + """P/S ratio""" + + +class ValuationComparisonItem: + """One security's valuation comparison item.""" + + symbol: str + """Symbol (e.g. ``"AAPL.US"``)""" + name: str + """Security name""" + currency: str + """Currency""" + market_value: str + """Market capitalisation""" + price_close: str + """Latest closing price""" + pe: str + """P/E ratio""" + pb: str + """P/B ratio""" + ps: str + """P/S ratio""" + roe: str + """Return on equity""" + eps: str + """Earnings per share""" + bps: str + """Book value per share""" + dps: str + """Dividends per share""" + div_yld: str + """Dividend yield""" + assets: str + """Total assets""" + history: List[ValuationHistoryPoint] + """Historical valuation points""" + + +class ValuationComparisonResponse: + """Valuation comparison response.""" + + list: List[ValuationComparisonItem] + """Valuation comparison items""" # ── MarketContext ───────────────────────────────────────────────── @@ -10533,6 +10380,261 @@ class MarketContext: """ ... + def top_movers( + self, + markets: List[str], + sort: int = 0, + date: Optional[str] = None, + limit: int = 20, + ) -> "TopMoversResponse": + """ + Get top movers (stocks with unusual price movements) 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:`TopMoversResponse` 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 TopMoversStock: + """Stock information within a top-movers event.""" + + symbol: str + """Symbol (e.g. ``"NVDA.US"``)""" + code: str + """Ticker code""" + name: str + """Security name""" + full_name: str + """Full name""" + change: str + """Price change (decimal ratio)""" + last_done: str + """Latest price""" + market: str + """Market code""" + labels: List[str] + """Labels / tags""" + logo: str + """Logo URL""" + + +class TopMoversEvent: + """One top-movers event entry.""" + + timestamp: str + """Event time (RFC 3339)""" + alert_reason: str + """Alert reason description""" + alert_type: int + """Alert type code""" + stock: TopMoversStock + """Stock information""" + post: object + """Associated news post (raw JSON object)""" + + +class TopMoversResponse: + """Top movers response.""" + + events: List[TopMoversEvent] + """Top-mover events""" + next_params: object + """Pagination cursor for next page (raw JSON object)""" + + +class RankCategoriesResponse: + """Rank categories response. ``data`` is a Python dict/list from JSON.""" + + data: object + """Raw rank categories data (JSON object / list)""" + + +class RankListItem: + """One ranked security item.""" + + symbol: str + """Symbol (e.g. ``"MU.US"``)""" + code: str + """Ticker code""" + name: str + """Security name""" + last_done: str + """Latest price""" + chg: str + """Price change ratio""" + change: str + """Absolute price change""" + inflow: str + """Net inflow""" + market_cap: str + """Market cap""" + industry: str + """Industry name""" + pre_post_price: str + """Pre/post market price""" + pre_post_chg: str + """Pre/post market change""" + amplitude: str + """Amplitude""" + five_day_chg: str + """5-day change""" + turnover_rate: str + """Turnover rate""" + volume_rate: str + """Volume ratio""" + pb_ttm: str + """P/B ratio (TTM)""" + + +class RankListResponse: + """Rank list response.""" + + bmp: bool + """Whether delayed / BMP data""" + lists: List[RankListItem] + """Ranked security items""" + + +# ── ScreenerContext ─────────────────────────────────────────────── + +class ScreenerRecommendStrategiesResponse: + """Recommended screener strategies response. ``data`` is a Python dict/list from JSON.""" + + data: object + """Raw recommended strategies data (JSON object / list)""" + + +class ScreenerUserStrategiesResponse: + """User screener strategies response. ``data`` is a Python dict/list from JSON.""" + + data: object + """Raw user strategies data (JSON object / list)""" + + +class ScreenerStrategyResponse: + """Single screener strategy response. ``data`` is a Python dict/list from JSON.""" + + data: object + """Raw strategy detail data (JSON object / list)""" + + +class ScreenerSearchResponse: + """Screener search results response. ``data`` is a Python dict/list from JSON.""" + + data: object + """Raw search results data (JSON object / list)""" + + +class ScreenerIndicatorsResponse: + """Screener indicator definitions response. ``data`` is a Python dict/list from JSON.""" + + data: object + """Raw indicator definitions data (JSON object / list)""" + + +class ScreenerContext: + """Stock screener context — strategies, search, and indicators.""" + + def __init__(self, config: Config) -> None: ... + + def screener_recommend_strategies(self) -> ScreenerRecommendStrategiesResponse: + """Get recommended built-in screener strategies.""" + ... + + def screener_user_strategies(self) -> ScreenerUserStrategiesResponse: + """Get the current user's saved screener strategies.""" + ... + + def screener_strategy(self, id: int) -> ScreenerStrategyResponse: + """Get detail for one screener strategy by ID.""" + ... + + def screener_search( + self, + market: str, + strategy_id: Optional[int] = None, + page: int = 1, + size: int = 20, + ) -> ScreenerSearchResponse: + """Search / screen securities using a strategy.""" + ... + + def screener_indicators(self) -> ScreenerIndicatorsResponse: + """Get all available screener indicator definitions.""" + ... + + +class AsyncScreenerContext: + """Async screener context for use with asyncio.""" + + @classmethod + def create(cls, config: Config) -> "AsyncScreenerContext": ... + + def screener_recommend_strategies( + self, + ) -> Awaitable[ScreenerRecommendStrategiesResponse]: + """Get recommended built-in screener strategies. Returns awaitable.""" + ... + + def screener_user_strategies( + self, + ) -> Awaitable[ScreenerUserStrategiesResponse]: + """Get the current user's saved screener strategies. Returns awaitable.""" + ... + + def screener_strategy( + self, id: int + ) -> Awaitable[ScreenerStrategyResponse]: + """Get detail for one screener strategy by ID. Returns awaitable.""" + ... + + def screener_search( + self, + market: str, + strategy_id: Optional[int] = None, + page: int = 1, + size: int = 20, + ) -> Awaitable[ScreenerSearchResponse]: + """Search / screen securities using a strategy. Returns awaitable.""" + ... + + def screener_indicators(self) -> Awaitable[ScreenerIndicatorsResponse]: + """Get all available screener indicator definitions. Returns awaitable.""" + ... + # ── CalendarContext ─────────────────────────────────────────────── @@ -11683,32 +11785,62 @@ class SharelistContext: # ── QuoteContext extensions ─────────────────────────────────────── -class ShortPosition: - """One short interest data point.""" +class ShortPositionsItem: + """One short-position data point (unified for US and HK markets).""" timestamp: str - """Settlement date (unix timestamp string)""" + """Trading date (RFC 3339)""" rate: str - """Short interest as a ratio of float shares""" - avg_daily_share_volume: str - """Average daily share volume""" + """Short ratio (both markets)""" + close: str + """Closing price (both markets)""" current_shares_short: str - """Current shares short""" + """[US] Number of short shares outstanding""" + avg_daily_share_volume: str + """[US] Average daily share volume""" days_to_cover: str - """Days to cover (short ratio)""" - close: str - """Closing price on the settlement date""" + """[US] Days to cover ratio""" + amount: str + """[HK] Short sale amount (HKD)""" + balance: str + """[HK] Short position balance""" + cost: str + """[HK] Cost / closing price""" class ShortPositionsResponse: - """Short interest response.""" + """Short interest / positions response (HK or US).""" - symbol: str - """Security symbol""" - data: list[ShortPosition] - """Short interest data points""" - sources: int - """Number of data sources""" + data: List[ShortPositionsItem] + """Short position data points""" + + +class ShortTradesItem: + """One short-trade data point (unified for US and HK markets).""" + + timestamp: str + """Trading date (RFC 3339)""" + rate: str + """Short ratio""" + close: str + """Closing price""" + nus_amount: str + """[US] NYSE short amount""" + ny_amount: str + """[US] NY short amount""" + total_amount: str + """[US] Total short amount""" + amount: str + """[HK] Short sale amount""" + balance: str + """[HK] Short position balance""" + + +class ShortTradesResponse: + """Short trade records response (HK or US).""" + + data: List[ShortTradesItem] + """Short trade data points""" class OptionVolumeStats: diff --git a/python/src/fundamental/context.rs b/python/src/fundamental/context.rs index 3f7e26e05..87f2e26fd 100644 --- a/python/src/fundamental/context.rs +++ b/python/src/fundamental/context.rs @@ -162,103 +162,39 @@ impl FundamentalContext { Ok(self.ctx.ratings(symbol).map_err(ErrorNewType)?.into()) } - /// Get business segment breakdowns (latest snapshot). - fn business_segments(&self, symbol: String) -> PyResult { + /// Get ranked list of top shareholders. + fn shareholder_top(&self, symbol: String) -> PyResult { Ok(self .ctx - .business_segments(symbol) + .shareholder_top(symbol) .map_err(ErrorNewType)? .into()) } - /// Get historical business segment breakdowns. - #[pyo3(signature = (symbol, report = None, cate = None))] - fn business_segments_history( + /// Get holding history and detail for one shareholder. + fn shareholder_detail( &self, symbol: String, - report: Option, - cate: Option, - ) -> PyResult { - let report_static: Option<&'static str> = match report.as_deref() { - Some("qf") => Some("qf"), - Some("saf") => Some("saf"), - Some("af") => Some("af"), - _ => None, - }; + object_id: i64, + ) -> PyResult { Ok(self .ctx - .business_segments_history(symbol, report_static, cate) + .shareholder_detail(symbol, object_id) .map_err(ErrorNewType)? .into()) } - /// Get historical institutional rating view time-series. - fn institution_rating_views(&self, symbol: String) -> PyResult { - Ok(self - .ctx - .institution_rating_views(symbol) - .map_err(ErrorNewType)? - .into()) - } - - /// Get industry rank for a market. - fn industry_rank( - &self, - market: String, - indicator: String, - sort_type: String, - limit: u32, - ) -> PyResult { - Ok(self - .ctx - .industry_rank(market, indicator, sort_type, limit) - .map_err(ErrorNewType)? - .into()) - } - - /// Get the industry peer chain for a security or industry. - #[pyo3(signature = (counter_id, market, industry_id = None))] - fn industry_peers( - &self, - counter_id: String, - market: String, - industry_id: Option, - ) -> PyResult { - Ok(self - .ctx - .industry_peers(counter_id, market, industry_id) - .map_err(ErrorNewType)? - .into()) - } - - /// Get a financial report snapshot (earnings snapshot). - #[pyo3(signature = (symbol, report = None, fiscal_year = None, fiscal_period = None))] - fn financial_report_snapshot( + /// Get valuation comparison between a security and optional peers. + #[pyo3(signature = (symbol, currency, comparison_symbols = None))] + fn valuation_comparison( &self, symbol: String, - report: Option, - fiscal_year: Option, - fiscal_period: Option, - ) -> PyResult { - let report_static: Option<&'static str> = match report.as_deref() { - Some("qf") => Some("qf"), - Some("saf") => Some("saf"), - Some("af") => Some("af"), - _ => None, - }; - let fiscal_period_static: Option<&'static str> = match fiscal_period.as_deref() { - Some("q1") => Some("q1"), - Some("q2") => Some("q2"), - Some("q3") => Some("q3"), - Some("q4") => Some("q4"), - Some("fy") => Some("fy"), - Some("h1") => Some("h1"), - Some("h2") => Some("h2"), - _ => None, - }; + currency: String, + comparison_symbols: Option>, + ) -> PyResult { Ok(self .ctx - .financial_report_snapshot(symbol, report_static, fiscal_year, fiscal_period_static) + .valuation_comparison(symbol, currency, comparison_symbols) .map_err(ErrorNewType)? .into()) } diff --git a/python/src/fundamental/context_async.rs b/python/src/fundamental/context_async.rs index 2a929dfcf..8c162b9a5 100644 --- a/python/src/fundamental/context_async.rs +++ b/python/src/fundamental/context_async.rs @@ -254,36 +254,28 @@ impl AsyncFundamentalContext { .map(|b| b.unbind()) } - /// Get business segment breakdowns. Returns awaitable. - fn business_segments(&self, py: Python<'_>, symbol: String) -> PyResult> { + /// Get ranked list of top shareholders. Returns awaitable. + fn shareholder_top(&self, py: Python<'_>, symbol: String) -> PyResult> { let ctx = self.ctx.clone(); pyo3_async_runtimes::tokio::future_into_py(py, async move { - Ok(BusinessSegments::from( - ctx.business_segments(symbol).await.map_err(ErrorNewType)?, + Ok(ShareholderTopResponse::from( + ctx.shareholder_top(symbol).await.map_err(ErrorNewType)?, )) }) .map(|b| b.unbind()) } - /// Get historical business segment breakdowns. Returns awaitable. - #[pyo3(signature = (symbol, report = None, cate = None))] - fn business_segments_history( + /// Get holding history and detail for one shareholder. Returns awaitable. + fn shareholder_detail( &self, py: Python<'_>, symbol: String, - report: Option, - cate: Option, + object_id: i64, ) -> PyResult> { let ctx = self.ctx.clone(); - let report_static: Option<&'static str> = match report.as_deref() { - Some("qf") => Some("qf"), - Some("saf") => Some("saf"), - Some("af") => Some("af"), - _ => None, - }; pyo3_async_runtimes::tokio::future_into_py(py, async move { - Ok(BusinessSegmentsHistory::from( - ctx.business_segments_history(symbol, report_static, cate) + Ok(ShareholderDetailResponse::from( + ctx.shareholder_detail(symbol, object_id) .await .map_err(ErrorNewType)?, )) @@ -291,99 +283,24 @@ impl AsyncFundamentalContext { .map(|b| b.unbind()) } - /// Get historical institutional rating view time-series. Returns awaitable. - fn institution_rating_views(&self, py: Python<'_>, symbol: String) -> PyResult> { - let ctx = self.ctx.clone(); - pyo3_async_runtimes::tokio::future_into_py(py, async move { - Ok(InstitutionRatingViews::from( - ctx.institution_rating_views(symbol) - .await - .map_err(ErrorNewType)?, - )) - }) - .map(|b| b.unbind()) - } - - /// Get industry rank for a market. Returns awaitable. - fn industry_rank( - &self, - py: Python<'_>, - market: String, - indicator: String, - sort_type: String, - limit: u32, - ) -> PyResult> { - let ctx = self.ctx.clone(); - pyo3_async_runtimes::tokio::future_into_py(py, async move { - Ok(IndustryRankResponse::from( - ctx.industry_rank(market, indicator, sort_type, limit) - .await - .map_err(ErrorNewType)?, - )) - }) - .map(|b| b.unbind()) - } - - /// Get the industry peer chain for a security or industry. Returns + /// Get valuation comparison between a security and optional peers. Returns /// awaitable. - #[pyo3(signature = (counter_id, market, industry_id = None))] - fn industry_peers( + #[pyo3(signature = (symbol, currency, comparison_symbols = None))] + fn valuation_comparison( &self, py: Python<'_>, - counter_id: String, - market: String, - industry_id: Option, + symbol: String, + currency: String, + comparison_symbols: Option>, ) -> PyResult> { let ctx = self.ctx.clone(); pyo3_async_runtimes::tokio::future_into_py(py, async move { - Ok(IndustryPeersResponse::from( - ctx.industry_peers(counter_id, market, industry_id) + Ok(ValuationComparisonResponse::from( + ctx.valuation_comparison(symbol, currency, comparison_symbols) .await .map_err(ErrorNewType)?, )) }) .map(|b| b.unbind()) } - - /// Get a financial report snapshot. Returns awaitable. - #[pyo3(signature = (symbol, report = None, fiscal_year = None, fiscal_period = None))] - fn financial_report_snapshot( - &self, - py: Python<'_>, - symbol: String, - report: Option, - fiscal_year: Option, - fiscal_period: Option, - ) -> PyResult> { - let ctx = self.ctx.clone(); - let report_static: Option<&'static str> = match report.as_deref() { - Some("qf") => Some("qf"), - Some("saf") => Some("saf"), - Some("af") => Some("af"), - _ => None, - }; - let fiscal_period_static: Option<&'static str> = match fiscal_period.as_deref() { - Some("q1") => Some("q1"), - Some("q2") => Some("q2"), - Some("q3") => Some("q3"), - Some("q4") => Some("q4"), - Some("fy") => Some("fy"), - Some("h1") => Some("h1"), - Some("h2") => Some("h2"), - _ => None, - }; - pyo3_async_runtimes::tokio::future_into_py(py, async move { - Ok(FinancialReportSnapshot::from( - ctx.financial_report_snapshot( - symbol, - report_static, - fiscal_year, - fiscal_period_static, - ) - .await - .map_err(ErrorNewType)?, - )) - }) - .map(|b| b.unbind()) - } } diff --git a/python/src/fundamental/mod.rs b/python/src/fundamental/mod.rs index 618da182d..62dfd8da4 100644 --- a/python/src/fundamental/mod.rs +++ b/python/src/fundamental/mod.rs @@ -64,6 +64,11 @@ pub(crate) fn register_types(parent: &Bound) -> PyResult<()> { parent.add_class::()?; parent.add_class::()?; parent.add_class::()?; + parent.add_class::()?; + parent.add_class::()?; + parent.add_class::()?; + parent.add_class::()?; + parent.add_class::()?; parent.add_class::()?; parent.add_class::()?; Ok(()) diff --git a/python/src/fundamental/types.rs b/python/src/fundamental/types.rs index 4ce28bd32..8219dc6f4 100644 --- a/python/src/fundamental/types.rs +++ b/python/src/fundamental/types.rs @@ -1704,365 +1704,145 @@ impl From for lb::FinancialReportPeriod { } } -// ── business_segments ───────────────────────────────────────────── +// ── ShareholderTopResponse ──────────────────────────────────────── -/// One business segment item (latest snapshot) -#[pyclass(get_all, skip_from_py_object)] -#[derive(Debug, Clone)] -pub(crate) struct BusinessSegmentItem { - pub name: String, - pub percent: String, -} - -impl From for BusinessSegmentItem { - fn from(v: lb::BusinessSegmentItem) -> Self { - Self { - name: v.name, - percent: v.percent, - } - } -} - -/// Business segments response -#[pyclass(get_all, skip_from_py_object)] -#[derive(Debug, Clone)] -pub(crate) struct BusinessSegments { - pub date: String, - pub total: String, - pub currency: String, - pub business: Vec, -} - -impl From for BusinessSegments { - fn from(v: lb::BusinessSegments) -> Self { - Self { - date: v.date, - total: v.total, - currency: v.currency, - business: v.business.into_iter().map(Into::into).collect(), - } - } -} - -/// One business/regional segment item in a historical snapshot +/// 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 BusinessSegmentHistoryItem { - pub name: String, - pub percent: String, - pub value: String, +pub(crate) struct ShareholderTopResponse { + /// Raw top-shareholder data (JSON object) + pub data: JsonValue, } -impl From for BusinessSegmentHistoryItem { - fn from(v: lb::BusinessSegmentHistoryItem) -> Self { +impl From for ShareholderTopResponse { + fn from(v: lb::ShareholderTopResponse) -> Self { Self { - name: v.name, - percent: v.percent, - value: v.value, + data: JsonValue(v.data), } } } -/// One historical business segments snapshot -#[pyclass(get_all, skip_from_py_object)] -#[derive(Debug, Clone)] -pub(crate) struct BusinessSegmentsHistoricalItem { - pub date: String, - pub total: String, - pub currency: String, - pub business: Vec, - pub regionals: Vec, -} - -impl From for BusinessSegmentsHistoricalItem { - fn from(v: lb::BusinessSegmentsHistoricalItem) -> Self { - Self { - date: v.date, - total: v.total, - currency: v.currency, - business: v.business.into_iter().map(Into::into).collect(), - regionals: v.regionals.into_iter().map(Into::into).collect(), - } - } -} +// ── ShareholderDetailResponse ───────────────────────────────────── -/// Business segments history response +/// 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 BusinessSegmentsHistory { - pub historical: Vec, +pub(crate) struct ShareholderDetailResponse { + /// Raw shareholder detail data (JSON object) + pub data: JsonValue, } -impl From for BusinessSegmentsHistory { - fn from(v: lb::BusinessSegmentsHistory) -> Self { +impl From for ShareholderDetailResponse { + fn from(v: lb::ShareholderDetailResponse) -> Self { Self { - historical: v.historical.into_iter().map(Into::into).collect(), + data: JsonValue(v.data), } } } -// ── institution_rating_views ────────────────────────────────────── +// ── ValuationComparisonResponse ─────────────────────────────────── -/// One historical rating distribution snapshot +/// One historical valuation data point. #[pyclass(get_all, skip_from_py_object)] #[derive(Debug, Clone)] -pub(crate) struct InstitutionRatingViewItem { +pub(crate) struct ValuationHistoryPoint { + /// Date (RFC 3339) pub date: String, - pub buy: String, - pub over: String, - pub hold: String, - pub under: String, - pub sell: String, - pub total: String, + /// P/E ratio + pub pe: String, + /// P/B ratio + pub pb: String, + /// P/S ratio + pub ps: String, } -impl From for InstitutionRatingViewItem { - fn from(v: lb::InstitutionRatingViewItem) -> Self { +impl From for ValuationHistoryPoint { + fn from(v: lb::ValuationHistoryPoint) -> Self { Self { date: v.date, - buy: v.buy, - over: v.over, - hold: v.hold, - under: v.under, - sell: v.sell, - total: v.total, - } - } -} - -/// Institution rating views response -#[pyclass(get_all, skip_from_py_object)] -#[derive(Debug, Clone)] -pub(crate) struct InstitutionRatingViews { - pub elist: Vec, -} - -impl From for InstitutionRatingViews { - fn from(v: lb::InstitutionRatingViews) -> Self { - Self { - elist: v.elist.into_iter().map(Into::into).collect(), + pe: v.pe, + pb: v.pb, + ps: v.ps, } } } -// ── industry_rank ───────────────────────────────────────────────── - -/// One ranked industry item +/// One security's valuation comparison item. #[pyclass(get_all, skip_from_py_object)] #[derive(Debug, Clone)] -pub(crate) struct IndustryRankItem { - pub name: String, - pub counter_id: String, - pub chg: String, - pub leading_name: String, - pub leading_ticker: String, - pub leading_chg: String, - pub value_name: String, - pub value_data: String, -} - -impl From for IndustryRankItem { - fn from(v: lb::IndustryRankItem) -> Self { - Self { - name: v.name, - counter_id: v.counter_id, - chg: v.chg, - leading_name: v.leading_name, - leading_ticker: v.leading_ticker, - leading_chg: v.leading_chg, - value_name: v.value_name, - value_data: v.value_data, - } - } -} - -/// A group of ranked industry items -#[pyclass(get_all, skip_from_py_object)] -#[derive(Debug, Clone)] -pub(crate) struct IndustryRankGroup { - pub lists: Vec, -} - -impl From for IndustryRankGroup { - fn from(v: lb::IndustryRankGroup) -> Self { - Self { - lists: v.lists.into_iter().map(Into::into).collect(), - } - } -} - -/// Industry rank response -#[pyclass(get_all, skip_from_py_object)] -#[derive(Debug, Clone)] -pub(crate) struct IndustryRankResponse { - pub items: Vec, -} - -impl From for IndustryRankResponse { - fn from(v: lb::IndustryRankResponse) -> Self { - Self { - items: v.items.into_iter().map(Into::into).collect(), - } - } -} - -// ── industry_peers ──────────────────────────────────────────────── - -/// Top-level industry info in the peers response -#[pyclass(get_all, skip_from_py_object)] -#[derive(Debug, Clone)] -pub(crate) struct IndustryPeersTop { - pub name: String, - pub market: String, -} - -impl From for IndustryPeersTop { - fn from(v: lb::IndustryPeersTop) -> Self { - Self { - name: v.name, - market: v.market, - } - } -} - -/// A node in the recursive industry peer chain. -/// -/// `next_json` contains the child nodes serialised as a JSON string. -#[pyclass(get_all, skip_from_py_object)] -#[derive(Debug, Clone)] -pub(crate) struct IndustryPeerNode { +pub(crate) struct ValuationComparisonItem { + /// Symbol (e.g. `"AAPL.US"`) + pub symbol: String, + /// Security name pub name: String, - pub counter_id: String, - pub stock_num: i32, - pub chg: String, - pub ytd_chg: String, - /// Child nodes as a JSON string - pub next_json: String, + /// Currency + pub currency: String, + /// Market capitalisation + pub market_value: String, + /// Latest closing price + pub price_close: String, + /// P/E ratio + pub pe: String, + /// P/B ratio + pub pb: String, + /// P/S ratio + pub ps: String, + /// Return on equity + pub roe: String, + /// Earnings per share + pub eps: String, + /// Book value per share + pub bps: String, + /// Dividends per share + pub dps: String, + /// Dividend yield + pub div_yld: String, + /// Total assets + pub assets: String, + /// Historical valuation points + pub history: Vec, } -impl From for IndustryPeerNode { - fn from(v: lb::IndustryPeerNode) -> Self { +impl From for ValuationComparisonItem { + fn from(v: lb::ValuationComparisonItem) -> Self { Self { + symbol: v.symbol, name: v.name, - counter_id: v.counter_id, - stock_num: v.stock_num, - chg: v.chg, - ytd_chg: v.ytd_chg, - next_json: serde_json::to_string(&v.next).unwrap_or_default(), - } - } -} - -/// Industry peers response -#[pyclass(get_all, skip_from_py_object)] -#[derive(Debug, Clone)] -pub(crate) struct IndustryPeersResponse { - pub top: IndustryPeersTop, - pub chain: Option, -} - -impl From for IndustryPeersResponse { - fn from(v: lb::IndustryPeersResponse) -> Self { - Self { - top: v.top.into(), - chain: v.chain.map(Into::into), - } - } -} - -// ── financial_report_snapshot ───────────────────────────────────── - -/// A forecast metric in the financial report snapshot -#[pyclass(get_all, skip_from_py_object)] -#[derive(Debug, Clone)] -pub(crate) struct SnapshotForecastMetric { - pub value: String, - pub yoy: String, - pub cmp_desc: String, - pub est_value: String, -} - -impl From for SnapshotForecastMetric { - fn from(v: lb::SnapshotForecastMetric) -> Self { - Self { - value: v.value, - yoy: v.yoy, - cmp_desc: v.cmp_desc, - est_value: v.est_value, + currency: v.currency, + market_value: v.market_value, + price_close: v.price_close, + pe: v.pe, + pb: v.pb, + ps: v.ps, + roe: v.roe, + eps: v.eps, + bps: v.bps, + dps: v.dps, + div_yld: v.div_yld, + assets: v.assets, + history: v.history.into_iter().map(Into::into).collect(), } } } -/// A reported metric in the financial report snapshot +/// Valuation comparison response. #[pyclass(get_all, skip_from_py_object)] #[derive(Debug, Clone)] -pub(crate) struct SnapshotReportedMetric { - pub value: String, - pub yoy: String, -} - -impl From for SnapshotReportedMetric { - fn from(v: lb::SnapshotReportedMetric) -> Self { - Self { - value: v.value, - yoy: v.yoy, - } - } +pub(crate) struct ValuationComparisonResponse { + /// Valuation comparison items + pub list: Vec, } -/// Financial report snapshot response -#[pyclass(get_all, skip_from_py_object)] -#[derive(Debug, Clone)] -pub(crate) struct FinancialReportSnapshot { - pub name: String, - pub ticker: String, - pub fp_start: String, - pub fp_end: String, - pub currency: String, - pub report_desc: String, - pub fo_revenue: Option, - pub fo_ebit: Option, - pub fo_eps: Option, - pub fr_revenue: Option, - pub fr_profit: Option, - pub fr_operate_cash: Option, - pub fr_invest_cash: Option, - pub fr_finance_cash: Option, - pub fr_total_assets: Option, - pub fr_total_liability: Option, - pub fr_roe_ttm: String, - pub fr_profit_margin: String, - pub fr_profit_margin_ttm: String, - pub fr_asset_turn_ttm: String, - pub fr_leverage_ttm: String, - pub fr_debt_assets_ratio: String, -} - -impl From for FinancialReportSnapshot { - fn from(v: lb::FinancialReportSnapshot) -> Self { +impl From for ValuationComparisonResponse { + fn from(v: lb::ValuationComparisonResponse) -> Self { Self { - name: v.name, - ticker: v.ticker, - fp_start: v.fp_start, - fp_end: v.fp_end, - currency: v.currency, - report_desc: v.report_desc, - fo_revenue: v.fo_revenue.map(Into::into), - fo_ebit: v.fo_ebit.map(Into::into), - fo_eps: v.fo_eps.map(Into::into), - fr_revenue: v.fr_revenue.map(Into::into), - fr_profit: v.fr_profit.map(Into::into), - fr_operate_cash: v.fr_operate_cash.map(Into::into), - fr_invest_cash: v.fr_invest_cash.map(Into::into), - fr_finance_cash: v.fr_finance_cash.map(Into::into), - fr_total_assets: v.fr_total_assets.map(Into::into), - fr_total_liability: v.fr_total_liability.map(Into::into), - fr_roe_ttm: v.fr_roe_ttm, - fr_profit_margin: v.fr_profit_margin, - fr_profit_margin_ttm: v.fr_profit_margin_ttm, - fr_asset_turn_ttm: v.fr_asset_turn_ttm, - fr_leverage_ttm: v.fr_leverage_ttm, - fr_debt_assets_ratio: v.fr_debt_assets_ratio, + list: v.list.into_iter().map(Into::into).collect(), } } } 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..b1efb188f 100644 --- a/python/src/market/context.rs +++ b/python/src/market/context.rs @@ -99,4 +99,36 @@ impl MarketContext { fn constituent(&self, symbol: String) -> PyResult { Ok(self.ctx.constituent(symbol).map_err(ErrorNewType)?.into()) } + + /// Get top movers (stocks with unusual price movements) across one or more + /// markets. + #[pyo3(signature = (markets, sort = 0, date = None, limit = 20))] + fn top_movers( + &self, + markets: Vec, + sort: u32, + date: Option, + limit: u32, + ) -> PyResult { + Ok(self + .ctx + .top_movers(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..0725bd9dc 100644 --- a/python/src/market/context_async.rs +++ b/python/src/market/context_async.rs @@ -137,4 +137,52 @@ impl AsyncMarketContext { }) .map(|b| b.unbind()) } + + /// Get top movers (stocks with unusual price movements) across one or more + /// markets. Returns awaitable. + #[pyo3(signature = (markets, sort = 0, date = None, limit = 20))] + fn top_movers( + &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(TopMoversResponse::from( + ctx.top_movers(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..5a19bf585 100644 --- a/python/src/market/mod.rs +++ b/python/src/market/mod.rs @@ -27,6 +27,12 @@ pub(crate) fn register_types(parent: &Bound) -> PyResult<()> { parent.add_class::()?; parent.add_class::()?; parent.add_class::()?; + parent.add_class::()?; + parent.add_class::()?; + parent.add_class::()?; + parent.add_class::()?; + parent.add_class::()?; + parent.add_class::()?; parent.add_class::()?; parent.add_class::()?; Ok(()) diff --git a/python/src/market/types.rs b/python/src/market/types.rs index 51e10f2cf..4ba37f306 100644 --- a/python/src/market/types.rs +++ b/python/src/market/types.rs @@ -1,6 +1,198 @@ use longbridge::market::types as lb; use pyo3::prelude::*; +// ── TopMoversResponse ───────────────────────────────────────────── + +/// Stock information within a top-movers event. +#[pyclass(get_all, skip_from_py_object)] +#[derive(Debug, Clone)] +pub(crate) struct TopMoversStock { + /// Symbol (e.g. `"NVDA.US"`) + pub symbol: String, + /// Ticker code + pub code: String, + /// Security name + pub name: String, + /// Full name + pub full_name: String, + /// Price change (decimal ratio) + pub change: String, + /// Latest price + pub last_done: String, + /// Market code + pub market: String, + /// Labels / tags + pub labels: Vec, + /// Logo URL + pub logo: String, +} + +impl From for TopMoversStock { + fn from(v: lb::TopMoversStock) -> Self { + Self { + symbol: v.symbol, + code: v.code, + name: v.name, + full_name: v.full_name, + change: v.change, + last_done: v.last_done, + market: v.market, + labels: v.labels, + logo: v.logo, + } + } +} + +/// One top-movers event entry. +#[pyclass(get_all, skip_from_py_object)] +#[derive(Debug, Clone)] +pub(crate) struct TopMoversEvent { + /// Event time (RFC 3339) + pub timestamp: String, + /// Alert reason description + pub alert_reason: String, + /// Alert type code + pub alert_type: i64, + /// Stock information + pub stock: TopMoversStock, + /// Associated news post (raw JSON object) + pub post: crate::fundamental::types::JsonValue, +} + +impl From for TopMoversEvent { + fn from(v: lb::TopMoversEvent) -> Self { + Self { + timestamp: v.timestamp, + alert_reason: v.alert_reason, + alert_type: v.alert_type, + stock: v.stock.into(), + post: crate::fundamental::types::JsonValue(v.post), + } + } +} + +/// Top movers response. +#[pyclass(get_all, skip_from_py_object)] +#[derive(Debug, Clone)] +pub(crate) struct TopMoversResponse { + /// Top-mover events + pub events: Vec, + /// Pagination cursor for next page (raw JSON object) + pub next_params: crate::fundamental::types::JsonValue, +} + +impl From for TopMoversResponse { + fn from(v: lb::TopMoversResponse) -> Self { + Self { + events: v.events.into_iter().map(Into::into).collect(), + next_params: crate::fundamental::types::JsonValue(v.next_params), + } + } +} + +// ── 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 ────────────────────────────────────────────── + +/// One ranked security item. +#[pyclass(get_all, skip_from_py_object)] +#[derive(Debug, Clone)] +pub(crate) struct RankListItem { + /// Symbol (e.g. `"MU.US"`) + pub symbol: String, + /// Ticker code + pub code: String, + /// Security name + pub name: String, + /// Latest price + pub last_done: String, + /// Price change ratio + pub chg: String, + /// Absolute price change + pub change: String, + /// Net inflow + pub inflow: String, + /// Market cap + pub market_cap: String, + /// Industry name + pub industry: String, + /// Pre/post market price + pub pre_post_price: String, + /// Pre/post market change + pub pre_post_chg: String, + /// Amplitude + pub amplitude: String, + /// 5-day change + pub five_day_chg: String, + /// Turnover rate + pub turnover_rate: String, + /// Volume ratio + pub volume_rate: String, + /// P/B ratio (TTM) + pub pb_ttm: String, +} + +impl From for RankListItem { + fn from(v: lb::RankListItem) -> Self { + Self { + symbol: v.symbol, + code: v.code, + name: v.name, + last_done: v.last_done, + chg: v.chg, + change: v.change, + inflow: v.inflow, + market_cap: v.market_cap, + industry: v.industry, + pre_post_price: v.pre_post_price, + pre_post_chg: v.pre_post_chg, + amplitude: v.amplitude, + five_day_chg: v.five_day_chg, + turnover_rate: v.turnover_rate, + volume_rate: v.volume_rate, + pb_ttm: v.pb_ttm, + } + } +} + +/// Rank list response. +#[pyclass(get_all, skip_from_py_object)] +#[derive(Debug, Clone)] +pub(crate) struct RankListResponse { + /// Whether delayed / BMP data + pub bmp: bool, + /// Ranked security items + pub lists: Vec, +} + +impl From for RankListResponse { + fn from(v: lb::RankListResponse) -> Self { + Self { + bmp: v.bmp, + lists: v.lists.into_iter().map(Into::into).collect(), + } + } +} + // ── 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..749f0deb0 100644 --- a/python/src/quote/mod.rs +++ b/python/src/quote/mod.rs @@ -64,8 +64,10 @@ pub(crate) fn register_types(parent: &Bound) -> PyResult<()> { parent.add_class::()?; parent.add_class::()?; parent.add_class::()?; + parent.add_class::()?; parent.add_class::()?; - parent.add_class::()?; + parent.add_class::()?; + parent.add_class::()?; parent.add_class::()?; parent.add_class::()?; parent.add_class::()?; diff --git a/python/src/quote/types.rs b/python/src/quote/types.rs index 0c4816dca..d9849dac0 100644 --- a/python/src/quote/types.rs +++ b/python/src/quote/types.rs @@ -1435,57 +1435,114 @@ pub(crate) struct HistoryMarketTemperatureResponse { records: Vec, } -// ── Step 3: short_positions / option_volume / option_volume_daily ─ +// ── Step 3: short_positions / short_trades / option_volume / +// option_volume_daily -/// Short interest response +/// One short-position data point (unified for US and HK markets). +#[pyclass(get_all, skip_from_py_object)] +#[derive(Debug, Clone)] +pub(crate) struct ShortPositionsItem { + /// Trading date (RFC 3339) + pub timestamp: String, + /// Short ratio + pub rate: String, + /// Closing price + pub close: String, + /// [US] Number of short shares outstanding + pub current_shares_short: String, + /// [US] Average daily share volume + pub avg_daily_share_volume: String, + /// [US] Days to cover ratio + pub days_to_cover: String, + /// [HK] Short sale amount (HKD) + pub amount: String, + /// [HK] Short position balance + pub balance: String, + /// [HK] Cost / closing price + pub cost: String, +} + +impl From for ShortPositionsItem { + fn from(v: longbridge::quote::ShortPositionsItem) -> Self { + Self { + timestamp: v.timestamp, + rate: v.rate, + close: v.close, + current_shares_short: v.current_shares_short, + avg_daily_share_volume: v.avg_daily_share_volume, + days_to_cover: v.days_to_cover, + amount: v.amount, + balance: v.balance, + cost: v.cost, + } + } +} + +/// Short interest / positions response (HK or US). #[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, + /// Short position data points + pub data: Vec, } 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, } } } -/// One short position data point +/// One short-trade data point (unified for US and HK markets). #[pyclass(get_all, skip_from_py_object)] #[derive(Debug, Clone)] -pub(crate) struct ShortPosition { - /// Settlement date (unix timestamp string) +pub(crate) struct ShortTradesItem { + /// Trading date (RFC 3339) 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 { + /// [US] NYSE short amount + pub nus_amount: String, + /// [US] NY short amount + pub ny_amount: String, + /// [US] Total short amount + pub total_amount: String, + /// [HK] Short sale amount + pub amount: String, + /// [HK] Short position balance + pub balance: String, +} + +impl From for ShortTradesItem { + fn from(v: longbridge::quote::ShortTradesItem) -> Self { Self { timestamp: v.timestamp, rate: v.rate, - 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, + nus_amount: v.nus_amount, + ny_amount: v.ny_amount, + total_amount: v.total_amount, + amount: v.amount, + balance: v.balance, + } + } +} + +/// Short trade records response (HK or US). +#[pyclass(get_all, skip_from_py_object)] +#[derive(Debug, Clone)] +pub(crate) struct ShortTradesResponse { + /// Short trade data points + pub data: Vec, +} + +impl From for ShortTradesResponse { + fn from(v: longbridge::quote::ShortTradesResponse) -> Self { + Self { + data: v.data.into_iter().map(Into::into).collect(), } } } 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/fundamental.rs b/rust/src/blocking/fundamental.rs index b0e539c51..90b209f48 100644 --- a/rust/src/blocking/fundamental.rs +++ b/rust/src/blocking/fundamental.rs @@ -248,4 +248,36 @@ impl FundamentalContextSync { .await }) } + + /// Get ranked list of top shareholders + pub fn shareholder_top( + &self, + symbol: impl Into + Send + 'static, + ) -> Result { + self.rt + .call(move |ctx| async move { ctx.shareholder_top(symbol).await }) + } + + /// Get holding history and detail for one shareholder object + pub fn shareholder_detail( + &self, + symbol: impl Into + Send + 'static, + object_id: i64, + ) -> Result { + self.rt + .call(move |ctx| async move { ctx.shareholder_detail(symbol, object_id).await }) + } + + /// Get valuation comparison between a security and optional peers + pub fn valuation_comparison( + &self, + symbol: impl Into + Send + 'static, + currency: impl Into + Send + 'static, + comparison_symbols: Option>, + ) -> Result { + self.rt.call(move |ctx| async move { + ctx.valuation_comparison(symbol, currency, comparison_symbols) + .await + }) + } } diff --git a/rust/src/blocking/market.rs b/rust/src/blocking/market.rs index 534758e72..f47cbe94e 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, + TopMoversResponse, TradeStatsResponse, + }, + }, }; /// Blocking market data context @@ -105,4 +113,33 @@ impl MarketContextSync { self.rt .call(move |ctx| async move { ctx.constituent(symbol).await }) } + + /// Get top movers (stocks with unusual price movements) across one or more + /// markets + pub fn top_movers( + &self, + markets: Vec, + sort: u32, + date: Option, + limit: u32, + ) -> Result { + self.rt + .call(move |ctx| async move { ctx.top_movers(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..fbba09273 100644 --- a/rust/src/blocking/quote.rs +++ b/rust/src/blocking/quote.rs @@ -13,9 +13,9 @@ use crate::{ 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, + 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 @@ -1202,4 +1203,14 @@ impl QuoteContextSync { self.rt .call(move |ctx| async move { ctx.update_pinned(mode, symbols).await }) } + + /// Get short trade records for a HK or US security + pub fn short_trades( + &self, + symbol: impl Into + Send + 'static, + count: u32, + ) -> Result { + self.rt + .call(move |ctx| async move { ctx.short_trades(symbol, count).await }) + } } diff --git a/rust/src/blocking/screener.rs b/rust/src/blocking/screener.rs new file mode 100644 index 000000000..b5cbd3e1b --- /dev/null +++ b/rust/src/blocking/screener.rs @@ -0,0 +1,73 @@ +use std::sync::Arc; + +use tokio::sync::mpsc; + +use crate::{ + Config, Result, + blocking::runtime::BlockingRuntime, + screener::{ + ScreenerContext, + types::{ + ScreenerIndicatorsResponse, ScreenerRecommendStrategiesResponse, + ScreenerSearchResponse, ScreenerStrategyResponse, ScreenerUserStrategiesResponse, + }, + }, +}; + +/// Blocking screener context +pub struct ScreenerContextSync { + rt: BlockingRuntime, +} + +impl ScreenerContextSync { + /// Create a [`ScreenerContextSync`] + pub fn new(config: Arc) -> Result { + let rt = BlockingRuntime::try_new( + move || { + let ctx = ScreenerContext::new(config); + let (tx, rx) = mpsc::unbounded_channel::(); + std::mem::forget(tx); + Ok::<_, crate::Error>((ctx, rx)) + }, + |_: std::convert::Infallible| {}, + )?; + Ok(Self { rt }) + } + + /// Get recommended built-in screener strategies + pub fn screener_recommend_strategies(&self) -> Result { + self.rt + .call(|ctx| async move { ctx.screener_recommend_strategies().await }) + } + + /// Get the current user's saved screener strategies + pub fn screener_user_strategies(&self) -> Result { + self.rt + .call(|ctx| async move { ctx.screener_user_strategies().await }) + } + + /// Get detail for one screener strategy by ID + pub fn screener_strategy(&self, id: i64) -> Result { + self.rt + .call(move |ctx| async move { ctx.screener_strategy(id).await }) + } + + /// Search / screen securities using a strategy + pub fn screener_search( + &self, + market: impl Into + Send + 'static, + strategy_id: Option, + page: u32, + size: u32, + ) -> Result { + self.rt.call(move |ctx| async move { + ctx.screener_search(market, strategy_id, page, size).await + }) + } + + /// Get all available screener indicator definitions + pub fn screener_indicators(&self) -> Result { + self.rt + .call(|ctx| async move { ctx.screener_indicators().await }) + } +} diff --git a/rust/src/fundamental/context.rs b/rust/src/fundamental/context.rs index a71fffbd0..b7c23aff4 100644 --- a/rust/src/fundamental/context.rs +++ b/rust/src/fundamental/context.rs @@ -4,7 +4,23 @@ use longbridge_httpcli::{HttpClient, Json, Method}; use serde::{Serialize, de::DeserializeOwned}; use tracing::{Subscriber, dispatcher, instrument::WithSubscriber}; -use crate::{Config, Result, fundamental::types::*, utils::counter::symbol_to_counter_id}; +use crate::{ + Config, Result, + fundamental::types::*, + utils::counter::{counter_id_to_symbol, symbol_to_counter_id}, +}; + +/// Convert a Unix-seconds string to RFC 3339. +fn unix_secs_str_to_rfc3339(s: &str) -> String { + s.parse::() + .ok() + .and_then(|ts| time::OffsetDateTime::from_unix_timestamp(ts).ok()) + .map(|dt| { + use time::format_description::well_known::Rfc3339; + dt.format(&Rfc3339).unwrap_or_default() + }) + .unwrap_or_else(|| s.to_string()) +} struct InnerFundamentalContext { http_cli: HttpClient, @@ -544,6 +560,30 @@ impl FundamentalContext { .await } + // ── shareholder_top ─────────────────────────────────────────── + + /// Get a ranked list of top shareholders for a security. + /// + /// Path: `GET /v1/quote/shareholders/top` + pub async fn shareholder_top( + &self, + symbol: impl Into, + ) -> Result { + #[derive(Serialize)] + struct Query { + counter_id: String, + } + let raw: serde_json::Value = self + .get( + "/v1/quote/shareholders/top", + Query { + counter_id: symbol_to_counter_id(&symbol.into()), + }, + ) + .await?; + Ok(ShareholderTopResponse { data: raw }) + } + // ── institution_rating_views ────────────────────────────────── /// Get historical institutional rating view time-series for a security. @@ -566,14 +606,38 @@ impl FundamentalContext { .await } + // ── shareholder_detail ──────────────────────────────────────── + + /// Get holding history and detail for one shareholder object. + /// + /// Path: `GET /v1/quote/shareholders/holding` + pub async fn shareholder_detail( + &self, + symbol: impl Into, + object_id: i64, + ) -> Result { + #[derive(Serialize)] + struct Query { + counter_id: String, + object_id: String, + } + let raw: serde_json::Value = self + .get( + "/v1/quote/shareholders/holding", + Query { + counter_id: symbol_to_counter_id(&symbol.into()), + object_id: object_id.to_string(), + }, + ) + .await?; + Ok(ShareholderDetailResponse { data: raw }) + } + // ── industry_rank ───────────────────────────────────────────── /// Get industry rank for a market. /// /// Path: `GET /v1/quote/industry/rank` - /// - /// `indicator` is a numeric string `"0"`–`"7"`; - /// `sort_type` is `"0"` (ascending) or `"1"` (descending). pub async fn industry_rank( &self, market: impl Into, @@ -605,10 +669,6 @@ impl FundamentalContext { /// Get the industry peer chain for a security or industry. /// /// Path: `GET /v1/quote/industries/peers` - /// - /// `counter_id` may be a regular symbol (e.g. `"AAPL.US"`) or an industry - /// counter ID (e.g. `"BK/US/123"`) — pass it through as-is if it already - /// contains a `/`. pub async fn industry_peers( &self, counter_id: impl Into, @@ -674,4 +734,76 @@ impl FundamentalContext { ) .await } + + // ── valuation_comparison ────────────────────────────────────── + + /// Get valuation comparison between a security and optional peers. + /// + /// Path: `GET /v1/quote/compare/valuation` + pub async fn valuation_comparison( + &self, + symbol: impl Into, + currency: impl Into, + comparison_symbols: Option>, + ) -> Result { + #[derive(Serialize)] + struct Query { + counter_id: String, + currency: String, + #[serde(skip_serializing_if = "Option::is_none")] + comparison_counter_ids: Option, + } + let comparison_counter_ids = comparison_symbols.map(|syms| { + let ids: Vec = syms.iter().map(|s| symbol_to_counter_id(s)).collect(); + serde_json::to_string(&ids).unwrap_or_default() + }); + let raw: serde_json::Value = self + .get( + "/v1/quote/compare/valuation", + Query { + counter_id: symbol_to_counter_id(&symbol.into()), + currency: currency.into(), + comparison_counter_ids, + }, + ) + .await?; + let list = raw["list"] + .as_array() + .cloned() + .unwrap_or_default() + .into_iter() + .map(|item| { + let history = item["history"] + .as_array() + .cloned() + .unwrap_or_default() + .into_iter() + .map(|h| ValuationHistoryPoint { + date: unix_secs_str_to_rfc3339(h["date"].as_str().unwrap_or("")), + pe: h["pe"].as_str().unwrap_or("").to_string(), + pb: h["pb"].as_str().unwrap_or("").to_string(), + ps: h["ps"].as_str().unwrap_or("").to_string(), + }) + .collect(); + ValuationComparisonItem { + symbol: counter_id_to_symbol(item["counter_id"].as_str().unwrap_or("")), + name: item["name"].as_str().unwrap_or("").to_string(), + currency: item["currency"].as_str().unwrap_or("").to_string(), + market_value: item["market_value"].as_str().unwrap_or("").to_string(), + price_close: item["price_close"].as_str().unwrap_or("").to_string(), + pe: item["pe"].as_str().unwrap_or("").to_string(), + pb: item["pb"].as_str().unwrap_or("").to_string(), + ps: item["ps"].as_str().unwrap_or("").to_string(), + roe: item["roe"].as_str().unwrap_or("").to_string(), + eps: item["eps"].as_str().unwrap_or("").to_string(), + bps: item["bps"].as_str().unwrap_or("").to_string(), + dps: item["dps"].as_str().unwrap_or("").to_string(), + div_yld: item["div_yld"].as_str().unwrap_or("").to_string(), + assets: item["assets"].as_str().unwrap_or("").to_string(), + history, + } + }) + .collect(); + Ok(ValuationComparisonResponse { list }) + } } diff --git a/rust/src/fundamental/types.rs b/rust/src/fundamental/types.rs index 556074048..b6332d4d2 100644 --- a/rust/src/fundamental/types.rs +++ b/rust/src/fundamental/types.rs @@ -1350,6 +1350,81 @@ pub struct SnapshotReportedMetric { pub yoy: String, } +// ── shareholder_top ─────────────────────────────────────────────── + +/// Response for [`crate::FundamentalContext::shareholder_top`] +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct ShareholderTopResponse { + /// Raw top-shareholder data + pub data: serde_json::Value, +} + +// ── shareholder_detail ──────────────────────────────────────────── + +/// Response for [`crate::FundamentalContext::shareholder_detail`] +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct ShareholderDetailResponse { + /// Raw shareholder detail data + pub data: serde_json::Value, +} + +// ── valuation_comparison ────────────────────────────────────────── + +/// One historical valuation data point. +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct ValuationHistoryPoint { + /// Date (RFC 3339, converted from Unix timestamp) + pub date: String, + /// P/E ratio + pub pe: String, + /// P/B ratio + pub pb: String, + /// P/S ratio + pub ps: String, +} + +/// One security's valuation comparison item. +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct ValuationComparisonItem { + /// Symbol (converted from counter_id) + pub symbol: String, + /// Security name + pub name: String, + /// Currency + pub currency: String, + /// Market capitalisation + pub market_value: String, + /// Latest closing price + pub price_close: String, + /// P/E ratio + pub pe: String, + /// P/B ratio + pub pb: String, + /// P/S ratio + pub ps: String, + /// Return on equity + pub roe: String, + /// Earnings per share + pub eps: String, + /// Book value per share + pub bps: String, + /// Dividends per share + pub dps: String, + /// Dividend yield + pub div_yld: String, + /// Total assets + pub assets: String, + /// Historical valuation points + pub history: Vec, +} + +/// Response for [`crate::FundamentalContext::valuation_comparison`] +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct ValuationComparisonResponse { + /// Valuation comparison items + pub list: Vec, +} + /// 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..4edba7306 100644 --- a/rust/src/market/context.rs +++ b/rust/src/market/context.rs @@ -7,9 +7,26 @@ use tracing::{Subscriber, dispatcher, instrument::WithSubscriber}; use crate::{ Config, Result, market::types::*, - utils::counter::{index_symbol_to_counter_id, symbol_to_counter_id}, + utils::counter::{counter_id_to_symbol, index_symbol_to_counter_id, symbol_to_counter_id}, }; +/// Convert a Unix-seconds value (integer or string) to RFC 3339. +fn unix_secs_to_rfc3339(ts: i64) -> String { + time::OffsetDateTime::from_unix_timestamp(ts) + .map(|dt| { + use time::format_description::well_known::Rfc3339; + dt.format(&Rfc3339).unwrap_or_default() + }) + .unwrap_or_else(|_| ts.to_string()) +} + +/// Convert a Unix-seconds string to RFC 3339. +fn unix_secs_str_to_rfc3339(s: &str) -> String { + s.parse::() + .map(unix_secs_to_rfc3339) + .unwrap_or_else(|_| s.to_string()) +} + struct InnerMarketContext { http_cli: HttpClient, log_subscriber: Arc, @@ -68,6 +85,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 +298,156 @@ impl MarketContext { ) .await } + + // ── top_movers ──────────────────────────────────────────────── + + /// Get top movers (stocks with unusual price movements) across one or more + /// markets. + /// + /// Path: `POST /v1/quote/market/stock-events` + /// + /// `sort` is the sort order code (0 = ascending, 1 = descending). + /// `date` is an optional date filter in `"YYYY-MM-DD"` format. + pub async fn top_movers( + &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, + } + let raw: serde_json::Value = self + .post( + "/v1/quote/market/stock-events", + Body { + limit, + sort, + markets, + date, + }, + ) + .await?; + + let events = raw["events"] + .as_array() + .cloned() + .unwrap_or_default() + .into_iter() + .map(|ev| { + let ts = if let Some(n) = ev["timestamp"].as_i64() { + unix_secs_to_rfc3339(n) + } else if let Some(s) = ev["timestamp"].as_str() { + unix_secs_str_to_rfc3339(s) + } else { + String::new() + }; + let stock_val = &ev["stock"]; + let stock = TopMoversStock { + symbol: counter_id_to_symbol(stock_val["counter_id"].as_str().unwrap_or("")), + code: stock_val["code"].as_str().unwrap_or("").to_string(), + name: stock_val["name"].as_str().unwrap_or("").to_string(), + full_name: stock_val["full_name"].as_str().unwrap_or("").to_string(), + change: stock_val["change"].as_str().unwrap_or("").to_string(), + last_done: stock_val["last_done"].as_str().unwrap_or("").to_string(), + market: stock_val["market"].as_str().unwrap_or("").to_string(), + labels: stock_val["labels"] + .as_array() + .map(|arr| { + arr.iter() + .filter_map(|l| l.as_str().map(|s| s.to_string())) + .collect() + }) + .unwrap_or_default(), + logo: stock_val["logo"].as_str().unwrap_or("").to_string(), + }; + TopMoversEvent { + timestamp: ts, + alert_reason: ev["alert_reason"].as_str().unwrap_or("").to_string(), + alert_type: ev["alert_type"].as_i64().unwrap_or(0), + stock, + post: ev["post"].clone(), + } + }) + .collect(); + let next_params = raw["next_params"].clone(); + Ok(TopMoversResponse { + events, + next_params, + }) + } + + // ── rank_categories ─────────────────────────────────────────── + + /// 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 {} + let raw: serde_json::Value = self + .get("/v1/quote/market/rank/categories", Empty {}) + .await?; + Ok(RankCategoriesResponse { data: raw }) + } + + // ── 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, + } + 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?; + let bmp = raw["bmp"].as_bool().unwrap_or(false); + let lists = raw["lists"] + .as_array() + .cloned() + .unwrap_or_default() + .into_iter() + .map(|item| RankListItem { + symbol: counter_id_to_symbol(item["counter_id"].as_str().unwrap_or("")), + code: item["code"].as_str().unwrap_or("").to_string(), + name: item["name"].as_str().unwrap_or("").to_string(), + last_done: item["last_done"].as_str().unwrap_or("").to_string(), + chg: item["chg"].as_str().unwrap_or("").to_string(), + change: item["change"].as_str().unwrap_or("").to_string(), + inflow: item["inflow"].as_str().unwrap_or("").to_string(), + market_cap: item["market_cap"].as_str().unwrap_or("").to_string(), + industry: item["industry"].as_str().unwrap_or("").to_string(), + pre_post_price: item["pre_post_price"].as_str().unwrap_or("").to_string(), + pre_post_chg: item["pre_post_chg"].as_str().unwrap_or("").to_string(), + amplitude: item["amplitude"].as_str().unwrap_or("").to_string(), + five_day_chg: item["five_day_chg"].as_str().unwrap_or("").to_string(), + turnover_rate: item["turnover_rate"].as_str().unwrap_or("").to_string(), + volume_rate: item["volume_rate"].as_str().unwrap_or("").to_string(), + pb_ttm: item["pb_ttm"].as_str().unwrap_or("").to_string(), + }) + .collect(); + Ok(RankListResponse { bmp, lists }) + } } diff --git a/rust/src/market/types.rs b/rust/src/market/types.rs index ea1fd8253..8f62cdd4c 100644 --- a/rust/src/market/types.rs +++ b/rust/src/market/types.rs @@ -331,6 +331,125 @@ pub struct ConstituentStock { pub trade_status: i32, } +// ── top_movers ──────────────────────────────────────────────────── + +/// Stock information within a top-movers event. +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct TopMoversStock { + /// Symbol (converted from counter_id, e.g. `"NVDA.US"`) + pub symbol: String, + /// Ticker code (e.g. `"NVDA"`) + pub code: String, + /// Security name + pub name: String, + /// Full name + #[serde(default)] + pub full_name: String, + /// Price change (decimal ratio) + pub change: String, + /// Latest price + pub last_done: String, + /// Market code + pub market: String, + /// Labels / tags + #[serde(default)] + pub labels: Vec, + /// Logo URL + #[serde(default)] + pub logo: String, +} + +/// One top-movers event entry. +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct TopMoversEvent { + /// Event time (RFC 3339) + pub timestamp: String, + /// Alert reason description + pub alert_reason: String, + /// Alert type code + pub alert_type: i64, + /// Stock information + pub stock: TopMoversStock, + /// Associated news post (raw JSON, complex structure) + pub post: serde_json::Value, +} + +/// Response for [`crate::MarketContext::top_movers`] +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct TopMoversResponse { + /// Top-mover events + pub events: Vec, + /// Pagination cursor for next page + pub next_params: 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 ───────────────────────────────────────────────────── + +/// One ranked security item. +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct RankListItem { + /// Symbol (converted from counter_id, e.g. `"MU.US"`) + pub symbol: String, + /// Ticker code (e.g. `"MU"`) + pub code: String, + /// Security name + pub name: String, + /// Latest price + pub last_done: String, + /// Price change ratio (decimal) + pub chg: String, + /// Absolute price change + pub change: String, + /// Net inflow + pub inflow: String, + /// Market cap + pub market_cap: String, + /// Industry name + pub industry: String, + /// Pre/post market price + #[serde(default)] + pub pre_post_price: String, + /// Pre/post market change + #[serde(default)] + pub pre_post_chg: String, + /// Amplitude + #[serde(default)] + pub amplitude: String, + /// 5-day change + #[serde(default)] + pub five_day_chg: String, + /// Turnover rate + #[serde(default)] + pub turnover_rate: String, + /// Volume ratio + #[serde(default)] + pub volume_rate: String, + /// P/B ratio (TTM) + #[serde(default)] + pub pb_ttm: String, +} + +/// Response for [`crate::MarketContext::rank_list`] +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct RankListResponse { + /// Whether delayed / BMP data + pub bmp: bool, + /// Ranked security items + pub lists: Vec, +} + // ── enums ───────────────────────────────────────────────────────── /// Broker holding lookback period diff --git a/rust/src/quote/context.rs b/rust/src/quote/context.rs index b8b428273..168525c95 100644 --- a/rust/src/quote/context.rs +++ b/rust/src/quote/context.rs @@ -20,8 +20,9 @@ use crate::{ ParticipantInfo, Period, PushEvent, QuotePackageDetail, RealtimeQuote, RequestCreateWatchlistGroup, RequestUpdateWatchlistGroup, Security, SecurityBrokers, SecurityCalcIndex, SecurityDepth, SecurityListCategory, SecurityQuote, SecurityStaticInfo, - ShortPositionsResponse, StrikePriceInfo, Subscription, Trade, TradeSessions, WarrantInfo, - WarrantQuote, WarrantType, WatchlistGroup, + ShortPositionsItem, ShortPositionsResponse, ShortTradesItem, ShortTradesResponse, + StrikePriceInfo, Subscription, Trade, TradeSessions, WarrantInfo, WarrantQuote, + WarrantType, WatchlistGroup, cache::{Cache, CacheWithKey}, cmd_code, core::{Command, Core, UserProfile}, @@ -37,6 +38,19 @@ use crate::{ const RETRY_COUNT: usize = 3; const PARTICIPANT_INFO_CACHE_TIMEOUT: Duration = Duration::from_secs(30 * 60); + +/// Convert a Unix-seconds string (or integer string) to an RFC 3339 timestamp. +/// If parsing fails, the original string is returned unchanged. +fn unix_secs_to_rfc3339(s: &str) -> String { + s.parse::() + .ok() + .and_then(|ts| time::OffsetDateTime::from_unix_timestamp(ts).ok()) + .map(|dt| { + use time::format_description::well_known::Rfc3339; + dt.format(&Rfc3339).unwrap_or_default() + }) + .unwrap_or_else(|| s.to_string()) +} const ISSUER_INFO_CACHE_TIMEOUT: Duration = Duration::from_secs(30 * 60); const OPTION_CHAIN_EXPIRY_DATE_LIST_CACHE_TIMEOUT: Duration = Duration::from_secs(30 * 60); const OPTION_CHAIN_STRIKE_INFO_CACHE_TIMEOUT: Duration = Duration::from_secs(30 * 60); @@ -1957,35 +1971,81 @@ 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 + // Response: {"counter_id":"ST/US/AAPL","data":[{...}]} + let outer: serde_json::Value = 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::>() + .response::>() .send() .with_subscriber(self.0.log_subscriber.clone()) - .await?; - Ok(resp.0) + .await? + .0; + let empty = vec![]; + let raw = outer["data"].as_array().unwrap_or(&empty); + let data = raw + .iter() + .map(|v| { + let ts_str = v["timestamp"].as_str().unwrap_or("").to_string(); + ShortPositionsItem { + timestamp: unix_secs_to_rfc3339(&ts_str), + rate: v["rate"].as_str().unwrap_or("").to_string(), + close: v["close"].as_str().unwrap_or("").to_string(), + current_shares_short: v["current_shares_short"] + .as_str() + .unwrap_or("") + .to_string(), + avg_daily_share_volume: v["avg_daily_share_volume"] + .as_str() + .unwrap_or("") + .to_string(), + days_to_cover: v["days_to_cover"].as_str().unwrap_or("").to_string(), + amount: v["amount"].as_str().unwrap_or("").to_string(), + balance: v["balance"].as_str().unwrap_or("").to_string(), + cost: v["cost"].as_str().unwrap_or("").to_string(), + } + }) + .collect(); + Ok(ShortPositionsResponse { data }) } // ── option_volume ───────────────────────────────────────────── @@ -2046,6 +2106,73 @@ impl QuoteContext { .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); + // Response: {"counter_id":"ST/HK/700","data":[{...}]} + let outer: serde_json::Value = 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? + .0; + let empty = vec![]; + let raw = outer["data"].as_array().unwrap_or(&empty); + let data = raw + .iter() + .map(|v| { + let ts_str = v["timestamp"].as_str().unwrap_or("").to_string(); + ShortTradesItem { + timestamp: unix_secs_to_rfc3339(&ts_str), + rate: v["rate"].as_str().unwrap_or("").to_string(), + close: v["close"].as_str().unwrap_or("").to_string(), + nus_amount: v["nus_amount"].as_str().unwrap_or("").to_string(), + ny_amount: v["ny_amount"].as_str().unwrap_or("").to_string(), + total_amount: v["total_amount"].as_str().unwrap_or("").to_string(), + amount: v["amount"].as_str().unwrap_or("").to_string(), + balance: v["balance"].as_str().unwrap_or("").to_string(), + } + }) + .collect(); + Ok(ShortTradesResponse { data }) + } + // ── update_pinned ───────────────────────────────────────────── /// Pin or unpin watchlist securities. diff --git a/rust/src/quote/mod.rs b/rust/src/quote/mod.rs index 3e945b894..78dcba0cb 100644 --- a/rust/src/quote/mod.rs +++ b/rust/src/quote/mod.rs @@ -58,8 +58,10 @@ pub use types::{ SecurityListCategory, SecurityQuote, SecurityStaticInfo, - ShortPosition, + ShortPositionsItem, ShortPositionsResponse, + ShortTradesItem, + ShortTradesResponse, SortOrderType, StrikePriceInfo, Subscription, diff --git a/rust/src/quote/types.rs b/rust/src/quote/types.rs index bfe3ae775..08df9b007 100644 --- a/rust/src/quote/types.rs +++ b/rust/src/quote/types.rs @@ -2032,36 +2032,40 @@ impl_default_for_enum_string!( // ── short_positions ─────────────────────────────────────────────── -/// Response for [`crate::QuoteContext::short_positions`] +/// One short-position data point (unified for US and HK markets). #[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 struct ShortPositionsItem { + /// Trading date (RFC 3339, e.g. `"2024-01-15T00:00:00Z"`) pub timestamp: String, - /// Short interest as a ratio of float shares + /// Short ratio (both markets) pub rate: String, - /// Average daily share volume - pub avg_daily_share_volume: String, - /// Current shares short + /// Closing price (both markets) + pub close: String, + /// [US] Number of short shares outstanding + #[serde(default)] pub current_shares_short: String, - /// Days to cover (short ratio) + /// [US] Average daily share volume + #[serde(default)] + pub avg_daily_share_volume: String, + /// [US] Days to cover ratio + #[serde(default)] pub days_to_cover: String, - /// Closing price on the settlement date - pub close: String, + /// [HK] Short sale amount (HKD) + #[serde(default)] + pub amount: String, + /// [HK] Short position balance + #[serde(default)] + pub balance: String, + /// [HK] Cost / closing price + #[serde(default)] + pub cost: String, +} + +/// Response for [`crate::QuoteContext::short_positions`] +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct ShortPositionsResponse { + /// Short position data points + pub data: Vec, } // ── option_volume ───────────────────────────────────────────────── @@ -2113,6 +2117,41 @@ pub struct OptionVolumeDailyStat { pub put_call_open_interest_ratio: String, } +// ── short_trades ────────────────────────────────────────────────── + +/// One short-trade data point (unified for US and HK markets). +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct ShortTradesItem { + /// Trading date (RFC 3339) + pub timestamp: String, + /// Short ratio + pub rate: String, + /// Closing price + pub close: String, + /// [US] NYSE short amount + #[serde(default)] + pub nus_amount: String, + /// [US] NY short amount + #[serde(default)] + pub ny_amount: String, + /// [US] Total short amount + #[serde(default)] + pub total_amount: String, + /// [HK] Short sale amount + #[serde(default)] + pub amount: String, + /// [HK] Short position balance + #[serde(default)] + pub balance: String, +} + +/// Response for [`crate::QuoteContext::short_trades`] +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct ShortTradesResponse { + /// Short trade data points + pub data: Vec, +} + // ── 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..d17746aaf --- /dev/null +++ b/rust/src/screener/context.rs @@ -0,0 +1,178 @@ +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 {} + let raw: serde_json::Value = self + .get("/v1/quote/screener/strategies/recommend", Empty {}) + .await?; + Ok(ScreenerRecommendStrategiesResponse { data: raw }) + } + + // ── 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 {} + let raw: serde_json::Value = self + .get("/v1/quote/screener/strategies/mine", Empty {}) + .await?; + Ok(ScreenerUserStrategiesResponse { data: raw }) + } + + // ── 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, + } + let raw: serde_json::Value = self + .get("/v1/quote/screener/strategy", Query { id }) + .await?; + Ok(ScreenerStrategyResponse { data: raw }) + } + + // ── 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, + } + 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 ─────────────────────────────────────── + + /// Get all available screener indicator definitions. + /// + /// Path: `GET /v1/quote/screener/indicators` + pub async fn screener_indicators(&self) -> Result { + #[derive(Serialize)] + struct Empty {} + let raw: serde_json::Value = self.get("/v1/quote/screener/indicators", Empty {}).await?; + Ok(ScreenerIndicatorsResponse { data: raw }) + } +} 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, +}