From 7df184e93603309db0fd8fbb752779cf4c7a3b57 Mon Sep 17 00:00:00 2001 From: Jason Lee Date: Thu, 4 Jun 2026 10:55:55 +0800 Subject: [PATCH] feat(quote): add etf_asset_allocation API MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add `etf_asset_allocation(symbol)` to QuoteContext across all SDKs (Rust, Python, Node.js, C, C++), querying `GET /v1/quote/etf-asset-allocation` for ETF asset allocation grouped by element type (Holdings / Regional / AssetClass / Industry). - New types: AssetAllocationResponse / AssetAllocationGroup / AssetAllocationItem / HoldingDetail and ElementType enum - Public param is `symbol`, converted internally via symbol_to_counter_id; counter_id in responses converted back to symbol - name_locales_map exposed as locale → name map (C uses lb_locale_name_t pair array, sorted by locale) Co-Authored-By: Claude Opus 4.8 (1M context) --- CHANGELOG.md | 6 + c/cbindgen.toml | 9 ++ c/csrc/include/longbridge.h | 152 +++++++++++++++++- c/src/quote_context/context.rs | 20 +++ c/src/quote_context/enum_types.rs | 23 +++ c/src/quote_context/types.rs | 231 +++++++++++++++++++++++++++- cpp/include/quote_context.hpp | 5 + cpp/include/types.hpp | 68 ++++++++ cpp/src/convert.hpp | 48 ++++++ cpp/src/quote_context.cpp | 25 +++ nodejs/index.d.ts | 69 ++++++++- nodejs/index.js | 1 + nodejs/src/quote/context.rs | 32 ++-- nodejs/src/quote/types.rs | 121 +++++++++++++++ python/pysrc/longbridge/openapi.pyi | 97 ++++++++++++ python/src/quote/context.rs | 13 ++ python/src/quote/context_async.rs | 15 ++ python/src/quote/mod.rs | 5 + python/src/quote/types.rs | 121 +++++++++++++++ rust/src/blocking/quote.rs | 14 +- rust/src/quote/context.rs | 37 ++++- rust/src/quote/mod.rs | 5 + rust/src/quote/types.rs | 107 +++++++++++++ 23 files changed, 1199 insertions(+), 25 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 661e9fc3ec..796462fac4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,12 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [Unreleased] + +### Added + +- **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 + ## [4.2.2] ### Fixed diff --git a/c/cbindgen.toml b/c/cbindgen.toml index ed12c8f594..229a40d585 100644 --- a/c/cbindgen.toml +++ b/c/cbindgen.toml @@ -304,6 +304,12 @@ cpp_compat = true "COptionVolumeStats" = "lb_option_volume_stats_t" "COptionVolumeDailyStat" = "lb_option_volume_daily_stat_t" "COptionVolumeDaily" = "lb_option_volume_daily_t" +"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" @@ -441,6 +447,9 @@ include = [ "CShortTradesItem", "CShortTradesResponse", "COptionVolumeStats", "COptionVolumeDailyStat", "COptionVolumeDaily", + "CElementType", + "CLocaleName", "CHoldingDetail", + "CAssetAllocationItem", "CAssetAllocationGroup", "CAssetAllocationResponse", # FundamentalContext new types "CShareholderTopResponse", "CShareholderDetailResponse", "CValuationHistoryPoint", "CValuationComparisonItem", "CValuationComparisonResponse", diff --git a/c/csrc/include/longbridge.h b/c/csrc/include/longbridge.h index 81fefdb020..b298458e70 100644 --- a/c/csrc/include/longbridge.h +++ b/c/csrc/include/longbridge.h @@ -1594,6 +1594,32 @@ typedef enum lb_asset_type_t { AssetTypeCrypto, } lb_asset_type_t; +/** + * ETF asset allocation element type + */ +typedef enum lb_element_type_t { + /** + * Unknown + */ + ElementTypeUnknown, + /** + * Holdings + */ + ElementTypeHoldings, + /** + * Regional + */ + ElementTypeRegional, + /** + * Asset class + */ + ElementTypeAssetClass, + /** + * Industry + */ + ElementTypeIndustry, +} lb_element_type_t; + typedef struct lb_alert_context_t lb_alert_context_t; /** @@ -7111,7 +7137,8 @@ typedef struct lb_calendar_events_response_t { */ uintptr_t num_list; /** - * Pagination cursor; pass as start to fetch the next page, empty when there are no more pages. + * Pagination cursor; pass as start to fetch the next page, empty when + * there are no more pages. */ const char *next_date; } lb_calendar_events_response_t; @@ -8318,6 +8345,120 @@ typedef struct lb_option_volume_daily_t { uintptr_t num_stats; } lb_option_volume_daily_t; +/** + * Localized name entry (locale → name) + */ +typedef struct lb_locale_name_t { + /** + * Locale (e.g. `zh-CN`) + */ + const char *locale; + /** + * Localized name + */ + const char *name; +} lb_locale_name_t; + +/** + * Holding detail of an ETF asset allocation element (holdings only) + */ +typedef struct lb_holding_detail_t { + /** + * Industry ID + */ + const char *industry_id; + /** + * Industry name + */ + const char *industry_name; + /** + * Index counter ID (e.g. `BK/US/CP99000`) + */ + const char *index; + /** + * Index name + */ + const char *index_name; + /** + * Holding type (e.g. `E` for stock) + */ + const char *holding_type; + /** + * Holding type name + */ + const char *holding_type_name; +} lb_holding_detail_t; + +/** + * One element of an ETF asset allocation group + */ +typedef struct lb_asset_allocation_item_t { + /** + * Element name + */ + const char *name; + /** + * Security code (holdings only, e.g. `NVDA`) + */ + const char *code; + /** + * Position ratio (e.g. `0.0861114`) + */ + const char *position_ratio; + /** + * Security symbol (holdings only, e.g. `NVDA.US`) + */ + const char *symbol; + /** + * Pointer to array of localized name entries + */ + const struct lb_locale_name_t *name_locales; + /** + * Number of elements in the localized name array + */ + uintptr_t num_name_locales; + /** + * Holding detail (holdings only, maybe null) + */ + const struct lb_holding_detail_t *holding_detail; +} lb_asset_allocation_item_t; + +/** + * One ETF asset allocation group (grouped by element type) + */ +typedef struct lb_asset_allocation_group_t { + /** + * Report date (e.g. `20260601`) + */ + const char *report_date; + /** + * Element type of this group + */ + enum lb_element_type_t asset_type; + /** + * Pointer to array of elements + */ + const struct lb_asset_allocation_item_t *lists; + /** + * Number of elements in the array + */ + uintptr_t num_lists; +} lb_asset_allocation_group_t; + +/** + * ETF asset allocation response + */ +typedef struct lb_asset_allocation_response_t { + /** + * Pointer to array of asset allocation groups + */ + const struct lb_asset_allocation_group_t *info; + /** + * Number of elements in the array + */ + uintptr_t num_info; +} lb_asset_allocation_response_t; + /** * Top-shareholder list response. `data` is a NUL-terminated JSON string. */ @@ -10186,6 +10327,15 @@ 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/quote_context/context.rs b/c/src/quote_context/context.rs index ce9feef9be..24cc31d107 100644 --- a/c/src/quote_context/context.rs +++ b/c/src/quote_context/context.rs @@ -1290,3 +1290,23 @@ 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 9bf17ebaab..e53a2900bd 100644 --- a/c/src/quote_context/enum_types.rs +++ b/c/src/quote_context/enum_types.rs @@ -632,3 +632,26 @@ 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 72cdaf5c80..312aeee155 100644 --- a/c/src/quote_context/types.rs +++ b/c/src/quote_context/types.rs @@ -1,11 +1,12 @@ use std::os::raw::c_char; use longbridge::quote::{ - 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, + 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, QuotePackageDetail, RealtimeQuote, Security, SecurityBoard, SecurityBrokers, SecurityCalcIndex, SecurityDepth, SecurityQuote, SecurityStaticInfo, ShortPositionsItem, ShortPositionsResponse, ShortTradesItem, ShortTradesResponse, StrikePriceInfo, Subscription, Trade, TradeDirection, @@ -15,7 +16,7 @@ use longbridge::quote::{ use crate::{ quote_context::enum_types::{ - CGranularity, COptionDirection, COptionType, CPeriod, CSecuritiesUpdateMode, + CElementType, CGranularity, COptionDirection, COptionType, CPeriod, CSecuritiesUpdateMode, CSecurityBoard, CTradeDirection, CTradeSession, CTradeStatus, CWarrantStatus, CWarrantType, }, types::{CDate, CDecimal, CMarket, COption, CString, CTime, CVec, ToFFI}, @@ -3442,3 +3443,221 @@ 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/quote_context.hpp b/cpp/include/quote_context.hpp index 01644f3321..5f5a98fe70 100644 --- a/cpp/include/quote_context.hpp +++ b/cpp/include/quote_context.hpp @@ -333,6 +333,11 @@ 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 0ba445c9a5..94684d5aed 100644 --- a/cpp/include/types.hpp +++ b/cpp/include/types.hpp @@ -1,6 +1,7 @@ #pragma once #include "decimal.hpp" +#include #include #include @@ -1346,6 +1347,73 @@ 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 { diff --git a/cpp/src/convert.hpp b/cpp/src/convert.hpp index 287bea0cbd..78a5f1beb1 100644 --- a/cpp/src/convert.hpp +++ b/cpp/src/convert.hpp @@ -2335,6 +2335,54 @@ 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) { + switch (ty) { + case ElementTypeUnknown: + return quote::ElementType::Unknown; + case ElementTypeHoldings: + return quote::ElementType::Holdings; + case ElementTypeRegional: + return quote::ElementType::Regional; + case ElementTypeAssetClass: + return quote::ElementType::AssetClass; + case ElementTypeIndustry: + return quote::ElementType::Industry; + default: + throw std::invalid_argument("unreachable"); + } +} +inline quote::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 : "", + d->index_name ? d->index_name : "", + 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) { + std::map name_locales; + for (size_t i = 0; i < item->num_name_locales; ++i) { + const auto& entry = item->name_locales[i]; + name_locales.emplace(entry.locale ? entry.locale : "", entry.name ? entry.name : ""); + } + return { item->name ? item->name : "", + item->code ? item->code : "", + item->position_ratio ? item->position_ratio : "", + item->symbol ? item->symbol : "", + std::move(name_locales), + 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; + 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; + for (size_t i = 0; i < r->num_info; ++i) info.push_back(convert(&r->info[i])); + return { std::move(info) }; +} // ── MarketContext conversions ───────────────────────────────────── diff --git a/cpp/src/quote_context.cpp b/cpp/src/quote_context.cpp index ab5e4f2b5b..2f0d054170 100644 --- a/cpp/src/quote_context.cpp +++ b/cpp/src/quote_context.cpp @@ -1759,5 +1759,30 @@ 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 5de5f15f77..55217b341c 100644 --- a/nodejs/index.d.ts +++ b/nodejs/index.d.ts @@ -2021,6 +2021,11 @@ 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 { @@ -3097,6 +3102,38 @@ export interface AnomalyResponse { changes: Array } +/** One ETF asset allocation group (grouped by element type) */ +export interface AssetAllocationGroup { + /** Report date (e.g. `20260601`) */ + reportDate: string + /** Element type of this group */ + assetType: ElementType + /** Elements */ + lists: Array +} + +/** One element of an ETF asset allocation group */ +export interface AssetAllocationItem { + /** Element name */ + name: string + /** Security code (holdings only, e.g. `NVDA`) */ + code: string + /** Position ratio (e.g. `0.0861114`) */ + positionRatio: string + /** Security symbol (holdings only, e.g. `NVDA.US`) */ + symbol: string + /** Localized names (locale → name) */ + nameLocales: Record + /** Holding detail (holdings only) */ + holdingDetail?: HoldingDetail +} + +/** ETF asset allocation response */ +export interface AssetAllocationResponse { + /** Asset allocation groups */ + info: Array +} + export declare const enum AssetType { /** Unknown */ Unknown = 0, @@ -3397,8 +3434,6 @@ export interface CalendarEventsResponse { date: string /** Per-day event groups */ list: Array - /** Pagination cursor; pass as start to fetch the next page, empty when there are no more pages */ - nextDate: string } export declare const enum CashFlowDirection { @@ -3844,6 +3879,20 @@ export interface DividendList { list: Array } +/** ETF asset allocation element type */ +export declare const enum ElementType { + /** Unknown */ + Unknown = 0, + /** Holdings */ + Holdings = 1, + /** Regional */ + Regional = 2, + /** Asset class */ + AssetClass = 3, + /** Industry */ + Industry = 4 +} + /** Options for get cash flow request */ export interface EstimateMaxPurchaseQuantityOptions { symbol: string @@ -4174,6 +4223,22 @@ export declare const enum Granularity { Monthly = 3 } +/** Holding detail of an ETF asset allocation element (holdings only) */ +export interface HoldingDetail { + /** Industry ID */ + industryId: string + /** Industry name */ + industryName: string + /** Index counter ID (e.g. `BK/US/CP99000`) */ + index: string + /** Index name */ + indexName: string + /** Holding type (e.g. `E` for stock) */ + holdingType: string + /** Holding type name */ + holdingTypeName: string +} + /** Index constituents response */ export interface IndexConstituents { /** Number of constituent stocks that fell today */ diff --git a/nodejs/index.js b/nodejs/index.js index e13776cb46..a8cb5ec7ac 100644 --- a/nodejs/index.js +++ b/nodejs/index.js @@ -680,6 +680,7 @@ module.exports.DCAFrequency = nativeBinding.DCAFrequency module.exports.DCAStatus = nativeBinding.DCAStatus module.exports.DeductionStatus = nativeBinding.DeductionStatus module.exports.DerivativeType = nativeBinding.DerivativeType +module.exports.ElementType = nativeBinding.ElementType module.exports.FilterWarrantExpiryDate = nativeBinding.FilterWarrantExpiryDate module.exports.FilterWarrantInOutBoundsType = nativeBinding.FilterWarrantInOutBoundsType module.exports.FinancialReportKind = nativeBinding.FinancialReportKind diff --git a/nodejs/src/quote/context.rs b/nodejs/src/quote/context.rs index 4ee04a1e25..7ede12509d 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, 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, 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, }, }, time::{NaiveDate, NaiveDatetime}, @@ -1290,4 +1290,16 @@ 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 acd0cfdd46..86379fa621 100644 --- a/nodejs/src/quote/types.rs +++ b/nodejs/src/quote/types.rs @@ -1,3 +1,5 @@ +use std::collections::HashMap; + use chrono::{DateTime, Utc}; use longbridge::quote::SubFlags; use longbridge_nodejs_macros::{JsEnum, JsObject}; @@ -1663,3 +1665,122 @@ 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 5641e2bf9e..3590f5d30e 100644 --- a/python/pysrc/longbridge/openapi.pyi +++ b/python/pysrc/longbridge/openapi.pyi @@ -4020,6 +4020,17 @@ 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. @@ -5372,6 +5383,21 @@ 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 @@ -11937,3 +11963,74 @@ 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/quote/context.rs b/python/src/quote/context.rs index a2d6f9e1e1..2357ebe785 100644 --- a/python/src/quote/context.rs +++ b/python/src/quote/context.rs @@ -687,4 +687,17 @@ 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 cd5ca44c92..f3637e2823 100644 --- a/python/src/quote/context_async.rs +++ b/python/src/quote/context_async.rs @@ -895,4 +895,19 @@ 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 749f0deb07..d694588415 100644 --- a/python/src/quote/mod.rs +++ b/python/src/quote/mod.rs @@ -71,6 +71,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::()?; diff --git a/python/src/quote/types.rs b/python/src/quote/types.rs index d9849dac0d..aeb7a5339e 100644 --- a/python/src/quote/types.rs +++ b/python/src/quote/types.rs @@ -1,3 +1,5 @@ +use std::collections::HashMap; + use longbridge::quote::SubFlags; use longbridge_python_macros::{PyEnum, PyObject}; use pyo3::prelude::*; @@ -1621,3 +1623,122 @@ 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/quote.rs b/rust/src/blocking/quote.rs index fbba092739..ba47fe6ab9 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, CalcIndex, Candlestick, CapitalDistributionResponse, CapitalFlowLine, - FilingItem, FilterWarrantExpiryDate, FilterWarrantInOutBoundsType, + AdjustType, AssetAllocationResponse, CalcIndex, Candlestick, CapitalDistributionResponse, + CapitalFlowLine, FilingItem, FilterWarrantExpiryDate, FilterWarrantInOutBoundsType, HistoryMarketTemperatureResponse, IntradayLine, IssuerInfo, MarketTemperature, MarketTradingDays, MarketTradingSession, OptionQuote, OptionVolumeDaily, OptionVolumeStats, ParticipantInfo, Period, PinnedMode, PushEvent, QuotePackageDetail, RealtimeQuote, @@ -1213,4 +1213,14 @@ 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/quote/context.rs b/rust/src/quote/context.rs index 168525c957..916a41a7e6 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, CalcIndex, Candlestick, CapitalDistributionResponse, CapitalFlowLine, - FilingItem, HistoryMarketTemperatureResponse, IntradayLine, IssuerInfo, MarketTemperature, - MarketTradingDays, MarketTradingSession, OptionQuote, OptionVolumeDaily, OptionVolumeStats, - ParticipantInfo, Period, PushEvent, QuotePackageDetail, RealtimeQuote, + AdjustType, AssetAllocationResponse, 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,6 +2198,35 @@ 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 78dcba0cba..6f91193fce 100644 --- a/rust/src/quote/mod.rs +++ b/rust/src/quote/mod.rs @@ -17,6 +17,9 @@ pub use push_types::{ }; pub use sub_flags::SubFlags; pub use types::{ + AssetAllocationGroup, + AssetAllocationItem, + AssetAllocationResponse, Brokers, CalcIndex, Candlestick, @@ -25,11 +28,13 @@ 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 08df9b0074..3407b1a62c 100644 --- a/rust/src/quote/types.rs +++ b/rust/src/quote/types.rs @@ -1,3 +1,5 @@ +use std::collections::HashMap; + use longbridge_candlesticks::CandlestickComponents; use longbridge_proto::quote::{self, Period, TradeStatus}; use num_enum::{FromPrimitive, IntoPrimitive, TryFromPrimitive}; @@ -2164,6 +2166,111 @@ 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;