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::fmt;
36use std::num::NonZeroU32;
37
38/// QOT broker-aware stock 唯一识别.
39///
40/// 对齐 C++ `NNProtoCenter_Define_StockKey.h::StockKey`:
41/// - `stock_id` = C++ `nStockID`
42/// - `broker_id = Some(N)` ⟺ C++ `m_hasBroker = true && enBrokerID = N`
43/// - `broker_id = None` ⟺ C++ `m_hasBroker = false`
44///
45/// 注意 `broker_id` 不接受 `0` (C++ 语义: 0 = `NN_BrokerID_Unknown` = no-broker).
46#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
47pub struct QotStockKey {
48    pub stock_id: u64,
49    pub broker_id: Option<NonZeroU32>,
50}
51
52impl QotStockKey {
53    /// 不带 broker 的 stock key (C++ `StockKey(stockID)`).
54    pub const fn no_broker(stock_id: u64) -> Self {
55        Self {
56            stock_id,
57            broker_id: None,
58        }
59    }
60
61    /// 带 broker 的 stock key (C++ `StockKey(stockID, brokerID)`).
62    ///
63    /// `broker_id = 0` 不接受 (C++ `NN_BrokerID_Unknown` 走 no-broker path).
64    /// 返 `None` 时, caller 应该改用 `no_broker(stock_id)`.
65    pub fn with_broker(stock_id: u64, broker_id: u32) -> Option<Self> {
66        NonZeroU32::new(broker_id).map(|nz| Self {
67            stock_id,
68            broker_id: Some(nz),
69        })
70    }
71
72    /// 从 u32 broker_id 安全构造 — `0` 自动降级到 no-broker.
73    ///
74    /// 等价 C++ `StockKey(stockID, brokerID == NN_BrokerID_Unknown ? no-broker : with broker)`.
75    pub fn from_broker_id_or_no_broker(stock_id: u64, broker_id: u32) -> Self {
76        match NonZeroU32::new(broker_id) {
77            Some(nz) => Self {
78                stock_id,
79                broker_id: Some(nz),
80            },
81            None => Self::no_broker(stock_id),
82        }
83    }
84
85    /// C++ `HasBroker()` 等价.
86    pub fn has_broker(&self) -> bool {
87        self.broker_id.is_some()
88    }
89
90    /// C++ `GetBrokerID()` 等价 (返 raw u32, no-broker 返 0).
91    pub fn broker_id_or_zero(&self) -> u32 {
92        self.broker_id.map(|nz| nz.get()).unwrap_or(0)
93    }
94}
95
96impl fmt::Display for QotStockKey {
97    /// 内部 cache key 编码: `"{stock_id}"` (no-broker) 或 `"{stock_id}@b{broker_id}"`.
98    ///
99    /// 仅供 cache key / log 使用; **绝不**直接暴露到 public `Security` 字段.
100    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
101        match self.broker_id {
102            Some(nz) => write!(f, "{}@b{}", self.stock_id, nz),
103            None => write!(f, "{}", self.stock_id),
104        }
105    }
106}
107
108/// QOT broker-aware security key (复合): public `"market_code"` + broker-aware `QotStockKey`.
109///
110/// - `public_sec_key`: FTAPI `Security` 字段回显形态 ("market_code"), 不带 broker.
111///   给 handler response / first-push replay / display 用.
112/// - `stock_key`: cache / subscription manager / push registry 内部识别, 带 broker.
113///
114/// 同 stock_id 不同 broker 的 crypto 订阅:
115/// ```ignore
116/// QotSecurityKey { public_sec_key: "91_BTCUSDT", stock_key: QotStockKey { stock_id: 12345, broker_id: Some(1007) } }
117/// QotSecurityKey { public_sec_key: "91_BTCUSDT", stock_key: QotStockKey { stock_id: 12345, broker_id: Some(1008) } }
118/// ```
119/// 两者 public_sec_key 相同 (用户看到同一 symbol), 但 stock_key 不同 (内部隔离).
120#[derive(Debug, Clone, PartialEq, Eq, Hash)]
121pub struct QotSecurityKey {
122    pub public_sec_key: String,
123    pub stock_key: QotStockKey,
124}
125
126impl QotSecurityKey {
127    /// 普通 no-broker security key.
128    pub fn no_broker(public_sec_key: String, stock_id: u64) -> Self {
129        Self {
130            public_sec_key,
131            stock_key: QotStockKey::no_broker(stock_id),
132        }
133    }
134
135    /// broker-aware security key. `broker_id = 0` 自动降级 no-broker.
136    pub fn from_broker_id(public_sec_key: String, stock_id: u64, broker_id: u32) -> Self {
137        Self {
138            public_sec_key,
139            stock_key: QotStockKey::from_broker_id_or_no_broker(stock_id, broker_id),
140        }
141    }
142
143    /// 内部 cache encoding: `"{market_code}@b{broker_id}"` (broker-aware) 或
144    /// `"{market_code}"` (no-broker).
145    pub fn cache_key(&self) -> String {
146        match self.stock_key.broker_id {
147            Some(nz) => format!("{}@b{}", self.public_sec_key, nz),
148            None => self.public_sec_key.clone(),
149        }
150    }
151
152    /// **v1.4.110 codex Phase 3 Slice 6b**: parse `cache_key()` display string
153    /// back to `(public_sec_key, Option<broker_id>)`.
154    ///
155    /// 返 `(public_sec_key, Some(broker_id))` if 输入含 `@bN` suffix; 否则
156    /// `(input, None)`. 解析失败 (e.g. `@bNot_A_Number`) 返 `None`.
157    ///
158    /// 用于 SubscriptionManager.qot_global_desired_keys() 返 display string 时,
159    /// rebuild 路径要还原 broker dimension 才能 cache lookup.
160    pub fn parse_cache_key(cache_key: &str) -> Option<(String, Option<u32>)> {
161        if let Some(idx) = cache_key.rfind("@b") {
162            let public = &cache_key[..idx];
163            let broker_part = &cache_key[idx + 2..];
164            match broker_part.parse::<u32>() {
165                Ok(b) if b > 0 => Some((public.to_string(), Some(b))),
166                _ => None,
167            }
168        } else {
169            Some((cache_key.to_string(), None))
170        }
171    }
172}
173
174#[cfg(test)]
175mod tests;