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    pub code: String,
65    pub name: String,
66    pub lot_size: i32,
67    pub sec_type: i32,
68    /// Backend `CSStockItem.spread_table_code` (field 13).
69    ///
70    /// C++ `Ndt_Qot_SecInfo::nSpreadCode` feeds
71    /// `INNBiz_Qot_Spread::AdjustPrice` for snapshot `priceSpread` and trade
72    /// price-grid validation. `0` means unavailable in the local cache.
73    pub spread_table_code: u32,
74    /// Backend `CSStockItem.sub_instrument_type_v2` (field 69).
75    ///
76    /// C++ exposes this as `Ndt_Qot_SecInfo::enSubTypeV2`. v10.6
77    /// `GetCompanyProfile` uses it to decide whether Trust securities route to
78    /// F10 CompanyDetail or HK ETF profile.
79    pub sub_instrument_type_v2: u32,
80    pub list_time: String,
81    /// 窝轮所属正股 ID, 0 表示无
82    pub warrnt_stock_owner: u64,
83    /// C++ internal `NN_QotWarrantType` from stock-list `warrnt_type`.
84    ///
85    /// Public `Qot_Common.WarrantType` only exposes 1-5. C++ stock-list also
86    /// carries DLC Long/Short as 6/7, and `GetReference(WARRANT)` explicitly
87    /// filters those out before returning `SecurityStaticInfo`.
88    pub warrant_type: i32,
89    /// 是否已退市
90    pub delisting: bool,
91    /// 交易所类型 (ExchType)
92    pub exch_type: i32,
93    /// 不可搜索标记 (对齐 C++ no_search 字段)
94    pub no_search: bool,
95    /// v1.4.106 codex F5: 期货主连合约关联 stock_id.
96    ///
97    /// 主连合约 (e.g. `HSImain`, `NQmain`) 此字段非 0, 指向真实月份合约
98    /// stock_id (CMD 6747 拉真实 code 用); 普通合约此字段为 0.
99    ///
100    /// 来源: backend stock_list_sync `CSStockItem.origin_id` (proto field 41).
101    /// PlaceOrder F5 用 `info.future_origin_id != 0` 判 NeedPullFutureContractInfo
102    /// (对齐 C++ `Ndt_Qot_SecInfo::nFutureOriginID` + `APIServer_Trd_PlaceOrder
103    /// .cpp:36-42` `NeedPullFutureContractInfo`).
104    ///
105    /// SQLite 历史行无此列时默认 0, 下次 stock_list sync (CMD 6746) 重新写入.
106    pub future_origin_id: u64,
107    /// v1.4.106 codex F5: 期货主力合约 stock_id.
108    ///
109    /// 主连合约持有此字段, 指向当前主力合约 stock_id (流动性最大). PlaceOrder
110    /// F5 在收到 CMD 6747 响应后用此字段反查真实 code 再下单.
111    ///
112    /// 来源: backend `CSStockItem.zhuli_id` (proto field 40). 普通合约 0.
113    pub zhuli_id: u64,
114    /// v1.4.106 codex 1148 F7 (P2): 数据来源 / 完整度标记。详见
115    /// `SecurityInfoSource` doc。**默认 `OnDemandBasic`** 以保守对待历史 caller
116    /// 漏标场景 (能漏标的就是没填全字段, 当 partial 处理最安全)。
117    pub source: SecurityInfoSource,
118    /// v1.4.110 final E.5 P2#6: 交易所字符串 (proto `CSStockItem.exchange` field 57).
119    ///
120    /// 数据驱动 (pitfall #35 C++ 数据驱动 vs Rust 启发式): trade handler
121    /// `derive_exchange_str` 优先用此字段, 缺失时 fallback 到 code prefix /
122    /// pattern match heuristic. 取值例: "SEHK", "NASDAQ", "NYSE", "SSE", "SZSE", "HKEX".
123    pub exchange: String,
124    /// v1.4.110 final E.5 P2#6: 上市交易所 (proto `CSStockItem.listed_exchange` field 74).
125    ///
126    /// 通常等同 `exchange`, dual-listed ADR / 双重主要上市等场景下可能不同.
127    pub listed_exchange: String,
128}
129
130impl CachedSecurityInfo {
131    /// v1.4.89 P2-A: 判断 cache row 是否需要后台 mkt_id refresh.
132    ///
133    /// **场景**: SQLite 历史行 (v1.4.77 Phase D 之前 upsert) 没有 `mkt_id` 字段,
134    /// 读取时默认为 0. 这导致 cache-first 路由走 heuristic fallback (坑 #35).
135    ///
136    /// 返 `true` 即需要 refresh; 如果 `mkt_id == 0 && market != 0`, 是历史行
137    /// (有基本 market 但缺 sub-exchange 精度).
138    ///
139    /// **使用**: `StaticDataCache::mark_stale_mkt_id` 在 get 时 opportunistic
140    /// mark stale, 背景 worker 批量 CMD 20106 refresh.
141    #[must_use]
142    pub fn needs_mkt_id_refresh(&self) -> bool {
143        self.mkt_id == 0 && self.market != 0
144    }
145
146    /// v1.4.106 codex 1148 F7: 是否含完整静态字段 (基于 source). 调用方需要
147    /// `list_time` / `warrnt_stock_owner` / `exch_type` 等字段时, 应先 check
148    /// 此 flag, false 时触发 backend 完整 refresh 或返 partial marker。
149    #[must_use]
150    pub fn is_complete(&self) -> bool {
151        self.source.is_complete()
152    }
153}
154
155/// Crypto 交易币对元数据。
156///
157/// C++ `NNProto_Trd_MaxQtyCrypto.cpp` 通过 `INNBiz_Qot_SecList::SearchSecByID`
158/// 读取 `Ndt_Qot_SecInfo::szCCOrigin/szCCDestination`,并把它们分别写入
159/// backend `GetMaxBuySellReq.coin/currency`。Rust 从 stock-list field 60/61
160/// 接入同一份数据,避免在交易路径按 symbol 字符串拆币种。
161#[derive(Debug, Clone, Default, PartialEq, Eq)]
162pub struct CryptoPairInfo {
163    pub cc_origin: String,
164    pub cc_destination: String,
165}
166
167/// Option contract static metadata from CMD20106 `OptionResultInfo`.
168///
169/// C++ stores these fields on `Ndt_Qot_SecInfo` (`nSecID_OptionOwner`,
170/// `nOptionStrikeTime`, `nOptionStrikePrice`, `enOptionType`) and uses them when
171/// projecting snapshot option extra data. Rust keeps the metadata separate from
172/// `CachedSecurityInfo` because stock-list sync does not carry the full option
173/// tuple; CMD20106 on-demand refresh is the authoritative source.
174#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
175pub struct OptionContractInfo {
176    pub underlying_stock_id: u64,
177    pub strike_date: u64,
178    /// C++ `Ndt_Qot_SecInfo::nOptionLastTradeTime`, sourced from
179    /// `stock_information.OptionResultInfo.real_expiration_time` field 36.
180    ///
181    /// `0` means backend did not return it; callers should fall back to
182    /// `strike_date`, matching C++ `APIServer_Qot_OptionQuote.cpp`.
183    pub real_expiration_time: u64,
184    pub strike_price: u64,
185    pub option_type: u32,
186    /// Backend `stock_information.OptionResultInfo.index_option_type` field 50.
187    ///
188    /// Only HSI/GQI index options use this as a backend query discriminator;
189    /// `0` means absent or not applicable.
190    pub index_option_type: i32,
191}
192
193/// Crypto 交易配置缓存。
194///
195/// C++ `INNData_Trd_CryptoTradeConfig` 以 `(broker_id, symbol, exchange)` 作为
196/// lookup key,MaxBuySellQty 投影用 `minimum_qty` 和 `base_tick_size` 做数量
197/// 步进对齐。这里保存解析后的 nano 整数,避免每个 surface 自己解析 decimal。
198#[derive(Debug, Clone, Default, PartialEq, Eq)]
199pub struct CryptoTradeConfig {
200    pub symbol: String,
201    pub exchange: String,
202    pub pi_only: bool,
203    pub tick_size_nano: i64,
204    pub min_trade_qty_nano: i64,
205    pub max_trade_qty_nano: i64,
206    pub qty_increment_nano: i64,
207}
208
209/// 交易日
210#[derive(Debug, Clone)]
211pub struct CachedTradeDate {
212    pub time: String,
213    pub timestamp: f64,
214}
215
216/// 板块信息
217#[derive(Debug, Clone)]
218pub struct CachedPlateInfo {
219    pub market: i32,
220    pub code: String,
221    pub name: String,
222    pub plate_type: i32,
223}