Skip to main content

futu_backend/stock_list/
market.rs

1/// 行情市场类型 (C++ NN_QuoteMktType + MarketID 分组)
2///
3/// 作 CMD 6822/6823 TCP header reserved[0] 时必须先经
4/// `quote_mkt_to_nn_mkt_type` 转成 C++ `NN_QuoteMktType`。
5///
6/// 其中 `DigitalCcy` 虽然是 v1.4.49 扩展进来的 market-id 分组,但线上
7/// C++ 10.5.6508 已有 `NN_QuoteMktType_CRYPTO = 17`,不能再当成
8/// "只用于过滤、不发 backend" 的本地分组。
9#[repr(u8)]
10#[derive(Debug, Clone, Copy, PartialEq, Eq)]
11#[non_exhaustive]
12pub enum QuoteMktType {
13    Unknown = 0,
14    HK = 1,
15    US = 2,
16    SH = 3,
17    SZ = 4,
18    HKFuture = 5,
19    HKFuture2 = 6,
20    USOption = 7,
21    USFuture = 8,
22    HKOption = 9,
23    SHKC = 10,
24    Forex = 11,
25    SGFuture = 12,
26    JPFuture = 13,
27    // v1.4.49 新增:MarketID 范围分组
28    Bond = 14,          // 130-159 债券
29    GlobalIndex = 15,   // 260-359 全球指数
30    SGSecurity = 16,    // 180-184 新加坡股票
31    StockConnect = 17,  // 50-51 港股通/A股通
32    DigitalCcy = 18,    // 360-459 数字货币
33    TreasuryYield = 19, // 460-559 国债收益率
34    // v1.4.50 新增:仅用于 MarketID 范围过滤,不作 TCP header
35    Fund = 20, // 560-569 基金市场 (C++ NN_QuoteMktID_FUND_*)
36    // v1.4.111: C++ 10.6 GetGlobalState exposes JP/MY stock market state.
37    JPSecurity = 21,    // 830-849 日本东证市场
38    MYSecurity = 22,    // 1350-1399 马来西亚股票
39    JPIndex = 23,       // 800-829 日本日经指数;header wire value 由转换函数映射为 24
40    EventContract = 24, // 1900-1949;header wire value 映射为 35
41}
42
43/// v1.4.68 修正(external reviewer v1.4.57 #5 P2 修):CMD 6822/6823 reserved[0] 实际接受
44/// `NN_QuoteMktType` 值(C++ enum),**不是 `QotCommon.QotMarket` 值**。
45///
46/// C++ 实锤对照 `NNBiz_Qot_EventNotice.cpp::SubEventNotice(enMktType)` L178-188:
47/// CMD 6822 订阅请求 reserved[0] = NN_QuoteMktType 枚举值:
48/// - NN_QuoteMktType_HK = 1
49/// - NN_QuoteMktType_US = 2
50/// - NN_QuoteMktType_SH = 3
51/// - NN_QuoteMktType_SZ = 4
52/// - NN_QuoteMktType_FUT_HK = 5     ← 港期老
53/// - NN_QuoteMktType_FUT_HK_NEW = 6 ← 港期新(主要)
54/// - NN_QuoteMktType_US_OPTIONS = 7
55/// - NN_QuoteMktType_US_FUT = 8     ← 美期
56/// - NN_QuoteMktType_HK_OPTIONS = 9
57/// - NN_QuoteMktType_SH_KCB = 10
58/// - NN_QuoteMktType_Forex = 11
59/// - NN_QuoteMktType_SG_FUTURE = 13 ← 新加坡期货(跳 12)
60/// - NN_QuoteMktType_SG_SECURITY = 15 ← 新加坡股票
61/// - NN_QuoteMktType_JP_FUTURE = 16 ← 日本期货
62/// - NN_QuoteMktType_CRYPTO = 17 ← 加密货币
63/// - NN_QuoteMktType_JP_SECURITY = 25 ← 日本东证市场
64/// - NN_QuoteMktType_MY_SECURITY = 27 ← 马来西亚股票
65///
66/// v1.4.47 P1.2 曾改用 QotMarket 值(HK=1, US=11, ...),以为"QuoteMktType
67/// 不等于 QotMarket 就不对"—— **反了方向**。CMD 6822 backend 接受的是
68/// NN_QuoteMktType。HK/US/SH/SZ 的 QotMarket 值(1/11/21/22)对 backend 来说
69/// 部分 work 是因为:1 ↔ NN_QuoteMktType_HK=1 巧合对齐;其他(US=11 / SH=21
70/// / SZ=22)不在 NN_QuoteMktType 枚举范围内 backend 可能 fallback 全市场
71/// 返回。但 HKFuture / USFuture / SGFuture / JPFuture 都返 0(因为 v1.4.47
72/// 把它们改 reserved[0]=0 导致 backend 不订阅 HK 期货 → external reviewer 报告 #5)。
73///
74/// 本次修法:**按 C++ SubEventNotice 实装**。期货按 FUT_HK_NEW=6 / US_FUT=8
75/// / SG_FUTURE=13 / JP_FUTURE=16 订阅 → backend CMD 6823 响应能返 HK 期货
76/// market_id(5-6 / 110-119)→ pick_market_state 能拿到 HK future state。
77pub(super) fn quote_mkt_to_nn_mkt_type(m: QuoteMktType) -> u8 {
78    match m {
79        QuoteMktType::HK => 1,             // NN_QuoteMktType_HK
80        QuoteMktType::US => 2,             // NN_QuoteMktType_US
81        QuoteMktType::SH => 3,             // NN_QuoteMktType_SH
82        QuoteMktType::SZ => 4,             // NN_QuoteMktType_SZ
83        QuoteMktType::HKFuture => 5,       // NN_QuoteMktType_FUT_HK(老港期)
84        QuoteMktType::HKFuture2 => 6,      // NN_QuoteMktType_FUT_HK_NEW(主要港期)
85        QuoteMktType::USOption => 7,       // NN_QuoteMktType_US_OPTIONS
86        QuoteMktType::USFuture => 8,       // NN_QuoteMktType_US_FUT
87        QuoteMktType::HKOption => 9,       // NN_QuoteMktType_HK_OPTIONS
88        QuoteMktType::SHKC => 10,          // NN_QuoteMktType_SH_KCB
89        QuoteMktType::Forex => 11,         // NN_QuoteMktType_Forex
90        QuoteMktType::SGFuture => 13,      // NN_QuoteMktType_SG_FUTURE(C++ 跳 12)
91        QuoteMktType::SGSecurity => 15,    // NN_QuoteMktType_SG_SECURITY
92        QuoteMktType::JPFuture => 16,      // NN_QuoteMktType_JP_FUTURE(C++ 跳 14-15)
93        QuoteMktType::DigitalCcy => 17,    // C++ NN_QuoteMktType_CRYPTO
94        QuoteMktType::JPIndex => 24,       // NN_QuoteMktType_JP_INDEX
95        QuoteMktType::JPSecurity => 25,    // NN_QuoteMktType_JP_SECURITY
96        QuoteMktType::MYSecurity => 27,    // NN_QuoteMktType_MY_SECURITY
97        QuoteMktType::EventContract => 35, // NN_QuoteMktType_EventContract
98        // v1.4.49 新增 variant 多数不在 NN_QuoteMktType,仅用于 pick_market_state
99        // 按 MarketID 过滤。返 0 → backend 忽略(CMD 6822 订阅 reserved[0]=0 可能
100        // 表示"通用",不影响其他订阅的市场)。
101        QuoteMktType::Bond
102        | QuoteMktType::GlobalIndex
103        | QuoteMktType::StockConnect
104        | QuoteMktType::TreasuryYield
105        | QuoteMktType::Fund
106        | QuoteMktType::Unknown => 0,
107    }
108}
109
110/// 构造行情命令的 header reserved 字段。
111///
112/// v1.4.47 P1.2 修:`reserved[0]` 用 Futu QotMarket 值(不是内部 enum 值)。
113pub fn make_quote_reserved(mkt: QuoteMktType, ex_type: u8) -> [u8; 10] {
114    let mut reserved = [0u8; 10];
115    reserved[0] = quote_mkt_to_nn_mkt_type(mkt);
116    reserved[1] = ex_type;
117    reserved
118}
119
120/// 参考 C++ `market_tradingDay.proto::MarketID` enum.
121pub fn market_id_matches(mkt: QuoteMktType, id: u32) -> bool {
122    match mkt {
123        // v1.4.50: HK 加 1000-1049(HSI Index 扩展,C++ 映射归 HK 市场)
124        QuoteMktType::HK => (1..=4).contains(&id) || id == 9 || (1000..=1049).contains(&id),
125        // C++ `NN_QuoteMktType_From_NN_QuoteMktID` keeps old/new HK futures
126        // distinct: 5 -> FUT_HK, 6 and 110..=119 -> FUT_HK_NEW.
127        QuoteMktType::HKFuture => id == 5,
128        QuoteMktType::HKFuture2 => id == 6 || (110..=119).contains(&id),
129        // v1.4.50: HKOption 加 570-579(HK Index Option 扩展)
130        QuoteMktType::HKOption => (7..=8).contains(&id) || (570..=579).contains(&id),
131        // v1.4.50: US 加 1200-1249(US New VIX,C++ 映射归 US 市场)
132        QuoteMktType::US => (10..=29).contains(&id) || (1200..=1249).contains(&id),
133        QuoteMktType::SH | QuoteMktType::SZ | QuoteMktType::SHKC => (30..=40).contains(&id),
134        QuoteMktType::USOption => (41..=49).contains(&id),
135        // Ref: latest C++ `NNBase_Define_Inline.h:330-336`: commodity futures
136        // and CME single-stock futures share exact head US_FUT(8).
137        QuoteMktType::USFuture => (60..=109).contains(&id) || (2_450..=2_499).contains(&id),
138        QuoteMktType::SGFuture => (160..=179).contains(&id),
139        QuoteMktType::JPFuture => (185..=194).contains(&id),
140        QuoteMktType::Forex => (120..=123).contains(&id),
141        // v1.4.49 新增 6 个 variant(对齐 C++ market_tradingDay.proto MarketID)
142        QuoteMktType::StockConnect => (50..=51).contains(&id),
143        QuoteMktType::Bond => (130..=159).contains(&id),
144        QuoteMktType::SGSecurity => (180..=184).contains(&id),
145        QuoteMktType::GlobalIndex => (260..=359).contains(&id),
146        QuoteMktType::DigitalCcy => (360..=459).contains(&id),
147        QuoteMktType::TreasuryYield => (460..=559).contains(&id),
148        // v1.4.50 新增
149        QuoteMktType::Fund => (560..=569).contains(&id),
150        QuoteMktType::JPSecurity => (830..=849).contains(&id),
151        QuoteMktType::JPIndex => (800..=829).contains(&id),
152        QuoteMktType::MYSecurity => (1350..=1399).contains(&id),
153        QuoteMktType::EventContract => (1900..=1949).contains(&id),
154        QuoteMktType::Unknown => false,
155    }
156}
157
158/// Convert backend `NN_QuoteMktID` into the C/S header `NN_QuoteMktType` group.
159///
160/// This is the Rust equivalent of C++
161/// `NNBase_Define_Inline.h:271-369 NN_QuoteMktType_From_NN_QuoteMktID`.
162/// It intentionally returns `None` for local filtering-only groups that C++
163/// maps to `UNKNOWN` (for example Bond/Fund/GlobalIndex).
164pub fn quote_market_from_market_id(market_id: u32) -> Option<QuoteMktType> {
165    match market_id {
166        1..=4 | 9 | 1_000..=1_049 => Some(QuoteMktType::HK),
167        5 => Some(QuoteMktType::HKFuture),
168        6 | 110..=119 => Some(QuoteMktType::HKFuture2),
169        7 | 8 | 570 => Some(QuoteMktType::HKOption),
170        10..=29 | 1_200..=1_249 => Some(QuoteMktType::US),
171        30 | 33 | 34 | 36..=40 => Some(QuoteMktType::SH),
172        31 | 35 => Some(QuoteMktType::SZ),
173        32 => Some(QuoteMktType::SHKC),
174        41..=49 => Some(QuoteMktType::USOption),
175        60..=109 | 2_450..=2_499 => Some(QuoteMktType::USFuture),
176        120..=123 => Some(QuoteMktType::Forex),
177        160..=179 => Some(QuoteMktType::SGFuture),
178        180..=184 => Some(QuoteMktType::SGSecurity),
179        185..=194 => Some(QuoteMktType::JPFuture),
180        360..=459 => Some(QuoteMktType::DigitalCcy),
181        800..=829 => Some(QuoteMktType::JPIndex),
182        830..=849 => Some(QuoteMktType::JPSecurity),
183        1_350..=1_399 => Some(QuoteMktType::MYSecurity),
184        1_900..=1_949 => Some(QuoteMktType::EventContract),
185        _ => None,
186    }
187}
188
189/// Convert one server `NN_QuoteMktID` into its canonical nonzero
190/// `NN_QuoteMktType` wire value.
191///
192/// Ref: C++ `NNBase_Define_Inline.h:271-369`
193/// `NN_QuoteMktType_From_NN_QuoteMktID`. Unsupported/filter-only market ids
194/// remain `None`; callers must not manufacture a market-zero backend request.
195pub fn quote_market_type_from_market_id(market_id: u32) -> Option<u8> {
196    quote_market_from_market_id(market_id)
197        .map(quote_mkt_to_nn_mkt_type)
198        .filter(|value| *value != 0)
199}
200
201/// v1.4.73 B1 D5 升级:按"描述市场实际情况"优先级对 `QotMarketState` 打分。
202pub(super) fn market_state_priority(status: u32) -> u8 {
203    match status {
204        0 => 0,
205        // 可交易 / 开盘状态
206        3 | 5 | 13 | 15 | 23 => 1,
207        // 不可交易 / 等待 / 休市 / 精确 pre/after-market 状态
208        1 | 2 | 4 | 6 | 8..=11 | 14 | 16..=19 | 12 => 2,
209        // 未知新增 variant → 保守 fallback
210        _ => 1,
211    }
212}
213
214/// FTAPI QotMarket → 后端 QuoteMktType
215pub fn qot_market_to_backend(qot_market: i32) -> Option<QuoteMktType> {
216    match qot_market {
217        1 => Some(QuoteMktType::HK),
218        2 => Some(QuoteMktType::HKFuture2),
219        11 => Some(QuoteMktType::US),
220        21 => Some(QuoteMktType::SH),
221        22 => Some(QuoteMktType::SZ),
222        31 => Some(QuoteMktType::SGSecurity),
223        41 => Some(QuoteMktType::JPSecurity),
224        61 => Some(QuoteMktType::MYSecurity),
225        91 => Some(QuoteMktType::DigitalCcy),
226        101 => Some(QuoteMktType::EventContract),
227        _ => None,
228    }
229}
230
231/// v1.4.47 P1.2 修(external reviewer 验收报告 §3 P2 / §14.3 根因):
232/// FTAPI **TrdMarket** → 后端 `QuoteMktType`。
233pub fn trd_market_to_backend(trd_market: i32) -> Option<QuoteMktType> {
234    match trd_market {
235        1 | 4 => Some(QuoteMktType::HK), // HK / HKCC → HK_Security
236        2 => Some(QuoteMktType::US),     // US stock
237        3 => Some(QuoteMktType::SH),     // CN A 股(默认 SH;SZ 由 code 前缀区分,
238        // 但 market-state 查询粒度到不了 symbol 层,先用 SH)
239        5 => Some(QuoteMktType::HKFuture), // Futures
240        6 | 124 => Some(QuoteMktType::SGSecurity),
241        7 => Some(QuoteMktType::DigitalCcy),
242        15 | 126 => Some(QuoteMktType::JPSecurity),
243        111 | 125 => Some(QuoteMktType::MYSecurity),
244        // AU=8 / CA=112: C++ NN_QuoteMktType 当前没有 AU/CA 股票枚举,保持 None。
245        _ => None,
246    }
247}