diff --git a/CHANGELOG.md b/CHANGELOG.md index 796462fac..415603edb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,7 +8,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added -- **All languages:** `QuoteContext` gains `etf_asset_allocation(symbol)` — queries `GET /v1/quote/etf-asset-allocation` for ETF asset allocation grouped by element type (`Holdings` / `Regional` / `AssetClass` / `Industry`); returns `AssetAllocationResponse` with report date, position ratios, localized names, and per-holding detail +- **All languages:** `FundamentalContext` gains `etf_asset_allocation(symbol)` — queries `GET /v1/quote/etf-asset-allocation` for ETF asset allocation grouped by element type (`Holdings` / `Regional` / `AssetClass` / `Industry`); returns `AssetAllocationResponse` with report date, position ratios, localized names, and per-holding detail ## [4.2.2] diff --git a/c/cbindgen.toml b/c/cbindgen.toml index 229a40d58..607547c80 100644 --- a/c/cbindgen.toml +++ b/c/cbindgen.toml @@ -304,13 +304,13 @@ cpp_compat = true "COptionVolumeStats" = "lb_option_volume_stats_t" "COptionVolumeDailyStat" = "lb_option_volume_daily_stat_t" "COptionVolumeDaily" = "lb_option_volume_daily_t" +# FundamentalContext new types "CElementType" = "lb_element_type_t" "CLocaleName" = "lb_locale_name_t" "CHoldingDetail" = "lb_holding_detail_t" "CAssetAllocationItem" = "lb_asset_allocation_item_t" "CAssetAllocationGroup" = "lb_asset_allocation_group_t" "CAssetAllocationResponse" = "lb_asset_allocation_response_t" -# FundamentalContext new types "CShareholderTopResponse" = "lb_shareholder_top_response_t" "CShareholderDetailResponse" = "lb_shareholder_detail_response_t" "CValuationHistoryPoint" = "lb_valuation_history_point_t" @@ -447,10 +447,10 @@ include = [ "CShortTradesItem", "CShortTradesResponse", "COptionVolumeStats", "COptionVolumeDailyStat", "COptionVolumeDaily", + # FundamentalContext new types "CElementType", "CLocaleName", "CHoldingDetail", "CAssetAllocationItem", "CAssetAllocationGroup", "CAssetAllocationResponse", - # FundamentalContext new types "CShareholderTopResponse", "CShareholderDetailResponse", "CValuationHistoryPoint", "CValuationComparisonItem", "CValuationComparisonResponse", # MarketContext new types diff --git a/c/csrc/include/longbridge.h b/c/csrc/include/longbridge.h index b298458e7..35ff1563c 100644 --- a/c/csrc/include/longbridge.h +++ b/c/csrc/include/longbridge.h @@ -9543,6 +9543,15 @@ void lb_fundamental_context_valuation_comparison(const struct lb_fundamental_con lb_async_callback_t callback, void *userdata); +/** + * Get ETF asset allocation (holdings / regional / asset class / industry). + * Returns `CAssetAllocationResponse`. + */ +void lb_fundamental_context_etf_asset_allocation(const struct lb_fundamental_context_t *ctx, + const char *symbol, + lb_async_callback_t callback, + void *userdata); + /** * Create a HTTP client using API Key authentication * @@ -10327,15 +10336,6 @@ void lb_quote_context_option_volume_daily(const struct lb_quote_context_t *ctx, lb_async_callback_t callback, void *userdata); -/** - * Get ETF asset allocation (holdings / regional / asset class / industry). - * Returns `CAssetAllocationResponse`. - */ -void lb_quote_context_etf_asset_allocation(const struct lb_quote_context_t *ctx, - const char *symbol, - 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); diff --git a/c/src/fundamental_context/context.rs b/c/src/fundamental_context/context.rs index 9d05866ce..a7d8fb357 100644 --- a/c/src/fundamental_context/context.rs +++ b/c/src/fundamental_context/context.rs @@ -661,3 +661,22 @@ pub unsafe extern "C" fn lb_fundamental_context_valuation_comparison( Ok(resp) }); } + +/// Get ETF asset allocation (holdings / regional / asset class / industry). +/// Returns `CAssetAllocationResponse`. +#[unsafe(no_mangle)] +pub unsafe extern "C" fn lb_fundamental_context_etf_asset_allocation( + 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( + CAssetAllocationResponseOwned::from(ctx_inner.etf_asset_allocation(symbol).await?), + ); + Ok(resp) + }); +} diff --git a/c/src/fundamental_context/enum_types.rs b/c/src/fundamental_context/enum_types.rs index f93fd38c4..ba4f79965 100644 --- a/c/src/fundamental_context/enum_types.rs +++ b/c/src/fundamental_context/enum_types.rs @@ -80,3 +80,26 @@ pub enum CFinancialReportPeriod { #[c(remote = "ThreeQ")] FinancialReportPeriodThreeQ, } + +/// ETF asset allocation element type +#[derive(Debug, Copy, Clone, Eq, PartialEq, CEnum)] +#[c(remote = "longbridge::fundamental::types::ElementType")] +#[allow(clippy::enum_variant_names)] +#[repr(C)] +pub enum CElementType { + /// Unknown + #[c(remote = "Unknown")] + ElementTypeUnknown, + /// Holdings + #[c(remote = "Holdings")] + ElementTypeHoldings, + /// Regional + #[c(remote = "Regional")] + ElementTypeRegional, + /// Asset class + #[c(remote = "AssetClass")] + ElementTypeAssetClass, + /// Industry + #[c(remote = "Industry")] + ElementTypeIndustry, +} diff --git a/c/src/fundamental_context/types.rs b/c/src/fundamental_context/types.rs index ef9198d0b..189a33c68 100644 --- a/c/src/fundamental_context/types.rs +++ b/c/src/fundamental_context/types.rs @@ -3,7 +3,7 @@ use std::os::raw::c_char; use longbridge::fundamental::types::*; use crate::{ - fundamental_context::enum_types::CInstitutionRecommend, + fundamental_context::enum_types::{CElementType, CInstitutionRecommend}, types::{COption, CString, CVec, ToFFI}, }; @@ -3842,3 +3842,221 @@ impl ToFFI for CFinancialReportSnapshotOwned { } } } + +// ── EtfAssetAllocation ──────────────────────────────────────────── + +/// Localized name entry (locale → name) +#[repr(C)] +pub struct CLocaleName { + /// Locale (e.g. `zh-CN`) + pub locale: *const c_char, + /// Localized name + pub name: *const c_char, +} + +pub(crate) struct CLocaleNameOwned { + locale: CString, + name: CString, +} + +impl From<(String, String)> for CLocaleNameOwned { + fn from((locale, name): (String, String)) -> Self { + Self { + locale: locale.into(), + name: name.into(), + } + } +} + +impl ToFFI for CLocaleNameOwned { + type FFIType = CLocaleName; + fn to_ffi_type(&self) -> Self::FFIType { + CLocaleName { + locale: self.locale.to_ffi_type(), + name: self.name.to_ffi_type(), + } + } +} + +/// Holding detail of an ETF asset allocation element (holdings only) +#[repr(C)] +pub struct CHoldingDetail { + /// Industry ID + pub industry_id: *const c_char, + /// Industry name + pub industry_name: *const c_char, + /// Index counter ID (e.g. `BK/US/CP99000`) + pub index: *const c_char, + /// Index name + pub index_name: *const c_char, + /// Holding type (e.g. `E` for stock) + pub holding_type: *const c_char, + /// Holding type name + pub holding_type_name: *const c_char, +} + +pub(crate) struct CHoldingDetailOwned { + industry_id: CString, + industry_name: CString, + index: CString, + index_name: CString, + holding_type: CString, + holding_type_name: CString, +} + +impl From for CHoldingDetailOwned { + fn from(v: HoldingDetail) -> Self { + Self { + industry_id: v.industry_id.into(), + industry_name: v.industry_name.into(), + index: v.index.into(), + index_name: v.index_name.into(), + holding_type: v.holding_type.into(), + holding_type_name: v.holding_type_name.into(), + } + } +} + +impl ToFFI for CHoldingDetailOwned { + type FFIType = CHoldingDetail; + fn to_ffi_type(&self) -> Self::FFIType { + CHoldingDetail { + industry_id: self.industry_id.to_ffi_type(), + industry_name: self.industry_name.to_ffi_type(), + index: self.index.to_ffi_type(), + index_name: self.index_name.to_ffi_type(), + holding_type: self.holding_type.to_ffi_type(), + holding_type_name: self.holding_type_name.to_ffi_type(), + } + } +} + +/// One element of an ETF asset allocation group +#[repr(C)] +pub struct CAssetAllocationItem { + /// Element name + pub name: *const c_char, + /// Security code (holdings only, e.g. `NVDA`) + pub code: *const c_char, + /// Position ratio (e.g. `0.0861114`) + pub position_ratio: *const c_char, + /// Security symbol (holdings only, e.g. `NVDA.US`) + pub symbol: *const c_char, + /// Pointer to array of localized name entries + pub name_locales: *const CLocaleName, + /// Number of elements in the localized name array + pub num_name_locales: usize, + /// Holding detail (holdings only, maybe null) + pub holding_detail: *const CHoldingDetail, +} + +pub(crate) struct CAssetAllocationItemOwned { + name: CString, + code: CString, + position_ratio: CString, + symbol: CString, + name_locales: CVec, + holding_detail: COption, +} + +impl From for CAssetAllocationItemOwned { + fn from(v: AssetAllocationItem) -> Self { + let mut name_locales = v.name_locales.into_iter().collect::>(); + name_locales.sort(); + Self { + name: v.name.into(), + code: v.code.into(), + position_ratio: v.position_ratio.into(), + symbol: v.symbol.into(), + name_locales: name_locales.into(), + holding_detail: v.holding_detail.into(), + } + } +} + +impl ToFFI for CAssetAllocationItemOwned { + type FFIType = CAssetAllocationItem; + fn to_ffi_type(&self) -> Self::FFIType { + CAssetAllocationItem { + name: self.name.to_ffi_type(), + code: self.code.to_ffi_type(), + position_ratio: self.position_ratio.to_ffi_type(), + symbol: self.symbol.to_ffi_type(), + name_locales: self.name_locales.to_ffi_type(), + num_name_locales: self.name_locales.len(), + holding_detail: self.holding_detail.to_ffi_type(), + } + } +} + +/// One ETF asset allocation group (grouped by element type) +#[repr(C)] +pub struct CAssetAllocationGroup { + /// Report date (e.g. `20260601`) + pub report_date: *const c_char, + /// Element type of this group + pub asset_type: CElementType, + /// Pointer to array of elements + pub lists: *const CAssetAllocationItem, + /// Number of elements in the array + pub num_lists: usize, +} + +pub(crate) struct CAssetAllocationGroupOwned { + report_date: CString, + asset_type: ElementType, + lists: CVec, +} + +impl From for CAssetAllocationGroupOwned { + fn from(v: AssetAllocationGroup) -> Self { + Self { + report_date: v.report_date.into(), + asset_type: v.asset_type, + lists: v.lists.into(), + } + } +} + +impl ToFFI for CAssetAllocationGroupOwned { + type FFIType = CAssetAllocationGroup; + fn to_ffi_type(&self) -> Self::FFIType { + CAssetAllocationGroup { + report_date: self.report_date.to_ffi_type(), + asset_type: self.asset_type.into(), + lists: self.lists.to_ffi_type(), + num_lists: self.lists.len(), + } + } +} + +/// ETF asset allocation response +#[repr(C)] +pub struct CAssetAllocationResponse { + /// Pointer to array of asset allocation groups + pub info: *const CAssetAllocationGroup, + /// Number of elements in the array + pub num_info: usize, +} + +pub(crate) struct CAssetAllocationResponseOwned { + info: CVec, +} + +impl From for CAssetAllocationResponseOwned { + fn from(v: AssetAllocationResponse) -> Self { + Self { + info: v.info.into(), + } + } +} + +impl ToFFI for CAssetAllocationResponseOwned { + type FFIType = CAssetAllocationResponse; + fn to_ffi_type(&self) -> Self::FFIType { + CAssetAllocationResponse { + info: self.info.to_ffi_type(), + num_info: self.info.len(), + } + } +} diff --git a/c/src/quote_context/context.rs b/c/src/quote_context/context.rs index 24cc31d10..ce9feef9b 100644 --- a/c/src/quote_context/context.rs +++ b/c/src/quote_context/context.rs @@ -1290,23 +1290,3 @@ pub unsafe extern "C" fn lb_quote_context_option_volume_daily( Ok(resp) }); } - -/// Get ETF asset allocation (holdings / regional / asset class / industry). -/// Returns `CAssetAllocationResponse`. -#[unsafe(no_mangle)] -pub unsafe extern "C" fn lb_quote_context_etf_asset_allocation( - ctx: *const CQuoteContext, - symbol: *const c_char, - callback: CAsyncCallback, - userdata: *mut c_void, -) { - use crate::{quote_context::types::CAssetAllocationResponseOwned, 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( - CAssetAllocationResponseOwned::from(ctx_inner.etf_asset_allocation(symbol).await?), - ); - Ok(resp) - }); -} diff --git a/c/src/quote_context/enum_types.rs b/c/src/quote_context/enum_types.rs index e53a2900b..9bf17ebaa 100644 --- a/c/src/quote_context/enum_types.rs +++ b/c/src/quote_context/enum_types.rs @@ -632,26 +632,3 @@ pub enum CGranularity { #[c(remote = "Monthly")] GranularityMonthly, } - -/// ETF asset allocation element type -#[derive(Debug, Copy, Clone, Eq, PartialEq, CEnum)] -#[c(remote = "longbridge::quote::ElementType")] -#[allow(clippy::enum_variant_names)] -#[repr(C)] -pub enum CElementType { - /// Unknown - #[c(remote = "Unknown")] - ElementTypeUnknown, - /// Holdings - #[c(remote = "Holdings")] - ElementTypeHoldings, - /// Regional - #[c(remote = "Regional")] - ElementTypeRegional, - /// Asset class - #[c(remote = "AssetClass")] - ElementTypeAssetClass, - /// Industry - #[c(remote = "Industry")] - ElementTypeIndustry, -} diff --git a/c/src/quote_context/types.rs b/c/src/quote_context/types.rs index 312aeee15..72cdaf5c8 100644 --- a/c/src/quote_context/types.rs +++ b/c/src/quote_context/types.rs @@ -1,12 +1,11 @@ use std::os::raw::c_char; use longbridge::quote::{ - AssetAllocationGroup, AssetAllocationItem, AssetAllocationResponse, Brokers, Candlestick, - CapitalDistribution, CapitalDistributionResponse, CapitalFlowLine, Depth, ElementType, - FilingItem, HistoryMarketTemperatureResponse, HoldingDetail, IntradayLine, IssuerInfo, - MarketTemperature, MarketTradingDays, MarketTradingSession, OptionDirection, OptionQuote, - OptionType, OptionVolumeDaily, OptionVolumeDailyStat, OptionVolumeStats, ParticipantInfo, - Period, PrePostQuote, PushBrokers, PushCandlestick, PushDepth, PushQuote, PushTrades, + Brokers, Candlestick, CapitalDistribution, CapitalDistributionResponse, CapitalFlowLine, Depth, + FilingItem, HistoryMarketTemperatureResponse, IntradayLine, IssuerInfo, MarketTemperature, + MarketTradingDays, MarketTradingSession, OptionDirection, OptionQuote, OptionType, + OptionVolumeDaily, OptionVolumeDailyStat, OptionVolumeStats, ParticipantInfo, Period, + PrePostQuote, PushBrokers, PushCandlestick, PushDepth, PushQuote, PushTrades, QuotePackageDetail, RealtimeQuote, Security, SecurityBoard, SecurityBrokers, SecurityCalcIndex, SecurityDepth, SecurityQuote, SecurityStaticInfo, ShortPositionsItem, ShortPositionsResponse, ShortTradesItem, ShortTradesResponse, StrikePriceInfo, Subscription, Trade, TradeDirection, @@ -16,7 +15,7 @@ use longbridge::quote::{ use crate::{ quote_context::enum_types::{ - CElementType, CGranularity, COptionDirection, COptionType, CPeriod, CSecuritiesUpdateMode, + CGranularity, COptionDirection, COptionType, CPeriod, CSecuritiesUpdateMode, CSecurityBoard, CTradeDirection, CTradeSession, CTradeStatus, CWarrantStatus, CWarrantType, }, types::{CDate, CDecimal, CMarket, COption, CString, CTime, CVec, ToFFI}, @@ -3443,221 +3442,3 @@ impl ToFFI for COptionVolumeDailyOwned { } } } - -// ── EtfAssetAllocation ──────────────────────────────────────────── - -/// Localized name entry (locale → name) -#[repr(C)] -pub struct CLocaleName { - /// Locale (e.g. `zh-CN`) - pub locale: *const c_char, - /// Localized name - pub name: *const c_char, -} - -pub(crate) struct CLocaleNameOwned { - locale: CString, - name: CString, -} - -impl From<(String, String)> for CLocaleNameOwned { - fn from((locale, name): (String, String)) -> Self { - Self { - locale: locale.into(), - name: name.into(), - } - } -} - -impl ToFFI for CLocaleNameOwned { - type FFIType = CLocaleName; - fn to_ffi_type(&self) -> Self::FFIType { - CLocaleName { - locale: self.locale.to_ffi_type(), - name: self.name.to_ffi_type(), - } - } -} - -/// Holding detail of an ETF asset allocation element (holdings only) -#[repr(C)] -pub struct CHoldingDetail { - /// Industry ID - pub industry_id: *const c_char, - /// Industry name - pub industry_name: *const c_char, - /// Index counter ID (e.g. `BK/US/CP99000`) - pub index: *const c_char, - /// Index name - pub index_name: *const c_char, - /// Holding type (e.g. `E` for stock) - pub holding_type: *const c_char, - /// Holding type name - pub holding_type_name: *const c_char, -} - -pub(crate) struct CHoldingDetailOwned { - industry_id: CString, - industry_name: CString, - index: CString, - index_name: CString, - holding_type: CString, - holding_type_name: CString, -} - -impl From for CHoldingDetailOwned { - fn from(v: HoldingDetail) -> Self { - Self { - industry_id: v.industry_id.into(), - industry_name: v.industry_name.into(), - index: v.index.into(), - index_name: v.index_name.into(), - holding_type: v.holding_type.into(), - holding_type_name: v.holding_type_name.into(), - } - } -} - -impl ToFFI for CHoldingDetailOwned { - type FFIType = CHoldingDetail; - fn to_ffi_type(&self) -> Self::FFIType { - CHoldingDetail { - industry_id: self.industry_id.to_ffi_type(), - industry_name: self.industry_name.to_ffi_type(), - index: self.index.to_ffi_type(), - index_name: self.index_name.to_ffi_type(), - holding_type: self.holding_type.to_ffi_type(), - holding_type_name: self.holding_type_name.to_ffi_type(), - } - } -} - -/// One element of an ETF asset allocation group -#[repr(C)] -pub struct CAssetAllocationItem { - /// Element name - pub name: *const c_char, - /// Security code (holdings only, e.g. `NVDA`) - pub code: *const c_char, - /// Position ratio (e.g. `0.0861114`) - pub position_ratio: *const c_char, - /// Security symbol (holdings only, e.g. `NVDA.US`) - pub symbol: *const c_char, - /// Pointer to array of localized name entries - pub name_locales: *const CLocaleName, - /// Number of elements in the localized name array - pub num_name_locales: usize, - /// Holding detail (holdings only, maybe null) - pub holding_detail: *const CHoldingDetail, -} - -pub(crate) struct CAssetAllocationItemOwned { - name: CString, - code: CString, - position_ratio: CString, - symbol: CString, - name_locales: CVec, - holding_detail: COption, -} - -impl From for CAssetAllocationItemOwned { - fn from(v: AssetAllocationItem) -> Self { - let mut name_locales = v.name_locales.into_iter().collect::>(); - name_locales.sort(); - Self { - name: v.name.into(), - code: v.code.into(), - position_ratio: v.position_ratio.into(), - symbol: v.symbol.into(), - name_locales: name_locales.into(), - holding_detail: v.holding_detail.into(), - } - } -} - -impl ToFFI for CAssetAllocationItemOwned { - type FFIType = CAssetAllocationItem; - fn to_ffi_type(&self) -> Self::FFIType { - CAssetAllocationItem { - name: self.name.to_ffi_type(), - code: self.code.to_ffi_type(), - position_ratio: self.position_ratio.to_ffi_type(), - symbol: self.symbol.to_ffi_type(), - name_locales: self.name_locales.to_ffi_type(), - num_name_locales: self.name_locales.len(), - holding_detail: self.holding_detail.to_ffi_type(), - } - } -} - -/// One ETF asset allocation group (grouped by element type) -#[repr(C)] -pub struct CAssetAllocationGroup { - /// Report date (e.g. `20260601`) - pub report_date: *const c_char, - /// Element type of this group - pub asset_type: CElementType, - /// Pointer to array of elements - pub lists: *const CAssetAllocationItem, - /// Number of elements in the array - pub num_lists: usize, -} - -pub(crate) struct CAssetAllocationGroupOwned { - report_date: CString, - asset_type: ElementType, - lists: CVec, -} - -impl From for CAssetAllocationGroupOwned { - fn from(v: AssetAllocationGroup) -> Self { - Self { - report_date: v.report_date.into(), - asset_type: v.asset_type, - lists: v.lists.into(), - } - } -} - -impl ToFFI for CAssetAllocationGroupOwned { - type FFIType = CAssetAllocationGroup; - fn to_ffi_type(&self) -> Self::FFIType { - CAssetAllocationGroup { - report_date: self.report_date.to_ffi_type(), - asset_type: self.asset_type.into(), - lists: self.lists.to_ffi_type(), - num_lists: self.lists.len(), - } - } -} - -/// ETF asset allocation response -#[repr(C)] -pub struct CAssetAllocationResponse { - /// Pointer to array of asset allocation groups - pub info: *const CAssetAllocationGroup, - /// Number of elements in the array - pub num_info: usize, -} - -pub(crate) struct CAssetAllocationResponseOwned { - info: CVec, -} - -impl From for CAssetAllocationResponseOwned { - fn from(v: AssetAllocationResponse) -> Self { - Self { - info: v.info.into(), - } - } -} - -impl ToFFI for CAssetAllocationResponseOwned { - type FFIType = CAssetAllocationResponse; - fn to_ffi_type(&self) -> Self::FFIType { - CAssetAllocationResponse { - info: self.info.to_ffi_type(), - num_info: self.info.len(), - } - } -} diff --git a/cpp/include/fundamental_context.hpp b/cpp/include/fundamental_context.hpp index 451da4256..799231d58 100644 --- a/cpp/include/fundamental_context.hpp +++ b/cpp/include/fundamental_context.hpp @@ -175,6 +175,11 @@ class FundamentalContext const std::string& currency, const std::vector* comparison_symbols, AsyncCallback callback) const; + + /// Get ETF asset allocation (holdings / regional / asset class / industry) + void etf_asset_allocation( + const std::string& symbol, + AsyncCallback callback) const; }; } // namespace fundamental diff --git a/cpp/include/quote_context.hpp b/cpp/include/quote_context.hpp index 5f5a98fe7..01644f332 100644 --- a/cpp/include/quote_context.hpp +++ b/cpp/include/quote_context.hpp @@ -333,11 +333,6 @@ class QuoteContext int64_t timestamp, uint32_t count, AsyncCallback callback) const; - - /// Get ETF asset allocation (holdings / regional / asset class / industry) - void etf_asset_allocation( - const std::string& symbol, - AsyncCallback callback) const; }; } // namespace quote diff --git a/cpp/include/types.hpp b/cpp/include/types.hpp index 94684d5ae..7c66849b0 100644 --- a/cpp/include/types.hpp +++ b/cpp/include/types.hpp @@ -1347,73 +1347,6 @@ struct OptionVolumeDaily std::vector stats; }; -/// ETF asset allocation element type -enum class ElementType -{ - /// Unknown - Unknown, - /// Holdings - Holdings, - /// Regional - Regional, - /// Asset class - AssetClass, - /// Industry - Industry, -}; - -/// Holding detail of an ETF asset allocation element (holdings only) -struct HoldingDetail -{ - /// Industry ID - std::string industry_id; - /// Industry name - std::string industry_name; - /// Index counter ID (e.g. `BK/US/CP99000`) - std::string index; - /// Index name - std::string index_name; - /// Holding type (e.g. `E` for stock) - std::string holding_type; - /// Holding type name - std::string holding_type_name; -}; - -/// One element of an ETF asset allocation group -struct AssetAllocationItem -{ - /// Element name - std::string name; - /// Security code (holdings only, e.g. `NVDA`) - std::string code; - /// Position ratio (e.g. `0.0861114`) - std::string position_ratio; - /// Security symbol (holdings only, e.g. `NVDA.US`) - std::string symbol; - /// Localized names (locale → name) - std::map name_locales; - /// Holding detail (holdings only) - std::optional holding_detail; -}; - -/// One ETF asset allocation group (grouped by element type) -struct AssetAllocationGroup -{ - /// Report date (e.g. `20260601`) - std::string report_date; - /// Element type of this group - ElementType asset_type; - /// Elements - std::vector lists; -}; - -/// ETF asset allocation response -struct AssetAllocationResponse -{ - /// Asset allocation groups - std::vector info; -}; - } // namespace quote namespace trade { @@ -3341,6 +3274,73 @@ struct ValuationComparisonResponse std::vector list; }; +/// ETF asset allocation element type +enum class ElementType +{ + /// Unknown + Unknown, + /// Holdings + Holdings, + /// Regional + Regional, + /// Asset class + AssetClass, + /// Industry + Industry, +}; + +/// Holding detail of an ETF asset allocation element (holdings only) +struct HoldingDetail +{ + /// Industry ID + std::string industry_id; + /// Industry name + std::string industry_name; + /// Index counter ID (e.g. `BK/US/CP99000`) + std::string index; + /// Index name + std::string index_name; + /// Holding type (e.g. `E` for stock) + std::string holding_type; + /// Holding type name + std::string holding_type_name; +}; + +/// One element of an ETF asset allocation group +struct AssetAllocationItem +{ + /// Element name + std::string name; + /// Security code (holdings only, e.g. `NVDA`) + std::string code; + /// Position ratio (e.g. `0.0861114`) + std::string position_ratio; + /// Security symbol (holdings only, e.g. `NVDA.US`) + std::string symbol; + /// Localized names (locale → name) + std::map name_locales; + /// Holding detail (holdings only) + std::optional holding_detail; +}; + +/// One ETF asset allocation group (grouped by element type) +struct AssetAllocationGroup +{ + /// Report date (e.g. `20260601`) + std::string report_date; + /// Element type of this group + ElementType asset_type; + /// Elements + std::vector lists; +}; + +/// ETF asset allocation response +struct AssetAllocationResponse +{ + /// Asset allocation groups + std::vector info; +}; + } // namespace fundamental namespace alert { diff --git a/cpp/src/convert.hpp b/cpp/src/convert.hpp index 78a5f1beb..8789e1a77 100644 --- a/cpp/src/convert.hpp +++ b/cpp/src/convert.hpp @@ -2335,23 +2335,23 @@ inline quote::OptionVolumeDaily convert(const lb_option_volume_daily_t* r) { for (size_t i = 0; i < r->num_stats; ++i) stats.push_back(convert(&r->stats[i])); return { std::move(stats) }; } -inline quote::ElementType convert(lb_element_type_t ty) { +inline fundamental::ElementType convert(lb_element_type_t ty) { switch (ty) { case ElementTypeUnknown: - return quote::ElementType::Unknown; + return fundamental::ElementType::Unknown; case ElementTypeHoldings: - return quote::ElementType::Holdings; + return fundamental::ElementType::Holdings; case ElementTypeRegional: - return quote::ElementType::Regional; + return fundamental::ElementType::Regional; case ElementTypeAssetClass: - return quote::ElementType::AssetClass; + return fundamental::ElementType::AssetClass; case ElementTypeIndustry: - return quote::ElementType::Industry; + return fundamental::ElementType::Industry; default: throw std::invalid_argument("unreachable"); } } -inline quote::HoldingDetail convert(const lb_holding_detail_t* d) { +inline fundamental::HoldingDetail convert(const lb_holding_detail_t* d) { return { d->industry_id ? d->industry_id : "", d->industry_name ? d->industry_name : "", d->index ? d->index : "", @@ -2359,7 +2359,7 @@ inline quote::HoldingDetail convert(const lb_holding_detail_t* d) { d->holding_type ? d->holding_type : "", d->holding_type_name ? d->holding_type_name : "" }; } -inline quote::AssetAllocationItem convert(const lb_asset_allocation_item_t* item) { +inline fundamental::AssetAllocationItem convert(const lb_asset_allocation_item_t* item) { std::map name_locales; for (size_t i = 0; i < item->num_name_locales; ++i) { const auto& entry = item->name_locales[i]; @@ -2373,13 +2373,13 @@ inline quote::AssetAllocationItem convert(const lb_asset_allocation_item_t* item item->holding_detail ? std::make_optional(convert(item->holding_detail)) : std::nullopt }; } -inline quote::AssetAllocationGroup convert(const lb_asset_allocation_group_t* g) { - std::vector lists; +inline fundamental::AssetAllocationGroup convert(const lb_asset_allocation_group_t* g) { + std::vector lists; for (size_t i = 0; i < g->num_lists; ++i) lists.push_back(convert(&g->lists[i])); return { g->report_date ? g->report_date : "", convert(g->asset_type), std::move(lists) }; } -inline quote::AssetAllocationResponse convert(const lb_asset_allocation_response_t* r) { - std::vector info; +inline fundamental::AssetAllocationResponse convert(const lb_asset_allocation_response_t* r) { + std::vector info; for (size_t i = 0; i < r->num_info; ++i) info.push_back(convert(&r->info[i])); return { std::move(info) }; } diff --git a/cpp/src/fundamental_context.cpp b/cpp/src/fundamental_context.cpp index c14148cd0..f108dda11 100644 --- a/cpp/src/fundamental_context.cpp +++ b/cpp/src/fundamental_context.cpp @@ -169,5 +169,19 @@ void FundamentalContext::valuation_comparison(const std::string& s, const std::s #undef F_JSON_STRUCT +void FundamentalContext::etf_asset_allocation(const std::string& symbol, AsyncCallback callback) const { + lb_fundamental_context_etf_asset_allocation(ctx_, symbol.c_str(), + [](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_asset_allocation_response_t*)res->data); + (*cb)(AsyncResult(fctx, std::move(status), &r)); + } else { + (*cb)(AsyncResult(fctx, std::move(status), nullptr)); + } + }, new AsyncCallback(callback)); +} + } // namespace fundamental } // namespace longbridge diff --git a/cpp/src/quote_context.cpp b/cpp/src/quote_context.cpp index 2f0d05417..ab5e4f2b5 100644 --- a/cpp/src/quote_context.cpp +++ b/cpp/src/quote_context.cpp @@ -1759,30 +1759,5 @@ QuoteContext::option_volume_daily(const std::string& symbol, new AsyncCallback(callback)); } -void -QuoteContext::etf_asset_allocation( - const std::string& symbol, - AsyncCallback callback) const -{ - lb_quote_context_etf_asset_allocation( - ctx_, - symbol.c_str(), - [](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_asset_allocation_response_t*)res->data); - (*callback_ptr)( - AsyncResult(ctx, std::move(status), &value)); - } else { - (*callback_ptr)( - AsyncResult(ctx, std::move(status), nullptr)); - } - }, - new AsyncCallback(callback)); -} - } // namespace quote } // namespace longbridge \ No newline at end of file diff --git a/nodejs/index.d.ts b/nodejs/index.d.ts index 55217b341..c7f7b1eb4 100644 --- a/nodejs/index.d.ts +++ b/nodejs/index.d.ts @@ -613,6 +613,11 @@ export declare class FundamentalContext { 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 + /** + * Get ETF asset allocation (holdings / regional / asset class / + * industry) + */ + etfAssetAllocation(symbol: string): Promise } /** Fund position */ @@ -2021,11 +2026,6 @@ export declare class QuoteContext { optionVolume(symbol: string): Promise /** Get daily historical option volume */ optionVolumeDaily(symbol: string, timestamp: number, count: number): Promise - /** - * Get ETF asset allocation (holdings / regional / asset class / - * industry) - */ - etfAssetAllocation(symbol: string): Promise } export declare class QuotePackageDetail { diff --git a/nodejs/src/fundamental/context.rs b/nodejs/src/fundamental/context.rs index 0878dad09..ca1cc3528 100644 --- a/nodejs/src/fundamental/context.rs +++ b/nodejs/src/fundamental/context.rs @@ -275,4 +275,16 @@ impl FundamentalContext { .map_err(ErrorNewType)? .into()) } + + /// Get ETF asset allocation (holdings / regional / asset class / + /// industry) + #[napi] + pub async fn etf_asset_allocation(&self, symbol: String) -> Result { + Ok(self + .ctx + .etf_asset_allocation(symbol) + .await + .map_err(ErrorNewType)? + .into()) + } } diff --git a/nodejs/src/fundamental/types.rs b/nodejs/src/fundamental/types.rs index eaf68e746..37664aba5 100644 --- a/nodejs/src/fundamental/types.rs +++ b/nodejs/src/fundamental/types.rs @@ -1783,3 +1783,122 @@ impl From for ValuationComparisonResponse { } } } + +// ── etf_asset_allocation ────────────────────────────────────────── + +/// ETF asset allocation element type +#[napi_derive::napi] +#[derive(longbridge_nodejs_macros::JsEnum, Debug, Hash, Eq, PartialEq, Copy, Clone)] +#[js(remote = "longbridge::fundamental::types::ElementType")] +pub enum ElementType { + /// Unknown + Unknown, + /// Holdings + Holdings, + /// Regional + Regional, + /// Asset class + AssetClass, + /// Industry + Industry, +} + +/// Holding detail of an ETF asset allocation element (holdings only) +#[napi_derive::napi(object)] +#[derive(Debug, Clone)] +pub struct HoldingDetail { + /// Industry ID + pub industry_id: String, + /// Industry name + pub industry_name: String, + /// Index counter ID (e.g. `BK/US/CP99000`) + pub index: String, + /// Index name + pub index_name: String, + /// Holding type (e.g. `E` for stock) + pub holding_type: String, + /// Holding type name + pub holding_type_name: String, +} + +impl From for HoldingDetail { + fn from(v: lb::HoldingDetail) -> Self { + Self { + industry_id: v.industry_id, + industry_name: v.industry_name, + index: v.index, + index_name: v.index_name, + holding_type: v.holding_type, + holding_type_name: v.holding_type_name, + } + } +} + +/// One element of an ETF asset allocation group +#[napi_derive::napi(object)] +#[derive(Debug, Clone)] +pub struct AssetAllocationItem { + /// Element name + pub name: String, + /// Security code (holdings only, e.g. `NVDA`) + pub code: String, + /// Position ratio (e.g. `0.0861114`) + pub position_ratio: String, + /// Security symbol (holdings only, e.g. `NVDA.US`) + pub symbol: String, + /// Localized names (locale → name) + pub name_locales: std::collections::HashMap, + /// Holding detail (holdings only) + pub holding_detail: Option, +} + +impl From for AssetAllocationItem { + fn from(v: lb::AssetAllocationItem) -> Self { + Self { + name: v.name, + code: v.code, + position_ratio: v.position_ratio, + symbol: v.symbol, + name_locales: v.name_locales, + holding_detail: v.holding_detail.map(Into::into), + } + } +} + +/// One ETF asset allocation group (grouped by element type) +#[napi_derive::napi(object)] +#[derive(Debug, Clone)] +pub struct AssetAllocationGroup { + /// Report date (e.g. `20260601`) + pub report_date: String, + /// Element type of this group + pub asset_type: ElementType, + /// Elements + pub lists: Vec, +} + +impl From for AssetAllocationGroup { + fn from(v: lb::AssetAllocationGroup) -> Self { + Self { + report_date: v.report_date, + asset_type: v.asset_type.into(), + lists: v.lists.into_iter().map(Into::into).collect(), + } + } +} + +/// ETF asset allocation response +#[napi_derive::napi(object)] +#[derive(Debug, Clone)] +pub struct AssetAllocationResponse { + /// Asset allocation groups + pub info: Vec, +} + +impl From for AssetAllocationResponse { + fn from(v: lb::AssetAllocationResponse) -> Self { + Self { + info: v.info.into_iter().map(Into::into).collect(), + } + } +} diff --git a/nodejs/src/quote/context.rs b/nodejs/src/quote/context.rs index 7ede12509..4ee04a1e2 100644 --- a/nodejs/src/quote/context.rs +++ b/nodejs/src/quote/context.rs @@ -13,16 +13,16 @@ use crate::{ }, requests::{CreateWatchlistGroup, DeleteWatchlistGroup, UpdateWatchlistGroup}, types::{ - AdjustType, AssetAllocationResponse, CalcIndex, Candlestick, - CapitalDistributionResponse, CapitalFlowLine, FilingItem, FilterWarrantExpiryDate, - FilterWarrantInOutBoundsType, HistoryMarketTemperatureResponse, IntradayLine, - IssuerInfo, MarketTemperature, MarketTradingDays, MarketTradingSession, OptionQuote, - OptionVolumeDaily, OptionVolumeStats, ParticipantInfo, Period, PinnedMode, - QuotePackageDetail, RealtimeQuote, Security, SecurityBrokers, SecurityCalcIndex, - SecurityDepth, SecurityListCategory, SecurityQuote, SecurityStaticInfo, - ShortPositionsResponse, ShortTradesResponse, SortOrderType, StrikePriceInfo, SubType, - SubTypes, Subscription, Trade, TradeSessions, WarrantInfo, WarrantQuote, WarrantSortBy, - WarrantStatus, WarrantType, WatchlistGroup, + AdjustType, CalcIndex, Candlestick, CapitalDistributionResponse, CapitalFlowLine, + FilingItem, FilterWarrantExpiryDate, FilterWarrantInOutBoundsType, + HistoryMarketTemperatureResponse, IntradayLine, IssuerInfo, MarketTemperature, + MarketTradingDays, MarketTradingSession, OptionQuote, OptionVolumeDaily, + OptionVolumeStats, ParticipantInfo, Period, PinnedMode, QuotePackageDetail, + RealtimeQuote, Security, SecurityBrokers, SecurityCalcIndex, SecurityDepth, + SecurityListCategory, SecurityQuote, SecurityStaticInfo, ShortPositionsResponse, + ShortTradesResponse, SortOrderType, StrikePriceInfo, SubType, SubTypes, Subscription, + Trade, TradeSessions, WarrantInfo, WarrantQuote, WarrantSortBy, WarrantStatus, + WarrantType, WatchlistGroup, }, }, time::{NaiveDate, NaiveDatetime}, @@ -1290,16 +1290,4 @@ impl QuoteContext { .map_err(ErrorNewType)? .into()) } - - /// Get ETF asset allocation (holdings / regional / asset class / - /// industry) - #[napi] - pub async fn etf_asset_allocation(&self, symbol: String) -> Result { - Ok(self - .ctx - .etf_asset_allocation(symbol) - .await - .map_err(ErrorNewType)? - .into()) - } } diff --git a/nodejs/src/quote/types.rs b/nodejs/src/quote/types.rs index 86379fa62..acd0cfdd4 100644 --- a/nodejs/src/quote/types.rs +++ b/nodejs/src/quote/types.rs @@ -1,5 +1,3 @@ -use std::collections::HashMap; - use chrono::{DateTime, Utc}; use longbridge::quote::SubFlags; use longbridge_nodejs_macros::{JsEnum, JsObject}; @@ -1665,122 +1663,3 @@ impl From for OptionVolumeDailyStat { } } } - -// ── etf_asset_allocation ────────────────────────────────────────── - -/// ETF asset allocation element type -#[napi_derive::napi] -#[derive(JsEnum, Debug, Hash, Eq, PartialEq, Copy, Clone)] -#[js(remote = "longbridge::quote::ElementType")] -pub enum ElementType { - /// Unknown - Unknown, - /// Holdings - Holdings, - /// Regional - Regional, - /// Asset class - AssetClass, - /// Industry - Industry, -} - -/// Holding detail of an ETF asset allocation element (holdings only) -#[napi_derive::napi(object)] -#[derive(Debug, Clone)] -pub struct HoldingDetail { - /// Industry ID - pub industry_id: String, - /// Industry name - pub industry_name: String, - /// Index counter ID (e.g. `BK/US/CP99000`) - pub index: String, - /// Index name - pub index_name: String, - /// Holding type (e.g. `E` for stock) - pub holding_type: String, - /// Holding type name - pub holding_type_name: String, -} - -impl From for HoldingDetail { - fn from(v: longbridge::quote::HoldingDetail) -> Self { - Self { - industry_id: v.industry_id, - industry_name: v.industry_name, - index: v.index, - index_name: v.index_name, - holding_type: v.holding_type, - holding_type_name: v.holding_type_name, - } - } -} - -/// One element of an ETF asset allocation group -#[napi_derive::napi(object)] -#[derive(Debug, Clone)] -pub struct AssetAllocationItem { - /// Element name - pub name: String, - /// Security code (holdings only, e.g. `NVDA`) - pub code: String, - /// Position ratio (e.g. `0.0861114`) - pub position_ratio: String, - /// Security symbol (holdings only, e.g. `NVDA.US`) - pub symbol: String, - /// Localized names (locale → name) - pub name_locales: HashMap, - /// Holding detail (holdings only) - pub holding_detail: Option, -} - -impl From for AssetAllocationItem { - fn from(v: longbridge::quote::AssetAllocationItem) -> Self { - Self { - name: v.name, - code: v.code, - position_ratio: v.position_ratio, - symbol: v.symbol, - name_locales: v.name_locales, - holding_detail: v.holding_detail.map(Into::into), - } - } -} - -/// One ETF asset allocation group (grouped by element type) -#[napi_derive::napi(object)] -#[derive(Debug, Clone)] -pub struct AssetAllocationGroup { - /// Report date (e.g. `20260601`) - pub report_date: String, - /// Element type of this group - pub asset_type: ElementType, - /// Elements - pub lists: Vec, -} - -impl From for AssetAllocationGroup { - fn from(v: longbridge::quote::AssetAllocationGroup) -> Self { - Self { - report_date: v.report_date, - asset_type: v.asset_type.into(), - lists: v.lists.into_iter().map(Into::into).collect(), - } - } -} - -/// ETF asset allocation response -#[napi_derive::napi(object)] -#[derive(Debug, Clone)] -pub struct AssetAllocationResponse { - /// Asset allocation groups - pub info: Vec, -} - -impl From for AssetAllocationResponse { - fn from(v: longbridge::quote::AssetAllocationResponse) -> Self { - Self { - info: v.info.into_iter().map(Into::into).collect(), - } - } -} diff --git a/python/pysrc/longbridge/openapi.pyi b/python/pysrc/longbridge/openapi.pyi index 3590f5d30..90bc788e3 100644 --- a/python/pysrc/longbridge/openapi.pyi +++ b/python/pysrc/longbridge/openapi.pyi @@ -4020,17 +4020,6 @@ class QuoteContext: :class:`ShortTradesResponse` with raw JSON data """ - 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 - """ - class AsyncQuoteContext: """ Async quote context for use with asyncio. Create via `AsyncQuoteContext.create(config)` and await inside asyncio. @@ -5383,21 +5372,6 @@ class AsyncQuoteContext: """ ... - def etf_asset_allocation( - self, symbol: str - ) -> Awaitable[AssetAllocationResponse]: - """ - Get ETF asset allocation (holdings / regional / asset class / industry). - Returns awaitable. - - Args: - symbol: ETF security code (e.g. ``"QQQ.US"``) - - Returns: - Awaitable resolving to :class:`AssetAllocationResponse` - """ - ... - class OrderSide: """ Order side @@ -9936,6 +9910,18 @@ class FundamentalContext: """ ... + 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 + """ + ... + # ── FundamentalContext new response types ───────────────────────── @@ -10008,6 +9994,77 @@ class ValuationComparisonResponse: """Valuation comparison items""" +class ElementType: + """ETF asset allocation element type.""" + + class Unknown(ElementType): + """Unknown""" + + class Holdings(ElementType): + """Holdings""" + + class Regional(ElementType): + """Regional""" + + class AssetClass(ElementType): + """Asset class""" + + class Industry(ElementType): + """Industry""" + + +class HoldingDetail: + """Holding detail of an ETF asset allocation element (holdings only).""" + + industry_id: str + """Industry ID""" + industry_name: str + """Industry name""" + index: str + """Index counter ID (e.g. ``BK/US/CP99000``)""" + index_name: str + """Index name""" + holding_type: str + """Holding type (e.g. ``E`` for stock)""" + holding_type_name: str + """Holding type name""" + + +class AssetAllocationItem: + """One element of an ETF asset allocation group.""" + + name: str + """Element name""" + code: str + """Security code (holdings only, e.g. ``NVDA``)""" + position_ratio: str + """Position ratio (e.g. ``0.0861114``)""" + symbol: str + """Security symbol (holdings only, e.g. ``NVDA.US``)""" + name_locales: dict[str, str] + """Localized names (locale → name)""" + holding_detail: Optional[HoldingDetail] + """Holding detail (holdings only)""" + + +class AssetAllocationGroup: + """One ETF asset allocation group (grouped by element type).""" + + report_date: str + """Report date (e.g. ``20260601``)""" + asset_type: Type[ElementType] + """Element type of this group""" + lists: list[AssetAllocationItem] + """Elements""" + + +class AssetAllocationResponse: + """ETF asset allocation response.""" + + info: list[AssetAllocationGroup] + """Asset allocation groups""" + + # ── MarketContext ───────────────────────────────────────────────── class MarketTimeItem: @@ -11963,74 +12020,3 @@ class OptionVolumeDaily: stats: list[OptionVolumeDailyStat] """Daily option volume statistics""" - - -class ElementType: - """ETF asset allocation element type.""" - - class Unknown(ElementType): - """Unknown""" - - class Holdings(ElementType): - """Holdings""" - - class Regional(ElementType): - """Regional""" - - class AssetClass(ElementType): - """Asset class""" - - class Industry(ElementType): - """Industry""" - - -class HoldingDetail: - """Holding detail of an ETF asset allocation element (holdings only).""" - - industry_id: str - """Industry ID""" - industry_name: str - """Industry name""" - index: str - """Index counter ID (e.g. ``BK/US/CP99000``)""" - index_name: str - """Index name""" - holding_type: str - """Holding type (e.g. ``E`` for stock)""" - holding_type_name: str - """Holding type name""" - - -class AssetAllocationItem: - """One element of an ETF asset allocation group.""" - - name: str - """Element name""" - code: str - """Security code (holdings only, e.g. ``NVDA``)""" - position_ratio: str - """Position ratio (e.g. ``0.0861114``)""" - symbol: str - """Security symbol (holdings only, e.g. ``NVDA.US``)""" - name_locales: dict[str, str] - """Localized names (locale → name)""" - holding_detail: Optional[HoldingDetail] - """Holding detail (holdings only)""" - - -class AssetAllocationGroup: - """One ETF asset allocation group (grouped by element type).""" - - report_date: str - """Report date (e.g. ``20260601``)""" - asset_type: Type[ElementType] - """Element type of this group""" - lists: list[AssetAllocationItem] - """Elements""" - - -class AssetAllocationResponse: - """ETF asset allocation response.""" - - info: list[AssetAllocationGroup] - """Asset allocation groups""" diff --git a/python/src/fundamental/context.rs b/python/src/fundamental/context.rs index 87f2e26fd..e65d93fd2 100644 --- a/python/src/fundamental/context.rs +++ b/python/src/fundamental/context.rs @@ -198,4 +198,14 @@ impl FundamentalContext { .map_err(ErrorNewType)? .into()) } + + /// Get ETF asset allocation (holdings / regional / asset class / + /// industry). + fn etf_asset_allocation(&self, symbol: String) -> PyResult { + Ok(self + .ctx + .etf_asset_allocation(symbol) + .map_err(ErrorNewType)? + .into()) + } } diff --git a/python/src/fundamental/context_async.rs b/python/src/fundamental/context_async.rs index 8c162b9a5..eed4d6142 100644 --- a/python/src/fundamental/context_async.rs +++ b/python/src/fundamental/context_async.rs @@ -303,4 +303,18 @@ impl AsyncFundamentalContext { }) .map(|b| b.unbind()) } + + /// Get ETF asset allocation (holdings / regional / asset class / + /// industry). Returns awaitable. + fn etf_asset_allocation(&self, py: Python<'_>, symbol: String) -> PyResult> { + let ctx = self.ctx.clone(); + pyo3_async_runtimes::tokio::future_into_py(py, async move { + Ok(AssetAllocationResponse::from( + ctx.etf_asset_allocation(symbol) + .await + .map_err(ErrorNewType)?, + )) + }) + .map(|b| b.unbind()) + } } diff --git a/python/src/fundamental/mod.rs b/python/src/fundamental/mod.rs index 62dfd8da4..a788e712e 100644 --- a/python/src/fundamental/mod.rs +++ b/python/src/fundamental/mod.rs @@ -69,6 +69,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 8219dc6f4..3a8f6ed59 100644 --- a/python/src/fundamental/types.rs +++ b/python/src/fundamental/types.rs @@ -1846,3 +1846,122 @@ impl From for ValuationComparisonResponse { } } } + +// ── etf_asset_allocation ────────────────────────────────────────── + +/// ETF asset allocation element type +#[pyclass(eq, eq_int, skip_from_py_object)] +#[derive(PyEnum, Debug, Copy, Clone, Hash, Eq, PartialEq)] +#[py(remote = "longbridge::fundamental::types::ElementType")] +pub(crate) enum ElementType { + /// Unknown + Unknown, + /// Holdings + Holdings, + /// Regional + Regional, + /// Asset class + AssetClass, + /// Industry + Industry, +} + +/// Holding detail of an ETF asset allocation element (holdings only) +#[pyclass(get_all, skip_from_py_object)] +#[derive(Debug, Clone)] +pub(crate) struct HoldingDetail { + /// Industry ID + pub industry_id: String, + /// Industry name + pub industry_name: String, + /// Index counter ID (e.g. `BK/US/CP99000`) + pub index: String, + /// Index name + pub index_name: String, + /// Holding type (e.g. `E` for stock) + pub holding_type: String, + /// Holding type name + pub holding_type_name: String, +} + +impl From for HoldingDetail { + fn from(v: lb::HoldingDetail) -> Self { + Self { + industry_id: v.industry_id, + industry_name: v.industry_name, + index: v.index, + index_name: v.index_name, + holding_type: v.holding_type, + holding_type_name: v.holding_type_name, + } + } +} + +/// One element of an ETF asset allocation group +#[pyclass(get_all, skip_from_py_object)] +#[derive(Debug, Clone)] +pub(crate) struct AssetAllocationItem { + /// Element name + pub name: String, + /// Security code (holdings only, e.g. `NVDA`) + pub code: String, + /// Position ratio (e.g. `0.0861114`) + pub position_ratio: String, + /// Security symbol (holdings only, e.g. `NVDA.US`) + pub symbol: String, + /// Localized names (locale → name) + pub name_locales: std::collections::HashMap, + /// Holding detail (holdings only) + pub holding_detail: Option, +} + +impl From for AssetAllocationItem { + fn from(v: lb::AssetAllocationItem) -> Self { + Self { + name: v.name, + code: v.code, + position_ratio: v.position_ratio, + symbol: v.symbol, + name_locales: v.name_locales, + holding_detail: v.holding_detail.map(Into::into), + } + } +} + +/// One ETF asset allocation group (grouped by element type) +#[pyclass(get_all, skip_from_py_object)] +#[derive(Debug, Clone)] +pub(crate) struct AssetAllocationGroup { + /// Report date (e.g. `20260601`) + pub report_date: String, + /// Element type of this group + pub asset_type: ElementType, + /// Elements + pub lists: Vec, +} + +impl From for AssetAllocationGroup { + fn from(v: lb::AssetAllocationGroup) -> Self { + Self { + report_date: v.report_date, + asset_type: v.asset_type.into(), + lists: v.lists.into_iter().map(Into::into).collect(), + } + } +} + +/// ETF asset allocation response +#[pyclass(get_all, skip_from_py_object)] +#[derive(Debug, Clone)] +pub(crate) struct AssetAllocationResponse { + /// Asset allocation groups + pub info: Vec, +} + +impl From for AssetAllocationResponse { + fn from(v: lb::AssetAllocationResponse) -> Self { + Self { + info: v.info.into_iter().map(Into::into).collect(), + } + } +} diff --git a/python/src/quote/context.rs b/python/src/quote/context.rs index 2357ebe78..a2d6f9e1e 100644 --- a/python/src/quote/context.rs +++ b/python/src/quote/context.rs @@ -687,17 +687,4 @@ impl QuoteContext { .map_err(ErrorNewType)? .into()) } - - /// Get ETF asset allocation (holdings / regional / asset class / - /// industry) - fn etf_asset_allocation( - &self, - symbol: String, - ) -> PyResult { - Ok(self - .ctx - .etf_asset_allocation(symbol) - .map_err(ErrorNewType)? - .into()) - } } diff --git a/python/src/quote/context_async.rs b/python/src/quote/context_async.rs index f3637e282..cd5ca44c9 100644 --- a/python/src/quote/context_async.rs +++ b/python/src/quote/context_async.rs @@ -895,19 +895,4 @@ impl AsyncQuoteContext { }) .map(|b| b.unbind()) } - - /// Get ETF asset allocation (holdings / regional / asset class / - /// industry). Returns awaitable. - fn etf_asset_allocation(&self, py: Python<'_>, symbol: String) -> PyResult> { - let ctx = self.ctx.clone(); - pyo3_async_runtimes::tokio::future_into_py(py, async move { - let r: crate::quote::types::AssetAllocationResponse = ctx - .etf_asset_allocation(symbol) - .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 d69458841..749f0deb0 100644 --- a/python/src/quote/mod.rs +++ b/python/src/quote/mod.rs @@ -71,11 +71,6 @@ 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::()?; diff --git a/python/src/quote/types.rs b/python/src/quote/types.rs index aeb7a5339..d9849dac0 100644 --- a/python/src/quote/types.rs +++ b/python/src/quote/types.rs @@ -1,5 +1,3 @@ -use std::collections::HashMap; - use longbridge::quote::SubFlags; use longbridge_python_macros::{PyEnum, PyObject}; use pyo3::prelude::*; @@ -1623,122 +1621,3 @@ impl From for OptionVolumeDailyStat { } } } - -// ── etf_asset_allocation ────────────────────────────────────────── - -/// ETF asset allocation element type -#[pyclass(eq, eq_int, skip_from_py_object)] -#[derive(PyEnum, Debug, Copy, Clone, Hash, Eq, PartialEq)] -#[py(remote = "longbridge::quote::ElementType")] -pub(crate) enum ElementType { - /// Unknown - Unknown, - /// Holdings - Holdings, - /// Regional - Regional, - /// Asset class - AssetClass, - /// Industry - Industry, -} - -/// Holding detail of an ETF asset allocation element (holdings only) -#[pyclass(get_all, skip_from_py_object)] -#[derive(Debug, Clone)] -pub(crate) struct HoldingDetail { - /// Industry ID - pub industry_id: String, - /// Industry name - pub industry_name: String, - /// Index counter ID (e.g. `BK/US/CP99000`) - pub index: String, - /// Index name - pub index_name: String, - /// Holding type (e.g. `E` for stock) - pub holding_type: String, - /// Holding type name - pub holding_type_name: String, -} - -impl From for HoldingDetail { - fn from(v: longbridge::quote::HoldingDetail) -> Self { - Self { - industry_id: v.industry_id, - industry_name: v.industry_name, - index: v.index, - index_name: v.index_name, - holding_type: v.holding_type, - holding_type_name: v.holding_type_name, - } - } -} - -/// One element of an ETF asset allocation group -#[pyclass(get_all, skip_from_py_object)] -#[derive(Debug, Clone)] -pub(crate) struct AssetAllocationItem { - /// Element name - pub name: String, - /// Security code (holdings only, e.g. `NVDA`) - pub code: String, - /// Position ratio (e.g. `0.0861114`) - pub position_ratio: String, - /// Security symbol (holdings only, e.g. `NVDA.US`) - pub symbol: String, - /// Localized names (locale → name) - pub name_locales: HashMap, - /// Holding detail (holdings only) - pub holding_detail: Option, -} - -impl From for AssetAllocationItem { - fn from(v: longbridge::quote::AssetAllocationItem) -> Self { - Self { - name: v.name, - code: v.code, - position_ratio: v.position_ratio, - symbol: v.symbol, - name_locales: v.name_locales, - holding_detail: v.holding_detail.map(Into::into), - } - } -} - -/// One ETF asset allocation group (grouped by element type) -#[pyclass(get_all, skip_from_py_object)] -#[derive(Debug, Clone)] -pub(crate) struct AssetAllocationGroup { - /// Report date (e.g. `20260601`) - pub report_date: String, - /// Element type of this group - pub asset_type: ElementType, - /// Elements - pub lists: Vec, -} - -impl From for AssetAllocationGroup { - fn from(v: longbridge::quote::AssetAllocationGroup) -> Self { - Self { - report_date: v.report_date, - asset_type: v.asset_type.into(), - lists: v.lists.into_iter().map(Into::into).collect(), - } - } -} - -/// ETF asset allocation response -#[pyclass(get_all, skip_from_py_object)] -#[derive(Debug, Clone)] -pub(crate) struct AssetAllocationResponse { - /// Asset allocation groups - pub info: Vec, -} - -impl From for AssetAllocationResponse { - fn from(v: longbridge::quote::AssetAllocationResponse) -> Self { - Self { - info: v.info.into_iter().map(Into::into).collect(), - } - } -} diff --git a/rust/src/blocking/fundamental.rs b/rust/src/blocking/fundamental.rs index 90b209f48..e2fb48d04 100644 --- a/rust/src/blocking/fundamental.rs +++ b/rust/src/blocking/fundamental.rs @@ -280,4 +280,14 @@ impl FundamentalContextSync { .await }) } + + /// Get ETF asset allocation (holdings / regional / asset class / + /// industry) + pub fn etf_asset_allocation( + &self, + symbol: impl Into + Send + 'static, + ) -> Result { + self.rt + .call(move |ctx| async move { ctx.etf_asset_allocation(symbol).await }) + } } diff --git a/rust/src/blocking/quote.rs b/rust/src/blocking/quote.rs index ba47fe6ab..fbba09273 100644 --- a/rust/src/blocking/quote.rs +++ b/rust/src/blocking/quote.rs @@ -6,8 +6,8 @@ use crate::{ Config, Market, QuoteContext, Result, blocking::runtime::BlockingRuntime, quote::{ - AdjustType, AssetAllocationResponse, CalcIndex, Candlestick, CapitalDistributionResponse, - CapitalFlowLine, FilingItem, FilterWarrantExpiryDate, FilterWarrantInOutBoundsType, + AdjustType, CalcIndex, Candlestick, CapitalDistributionResponse, CapitalFlowLine, + FilingItem, FilterWarrantExpiryDate, FilterWarrantInOutBoundsType, HistoryMarketTemperatureResponse, IntradayLine, IssuerInfo, MarketTemperature, MarketTradingDays, MarketTradingSession, OptionQuote, OptionVolumeDaily, OptionVolumeStats, ParticipantInfo, Period, PinnedMode, PushEvent, QuotePackageDetail, RealtimeQuote, @@ -1213,14 +1213,4 @@ impl QuoteContextSync { self.rt .call(move |ctx| async move { ctx.short_trades(symbol, count).await }) } - - /// Get ETF asset allocation (holdings / regional / asset class / - /// industry) - pub fn etf_asset_allocation( - &self, - symbol: impl Into + Send + 'static, - ) -> Result { - self.rt - .call(move |ctx| async move { ctx.etf_asset_allocation(symbol).await }) - } } diff --git a/rust/src/fundamental/context.rs b/rust/src/fundamental/context.rs index b7c23aff4..a5d9e64b5 100644 --- a/rust/src/fundamental/context.rs +++ b/rust/src/fundamental/context.rs @@ -806,4 +806,27 @@ impl FundamentalContext { .collect(); Ok(ValuationComparisonResponse { list }) } + + // ── etf_asset_allocation ───────────────────────────────────── + + /// Get ETF asset allocation (holdings / regional / asset class / + /// industry). + /// + /// Path: `GET /v1/quote/etf-asset-allocation` + pub async fn etf_asset_allocation( + &self, + symbol: impl Into, + ) -> Result { + #[derive(Serialize)] + struct Query { + counter_id: String, + } + self.get( + "/v1/quote/etf-asset-allocation", + Query { + counter_id: symbol_to_counter_id(&symbol.into()), + }, + ) + .await + } } diff --git a/rust/src/fundamental/types.rs b/rust/src/fundamental/types.rs index bbae2ec51..929f8ce98 100644 --- a/rust/src/fundamental/types.rs +++ b/rust/src/fundamental/types.rs @@ -1,5 +1,8 @@ #![allow(missing_docs)] +use std::collections::HashMap; + +use num_enum::{FromPrimitive, IntoPrimitive}; use rust_decimal::Decimal; use serde::{Deserialize, Serialize}; use strum_macros::{Display, EnumString}; @@ -1454,3 +1457,108 @@ pub enum FinancialReportPeriod { #[serde(rename = "3q")] ThreeQ, } + +// ── etf_asset_allocation ────────────────────────────────────────── + +/// ETF asset allocation element type +#[derive(Debug, FromPrimitive, IntoPrimitive, Copy, Clone, Hash, Eq, PartialEq)] +#[repr(i32)] +pub enum ElementType { + /// Unknown + #[num_enum(default)] + Unknown = 0, + /// Holdings + Holdings = 1, + /// Regional + Regional = 2, + /// Asset class + AssetClass = 3, + /// Industry + Industry = 4, +} + +impl Serialize for ElementType { + fn serialize(&self, serializer: S) -> std::result::Result + where + S: serde::Serializer, + { + serializer.serialize_i32((*self).into()) + } +} + +impl<'de> Deserialize<'de> for ElementType { + fn deserialize(deserializer: D) -> std::result::Result + where + D: serde::Deserializer<'de>, + { + Ok(ElementType::from(i32::deserialize(deserializer)?)) + } +} + +/// Holding detail of an ETF asset allocation element (holdings only) +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct HoldingDetail { + /// Industry ID + #[serde(default)] + pub industry_id: String, + /// Industry name + #[serde(default)] + pub industry_name: String, + /// Index counter ID (e.g. `BK/US/CP99000`) + #[serde(default)] + pub index: String, + /// Index name + #[serde(default)] + pub index_name: String, + /// Holding type (e.g. `E` for stock) + #[serde(default)] + pub holding_type: String, + /// Holding type name + #[serde(default)] + pub holding_type_name: String, +} + +/// One element of an ETF asset allocation group +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct AssetAllocationItem { + /// Element name + pub name: String, + /// Security code (holdings only, e.g. `NVDA`) + #[serde(default)] + pub code: String, + /// Position ratio (e.g. `0.0861114`) + pub position_ratio: String, + /// Security symbol (holdings only, e.g. `NVDA.US`) + #[serde( + rename = "counter_id", + deserialize_with = "deserialize_counter_id_as_symbol", + default + )] + pub symbol: String, + /// Localized names (locale → name, e.g. `zh-CN` → `英伟达`) + #[serde(rename = "name_locales_map", default)] + pub name_locales: HashMap, + /// Holding detail (holdings only) + #[serde(default)] + pub holding_detail: Option, +} + +/// One ETF asset allocation group (grouped by element type) +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct AssetAllocationGroup { + /// Report date (e.g. `20260601`) + pub report_date: String, + /// Element type of this group + pub asset_type: ElementType, + /// Elements + #[serde(default)] + pub lists: Vec, +} + +/// Response for [`crate::FundamentalContext::etf_asset_allocation`] +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct AssetAllocationResponse { + /// Asset allocation groups + #[serde(default)] + pub info: Vec, +} diff --git a/rust/src/quote/context.rs b/rust/src/quote/context.rs index 916a41a7e..168525c95 100644 --- a/rust/src/quote/context.rs +++ b/rust/src/quote/context.rs @@ -14,10 +14,10 @@ use tracing::{Subscriber, dispatcher, instrument::WithSubscriber}; use crate::{ Config, Error, Language, Market, Result, quote::{ - AdjustType, AssetAllocationResponse, CalcIndex, Candlestick, CapitalDistributionResponse, - CapitalFlowLine, FilingItem, HistoryMarketTemperatureResponse, IntradayLine, IssuerInfo, - MarketTemperature, MarketTradingDays, MarketTradingSession, OptionQuote, OptionVolumeDaily, - OptionVolumeStats, ParticipantInfo, Period, PushEvent, QuotePackageDetail, RealtimeQuote, + AdjustType, CalcIndex, Candlestick, CapitalDistributionResponse, CapitalFlowLine, + FilingItem, HistoryMarketTemperatureResponse, IntradayLine, IssuerInfo, MarketTemperature, + MarketTradingDays, MarketTradingSession, OptionQuote, OptionVolumeDaily, OptionVolumeStats, + ParticipantInfo, Period, PushEvent, QuotePackageDetail, RealtimeQuote, RequestCreateWatchlistGroup, RequestUpdateWatchlistGroup, Security, SecurityBrokers, SecurityCalcIndex, SecurityDepth, SecurityListCategory, SecurityQuote, SecurityStaticInfo, ShortPositionsItem, ShortPositionsResponse, ShortTradesItem, ShortTradesResponse, @@ -2198,35 +2198,6 @@ impl QuoteContext { Ok(()) } - - // ── etf_asset_allocation ────────────────────────────────────── - - /// Get ETF asset allocation (holdings / regional / asset class / - /// industry). - /// - /// Path: `GET /v1/quote/etf-asset-allocation` - pub async fn etf_asset_allocation( - &self, - symbol: impl Into, - ) -> Result { - use crate::utils::counter::symbol_to_counter_id; - #[derive(serde::Serialize)] - struct Query { - counter_id: String, - } - let resp = self - .0 - .http_cli - .request(Method::GET, "/v1/quote/etf-asset-allocation") - .query_params(Query { - counter_id: symbol_to_counter_id(&symbol.into()), - }) - .response::>() - .send() - .with_subscriber(self.0.log_subscriber.clone()) - .await?; - Ok(resp.0) - } } fn normalize_symbol(symbol: &str) -> &str { diff --git a/rust/src/quote/mod.rs b/rust/src/quote/mod.rs index 6f91193fc..78dcba0cb 100644 --- a/rust/src/quote/mod.rs +++ b/rust/src/quote/mod.rs @@ -17,9 +17,6 @@ pub use push_types::{ }; pub use sub_flags::SubFlags; pub use types::{ - AssetAllocationGroup, - AssetAllocationItem, - AssetAllocationResponse, Brokers, CalcIndex, Candlestick, @@ -28,13 +25,11 @@ pub use types::{ CapitalFlowLine, Depth, DerivativeType, - ElementType, FilingItem, FilterWarrantExpiryDate, FilterWarrantInOutBoundsType, Granularity, HistoryMarketTemperatureResponse, - HoldingDetail, IntradayLine, IssuerInfo, MarketTemperature, diff --git a/rust/src/quote/types.rs b/rust/src/quote/types.rs index 3407b1a62..08df9b007 100644 --- a/rust/src/quote/types.rs +++ b/rust/src/quote/types.rs @@ -1,5 +1,3 @@ -use std::collections::HashMap; - use longbridge_candlesticks::CandlestickComponents; use longbridge_proto::quote::{self, Period, TradeStatus}; use num_enum::{FromPrimitive, IntoPrimitive, TryFromPrimitive}; @@ -2166,111 +2164,6 @@ pub enum PinnedMode { Remove, } -// ── etf_asset_allocation ────────────────────────────────────────── - -/// ETF asset allocation element type -#[derive(Debug, FromPrimitive, IntoPrimitive, Copy, Clone, Hash, Eq, PartialEq)] -#[repr(i32)] -pub enum ElementType { - /// Unknown - #[num_enum(default)] - Unknown = 0, - /// Holdings - Holdings = 1, - /// Regional - Regional = 2, - /// Asset class - AssetClass = 3, - /// Industry - Industry = 4, -} - -impl Serialize for ElementType { - fn serialize(&self, serializer: S) -> std::result::Result - where - S: serde::Serializer, - { - serializer.serialize_i32((*self).into()) - } -} - -impl<'de> Deserialize<'de> for ElementType { - fn deserialize(deserializer: D) -> std::result::Result - where - D: serde::Deserializer<'de>, - { - Ok(ElementType::from(i32::deserialize(deserializer)?)) - } -} - -/// Holding detail of an ETF asset allocation element (holdings only) -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct HoldingDetail { - /// Industry ID - #[serde(default)] - pub industry_id: String, - /// Industry name - #[serde(default)] - pub industry_name: String, - /// Index counter ID (e.g. `BK/US/CP99000`) - #[serde(default)] - pub index: String, - /// Index name - #[serde(default)] - pub index_name: String, - /// Holding type (e.g. `E` for stock) - #[serde(default)] - pub holding_type: String, - /// Holding type name - #[serde(default)] - pub holding_type_name: String, -} - -/// One element of an ETF asset allocation group -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct AssetAllocationItem { - /// Element name - pub name: String, - /// Security code (holdings only, e.g. `NVDA`) - #[serde(default)] - pub code: String, - /// Position ratio (e.g. `0.0861114`) - pub position_ratio: String, - /// Security symbol (holdings only, e.g. `NVDA.US`) - #[serde( - rename = "counter_id", - deserialize_with = "crate::utils::counter::deserialize_counter_id_as_symbol", - default - )] - pub symbol: String, - /// Localized names (locale → name, e.g. `zh-CN` → `英伟达`) - #[serde(rename = "name_locales_map", default)] - pub name_locales: HashMap, - /// Holding detail (holdings only) - #[serde(default)] - pub holding_detail: Option, -} - -/// One ETF asset allocation group (grouped by element type) -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct AssetAllocationGroup { - /// Report date (e.g. `20260601`) - pub report_date: String, - /// Element type of this group - pub asset_type: ElementType, - /// Elements - #[serde(default)] - pub lists: Vec, -} - -/// Response for [`crate::QuoteContext::etf_asset_allocation`] -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct AssetAllocationResponse { - /// Asset allocation groups - #[serde(default)] - pub info: Vec, -} - #[cfg(test)] mod tests { use serde::Deserialize;