Skip to main content

futu_cache/qot_right/
security_rules.rs

1use crate::static_data::CachedSecurityInfo;
2
3use super::{
4    LV2_ORDER_US_FUTURE, Lv2OrderSubDescriptor, QOT_MARKET_CC_SECURITY, QOT_RIGHT_BMP,
5    QOT_RIGHT_LEVEL1, QOT_RIGHT_LEVEL2, QOT_RIGHT_LEVEL3, QOT_RIGHT_NO, QOT_RIGHT_SF,
6    QOT_RIGHT_UNKNOWN, QotRightData, SECURITY_TYPE_CRYPTO, SECURITY_TYPE_DRVT,
7    SECURITY_TYPE_FUTURE, SECURITY_TYPE_INDEX, SG_LV2_ORDER_STOCK, SG_LV2_ORDER_STOCK_ODD_LOT,
8    US_LV2_ORDER_ARCA, US_LV2_ORDER_NASDAQ_TV, US_LV2_ORDER_OVERNIGHT,
9};
10
11fn is_hk_future_market(info: &CachedSecurityInfo) -> bool {
12    info.sec_type == SECURITY_TYPE_FUTURE && matches!(info.mkt_id, 5 | 6 | 110..=119)
13}
14
15fn is_us_future_market(info: &CachedSecurityInfo) -> bool {
16    info.sec_type == SECURITY_TYPE_FUTURE && (60..=109).contains(&info.mkt_id)
17}
18
19fn is_sg_future_market(info: &CachedSecurityInfo) -> bool {
20    info.sec_type == SECURITY_TYPE_FUTURE && (160..=179).contains(&info.mkt_id)
21}
22
23fn is_sg_security_market(info: &CachedSecurityInfo) -> bool {
24    (180..=184).contains(&info.mkt_id)
25        || (info.market == 31
26            && info.sec_type != SECURITY_TYPE_FUTURE
27            && info.sec_type != SECURITY_TYPE_DRVT)
28}
29
30fn is_jp_future_market(info: &CachedSecurityInfo) -> bool {
31    info.sec_type == SECURITY_TYPE_FUTURE && (185..=194).contains(&info.mkt_id)
32}
33
34fn is_jp_security_market(info: &CachedSecurityInfo) -> bool {
35    (830..=849).contains(&info.mkt_id)
36        || (info.market == 41
37            && info.sec_type != SECURITY_TYPE_FUTURE
38            && info.sec_type != SECURITY_TYPE_DRVT
39            && !is_jp_future_market(info)
40            && !(800..=829).contains(&info.mkt_id))
41}
42
43fn is_my_security_market(info: &CachedSecurityInfo) -> bool {
44    (1350..=1399).contains(&info.mkt_id)
45        || (info.market == 61
46            && info.sec_type != SECURITY_TYPE_FUTURE
47            && info.sec_type != SECURITY_TYPE_DRVT)
48}
49
50fn is_hk_option_market(info: &CachedSecurityInfo) -> bool {
51    info.sec_type == SECURITY_TYPE_DRVT && matches!(info.mkt_id, 7 | 8 | 570..=579)
52}
53
54fn is_us_option_market(info: &CachedSecurityInfo) -> bool {
55    info.sec_type == SECURITY_TYPE_DRVT && (41..=49).contains(&info.mkt_id)
56}
57
58fn is_hk_security_market(info: &CachedSecurityInfo) -> bool {
59    matches!(info.mkt_id, 1..=4 | 1000..=1049)
60        || (info.market == 1
61            && !is_hk_future_market(info)
62            && !is_hk_option_market(info)
63            && info.sec_type != SECURITY_TYPE_FUTURE
64            && info.sec_type != SECURITY_TYPE_DRVT)
65}
66
67/// C++ `APIServer_Qot_StockSnapshot.cpp:18-43` (`IsHKBMP_OneStock`).
68///
69/// When this returns true, GetSecuritySnapshot must omit bid/ask price and
70/// volume fields instead of returning delayed BMP values.
71pub fn snapshot_masks_hk_bmp_bid_ask(info: &CachedSecurityInfo, qr: &QotRightData) -> bool {
72    let is_hk = matches!(info.market, 1 | 2);
73    if !is_hk {
74        return false;
75    }
76
77    if is_hk_option_market(info) {
78        return qr.hk_option_qot_right == QOT_RIGHT_BMP;
79    }
80    if is_hk_future_market(info) {
81        return qr.hk_future_qot_right == QOT_RIGHT_BMP;
82    }
83    is_hk_security_market(info) && qr.hk_qot_right == QOT_RIGHT_BMP
84}
85
86fn is_us_security_market(info: &CachedSecurityInfo) -> bool {
87    matches!(info.mkt_id, 10..=29 | 1200..=1249)
88        || (info.market == 11
89            && !is_us_future_market(info)
90            && !is_us_option_market(info)
91            && info.sec_type != SECURITY_TYPE_FUTURE
92            && info.sec_type != SECURITY_TYPE_DRVT)
93}
94
95/// v1.4.110 codex Phase 4 Slice 7: 把 crypto detection helper 暴露给 push_parser
96/// / handler (crypto LV2 sub-system 入口).
97///
98/// 对齐 C++ `IsCrypto(stockID)` (NNBiz_Qot_SecList.cpp). 三个条件任一: sec_type==Crypto
99/// / FTAPI QotMarket=91 (CC_Security) / mkt_id ∈ [360,459] (DigitalCcy range).
100pub fn is_crypto_market(info: &CachedSecurityInfo) -> bool {
101    info.sec_type == SECURITY_TYPE_CRYPTO
102        || info.market == QOT_MARKET_CC_SECURITY
103        || (360..=459).contains(&info.mkt_id)
104}
105
106/// C++ `SubBitUtil::GetPushTypeSvrSubBit(NN_PushQot_Type_Most)` 对 US /
107/// US options / US futures 保留 `SBIT_US_PREMARKET_AFTERHOURS_DETAIL`, 让
108/// BasicQot 可以随订阅收到 preMarket / afterMarket / overnight。
109///
110/// Ref:
111/// - `FutuOpenD/Src/NNProtoCenter/Quote/SubBitUtil.cpp:7-17`
112/// - `FutuOpenD/Src/NNProtoCenter/Quote/SubBitUtil.cpp:91-95`
113pub fn basic_qot_uses_us_pre_after_detail(info: &CachedSecurityInfo) -> bool {
114    matches!(info.mkt_id, 10..=29 | 41..=45 | 60..=109 | 1200..=1249) || info.market == 11
115}
116
117fn is_sh_market(info: &CachedSecurityInfo) -> bool {
118    matches!(info.mkt_id, 30 | 32 | 33 | 34 | 36..=40) || info.market == 21
119}
120
121fn is_sz_market(info: &CachedSecurityInfo) -> bool {
122    matches!(info.mkt_id, 31 | 35) || info.market == 22
123}
124
125fn us_future_right_for_market(qr: &QotRightData, mkt_id: u32) -> i32 {
126    match mkt_id {
127        60..=69 => qr.us_nymex_future_qot_right,
128        70..=79 => qr.us_comex_future_qot_right,
129        80..=89 => qr.us_cbot_future_qot_right,
130        90..=99 => qr.us_cme_future_qot_right,
131        100..=109 => qr.us_cboe_future_qot_right,
132        _ => QOT_RIGHT_UNKNOWN,
133    }
134}
135
136pub fn merged_lv2_order_subs_for_security(
137    info: &CachedSecurityInfo,
138    qr: &QotRightData,
139    _extended_time: bool,
140) -> Vec<Lv2OrderSubDescriptor> {
141    if is_us_future_market(info) {
142        if us_future_right_for_market(qr, info.mkt_id) == QOT_RIGHT_LEVEL2 {
143            return vec![Lv2OrderSubDescriptor {
144                lv2_type: LV2_ORDER_US_FUTURE,
145                level: 60,
146                prob2_v2: true,
147            }];
148        }
149        return vec![];
150    }
151
152    if is_sg_security_market(info) {
153        if qr.sg_stock_qot_right == QOT_RIGHT_LEVEL2 {
154            // Ref: FutuOpenD/Src/NNProtoCenter/Quote/MktQotSubInstance.cpp:397-413.
155            // SG Level2 orderbook subscribes both normal and odd-lot depth at
156            // level 40 through prob2_v2 before the base SBIT_ORDER_BOOK prob.
157            return vec![
158                Lv2OrderSubDescriptor {
159                    lv2_type: SG_LV2_ORDER_STOCK,
160                    level: 40,
161                    prob2_v2: true,
162                },
163                Lv2OrderSubDescriptor {
164                    lv2_type: SG_LV2_ORDER_STOCK_ODD_LOT,
165                    level: 40,
166                    prob2_v2: true,
167                },
168            ];
169        }
170        return vec![];
171    }
172
173    if !is_us_security_market(info) || info.sec_type == SECURITY_TYPE_INDEX {
174        return vec![];
175    }
176    if !matches!(qr.us_qot_right, QOT_RIGHT_LEVEL1 | QOT_RIGHT_LEVEL2) {
177        return vec![];
178    }
179
180    let mut subs = Vec::new();
181    // Ref: FutuOpenD/Src/NNProtoCenter/Quote/MktQotSubInstance.cpp:118-137.
182    // C++ adds E_US_LV2_ORDER_OVERNIGHT for US stock OrderBook whenever any
183    // US LV2 orderbook right exists; it does not gate this on Qot_Sub.extendedTime.
184    if qr.us_lv2_arca_qot_right || qr.us_lv2_nyse_qot_right || qr.us_lv2_nasdaq_totalview_qot_right
185    {
186        subs.push(Lv2OrderSubDescriptor {
187            lv2_type: US_LV2_ORDER_OVERNIGHT,
188            level: 60,
189            prob2_v2: true,
190        });
191    }
192    if qr.us_lv2_nasdaq_totalview_qot_right {
193        subs.push(Lv2OrderSubDescriptor {
194            lv2_type: US_LV2_ORDER_NASDAQ_TV,
195            level: 60,
196            prob2_v2: false,
197        });
198    }
199    if qr.us_lv2_arca_qot_right {
200        subs.push(Lv2OrderSubDescriptor {
201            lv2_type: US_LV2_ORDER_ARCA,
202            level: 60,
203            prob2_v2: false,
204        });
205    }
206    subs
207}
208
209pub fn has_merged_lv2_order_subs_for_security(
210    info: &CachedSecurityInfo,
211    qr: &QotRightData,
212) -> bool {
213    !merged_lv2_order_subs_for_security(info, qr, true).is_empty()
214}
215
216/// C++ `GetOrderBookMaxNum` 对齐版:根据证券静态市场 + 行情权限决定
217/// orderbook push/cache 应保留的档位数。
218///
219/// Ref:
220/// - `FutuOpenD/Src/APIServer/APIServer_Inner_API.cpp:4331-4412`
221/// - `FutuOpenD/Src/APIServer/Business/Quote/QotRealTimeData.cpp:1211-1215`
222///
223/// Hardcoded / Assumption Ledger:
224/// - `mkt_id` 范围复用 backend stock-list 的 NN_QuoteMktID 区间。C++ 10.7
225///   已把 `1400..=1449` 命名为 MY futures,但尚未给出 public orderbook
226///   right/depth 映射,因此这里不再作为 HK futures 兜底。
227/// - HK option/future depth 缺省为 0,和 C++ `NNData_Qot_Right` 构造默认值一致。
228/// - 普通 HK/US/CN 股票在 Unknown 权限下保留 C++ 初始化默认值/分支语义,
229///   避免 push parser 在权限刷新窗口内把有效 backend push 误清空。
230pub fn order_book_max_depth_for_security(info: &CachedSecurityInfo, qr: &QotRightData) -> usize {
231    if is_crypto_market(info) {
232        return if qr.cc_qot_right == QOT_RIGHT_LEVEL1 {
233            40
234        } else {
235            0
236        };
237    }
238
239    if is_us_option_market(info) {
240        return if qr.us_option_qot_right == QOT_RIGHT_BMP {
241            0
242        } else {
243            1
244        };
245    }
246
247    if is_us_future_market(info) {
248        return match us_future_right_for_market(qr, info.mkt_id) {
249            QOT_RIGHT_LEVEL1 => 1,
250            QOT_RIGHT_LEVEL2 => 40,
251            _ => 0,
252        };
253    }
254
255    if is_hk_option_market(info) {
256        return if qr.hk_option_qot_right == QOT_RIGHT_BMP {
257            0
258        } else {
259            qr.hk_option_orderbook_depth.unwrap_or(0) as usize
260        };
261    }
262
263    if is_hk_future_market(info) {
264        return if qr.hk_future_qot_right == QOT_RIGHT_BMP {
265            0
266        } else {
267            qr.hk_future_orderbook_depth.unwrap_or(0) as usize
268        };
269    }
270
271    if is_hk_security_market(info) {
272        return match qr.hk_qot_right {
273            QOT_RIGHT_BMP | QOT_RIGHT_NO => 0,
274            QOT_RIGHT_LEVEL1 => 1,
275            QOT_RIGHT_LEVEL2 | QOT_RIGHT_SF => 10,
276            _ => 10,
277        };
278    }
279
280    if is_us_security_market(info) {
281        return if info.sec_type == SECURITY_TYPE_INDEX || qr.us_qot_right == QOT_RIGHT_BMP {
282            0
283        } else {
284            1
285        };
286    }
287
288    if is_sh_market(info) {
289        return if qr.sh_qot_right == QOT_RIGHT_BMP {
290            0
291        } else {
292            10
293        };
294    }
295
296    if is_sz_market(info) {
297        return if qr.sz_qot_right == QOT_RIGHT_BMP {
298            0
299        } else {
300            10
301        };
302    }
303
304    if is_sg_future_market(info) {
305        return match qr.sg_future_qot_right {
306            QOT_RIGHT_LEVEL1 => 1,
307            QOT_RIGHT_LEVEL2 => 40,
308            _ => 0,
309        };
310    }
311
312    if is_jp_future_market(info) {
313        return match qr.jp_future_qot_right {
314            QOT_RIGHT_LEVEL1 => 1,
315            QOT_RIGHT_LEVEL2 => 40,
316            _ => 0,
317        };
318    }
319
320    if is_my_security_market(info) {
321        return match qr.my_stock_qot_right {
322            QOT_RIGHT_LEVEL1 => 3,
323            QOT_RIGHT_LEVEL2 => 5,
324            QOT_RIGHT_LEVEL3 => 10,
325            _ => 0,
326        };
327    }
328
329    if is_sg_security_market(info) {
330        return match qr.sg_stock_qot_right {
331            QOT_RIGHT_LEVEL1 => 1,
332            QOT_RIGHT_LEVEL2 => 40,
333            _ => 0,
334        };
335    }
336
337    if is_jp_security_market(info) {
338        return match qr.jp_stock_qot_right {
339            QOT_RIGHT_LEVEL1 => 1,
340            QOT_RIGHT_LEVEL2 => 5,
341            QOT_RIGHT_LEVEL3 => 10,
342            _ => 0,
343        };
344    }
345
346    10
347}
348
349/// C++ `IsHKSF`: HK 股票 + HK SF 权限要求 backend 全档摆盘。
350///
351/// Ref:
352/// - `FutuOpenD/Src/APIServer/APIServer_Inner_API.cpp:900-913`
353/// - `FutuOpenD/Src/APIServer/Business/Quote/QotSubscribe.cpp:1282-1286`
354pub fn order_book_requires_backend_full_depth(
355    info: &CachedSecurityInfo,
356    qr: &QotRightData,
357) -> bool {
358    qr.hk_qot_right == QOT_RIGHT_SF && is_hk_security_market(info)
359}
360
361pub fn order_book_uses_backend_side_count(info: &CachedSecurityInfo, qr: &QotRightData) -> bool {
362    order_book_requires_backend_full_depth(info, qr)
363}
364
365/// C++ `GetOrderBook` read path skips `GetOrderBookMaxNum` clipping for HK SF
366/// and US TotalView Lv2.
367///
368/// Ref:
369/// - `FutuOpenD/Src/APIServer/Business/Quote/APIServer_Qot_OrderBook.cpp:86-89`
370/// - `FutuOpenD/Src/APIServer/APIServer_Inner_API.cpp:900-929`
371pub fn order_book_read_uses_requested_count(info: &CachedSecurityInfo, qr: &QotRightData) -> bool {
372    order_book_uses_backend_side_count(info, qr)
373        || (qr.us_lv2_nasdaq_totalview_qot_right && is_us_security_market(info))
374}
375
376/// C++ `GetOrderBook` 对 US TotalView Lv2 / Crypto orderbook 还有一层
377/// `IsHadAcceptUSLv2TotalViewData` gate: 普通 orderbook cache 存在不等于
378/// LV2 真盘口已经到达。
379///
380/// Ref:
381/// - `FutuOpenD/Src/APIServer/Business/Quote/APIServer_Qot_OrderBook.cpp:93-99`
382/// - `FutuOpenD/Src/APIServer/Business/Quote/QotRealTimeData.cpp:576-579`
383pub fn order_book_requires_accepted_lv2_push(info: &CachedSecurityInfo, qr: &QotRightData) -> bool {
384    (qr.us_lv2_nasdaq_totalview_qot_right && is_us_security_market(info)) || is_crypto_market(info)
385}