From cf532d6bf329cc7107a057422bf38df50d493b38 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=A2=81=E7=AB=A0=E6=B4=AA?= Date: Mon, 7 Sep 2026 10:05:27 +0800 Subject: [PATCH 1/2] fix(java): repair JNI signatures out of sync with the Java native declarations Six JNI methods had a Rust extern "system" signature that no longer matched the Java native declaration, so every call aborted with "JNI call failed" (or read misaligned stack arguments): - getRankList / getShortTrades / getStrategy / shareholderDetail / valuationComparison: the Java side was migrated to an options object but the Rust JNI still read the old positional args. They now read the fields off the opts object via get_field. - getShortPositions: was missing the count parameter that the Rust core, Node.js and Python all require (the Rust JNI still expected it). Added count to the Java native decl and getShortPositions signature. Reported as longbridge/developers#1249 (getRankList). --- CHANGELOG.md | 1 + .../main/java/com/longbridge/SdkNative.java | 2 +- .../com/longbridge/quote/QuoteContext.java | 5 ++-- java/src/fundamental_context.rs | 23 ++++++++----------- java/src/market_context.rs | 6 ++--- java/src/quote_context.rs | 6 ++--- java/src/screener_context.rs | 3 ++- 7 files changed, 22 insertions(+), 24 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 048f440bf..5c1856f48 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,6 +12,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Fixed +- **Java SDK:** fixed six JNI methods whose Rust `extern "system"` signature no longer matched the Java `native` declaration, so every call aborted with `java.lang.RuntimeException: JNI call failed` (or read misaligned stack arguments). The Java layer had been migrated to options objects / trimmed argument lists but the Rust JNI side was left in the old positional form. `MarketContext.getRankList` (`RankListOptions`), `QuoteContext.getShortTrades` (`ShortTradesOptions`), `ScreenerContext.getStrategy` (`ScreenerStrategyOptions`), `FundamentalContext.shareholderDetail` (`ShareholderDetailOptions`) and `FundamentalContext.valuationComparison` (`ValuationComparisonOptions`) now read their fields off the options object. Separately, `QuoteContext.getShortPositions` was missing the `count` parameter that the Rust core, Node.js and Python bindings all require — the Rust JNI still expected it, so the call crashed — so `getShortPositions(String symbol)` becomes `getShortPositions(String symbol, int count)`. Reported as longbridge/developers#1249 (`getRankList`) - **C/C++ SDKs:** every list argument that crosses the FFI boundary now tolerates a null pointer with a zero length. `std::vector::data()` is allowed to return `nullptr` for an empty vector, which is exactly what the C++ binding passes for an omitted list argument, but the C layer fed it straight to `std::slice::from_raw_parts` — undefined behaviour that **aborts the process** under the debug UB checks. Hit live by `QuoteContext::warrant_list` with no filters (`c/src/quote_context/context.rs:784`); all 17 call sites across `quote_context`, `trade_context`, `agent_context`, `alert_context`, and `types` now go through a null-tolerant `slice_from_raw_parts` helper - **C++ SDK:** `asset::AssetContext` (`statements` / `statement_download_url`) is now actually built and usable. `longbridge.hpp` has always included `asset_context.hpp`, but `cpp/src/asset_context.cpp` was never listed in `cpp/CMakeLists.txt`, so the class was declared to users and then failed to link. It had also never compiled: it included neither `longbridge.h` nor the C declarations, and `statement_download_url` read `res->data` as a `lb_statement_download_url_response_t*` — a type that does not exist anywhere in the C layer, which delivers the URL as a bare `const char*` (the same convention as `QuoteContext::quote_level`). Fixed the include and the callback, and added the file to the build - **C SDK:** export `lb_statement_item_t` from `longbridge.h`. `CStatementItem` is only reachable through the `void*` async-result pointer, so cbindgen did not emit it and no C or C++ caller could read what `lb_asset_context_statements` returns. Also added the missing `CAssetContext` → `lb_asset_context_t` entry to the cbindgen rename map: every other context type was mapped, so the header exposed the raw Rust name (`const struct CAssetContext *lb_asset_context_new(...)`) while the C++ side forward-declared `lb_asset_context_t` diff --git a/java/javasrc/src/main/java/com/longbridge/SdkNative.java b/java/javasrc/src/main/java/com/longbridge/SdkNative.java index 211e29544..9e8011782 100644 --- a/java/javasrc/src/main/java/com/longbridge/SdkNative.java +++ b/java/javasrc/src/main/java/com/longbridge/SdkNative.java @@ -353,7 +353,7 @@ public static native void gridContextTriggerHistory(long context, GetGridTrigger // ── QuoteContext extensions (Step 3) ───────────────────────── - public static native void quoteContextShortPositions(long context, String symbol, AsyncCallback callback); + public static native void quoteContextShortPositions(long context, String symbol, int count, AsyncCallback callback); public static native void quoteContextOptionVolume(long context, String symbol, AsyncCallback callback); public static native void quoteContextOptionVolumeDaily(long context, Object opts, AsyncCallback callback); 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 9fadca066..c93809aea 100644 --- a/java/javasrc/src/main/java/com/longbridge/quote/QuoteContext.java +++ b/java/javasrc/src/main/java/com/longbridge/quote/QuoteContext.java @@ -1396,12 +1396,13 @@ public synchronized CompletableFuture getRealtimeTrades(String symbol, * Get short positions for a symbol * * @param symbol Security symbol + * @param count Number of records to return * @return A Future representing the short positions response * @throws OpenApiException If an error occurs */ - public synchronized CompletableFuture getShortPositions(String symbol) throws OpenApiException { + public synchronized CompletableFuture getShortPositions(String symbol, int count) throws OpenApiException { return AsyncCallback.executeTask((callback) -> { - SdkNative.quoteContextShortPositions(raw(), symbol, callback); + SdkNative.quoteContextShortPositions(raw(), symbol, count, callback); }); } diff --git a/java/src/fundamental_context.rs b/java/src/fundamental_context.rs index 87bb4146e..2c50d6c02 100644 --- a/java/src/fundamental_context.rs +++ b/java/src/fundamental_context.rs @@ -344,14 +344,14 @@ pub unsafe extern "system" fn Java_com_longbridge_SdkNative_fundamentalContextSh mut env: JNIEnv, _class: JClass, context: i64, - symbol: JObject, - object_id: i64, + opts: JObject, callback: JObject, ) { jni_result(&mut env, (), |env| { let context = &*(context as *const ContextObj); let __owned_ctx = context.ctx.clone(); - let symbol: String = FromJValue::from_jvalue(env, symbol.into())?; + let symbol: String = get_field(env, &opts, "symbol")?; + let object_id: i64 = get_field(env, &opts, "objectId")?; async_util::execute(env, callback, async move { let resp = __owned_ctx.shareholder_detail(symbol, object_id).await?; Ok(resp) @@ -365,22 +365,17 @@ pub unsafe extern "system" fn Java_com_longbridge_SdkNative_fundamentalContextVa mut env: JNIEnv, _class: JClass, context: i64, - symbol: JObject, - currency: JObject, - comparison_symbols: JObject, + opts: JObject, callback: JObject, ) { jni_result(&mut env, (), |env| { let context = &*(context as *const ContextObj); let __owned_ctx = context.ctx.clone(); - 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) - }; + let symbol: String = get_field(env, &opts, "symbol")?; + let currency: String = get_field(env, &opts, "currency")?; + let comparison_syms: Option> = + get_field::<_, _, Option>>(env, &opts, "comparisonSymbols")? + .map(|arr| arr.0); async_util::execute(env, callback, async move { let resp = __owned_ctx .valuation_comparison(symbol, currency, comparison_syms) diff --git a/java/src/market_context.rs b/java/src/market_context.rs index 25bc9c6cc..d0359bc70 100644 --- a/java/src/market_context.rs +++ b/java/src/market_context.rs @@ -238,14 +238,14 @@ pub unsafe extern "system" fn Java_com_longbridge_SdkNative_marketContextRankLis mut env: JNIEnv, _class: JClass, context: i64, - key: JObject, - need_article: bool, + opts: JObject, callback: JObject, ) { jni_result(&mut env, (), |env| { let context = &*(context as *const ContextObj); let __owned_ctx = context.ctx.clone(); - let key: String = FromJValue::from_jvalue(env, key.into())?; + let key: String = get_field(env, &opts, "key")?; + let need_article: bool = get_field(env, &opts, "needArticle")?; async_util::execute(env, callback, async move { let resp = __owned_ctx.rank_list(key, need_article).await?; Ok(resp) diff --git a/java/src/quote_context.rs b/java/src/quote_context.rs index dcea00cf4..67ea1831f 100644 --- a/java/src/quote_context.rs +++ b/java/src/quote_context.rs @@ -1250,14 +1250,14 @@ pub unsafe extern "system" fn Java_com_longbridge_SdkNative_quoteContextShortTra mut env: JNIEnv, _class: JClass, context: i64, - symbol: JObject, - count: i32, + opts: JObject, callback: JObject, ) { jni_result(&mut env, (), |env| { let context = &*(context as *const ContextObj); let __owned_ctx = context.ctx.clone(); - let symbol: String = FromJValue::from_jvalue(env, symbol.into())?; + let symbol: String = get_field(env, &opts, "symbol")?; + let count: i32 = get_field(env, &opts, "count")?; let count = count.max(1) as u32; async_util::execute(env, callback, async move { let resp = __owned_ctx.short_trades(symbol, count).await?; diff --git a/java/src/screener_context.rs b/java/src/screener_context.rs index 3d2aaa4d4..0a7466b91 100644 --- a/java/src/screener_context.rs +++ b/java/src/screener_context.rs @@ -81,12 +81,13 @@ pub unsafe extern "system" fn Java_com_longbridge_SdkNative_screenerContextStrat mut env: JNIEnv, _class: JClass, context: i64, - id: i64, + opts: JObject, callback: JObject, ) { jni_result(&mut env, (), |env| { let context = &*(context as *const ContextObj); let __owned_ctx = context.ctx.clone(); + let id: i64 = get_field(env, &opts, "id")?; async_util::execute(env, callback, async move { let resp = __owned_ctx.screener_strategy(id).await?; Ok(resp) From 1b6ed1c5c74761f3c793837d8fdbd10e90cc36f2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=A2=81=E7=AB=A0=E6=B4=AA?= Date: Mon, 7 Sep 2026 15:21:16 +0800 Subject: [PATCH 2/2] fix(python): reconcile openapi.pyi type stub with the PyO3 implementation The hand-maintained stub had drifted from python/src: - Removed phantom methods (AttributeError if called): AlertContext.enable/ disable, AsyncQuoteContext.option_volume/option_volume_daily. - Fixed wrong signatures/returns: macroeconomic_indicators (dropped country/keyword + wrong return type), macroeconomic (missing offset), DCAContext.create/update (-> DcaCreateResult), pause/resume/stop (-> None), SharelistContext.create (-> None). - Added missing methods: QuoteContext.filings (+async), AlertContext.update, DCAContext.update, TradeContext.set_on_grid_order_changed (+async). - Added the three entirely-missing async classes (AsyncMarketContext, AsyncCalendarContext, AsyncPortfolioContext) and the missing method surfaces of AsyncFundamentalContext / AsyncContentContext. - Added missing referenced types: FilingItem, DcaCreateResult, MacroeconomicCountry, MacroeconomicIndicatorListResponse, PushGridOrderChanged. Type hints only; the native module is unchanged. --- CHANGELOG.md | 1 + python/pysrc/longbridge/openapi.pyi | 1051 ++++++++++++++++++++++++--- 2 files changed, 932 insertions(+), 120 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5c1856f48..23ede6a26 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,6 +12,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Fixed +- **Python SDK:** reconciled the hand-maintained type stub `python/pysrc/longbridge/openapi.pyi` with the actual PyO3 implementation. Removed phantom methods that did not exist and would raise `AttributeError` when called (`AlertContext.enable`/`disable`, `AsyncQuoteContext.option_volume`/`option_volume_daily`); fixed wrong signatures/return types (`FundamentalContext.macroeconomic_indicators` had dropped its `country`/`keyword` params and had the wrong return type, `macroeconomic` was missing `offset`, `DCAContext.create`/`update` return `DcaCreateResult` not `DcaList`, `DCAContext.pause`/`resume`/`stop` and `SharelistContext.create` return `None`); added missing methods (`QuoteContext.filings` + async, `AlertContext.update`, `DCAContext.update`, `TradeContext.set_on_grid_order_changed` + async); added the three entirely-missing async context classes (`AsyncMarketContext`, `AsyncCalendarContext`, `AsyncPortfolioContext`) plus the missing method surfaces of `AsyncFundamentalContext` and `AsyncContentContext`; and added the missing referenced types (`FilingItem`, `DcaCreateResult`, `MacroeconomicCountry`, `MacroeconomicIndicatorListResponse`, `PushGridOrderChanged`). Type hints only — no runtime/behaviour change to the native module - **Java SDK:** fixed six JNI methods whose Rust `extern "system"` signature no longer matched the Java `native` declaration, so every call aborted with `java.lang.RuntimeException: JNI call failed` (or read misaligned stack arguments). The Java layer had been migrated to options objects / trimmed argument lists but the Rust JNI side was left in the old positional form. `MarketContext.getRankList` (`RankListOptions`), `QuoteContext.getShortTrades` (`ShortTradesOptions`), `ScreenerContext.getStrategy` (`ScreenerStrategyOptions`), `FundamentalContext.shareholderDetail` (`ShareholderDetailOptions`) and `FundamentalContext.valuationComparison` (`ValuationComparisonOptions`) now read their fields off the options object. Separately, `QuoteContext.getShortPositions` was missing the `count` parameter that the Rust core, Node.js and Python bindings all require — the Rust JNI still expected it, so the call crashed — so `getShortPositions(String symbol)` becomes `getShortPositions(String symbol, int count)`. Reported as longbridge/developers#1249 (`getRankList`) - **C/C++ SDKs:** every list argument that crosses the FFI boundary now tolerates a null pointer with a zero length. `std::vector::data()` is allowed to return `nullptr` for an empty vector, which is exactly what the C++ binding passes for an omitted list argument, but the C layer fed it straight to `std::slice::from_raw_parts` — undefined behaviour that **aborts the process** under the debug UB checks. Hit live by `QuoteContext::warrant_list` with no filters (`c/src/quote_context/context.rs:784`); all 17 call sites across `quote_context`, `trade_context`, `agent_context`, `alert_context`, and `types` now go through a null-tolerant `slice_from_raw_parts` helper - **C++ SDK:** `asset::AssetContext` (`statements` / `statement_download_url`) is now actually built and usable. `longbridge.hpp` has always included `asset_context.hpp`, but `cpp/src/asset_context.cpp` was never listed in `cpp/CMakeLists.txt`, so the class was declared to users and then failed to link. It had also never compiled: it included neither `longbridge.h` nor the C declarations, and `statement_download_url` read `res->data` as a `lb_statement_download_url_response_t*` — a type that does not exist anywhere in the C layer, which delivers the URL as a bare `const char*` (the same convention as `QuoteContext::quote_level`). Fixed the include and the callback, and added the file to the build diff --git a/python/pysrc/longbridge/openapi.pyi b/python/pysrc/longbridge/openapi.pyi index b0e6e23d0..09c16f65d 100644 --- a/python/pysrc/longbridge/openapi.pyi +++ b/python/pysrc/longbridge/openapi.pyi @@ -2875,6 +2875,41 @@ class TradeSessions: All """ +class FilingItem: + """ + Filing item + """ + + id: str + """ + Filing ID + """ + + title: str + """ + Title + """ + + description: str + """ + Description + """ + + file_name: str + """ + File name + """ + + file_urls: List[str] + """ + File URLs + """ + + published_at: datetime + """ + Published time + """ + class MarketTemperature: """ Market temperature @@ -4086,6 +4121,17 @@ class QuoteContext: """ ... + def filings(self, symbol: str) -> List["FilingItem"]: + """Get corporate filings for a security. + + Args: + symbol: Security symbol, e.g. ``"AAPL.US"`` + + Returns: + List of :class:`FilingItem` + """ + ... + class AsyncQuoteContext: """ Async quote context for use with asyncio. Create via `AsyncQuoteContext.create(config)` and await inside asyncio. @@ -5438,40 +5484,25 @@ class AsyncQuoteContext: """ ... - def option_volume(self, symbol: str) -> "Awaitable[OptionVolumeStats]": - """Get real-time option call/put volume. Returns awaitable. - - Args: - symbol: Underlying symbol, e.g. ``"AAPL.US"`` - - Returns: - Awaitable resolving to :class:`OptionVolumeStats` - """ - ... - - def option_volume_daily( - self, symbol: str, timestamp: int = 0, count: int = 30 - ) -> "Awaitable[OptionVolumeDaily]": - """Get daily historical option volume. Returns awaitable. + def us_crypto_overview(self, symbol: str) -> "Awaitable[USCryptoOverview]": + """Get US cryptocurrency market overview. US token required. Returns awaitable. Args: - symbol: Underlying symbol, e.g. ``"AAPL.US"`` - timestamp: Start timestamp (0 = most recent) - count: Number of days to return (default 30) + symbol: Trading-pair symbol, e.g. ``"BTCUSD.BKKT"`` Returns: - Awaitable resolving to :class:`OptionVolumeDaily` + Awaitable resolving to :class:`USCryptoOverview` """ ... - def us_crypto_overview(self, symbol: str) -> "Awaitable[USCryptoOverview]": - """Get US cryptocurrency market overview. US token required. Returns awaitable. + async def filings(self, symbol: str) -> List["FilingItem"]: + """Get corporate filings for a security. Args: - symbol: Trading-pair symbol, e.g. ``"BTCUSD.BKKT"`` + symbol: Security symbol, e.g. ``"AAPL.US"`` Returns: - Awaitable resolving to :class:`USCryptoOverview` + List of :class:`FilingItem` """ ... @@ -5770,6 +5801,56 @@ class AllExecutionsResponse: Execution list """ +class PushGridOrderChanged: + """ + Grid order changed push event + """ + + order_id: str + """Grid master order ID""" + + status: str + """Order status""" + + symbol: str + """Security symbol (e.g. ``700.HK``)""" + + suspend_reason: str + """Suspend reason, if any""" + + submitted_base_price: str + """Submitted base price""" + + current_base_price: str + """Current base price""" + + upper_limit_price: str + """Upper price bound""" + + lower_limit_price: str + """Lower price bound""" + + trigger_price_type: int + """Trigger price type""" + + trigger_quantity: str + """Quantity per trigger""" + + settlement_currency: str + """Settlement currency""" + + time_in_force: int + """Time in force (``0`` = Day, ``1`` = GTC, ``6`` = GTD)""" + + rth: int + """Regular trading hours flag""" + + grid_order_type_up: str + """Sell-side order type when depth is 0""" + + grid_order_type_down: str + """Buy-side order type when depth is 0""" + class PushOrderChanged: """ Order changed message @@ -7926,6 +8007,10 @@ class TradeContext: """ ... + def set_on_grid_order_changed(self, callback: Callable[["PushGridOrderChanged"], None]) -> None: + """Set the grid-order-changed push callback.""" + ... + class TriggerPriceType: """ How grid trigger thresholds are interpreted @@ -9648,6 +9733,10 @@ class AsyncTradeContext: """ ... + async def set_on_grid_order_changed(self, callback: Callable[["PushGridOrderChanged"], None]) -> None: + """Set the grid-order-changed push callback.""" + ... + class StatementType: """ Statement type @@ -10495,103 +10584,319 @@ class AsyncContentContext: # ── FundamentalContext ──────────────────────────────────────────── -class FinancialReports: - """ - Financial reports response. - - ``list`` contains raw nested data keyed by report kind - (``"IS"``, ``"BS"``, ``"CF"``). - """ + async def create_topic( + self, + title: str, + body: str, + topic_type: Optional[str] = None, + tickers: Optional[List[str]] = None, + hashtags: Optional[List[str]] = None, + ) -> str: + """ + Create a new community topic - list: object - """Raw financial data dict (IS/BS/CF indicators)""" + Args: + title: Topic title (required for "article"; optional for "post") + body: Topic body (plain text for "post", Markdown for "article") + topic_type: "post" (default) or "article" + tickers: Associated stock symbols, e.g. ["700.HK"], max 10 + hashtags: Hashtag names, max 5 + Returns: + The new topic ID -class DividendItem: - """One dividend or distribution event.""" + Examples: + :: - symbol: str - """Security symbol, e.g. ``"700.HK"``""" - id: str - """Internal record ID""" - desc: str - """Human-readable description, e.g. ``"每股派息 5.3 HKD"``""" - record_date: str - """Record / book-close date""" - ex_date: str - """Ex-dividend date""" - payment_date: str - """Payment date""" + from longbridge.openapi import OAuthBuilder, ContentContext, Config + oauth = OAuthBuilder("your-client-id").build( + lambda url: print("Visit:", url) + ) + config = Config.from_oauth(oauth) + ctx = ContentContext(config) + topic_id = ctx.create_topic( + title="My Article", + body="Hello world", + topic_type="article", + tickers=["700.HK"], + ) + print(topic_id) + """ + ... -class DividendList: - """Dividend history response.""" + async def create_topic_reply( + self, + topic_id: str, + body: str, + reply_to_id: Optional[str] = None, + ) -> TopicReply: + """ + Post a reply to a community topic - list: list[DividendItem] - """List of dividend events""" + Args: + topic_id: Topic ID + body: Reply body (plain text only) + reply_to_id: ID of the parent reply to nest under; empty or "0" for top-level + Returns: + The created reply -class RatingEvaluate: - """Analyst rating distribution counts.""" + Examples: + :: - buy: int - """Number of Buy ratings""" - over: int - """Number of Strong Buy / Outperform ratings""" - hold: int - """Number of Hold ratings""" - under: int - """Number of Underperform ratings""" - sell: int - """Number of Sell ratings""" - no_opinion: int - """Number of No Opinion ratings""" - total: int - """Total analyst count""" - start_date: str - """Window start (unix timestamp string)""" - end_date: str - """Window end (unix timestamp string)""" + from longbridge.openapi import OAuthBuilder, ContentContext, Config + oauth = OAuthBuilder("your-client-id").build( + lambda url: print("Visit:", url) + ) + config = Config.from_oauth(oauth) + ctx = ContentContext(config) + reply = ctx.create_topic_reply("123456", "Great post!") + print(reply.id) + """ + ... -class RatingTarget: - """Analyst target price range.""" + async def list_topic_replies( + self, + topic_id: str, + page: Optional[int] = None, + size: Optional[int] = None, + ) -> List[TopicReply]: + """ + List replies on a topic - highest_price: str - """Highest price target""" - lowest_price: str - """Lowest price target""" - prev_close: str - """Previous close price""" - start_date: str - """Window start""" - end_date: str - """Window end""" + Args: + topic_id: Topic ID + page: Page number (default 1) + size: Page size (default 20, range 1-50) + Returns: + List of topic replies -class InstitutionRatingLatest: - """Latest analyst rating snapshot.""" + Examples: + :: - evaluate: RatingEvaluate - """Rating distribution counts""" - target: RatingTarget - """Target price range""" - industry_id: int - """Industry classification ID""" - industry_name: str - """Industry name""" - industry_rank: int - """Rank within the industry (1 = highest)""" - industry_total: int - """Total securities in the industry""" - industry_mean: int - """Mean analyst count in the industry""" - industry_median: int - """Median analyst count in the industry""" + from longbridge.openapi import OAuthBuilder, ContentContext, Config + oauth = OAuthBuilder("your-client-id").build( + lambda url: print("Visit:", url) + ) + config = Config.from_oauth(oauth) + ctx = ContentContext(config) + replies = ctx.list_topic_replies("123456") + for r in replies: + print(r.id, r.body) + """ + ... -class RatingSummaryEvaluate: - """Simplified rating distribution for consensus summary.""" + async def my_topics( + self, + page: Optional[int] = None, + size: Optional[int] = None, + topic_type: Optional[str] = None, + ) -> List[OwnedTopic]: + """ + Get topics created by the current authenticated user + + Args: + page: Page number (default 1) + size: Page size (default 50, range 1-500) + topic_type: Filter by type: "article" or "post"; empty returns all + + Returns: + List of owned topics + + Examples: + :: + + from longbridge.openapi import OAuthBuilder, ContentContext, Config + + oauth = OAuthBuilder("your-client-id").build( + lambda url: print("Visit:", url) + ) + config = Config.from_oauth(oauth) + ctx = ContentContext(config) + topics = ctx.my_topics(size=20) + for t in topics: + print(t.id, t.title) + """ + ... + + async def news(self, symbol: str) -> List[NewsItem]: + """ + Get news list for a symbol + + Args: + symbol: Security symbol, e.g. "700.HK" + + Returns: + List of news items + + Examples: + :: + + from longbridge.openapi import OAuthBuilder, ContentContext, Config + + oauth = OAuthBuilder("your-client-id").build( + lambda url: print("Visit:", url) + ) + config = Config.from_oauth(oauth) + ctx = ContentContext(config) + news = ctx.news("700.HK") + for n in news: + print(n.id, n.title) + """ + ... + + async def topic_detail(self, id: str) -> OwnedTopic: + """ + Get full details of a topic by its ID + + Args: + id: Topic ID + + Returns: + Full topic detail + + Examples: + :: + + from longbridge.openapi import OAuthBuilder, ContentContext, Config + + oauth = OAuthBuilder("your-client-id").build( + lambda url: print("Visit:", url) + ) + config = Config.from_oauth(oauth) + ctx = ContentContext(config) + topic = ctx.topic_detail("123456") + print(topic.title, topic.body) + """ + ... + + async def topics(self, symbol: str) -> List[TopicItem]: + """ + Get discussion topics list for a symbol + + Args: + symbol: Security symbol, e.g. "700.HK" + + Returns: + List of topic items + + Examples: + :: + + from longbridge.openapi import OAuthBuilder, ContentContext, Config + + oauth = OAuthBuilder("your-client-id").build( + lambda url: print("Visit:", url) + ) + config = Config.from_oauth(oauth) + ctx = ContentContext(config) + topics = ctx.topics("700.HK") + for t in topics: + print(t.id, t.title) + """ + ... + +class FinancialReports: + """ + Financial reports response. + + ``list`` contains raw nested data keyed by report kind + (``"IS"``, ``"BS"``, ``"CF"``). + """ + + list: object + """Raw financial data dict (IS/BS/CF indicators)""" + + +class DividendItem: + """One dividend or distribution event.""" + + symbol: str + """Security symbol, e.g. ``"700.HK"``""" + id: str + """Internal record ID""" + desc: str + """Human-readable description, e.g. ``"每股派息 5.3 HKD"``""" + record_date: str + """Record / book-close date""" + ex_date: str + """Ex-dividend date""" + payment_date: str + """Payment date""" + + +class DividendList: + """Dividend history response.""" + + list: list[DividendItem] + """List of dividend events""" + + +class RatingEvaluate: + """Analyst rating distribution counts.""" + + buy: int + """Number of Buy ratings""" + over: int + """Number of Strong Buy / Outperform ratings""" + hold: int + """Number of Hold ratings""" + under: int + """Number of Underperform ratings""" + sell: int + """Number of Sell ratings""" + no_opinion: int + """Number of No Opinion ratings""" + total: int + """Total analyst count""" + start_date: str + """Window start (unix timestamp string)""" + end_date: str + """Window end (unix timestamp string)""" + + +class RatingTarget: + """Analyst target price range.""" + + highest_price: str + """Highest price target""" + lowest_price: str + """Lowest price target""" + prev_close: str + """Previous close price""" + start_date: str + """Window start""" + end_date: str + """Window end""" + + +class InstitutionRatingLatest: + """Latest analyst rating snapshot.""" + + evaluate: RatingEvaluate + """Rating distribution counts""" + target: RatingTarget + """Target price range""" + industry_id: int + """Industry classification ID""" + industry_name: str + """Industry name""" + industry_rank: int + """Rank within the industry (1 = highest)""" + industry_total: int + """Total securities in the industry""" + industry_mean: int + """Mean analyst count in the industry""" + industry_median: int + """Median analyst count in the industry""" + + +class RatingSummaryEvaluate: + """Simplified rating distribution for consensus summary.""" buy: int """Number of Buy ratings""" @@ -11604,18 +11909,22 @@ class FundamentalContext: def macroeconomic_indicators( self, + country: "MacroeconomicCountry | None" = None, + keyword: str | None = None, offset: int | None = None, limit: int | None = None, - ) -> list["MacroeconomicIndicator"]: + ) -> "MacroeconomicIndicatorListResponse": """ List macroeconomic indicators. Args: + country: Filter by country / region (optional) + keyword: Filter by keyword (optional) offset: Pagination offset (default 0) limit: Page size (default 100, max 1000) Returns: - List of :class:`MacroeconomicIndicator` + :class:`MacroeconomicIndicatorListResponse` """ ... @@ -11624,6 +11933,7 @@ class FundamentalContext: indicator_code: str, start_date: str | None = None, end_date: str | None = None, + offset: int | None = None, limit: int | None = None, ) -> "MacroeconomicResponse": """ @@ -11633,6 +11943,7 @@ class FundamentalContext: indicator_code: External vendor code from ``macroeconomic_indicators`` start_date: Start date in ``"YYYY-MM-DD"`` format (optional) end_date: End date in ``"YYYY-MM-DD"`` format (optional) + offset: Pagination offset (optional) limit: Max records to return (default 100, max 100) Returns: @@ -11850,16 +12161,228 @@ class AsyncFundamentalContext: """Get US ETF document list. US token required. Args: - symbol: ETF symbol, e.g. ``"SPY.US"`` - size: Number of files to return; ``None`` returns all + symbol: ETF symbol, e.g. ``"SPY.US"`` + size: Number of files to return; ``None`` returns all + + Returns: + Awaitable[:class:`USETFFilesResponse`] + """ + ... + + +# ── FundamentalContext new response types ───────────────────────── + + @classmethod + def create(cls, config: Config) -> AsyncFundamentalContext: ... + + async def buyback(self, symbol: str) -> "BuybackData": + """ + Get buyback data for a security. + + Args: + symbol: Security symbol, e.g. ``"AAPL.US"`` + + Returns: + :class:`BuybackData` + """ + ... + + async def company(self, symbol: str) -> "CompanyOverview": + """Get company overview.""" + ... + + async def consensus(self, symbol: str) -> "FinancialConsensus": + """Get financial consensus estimates.""" + ... + + async def corp_action(self, symbol: str) -> "CorpActions": + """Get corporate actions (dividends, splits, buybacks, etc.).""" + ... + + async def dividend(self, symbol: str) -> "DividendList": + """Get dividend history.""" + ... + + async def dividend_detail(self, symbol: str) -> "DividendList": + """Get detailed dividend information.""" + ... + + async def etf_asset_allocation(self, symbol: str) -> "AssetAllocationResponse": + """ + Get ETF asset allocation (holdings / regional / asset class / industry). + + Args: + symbol: ETF security code (e.g. ``"QQQ.US"``) + + Returns: + :class:`AssetAllocationResponse` with allocation groups + """ + ... + + async def executive(self, symbol: str) -> "ExecutiveList": + """Get executive and board member information.""" + ... + + async def financial_report( + self, + symbol: str, + kind: "FinancialReportKind" = ..., + period: "FinancialReportPeriod | None" = None, + ) -> "FinancialReports": + """ + Get financial reports. + + Args: + symbol: Security symbol, e.g. ``"700.HK"`` + kind: Report kind (default ``All``) + period: Report period (``None`` means not specified) + + Returns: + Financial reports response + """ + ... + + async def forecast_eps(self, symbol: str) -> "ForecastEps": + """Get EPS forecasts.""" + ... + + async def fund_holder(self, symbol: str) -> "FundHolders": + """Get funds and ETFs that hold the security.""" + ... + + async def industry_valuation(self, symbol: str) -> "IndustryValuationList": + """Get industry peer valuation comparison.""" + ... + + async def industry_valuation_dist(self, symbol: str) -> "IndustryValuationDist": + """Get industry valuation distribution.""" + ... + + async def institution_rating(self, symbol: str) -> "InstitutionRating": + """ + Get analyst ratings (latest snapshot + consensus summary). + + Args: + symbol: Security symbol + + Returns: + Combined analyst rating response + """ + ... + + async def institution_rating_detail(self, symbol: str) -> "InstitutionRatingDetail": + """Get historical analyst rating details.""" + ... + + async def invest_relation(self, symbol: str) -> "InvestRelations": + """Get investor relations / investment holdings.""" + ... + + async def macroeconomic( + self, + indicator_code: str, + start_date: str | None = None, + end_date: str | None = None, + offset: int | None = None, + limit: int | None = None, + ) -> "MacroeconomicResponse": + """ + Get historical data for a macroeconomic indicator. + + Args: + indicator_code: External vendor code from ``macroeconomic_indicators`` + start_date: Start date in ``"YYYY-MM-DD"`` format (optional) + end_date: End date in ``"YYYY-MM-DD"`` format (optional) + offset: Pagination offset (optional) + limit: Max records to return (default 100, max 100) + + Returns: + :class:`MacroeconomicResponse` + """ + ... + + async def macroeconomic_indicators( + self, + country: "MacroeconomicCountry | None" = None, + keyword: str | None = None, + offset: int | None = None, + limit: int | None = None, + ) -> "MacroeconomicIndicatorListResponse": + """ + List macroeconomic indicators. + + Args: + country: Filter by country / region (optional) + keyword: Filter by keyword (optional) + offset: Pagination offset (default 0) + limit: Page size (default 100, max 1000) + + Returns: + :class:`MacroeconomicIndicatorListResponse` + """ + ... + + async def operating(self, symbol: str) -> "OperatingList": + """Get operating metrics and financial report summaries.""" + ... + + async def shareholder(self, symbol: str) -> "ShareholderList": + """Get major shareholders.""" + ... + + async def shareholder_detail( + self, symbol: str, object_id: int + ) -> "ShareholderDetailResponse": + """ + Get holding history and detail for one shareholder. + + Args: + symbol: Security symbol + object_id: Shareholder object ID + + Returns: + :class:`ShareholderDetailResponse` with raw JSON data + """ + ... + + async def shareholder_top(self, symbol: str) -> "ShareholderTopResponse": + """ + Get ranked list of top shareholders. + + Args: + symbol: Security symbol + + Returns: + :class:`ShareholderTopResponse` with raw JSON data + """ + ... + + async def valuation(self, symbol: str) -> "ValuationData": + """Get valuation metrics (PE / PB / PS / dividend yield).""" + ... + + async def valuation_comparison( + self, + symbol: str, + currency: str, + comparison_symbols: Optional[List[str]] = None, + ) -> "ValuationComparisonResponse": + """ + Get valuation comparison between a security and optional peers. + + Args: + symbol: Security symbol + currency: Currency code (e.g. ``"USD"``) + comparison_symbols: Optional list of peer symbols Returns: - Awaitable[:class:`USETFFilesResponse`] + :class:`ValuationComparisonResponse` with raw JSON data """ ... - -# ── FundamentalContext new response types ───────────────────────── + async def valuation_history(self, symbol: str) -> "ValuationHistoryResponse": + """Get historical valuation data.""" + ... class ShareholderTopResponse: """Top-shareholder list response. ``data`` is a Python dict/list from JSON.""" @@ -12009,6 +12532,44 @@ class MultiLanguageText: traditional_chinese: str +class MacroeconomicCountry: + """ + Macroeconomic country / region + """ + + class HongKong(MacroeconomicCountry): + """Hong Kong""" + + class China(MacroeconomicCountry): + """China""" + + class UnitedStates(MacroeconomicCountry): + """United States""" + + class EuroZone(MacroeconomicCountry): + """Euro Zone""" + + class Japan(MacroeconomicCountry): + """Japan""" + + class Singapore(MacroeconomicCountry): + """Singapore""" + +class MacroeconomicIndicatorListResponse: + """ + Response for :meth:`FundamentalContext.macroeconomic_indicators` + """ + + data: List["MacroeconomicIndicator"] + """ + Macroeconomic indicators + """ + + count: int + """ + Total number of indicators + """ + class MacroeconomicIndicator: """Metadata for one macroeconomic indicator.""" @@ -12497,6 +13058,136 @@ class MarketContext: # ── MarketContext new response types ────────────────────────────── + +class AsyncMarketContext: + """ + Async market context. Create via ``AsyncMarketContext.create(config)``. + """ + + @classmethod + def create(cls, config: Config) -> AsyncMarketContext: ... + + async def market_status(self) -> "MarketStatusResponse": + """Get current trading status for all markets.""" + ... + + async def broker_holding( + self, + symbol: str, + period: "BrokerHoldingPeriod" = ..., + ) -> "BrokerHoldingTop": + """ + Get top broker holdings (buy/sell leaders) for a security. + + Args: + symbol: Security symbol + period: Lookback period (default ``Rct1``) + """ + ... + + async def broker_holding_detail(self, symbol: str) -> "BrokerHoldingDetail": + """Get full broker holding details for a security.""" + ... + + async def broker_holding_daily( + self, symbol: str, broker_id: str + ) -> "BrokerHoldingDailyHistory": + """ + Get daily holding history for a specific broker. + + Args: + symbol: Security symbol + broker_id: Broker participant number, e.g. ``"B01451"`` + """ + ... + + async def ah_premium( + self, + symbol: str, + period: "AhPremiumPeriod" = ..., + count: int = 100, + ) -> "AhPremiumKlines": + """ + Get A/H premium K-line data for a dual-listed security. + + Args: + symbol: H-share symbol, e.g. ``"2318.HK"`` + period: K-line period (default ``Day``) + count: Number of K-lines to return + """ + ... + + async def ah_premium_intraday(self, symbol: str) -> "AhPremiumIntraday": + """Get A/H premium intraday data for a dual-listed security.""" + ... + + async def trade_stats(self, symbol: str) -> "TradeStatsResponse": + """Get buy/sell/neutral trade statistics for a security.""" + ... + + async def anomaly(self, market: str) -> "AnomalyResponse": + """ + Get market anomaly alerts (unusual price/volume events). + + Args: + market: Market code: ``"HK"``, ``"US"``, ``"CN"``, ``"SG"`` + """ + ... + + async def constituent(self, symbol: str) -> "IndexConstituents": + """ + Get constituent stocks for an index. + + Args: + symbol: Index symbol, e.g. ``"HSI.HK"`` + """ + ... + + async 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 + """ + ... + + async def rank_categories(self) -> "RankCategoriesResponse": + """ + Get all available rank category keys and labels. + + Returns: + :class:`RankCategoriesResponse` with typed categories + """ + ... + + async 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 + """ + ... + class TopMoversStock: """Stock information within a top-movers event.""" @@ -12916,6 +13607,33 @@ class CalendarContext: # ── PortfolioContext ────────────────────────────────────────────── + +class AsyncCalendarContext: + """ + Async calendar context. Create via ``AsyncCalendarContext.create(config)``. + """ + + @classmethod + def create(cls, config: Config) -> AsyncCalendarContext: ... + + async def finance_calendar( + self, + category: "CalendarCategory", + start: str, + end: str, + market: str | None = None, + ) -> "CalendarEventsResponse": + """ + Get financial calendar events. + + Args: + category: Event category + start: Start date in ``YYYY-MM-DD`` format + end: End date in ``YYYY-MM-DD`` format + market: Optional market filter, e.g. ``"HK"`` + """ + ... + class ExchangeRate: """One currency exchange rate.""" @@ -13266,6 +13984,71 @@ class PortfolioContext: ... +class AsyncPortfolioContext: + """ + Async portfolio context. Create via ``AsyncPortfolioContext.create(config)``. + """ + + @classmethod + def create(cls, config: Config) -> AsyncPortfolioContext: ... + + async def exchange_rate(self) -> "ExchangeRates": + """Get exchange rates for supported currencies.""" + ... + + async def profit_analysis( + self, + start: str | None = None, + end: str | None = None, + ) -> "ProfitAnalysis": + """ + Get portfolio P&L analysis (summary + per-security breakdown). + + Args: + start: Optional start date in ``YYYY-MM-DD`` format + end: Optional end date in ``YYYY-MM-DD`` format + """ + ... + + async def profit_analysis_detail( + self, + symbol: str, + start: str | None = None, + end: str | None = None, + ) -> "ProfitAnalysisDetail": + """ + Get P&L detail for a specific security. + + Args: + symbol: Security symbol, e.g. ``"700.HK"`` + start: Optional start date + end: Optional end date + """ + ... + + async def profit_analysis_flows( + self, + symbol: str, + page: int, + size: int, + derivative: bool, + start: str | None = None, + end: str | None = None, + ) -> "ProfitAnalysisFlows": + """ + Get paginated P&L flow records for a security. + + Args: + symbol: Security symbol, e.g. ``"700.HK"`` + page: Page number (1-based) + size: Page size + derivative: Whether to include derivative flows + start: Optional start date in ``YYYY-MM-DD`` format + end: Optional end date in ``YYYY-MM-DD`` format + """ + ... + + class ProfitAnalysisByMarketItem: """One security entry in a by-market P&L response.""" @@ -13450,12 +14233,8 @@ class AlertContext: """ ... - def enable(self, alert_id: str) -> None: - """Enable a price alert.""" - ... - - def disable(self, alert_id: str) -> None: - """Disable a price alert.""" + def update(self, item: "AlertItem") -> None: + """Update an existing price alert.""" ... def delete(self, alert_ids: list[str]) -> None: @@ -13514,6 +14293,16 @@ class DcaPlan: """Cumulative profit/loss""" +class DcaCreateResult: + """ + Result of creating or updating a DCA plan + """ + + plan_id: str + """ + The created or updated plan ID + """ + class DcaList: """DCA plan list response.""" @@ -13669,7 +14458,7 @@ class DCAContext: day_of_week: str | None = None, day_of_month: int | None = None, allow_margin: bool = False, - ) -> "DcaList": + ) -> "DcaCreateResult": """ Create a new DCA plan. @@ -13683,15 +14472,37 @@ class DCAContext: """ ... - def pause(self, plan_id: str) -> "DcaList": + def update( + self, + plan_id: str, + amount: str | None = None, + frequency: "DCAFrequency | None" = None, + day_of_week: str | None = None, + day_of_month: int | None = None, + allow_margin: bool | None = None, + ) -> "DcaCreateResult": + """ + Update an existing DCA plan. Only the provided fields are changed. + + Args: + plan_id: Plan ID + amount: Investment amount per period + frequency: Investment frequency + day_of_week: Day of week for weekly plans, e.g. ``"Mon"`` + day_of_month: Day of month for monthly plans (1–28) + allow_margin: Whether to allow margin finance + """ + ... + + def pause(self, plan_id: str) -> None: """Pause (suspend) a DCA plan.""" ... - def resume(self, plan_id: str) -> "DcaList": + def resume(self, plan_id: str) -> None: """Resume a suspended DCA plan.""" ... - def stop(self, plan_id: str) -> "DcaList": + def stop(self, plan_id: str) -> None: """Permanently stop a DCA plan.""" ... @@ -13888,7 +14699,7 @@ class SharelistContext: """ ... - def create(self, name: str, description: str | None = None) -> "SharelistDetail": + def create(self, name: str, description: str | None = None) -> None: """ Create a new community sharelist.