Skip to main content

futu_cache/qot_right/
security_rules.rs

1use crate::static_data::CachedSecurityInfo;
2
3use super::{
4    JP_LV2_ORDER_STOCK, JP_LV2_ORDER_STOCK_FULL, LV2_ORDER_US_FUTURE, Lv2OrderSubDescriptor,
5    QOT_MARKET_CC_SECURITY, QOT_RIGHT_BMP, QOT_RIGHT_LEVEL1, QOT_RIGHT_LEVEL2, QOT_RIGHT_LEVEL3,
6    QOT_RIGHT_NO, QOT_RIGHT_SF, QOT_RIGHT_UNKNOWN, QotRightData, SECURITY_TYPE_CRYPTO,
7    SECURITY_TYPE_DRVT, SECURITY_TYPE_FUTURE, SECURITY_TYPE_INDEX, SG_LV2_ORDER_STOCK,
8    SG_LV2_ORDER_STOCK_ODD_LOT, 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(super) fn snapshot_masks_hk_bmp_bid_ask_impl(
72    info: &CachedSecurityInfo,
73    qr: &QotRightData,
74) -> bool {
75    let is_hk = matches!(info.market, 1 | 2);
76    if !is_hk {
77        return false;
78    }
79
80    if is_hk_option_market(info) {
81        return qr.hk_option_qot_right == QOT_RIGHT_BMP;
82    }
83    if is_hk_future_market(info) {
84        return qr.hk_future_qot_right == QOT_RIGHT_BMP;
85    }
86    is_hk_security_market(info) && qr.hk_qot_right == QOT_RIGHT_BMP
87}
88
89pub fn snapshot_masks_hk_bmp_bid_ask(info: &CachedSecurityInfo, qr: &QotRightData) -> bool {
90    snapshot_masks_hk_bmp_bid_ask_impl(info, qr)
91}
92
93fn is_us_security_market(info: &CachedSecurityInfo) -> bool {
94    matches!(info.mkt_id, 10..=29 | 1200..=1249)
95        || (info.market == 11
96            && !is_us_future_market(info)
97            && !is_us_option_market(info)
98            && info.sec_type != SECURITY_TYPE_FUTURE
99            && info.sec_type != SECURITY_TYPE_DRVT)
100}
101
102/// C++ `IsUSStockLevel2` returns true for US securities when either NASDAQ
103/// TotalView or ARCA entitlement is present. OpenBook/NYSE is intentionally
104/// not included here because the C++ guard only checks TotalView || ARCA.
105///
106/// Ref: `FutuOpenD/Src/APIServer/APIServer_Inner_API.cpp:957-971`
107fn is_us_stock_level2_like_cpp(info: &CachedSecurityInfo, qr: &QotRightData) -> bool {
108    is_us_security_market(info)
109        && (qr.us_lv2_nasdaq_totalview_qot_right || qr.us_lv2_arca_qot_right)
110}
111
112/// C++ `QotRealTimeData::ParseUSLv2ToMainCache` treats normal US securities
113/// (for example AAPL whose backend `instrument_type` is 3) as exchange-level
114/// LV2 sources: NASDAQ TotalView / ARCA first write `(stock_id, lv2_type)`,
115/// then all sources are merged into the public orderbook cache. Indexes do
116/// not use this stock LV2 merge path.
117pub fn us_lv2_order_uses_exchange_cache(info: &CachedSecurityInfo) -> bool {
118    is_us_security_market(info) && info.sec_type != SECURITY_TYPE_INDEX
119}
120
121/// v1.4.110 codex Phase 4 Slice 7: 把 crypto detection helper 暴露给 push_parser
122/// / handler (crypto LV2 sub-system 入口).
123///
124/// 对齐 C++ `IsCrypto(stockID)` (NNBiz_Qot_SecList.cpp). 三个条件任一: sec_type==Crypto
125/// / FTAPI QotMarket=91 (CC_Security) / mkt_id ∈ [360,459] (DigitalCcy range).
126pub fn is_crypto_market(info: &CachedSecurityInfo) -> bool {
127    info.sec_type == SECURITY_TYPE_CRYPTO
128        || info.market == QOT_MARKET_CC_SECURITY
129        || (360..=459).contains(&info.mkt_id)
130}
131
132/// C++ `SubBitUtil::GetPushTypeSvrSubBit(NN_PushQot_Type_Most)` 对 US /
133/// US options / US futures 保留 `SBIT_US_PREMARKET_AFTERHOURS_DETAIL`, 让
134/// BasicQot 可以随订阅收到 preMarket / afterMarket / overnight。
135///
136/// Ref:
137/// - `FutuOpenD/Src/NNProtoCenter/Quote/SubBitUtil.cpp:7-17`
138/// - `FutuOpenD/Src/NNProtoCenter/Quote/SubBitUtil.cpp:91-95`
139pub fn basic_qot_uses_us_pre_after_detail(info: &CachedSecurityInfo) -> bool {
140    matches!(info.mkt_id, 10..=29 | 41..=45 | 60..=109 | 1200..=1249) || info.market == 11
141}
142
143fn is_sh_market(info: &CachedSecurityInfo) -> bool {
144    matches!(info.mkt_id, 30 | 32 | 33 | 34 | 36..=40) || info.market == 21
145}
146
147fn is_sz_market(info: &CachedSecurityInfo) -> bool {
148    matches!(info.mkt_id, 31 | 35) || info.market == 22
149}
150
151fn us_future_right_for_market(qr: &QotRightData, mkt_id: u32) -> i32 {
152    match mkt_id {
153        60..=69 => qr.us_nymex_future_qot_right,
154        70..=79 => qr.us_comex_future_qot_right,
155        80..=89 => qr.us_cbot_future_qot_right,
156        90..=99 => qr.us_cme_future_qot_right,
157        100..=109 => qr.us_cboe_future_qot_right,
158        _ => QOT_RIGHT_UNKNOWN,
159    }
160}
161
162pub(super) fn merged_lv2_order_subs_for_security_impl(
163    info: &CachedSecurityInfo,
164    qr: &QotRightData,
165    _extended_time: bool,
166) -> Vec<Lv2OrderSubDescriptor> {
167    if is_us_future_market(info) {
168        if us_future_right_for_market(qr, info.mkt_id) == QOT_RIGHT_LEVEL2 {
169            return vec![Lv2OrderSubDescriptor {
170                lv2_type: LV2_ORDER_US_FUTURE,
171                level: 60,
172                prob2_v2: true,
173            }];
174        }
175        return vec![];
176    }
177
178    if is_sg_security_market(info) {
179        if qr.sg_stock_qot_right == QOT_RIGHT_LEVEL2 {
180            // Ref: FutuOpenD/Src/NNProtoCenter/Quote/MktQotSubInstance.cpp:397-413.
181            // SG Level2 orderbook subscribes both normal and odd-lot depth at
182            // level 40 through prob2_v2 before the base SBIT_ORDER_BOOK prob.
183            return vec![
184                Lv2OrderSubDescriptor {
185                    lv2_type: SG_LV2_ORDER_STOCK,
186                    level: 40,
187                    prob2_v2: true,
188                },
189                Lv2OrderSubDescriptor {
190                    lv2_type: SG_LV2_ORDER_STOCK_ODD_LOT,
191                    level: 40,
192                    prob2_v2: true,
193                },
194            ];
195        }
196        return vec![];
197    }
198
199    if is_jp_security_market(info) {
200        return match qr.jp_stock_qot_right {
201            QOT_RIGHT_LEVEL2 => {
202                // Ref: FutuOpenD/Src/NNProtoCenter/Quote/MktQotSubInstance.cpp
203                // `JPSecurityMktQotSub::AddSubBit`: JP Lv2 uses
204                // E_LV2_ORDER_JPX_STOCK_FULL with level=10, followed by the
205                // base SBIT_ORDER_BOOK for BBO one-level.
206                vec![Lv2OrderSubDescriptor {
207                    lv2_type: JP_LV2_ORDER_STOCK_FULL,
208                    level: 10,
209                    prob2_v2: true,
210                }]
211            }
212            QOT_RIGHT_LEVEL3 => {
213                // Same C++ path: JP Lv3 keeps the same JPX_STOCK_FULL type
214                // and only raises the requested depth marker to level=40.
215                vec![Lv2OrderSubDescriptor {
216                    lv2_type: JP_LV2_ORDER_STOCK_FULL,
217                    level: 40,
218                    prob2_v2: true,
219                }]
220            }
221            _ => vec![],
222        };
223    }
224
225    if !us_lv2_order_uses_exchange_cache(info) {
226        return vec![];
227    }
228    if !matches!(qr.us_qot_right, QOT_RIGHT_LEVEL1 | QOT_RIGHT_LEVEL2) {
229        return vec![];
230    }
231
232    let mut subs = Vec::new();
233    // Ref: FutuOpenD/Src/NNProtoCenter/Quote/MktQotSubInstance.cpp:118-137.
234    // C++ adds E_US_LV2_ORDER_OVERNIGHT for US stock OrderBook whenever any
235    // US LV2 orderbook right exists; it does not gate this on Qot_Sub.extendedTime.
236    if qr.us_lv2_arca_qot_right || qr.us_lv2_nyse_qot_right || qr.us_lv2_nasdaq_totalview_qot_right
237    {
238        subs.push(Lv2OrderSubDescriptor {
239            lv2_type: US_LV2_ORDER_OVERNIGHT,
240            level: 60,
241            prob2_v2: true,
242        });
243    }
244    if qr.us_lv2_nasdaq_totalview_qot_right {
245        subs.push(Lv2OrderSubDescriptor {
246            lv2_type: US_LV2_ORDER_NASDAQ_TV,
247            level: 60,
248            prob2_v2: false,
249        });
250    }
251    if qr.us_lv2_arca_qot_right {
252        subs.push(Lv2OrderSubDescriptor {
253            lv2_type: US_LV2_ORDER_ARCA,
254            level: 60,
255            prob2_v2: false,
256        });
257    }
258    subs
259}
260
261pub fn merged_lv2_order_subs_for_security(
262    info: &CachedSecurityInfo,
263    qr: &QotRightData,
264    extended_time: bool,
265) -> Vec<Lv2OrderSubDescriptor> {
266    merged_lv2_order_subs_for_security_impl(info, qr, extended_time)
267}
268
269pub fn has_merged_lv2_order_subs_for_security(
270    info: &CachedSecurityInfo,
271    qr: &QotRightData,
272) -> bool {
273    !merged_lv2_order_subs_for_security_impl(info, qr, true).is_empty()
274}
275
276/// C++ `GetOrderBookMaxNum` 对齐版:根据证券静态市场 + 行情权限决定
277/// orderbook push/cache 应保留的档位数。
278///
279/// Ref:
280/// - `FutuOpenD/Src/APIServer/APIServer_Inner_API.cpp:4331-4412`
281/// - `FutuOpenD/Src/APIServer/Business/Quote/QotRealTimeData.cpp:1211-1215`
282///
283/// Hardcoded / Assumption Ledger:
284/// - `mkt_id` 范围复用 backend stock-list 的 NN_QuoteMktID 区间。C++ 10.7
285///   已把 `1400..=1449` 命名为 MY futures,但尚未给出 public orderbook
286///   right/depth 映射,因此这里不再作为 HK futures 兜底。
287/// - HK option/future depth 缺省为 0,和 C++ `NNData_Qot_Right` 构造默认值一致。
288/// - 普通 HK/US/CN 股票在 Unknown 权限下保留 C++ 初始化默认值/分支语义,
289///   避免 push parser 在权限刷新窗口内把有效 backend push 误清空。
290pub(super) fn order_book_max_depth_for_security_impl(
291    info: &CachedSecurityInfo,
292    qr: &QotRightData,
293) -> usize {
294    if is_crypto_market(info) {
295        return if qr.cc_qot_right == QOT_RIGHT_LEVEL1 {
296            40
297        } else {
298            0
299        };
300    }
301
302    if is_us_option_market(info) {
303        return if qr.us_option_qot_right == QOT_RIGHT_BMP {
304            0
305        } else {
306            1
307        };
308    }
309
310    if is_us_future_market(info) {
311        return match us_future_right_for_market(qr, info.mkt_id) {
312            QOT_RIGHT_LEVEL1 => 1,
313            QOT_RIGHT_LEVEL2 => 40,
314            _ => 0,
315        };
316    }
317
318    if is_hk_option_market(info) {
319        return if qr.hk_option_qot_right == QOT_RIGHT_BMP {
320            0
321        } else {
322            qr.hk_option_orderbook_depth.unwrap_or(0) as usize
323        };
324    }
325
326    if is_hk_future_market(info) {
327        return if qr.hk_future_qot_right == QOT_RIGHT_BMP {
328            0
329        } else {
330            qr.hk_future_orderbook_depth.unwrap_or(0) as usize
331        };
332    }
333
334    if is_hk_security_market(info) {
335        return match qr.hk_qot_right {
336            QOT_RIGHT_BMP | QOT_RIGHT_NO => 0,
337            QOT_RIGHT_LEVEL1 => 1,
338            QOT_RIGHT_LEVEL2 | QOT_RIGHT_SF => 10,
339            _ => 10,
340        };
341    }
342
343    if is_us_security_market(info) {
344        return if info.sec_type == SECURITY_TYPE_INDEX || qr.us_qot_right == QOT_RIGHT_BMP {
345            0
346        } else {
347            1
348        };
349    }
350
351    if is_sh_market(info) {
352        return if qr.sh_qot_right == QOT_RIGHT_BMP {
353            0
354        } else {
355            10
356        };
357    }
358
359    if is_sz_market(info) {
360        return if qr.sz_qot_right == QOT_RIGHT_BMP {
361            0
362        } else {
363            10
364        };
365    }
366
367    if is_sg_future_market(info) {
368        return match qr.sg_future_qot_right {
369            QOT_RIGHT_LEVEL1 => 1,
370            QOT_RIGHT_LEVEL2 => 40,
371            _ => 0,
372        };
373    }
374
375    if is_jp_future_market(info) {
376        return match qr.jp_future_qot_right {
377            QOT_RIGHT_LEVEL1 => 1,
378            QOT_RIGHT_LEVEL2 => 40,
379            _ => 0,
380        };
381    }
382
383    if is_my_security_market(info) {
384        return match qr.my_stock_qot_right {
385            QOT_RIGHT_LEVEL1 => 3,
386            QOT_RIGHT_LEVEL2 => 5,
387            QOT_RIGHT_LEVEL3 => 10,
388            _ => 0,
389        };
390    }
391
392    if is_sg_security_market(info) {
393        return match qr.sg_stock_qot_right {
394            QOT_RIGHT_LEVEL1 => 1,
395            QOT_RIGHT_LEVEL2 => 40,
396            _ => 0,
397        };
398    }
399
400    if is_jp_security_market(info) {
401        return match qr.jp_stock_qot_right {
402            QOT_RIGHT_LEVEL1 => 1,
403            QOT_RIGHT_LEVEL2 => 10,
404            QOT_RIGHT_LEVEL3 => 40,
405            _ => 0,
406        };
407    }
408
409    10
410}
411
412pub fn order_book_max_depth_for_security(info: &CachedSecurityInfo, qr: &QotRightData) -> usize {
413    order_book_max_depth_for_security_impl(info, qr)
414}
415
416/// C++ `IsHKSF`: HK 股票 + HK SF 权限要求 backend 全档摆盘。
417///
418/// Ref:
419/// - `FutuOpenD/Src/APIServer/APIServer_Inner_API.cpp:900-913`
420/// - `FutuOpenD/Src/APIServer/Business/Quote/QotSubscribe.cpp:1282-1286`
421pub(super) fn order_book_requires_backend_full_depth_impl(
422    info: &CachedSecurityInfo,
423    qr: &QotRightData,
424) -> bool {
425    qr.hk_qot_right == QOT_RIGHT_SF && is_hk_security_market(info)
426}
427
428pub fn order_book_requires_backend_full_depth(
429    info: &CachedSecurityInfo,
430    qr: &QotRightData,
431) -> bool {
432    order_book_requires_backend_full_depth_impl(info, qr)
433}
434
435pub fn order_book_uses_backend_side_count(info: &CachedSecurityInfo, qr: &QotRightData) -> bool {
436    order_book_requires_backend_full_depth_impl(info, qr)
437}
438
439/// C++ `GetOrderBook` read path skips `GetOrderBookMaxNum` clipping for HK SF
440/// and US TotalView Lv2.
441///
442/// Ref:
443/// - `FutuOpenD/Src/APIServer/Business/Quote/APIServer_Qot_OrderBook.cpp:86-89`
444/// - `FutuOpenD/Src/APIServer/APIServer_Inner_API.cpp:900-929`
445pub(super) fn order_book_read_uses_requested_count_impl(
446    info: &CachedSecurityInfo,
447    qr: &QotRightData,
448) -> bool {
449    order_book_requires_backend_full_depth_impl(info, qr)
450        || is_us_stock_level2_like_cpp(info, qr)
451        || (matches!(qr.jp_stock_qot_right, QOT_RIGHT_LEVEL2 | QOT_RIGHT_LEVEL3)
452            && is_jp_security_market(info))
453}
454
455pub fn order_book_read_uses_requested_count(info: &CachedSecurityInfo, qr: &QotRightData) -> bool {
456    order_book_read_uses_requested_count_impl(info, qr)
457}
458
459/// C++ `GetOrderBook` 对 US TotalView Lv2 / Crypto orderbook 还有一层
460/// `IsHadAcceptUSLv2TotalViewData` gate: 普通 orderbook cache 存在不等于
461/// LV2 真盘口已经到达。
462///
463/// Ref:
464/// - `FutuOpenD/Src/APIServer/Business/Quote/APIServer_Qot_OrderBook.cpp:93-99`
465/// - `FutuOpenD/Src/APIServer/Business/Quote/QotRealTimeData.cpp:576-579`
466pub(super) fn order_book_requires_accepted_lv2_push_impl(
467    info: &CachedSecurityInfo,
468    qr: &QotRightData,
469) -> bool {
470    is_us_stock_level2_like_cpp(info, qr)
471        || (matches!(qr.jp_stock_qot_right, QOT_RIGHT_LEVEL2 | QOT_RIGHT_LEVEL3)
472            && is_jp_security_market(info))
473        || is_crypto_market(info)
474}
475
476pub fn order_book_requires_accepted_lv2_push(info: &CachedSecurityInfo, qr: &QotRightData) -> bool {
477    order_book_requires_accepted_lv2_push_impl(info, qr)
478}
479
480pub fn lv2_order_uses_sg_odd_lot_cache(info: &CachedSecurityInfo, lv2_type: i32) -> bool {
481    is_sg_security_market(info) && lv2_type == SG_LV2_ORDER_STOCK_ODD_LOT as i32
482}
483
484pub fn lv2_order_uses_jp_main_cache(info: &CachedSecurityInfo, lv2_type: i32) -> bool {
485    is_jp_security_market(info)
486        && matches!(
487            lv2_type,
488            value if value == JP_LV2_ORDER_STOCK as i32
489                || value == JP_LV2_ORDER_STOCK_FULL as i32
490        )
491}