Skip to main content

futu_cache/static_data/
types.rs

1/// v1.4.106 codex 1148 F7 (P2): cache row "完整度 / 来源" 标记。
2///
3/// 此 enum 区分一个 `CachedSecurityInfo` 是从 stock-list 完整 sync 来的
4/// (字段全), 还是 subscribe on-demand CMD 20106 临时补的 (大量字段为空)。
5/// `GetStaticInfo` / `GetSecuritySnapshot` / `make_static_info` 等需要完整
6/// 静态字段的路径在收到 `OnDemandBasic` 行时**应**触发完整 stock-list /
7/// 20106 completed refresh, 或明确返 partial / unavailable, 不假装完整数据。
8///
9/// 来源 audit: `codex/2026-05-01-1148-v1.4.106-codex-qot-static-data-review.md`
10/// Finding 7。
11#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
12pub enum SecurityInfoSource {
13    /// 来自 backend stock-list full sync (含 list_time / warrnt_stock_owner
14    /// / exch_type / no_search 等所有字段, 是权威数据)。
15    StockListFull,
16    /// 来自 subscribe / GetStaticInfo / GetSecuritySnapshot 的 on-demand CMD 20106
17    /// 临时补行 — 仅含 stock_id / market / mkt_id / code / name / lot_size /
18    /// sec_type, 其余字段缺省 (list_time="", warrnt_stock_owner=0,
19    /// exch_type=0, no_search=false)。**不**应当作权威静态数据源。
20    ///
21    /// **作 Default**: 保守策略 — 历史 caller 漏标 source 时, 当 partial 处理
22    /// (is_complete()=false), 后续 stock_list_sync / Bootstrap 自动覆写为权威
23    /// 来源。这样 cache miss → on-demand fetch → 默认 source = OnDemandBasic
24    /// 不会被误当 StockListFull 权威数据消费。
25    #[default]
26    OnDemandBasic,
27    /// SQLite bootstrap reload (启动时从本地数据库恢复)。字段完整度同
28    /// `StockListFull` (因为 SQLite 是上次 full sync 的快照), 但**生命期更长**
29    /// (跨 daemon 重启)。一般 caller 当 `StockListFull` 处理。
30    Bootstrap,
31}
32
33impl SecurityInfoSource {
34    /// 是否含完整静态字段 (list_time / warrnt_stock_owner / exch_type /
35    /// no_search 等)。`OnDemandBasic` 缺这些, 其他 source 都齐。
36    #[must_use]
37    pub fn is_complete(self) -> bool {
38        matches!(self, Self::StockListFull | Self::Bootstrap)
39    }
40}
41
42/// 股票静态信息
43///
44/// **v1.4.106 codex F5**: 加 `Default` impl 让 caller 可用
45/// `CachedSecurityInfo { stock_id: ..., market: ..., ..Default::default() }`.
46/// 现有显式 callsite 不动, 新加 callsite 可省冗余字段.
47#[derive(Debug, Clone, Default)]
48pub struct CachedSecurityInfo {
49    pub stock_id: u64,
50    /// FTAPI QotMarket 值 (1=HK_Security, 11=US_Security, 21=CNSH, 22=CNSZ 等)
51    ///
52    /// 由 bridge.rs `market_code_to_qot_market()` 从 backend `market_code`
53    /// (NN_QuoteMktID) 映射而来; 用于构造 cache key `"{market}_{code}"`
54    pub market: i32,
55    /// Backend 原始 NN_QuoteMktID (market_code), **保留子交易所精度**
56    ///
57    /// 用于 cache-first 路由决策: 比如 CME Group 子交易所 (60=NYMEX, 70=COMEX,
58    /// 80=CBOT, 90=CME, 100=CBOE) 都被 `market_code_to_qot_market()` 统一压到
59    /// FTAPI QotMarket=11 (US_Security), 但 `mkt_id` 保留原始 range, 使
60    /// `derive_security_type` / `us_futures_sub_exchange` 等能做 O(1) 范围
61    /// 判别, 替代 CLAUDE.md 坑 #35 的 code-pattern 启发式. SQLite 历史行无此
62    /// 字段时读为 0 (默认 Unknown), 下次 upsert 自动修正.
63    pub mkt_id: u32,
64    /// Backend `CSStockItem.currency_code` (field 11), an `NN_QotCurrencyCode` value.
65    ///
66    /// `0` is unavailable and must not be treated as an authoritative currency.
67    pub qot_currency_code: u32,
68    pub code: String,
69    pub name: String,
70    pub lot_size: i32,
71    pub sec_type: i32,
72    /// Backend `CSStockItem.sub_instrument_type` (field 30). Required to
73    /// distinguish stock options from index options in source-pinned trade
74    /// capability tables.
75    pub sub_instrument_type: u32,
76    /// Backend `CSStockItem.spread_table_code` (field 13).
77    ///
78    /// C++ `Ndt_Qot_SecInfo::nSpreadCode` feeds
79    /// `INNBiz_Qot_Spread::AdjustPrice` for snapshot `priceSpread` and trade
80    /// price-grid validation. `0` means unavailable in the local cache.
81    pub spread_table_code: u32,
82    /// Backend `CSStockItem.sub_instrument_type_v2` (field 69).
83    ///
84    /// C++ exposes this as `Ndt_Qot_SecInfo::enSubTypeV2`. v10.6
85    /// `GetCompanyProfile` uses it to decide whether Trust securities route to
86    /// F10 CompanyDetail or HK ETF profile.
87    pub sub_instrument_type_v2: u32,
88    pub list_time: String,
89    /// 窝轮所属正股 ID, 0 表示无
90    pub warrnt_stock_owner: u64,
91    /// C++ internal `NN_QotWarrantType` from stock-list `warrnt_type`.
92    ///
93    /// Public `Qot_Common.WarrantType` only exposes 1-5. C++ stock-list also
94    /// carries DLC Long/Short as 6/7, and `GetReference(WARRANT)` explicitly
95    /// filters those out before returning `SecurityStaticInfo`.
96    pub warrant_type: i32,
97    /// 是否已退市
98    pub delisting: bool,
99    /// 交易所类型 (ExchType)
100    pub exch_type: i32,
101    /// 不可搜索标记 (对齐 C++ no_search 字段)
102    pub no_search: bool,
103    /// v1.4.106 codex F5: 期货主连合约关联 stock_id.
104    ///
105    /// 主连合约 (e.g. `HSImain`, `NQmain`) 此字段非 0, 指向真实月份合约
106    /// stock_id (CMD 6747 拉真实 code 用); 普通合约此字段为 0.
107    ///
108    /// 来源: backend stock_list_sync `CSStockItem.origin_id` (proto field 41).
109    /// PlaceOrder F5 用 `info.future_origin_id != 0` 判 NeedPullFutureContractInfo
110    /// (对齐 C++ `Ndt_Qot_SecInfo::nFutureOriginID` + `APIServer_Trd_PlaceOrder
111    /// .cpp:36-42` `NeedPullFutureContractInfo`).
112    ///
113    /// SQLite 历史行无此列时默认 0, 下次 stock_list sync (CMD 6746) 重新写入.
114    pub future_origin_id: u64,
115    /// Backend `CSStockItem.linkage_future_id` (field 36). RollPosition
116    /// requires source and target monthly contracts to share this exact ID.
117    pub linkage_future_id: u64,
118    /// Backend `CSStockItem.future_strategy_type` (field 75): `100000` normal
119    /// futures and `100001` calendar-spread futures.
120    pub future_strategy_type: u32,
121    /// v1.4.106 codex F5: 期货主力合约 stock_id.
122    ///
123    /// 主连合约持有此字段, 指向当前主力合约 stock_id (流动性最大). PlaceOrder
124    /// F5 在收到 CMD 6747 响应后用此字段反查真实 code 再下单.
125    ///
126    /// 来源: backend `CSStockItem.zhuli_id` (proto field 40). 普通合约 0.
127    pub zhuli_id: u64,
128    /// v1.4.106 codex 1148 F7 (P2): 数据来源 / 完整度标记。详见
129    /// `SecurityInfoSource` doc。**默认 `OnDemandBasic`** 以保守对待历史 caller
130    /// 漏标场景 (能漏标的就是没填全字段, 当 partial 处理最安全)。
131    pub source: SecurityInfoSource,
132    /// v1.4.110 final E.5 P2#6: 交易所字符串 (proto `CSStockItem.exchange` field 57).
133    ///
134    /// 数据驱动 (pitfall #35 C++ 数据驱动 vs Rust 启发式): trade handler
135    /// `derive_exchange_str` 优先用此字段, 缺失时 fallback 到 code prefix /
136    /// pattern match heuristic. 取值例: "SEHK", "NASDAQ", "NYSE", "SSE", "SZSE", "HKEX".
137    pub exchange: String,
138    /// v1.4.110 final E.5 P2#6: 上市交易所 (proto `CSStockItem.listed_exchange` field 74).
139    ///
140    /// 通常等同 `exchange`, dual-listed ADR / 双重主要上市等场景下可能不同.
141    pub listed_exchange: String,
142}
143
144impl CachedSecurityInfo {
145    /// v1.4.89 P2-A: 判断 cache row 是否需要后台 mkt_id refresh.
146    ///
147    /// **场景**: SQLite 历史行 (v1.4.77 Phase D 之前 upsert) 没有 `mkt_id` 字段,
148    /// 读取时默认为 0. 这导致 cache-first 路由走 heuristic fallback (坑 #35).
149    ///
150    /// 返 `true` 即需要 refresh; 如果 `mkt_id == 0 && market != 0`, 是历史行
151    /// (有基本 market 但缺 sub-exchange 精度).
152    ///
153    /// **使用**: `StaticDataCache::mark_stale_mkt_id` 在 get 时 opportunistic
154    /// mark stale, 背景 worker 批量 CMD 20106 refresh.
155    #[must_use]
156    pub fn needs_mkt_id_refresh(&self) -> bool {
157        self.mkt_id == 0 && self.market != 0
158    }
159
160    /// v1.4.106 codex 1148 F7: 是否含完整静态字段 (基于 source). 调用方需要
161    /// `list_time` / `warrnt_stock_owner` / `exch_type` 等字段时, 应先 check
162    /// 此 flag, false 时触发 backend 完整 refresh 或返 partial marker。
163    #[must_use]
164    pub fn is_complete(&self) -> bool {
165        self.source.is_complete()
166    }
167}
168
169/// Crypto 交易币对元数据。
170///
171/// C++ `NNProto_Trd_MaxQtyCrypto.cpp` 通过 `INNBiz_Qot_SecList::SearchSecByID`
172/// 读取 `Ndt_Qot_SecInfo::szCCOrigin/szCCDestination`,并把它们分别写入
173/// backend `GetMaxBuySellReq.coin/currency`。Rust 从 stock-list field 60/61
174/// 接入同一份数据,避免在交易路径按 symbol 字符串拆币种。
175#[derive(Debug, Clone, Default, PartialEq, Eq)]
176pub struct CryptoPairInfo {
177    pub cc_origin: String,
178    pub cc_destination: String,
179}
180
181/// Option contract static metadata from CMD20106 `OptionResultInfo`.
182///
183/// C++ stores these fields on `Ndt_Qot_SecInfo` (`nSecID_OptionOwner`,
184/// `nOptionStrikeTime`, `nOptionStrikePrice`, `enOptionType`) and uses them when
185/// projecting snapshot option extra data. Rust keeps the metadata separate from
186/// `CachedSecurityInfo` because stock-list sync does not carry the full option
187/// tuple; CMD20106 on-demand refresh is the authoritative source.
188#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
189pub struct OptionContractInfo {
190    pub underlying_stock_id: u64,
191    pub strike_date: u64,
192    /// C++ `Ndt_Qot_SecInfo::nOptionLastTradeTime`, sourced from
193    /// `stock_information.OptionResultInfo.real_expiration_time` field 36.
194    ///
195    /// `0` means backend did not return it; callers should fall back to
196    /// `strike_date`, matching C++ `APIServer_Qot_OptionQuote.cpp`.
197    pub real_expiration_time: u64,
198    pub strike_price: u64,
199    pub option_type: u32,
200    /// Backend `stock_information.OptionResultInfo.index_option_type` field 50.
201    ///
202    /// Only HSI/GQI index options use this as a backend query discriminator;
203    /// `0` means absent or not applicable.
204    pub index_option_type: i32,
205    /// C++ `Ndt_Qot_SecInfo::nHpOptionMultiplier`, fixed-point scale 1e9.
206    /// Used by public ticker turnover projection for option contracts.
207    pub hp_multiplier: u64,
208}
209
210/// Crypto 交易配置缓存。
211///
212/// C++ `INNData_Trd_CryptoTradeConfig` 以 `(broker_id, symbol, exchange)` 作为
213/// lookup key,MaxBuySellQty 投影用 `minimum_qty` 和 `base_tick_size` 做数量
214/// 步进对齐。这里保存解析后的 nano 整数,避免每个 surface 自己解析 decimal。
215#[derive(Debug, Clone, Default, PartialEq, Eq)]
216pub struct CryptoTradeConfig {
217    pub symbol: String,
218    pub exchange: String,
219    pub pi_only: bool,
220    pub tick_size_nano: i64,
221    pub min_trade_qty_nano: i64,
222    pub max_trade_qty_nano: i64,
223    pub qty_increment_nano: i64,
224}
225
226/// 交易日
227#[derive(Debug, Clone)]
228pub struct CachedTradeDate {
229    pub time: String,
230    pub timestamp: f64,
231}
232
233/// 板块信息
234#[derive(Debug, Clone)]
235pub struct CachedPlateInfo {
236    pub market: i32,
237    pub code: String,
238    pub name: String,
239    pub plate_type: i32,
240}