Skip to main content

futu_core/
qot_stock_key.rs

1//! `QotStockKey` / `QotSecurityKey` —— QOT 行情订阅 + cache + push broker-aware key.
2//!
3//! ## 背景 (v1.4.110 codex QOT C++ alignment Slice 2)
4//!
5//! C++ `NNProtoCenter/NNProtoCenter_Define_StockKey.h` 定义 `StockKey` 为:
6//!
7//! ```cpp
8//! StockKey(stockID)              // 不区分 broker (m_hasBroker=false)
9//! StockKey(stockID, brokerID)    // 仅 brokerID != NN_BrokerID_Unknown 时 m_hasBroker=true
10//! ```
11//!
12//! `NN_BrokerID_Unknown = 0` 不是一个独立 broker key, 等价 "no broker".
13//! Equality 比较: `nStockID + m_hasBroker + (m_hasBroker ? brokerID : ignored)`.
14//!
15//! QOT 全模块 (subscription, cache, push registry, quota, GetSubInfo response)
16//! 都围绕 `StockKey` 作 first-class identity. Rust 之前用 public string
17//! `"market_code"` 把不同 broker 合并 → crypto multi-broker 行为系统性偏差.
18//!
19//! ## 设计要点
20//!
21//! - **`broker_id: Option<NonZeroU32>`**: 用 `NonZeroU32` 类型层 enforce
22//!   "Some(0) 不可能存在", 严格对齐 C++ `m_hasBroker` 语义 (codex 调研 12:18 增量).
23//! - **`QotSecurityKey`** 复合: `public_sec_key` ("market_code", 给 FTAPI
24//!   `Security` 字段回显) + `stock_key` (cache/subscription 内部识别).
25//! - **Display**: broker-aware 编码用 `"market_code@b1007"` (仅内部使用,
26//!   不能让 public `Security.code` 泄漏 `@b1007` suffix).
27//!
28//! ## Hardcoded / Assumption Ledger
29//!
30//! - `NonZeroU32` 用作 `broker_id` enforcement, 不允许 `Some(0)` 出现 —
31//!   C++ `NN_BrokerID_Unknown = 0` 永远走 no-broker 路径.
32//! - `display` 后缀 `@b{N}` 是 Rust 内部约定 (codex 调研 17:22 推荐), C++ 无
33//!   对应 string 格式 (它用 typed `StockKey` 比较), 仅内部 cache key encoding.
34
35use std::borrow::Cow;
36use std::fmt;
37use std::num::NonZeroU32;
38
39/// QOT broker-aware stock 唯一识别.
40///
41/// 对齐 C++ `NNProtoCenter_Define_StockKey.h::StockKey`:
42/// - `stock_id` = C++ `nStockID`
43/// - `broker_id = Some(N)` ⟺ C++ `m_hasBroker = true && enBrokerID = N`
44/// - `broker_id = None` ⟺ C++ `m_hasBroker = false`
45///
46/// 注意 `broker_id` 不接受 `0` (C++ 语义: 0 = `NN_BrokerID_Unknown` = no-broker).
47#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
48pub struct QotStockKey {
49    pub stock_id: u64,
50    pub broker_id: Option<NonZeroU32>,
51}
52
53impl QotStockKey {
54    /// 不带 broker 的 stock key (C++ `StockKey(stockID)`).
55    pub const fn no_broker(stock_id: u64) -> Self {
56        Self {
57            stock_id,
58            broker_id: None,
59        }
60    }
61
62    /// 带 broker 的 stock key (C++ `StockKey(stockID, brokerID)`).
63    ///
64    /// `broker_id = 0` 不接受 (C++ `NN_BrokerID_Unknown` 走 no-broker path).
65    /// 返 `None` 时, caller 应该改用 `no_broker(stock_id)`.
66    pub fn with_broker(stock_id: u64, broker_id: u32) -> Option<Self> {
67        NonZeroU32::new(broker_id).map(|nz| Self {
68            stock_id,
69            broker_id: Some(nz),
70        })
71    }
72
73    /// 从 u32 broker_id 安全构造 — `0` 自动降级到 no-broker.
74    ///
75    /// 等价 C++ `StockKey(stockID, brokerID == NN_BrokerID_Unknown ? no-broker : with broker)`.
76    pub fn from_broker_id_or_no_broker(stock_id: u64, broker_id: u32) -> Self {
77        match NonZeroU32::new(broker_id) {
78            Some(nz) => Self {
79                stock_id,
80                broker_id: Some(nz),
81            },
82            None => Self::no_broker(stock_id),
83        }
84    }
85
86    /// C++ `HasBroker()` 等价.
87    pub fn has_broker(&self) -> bool {
88        self.broker_id.is_some()
89    }
90
91    /// C++ `GetBrokerID()` 等价 (返 raw u32, no-broker 返 0).
92    pub fn broker_id_or_zero(&self) -> u32 {
93        self.broker_id.map(|nz| nz.get()).unwrap_or(0)
94    }
95}
96
97impl fmt::Display for QotStockKey {
98    /// 内部 cache key 编码: `"{stock_id}"` (no-broker) 或 `"{stock_id}@b{broker_id}"`.
99    ///
100    /// 仅供 cache key / log 使用; **绝不**直接暴露到 public `Security` 字段.
101    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
102        match self.broker_id {
103            Some(nz) => write!(f, "{}@b{}", self.stock_id, nz),
104            None => write!(f, "{}", self.stock_id),
105        }
106    }
107}
108
109/// QOT broker-aware security key (复合): public `"market_code"` + broker-aware `QotStockKey`.
110///
111/// - `public_sec_key`: FTAPI `Security` 字段回显形态 ("market_code"), 不带 broker.
112///   给 handler response / first-push replay / display 用.
113/// - `stock_key`: cache / subscription manager / push registry 内部识别, 带 broker.
114///
115/// 同 stock_id 不同 broker 的 crypto 订阅:
116/// ```ignore
117/// QotSecurityKey { public_sec_key: "91_BTCUSDT", stock_key: QotStockKey { stock_id: 12345, broker_id: Some(1007) } }
118/// QotSecurityKey { public_sec_key: "91_BTCUSDT", stock_key: QotStockKey { stock_id: 12345, broker_id: Some(1008) } }
119/// ```
120/// 两者 public_sec_key 相同 (用户看到同一 symbol), 但 stock_key 不同 (内部隔离).
121#[derive(Debug, Clone, PartialEq, Eq, Hash)]
122pub struct QotSecurityKey {
123    pub public_sec_key: String,
124    pub stock_key: QotStockKey,
125}
126
127/// Borrowed parse result for public FTAPI sec_key envelope `"{market}_{code}"`.
128#[derive(Debug, Clone, Copy, PartialEq, Eq)]
129pub struct QotPublicSecKey<'a> {
130    pub market: i32,
131    pub code: &'a str,
132}
133
134impl QotSecurityKey {
135    /// 普通 no-broker security key.
136    pub fn no_broker(public_sec_key: String, stock_id: u64) -> Self {
137        Self {
138            public_sec_key,
139            stock_key: QotStockKey::no_broker(stock_id),
140        }
141    }
142
143    /// broker-aware security key. `broker_id = 0` 自动降级 no-broker.
144    pub fn from_broker_id(public_sec_key: String, stock_id: u64, broker_id: u32) -> Self {
145        Self {
146            public_sec_key,
147            stock_key: QotStockKey::from_broker_id_or_no_broker(stock_id, broker_id),
148        }
149    }
150
151    /// 内部 cache encoding: `"{market_code}@b{broker_id}"` (broker-aware) 或
152    /// `"{market_code}"` (no-broker).
153    pub fn cache_key(&self) -> String {
154        self.cache_key_cow().into_owned()
155    }
156
157    /// Borrow the public key for ordinary securities and allocate only when a
158    /// broker suffix is required.
159    pub fn cache_key_cow(&self) -> Cow<'_, str> {
160        match self.stock_key.broker_id {
161            Some(nz) => Cow::Owned(format!("{}@b{}", self.public_sec_key, nz)),
162            None => Cow::Borrowed(self.public_sec_key.as_str()),
163        }
164    }
165
166    /// Parse public FTAPI sec_key envelope `"{market}_{code}"`.
167    ///
168    /// Only the first underscore belongs to the envelope; the code suffix may
169    /// contain additional underscores (e.g. `11_BRK_B`).
170    pub fn parse_public_sec_key(sec_key: &str) -> Option<QotPublicSecKey<'_>> {
171        let (market, code) = sec_key.split_once('_')?;
172        if code.is_empty() {
173            return None;
174        }
175        let market = market.parse::<i32>().ok()?;
176        if market <= 0 {
177            return None;
178        }
179        Some(QotPublicSecKey { market, code })
180    }
181
182    /// **v1.4.110 codex Phase 3 Slice 6b**: parse `cache_key()` display string
183    /// back to `(public_sec_key, Option<broker_id>)`.
184    ///
185    /// 返 `(public_sec_key, Some(broker_id))` if 输入含 `@bN` suffix; 否则
186    /// `(input, None)`. 解析失败 (e.g. `@bNot_A_Number`) 返 `None`.
187    ///
188    /// 用于 SubscriptionManager.qot_global_desired_keys() 返 display string 时,
189    /// rebuild 路径要还原 broker dimension 才能 cache lookup.
190    pub fn parse_cache_key(cache_key: &str) -> Option<(String, Option<u32>)> {
191        if let Some(idx) = cache_key.rfind("@b") {
192            let public = &cache_key[..idx];
193            let broker_part = &cache_key[idx + 2..];
194            match broker_part.parse::<u32>() {
195                Ok(b) if b > 0 => Some((public.to_string(), Some(b))),
196                _ => None,
197            }
198        } else {
199            Some((cache_key.to_string(), None))
200        }
201    }
202}
203
204#[cfg(test)]
205mod tests;