Skip to main content

futu_backend/quote_sub/
sub_bits.rs

1use prost::Message;
2
3use crate::proto_internal::ft_cmd_stock_quote_sub_data;
4
5use super::{sbit, sub_type};
6
7/// FTAPI SubType → 后端 SubscribeBit 列表映射
8/// 返回 (bit, probability) 列表
9pub fn sub_type_to_bits(sub_type: i32) -> Vec<(u32, i64)> {
10    match sub_type {
11        sub_type::BASIC => vec![
12            (sbit::PRICE, 0),
13            (sbit::STOCK_STATE, 0),
14            (sbit::STOCK_TYPE_SPECIFIC, 0),
15            (sbit::DEAL_STATISTICS, 0),
16        ],
17        sub_type::ORDER_BOOK => vec![(sbit::ORDER_BOOK, 0)],
18        sub_type::TICKER => vec![(sbit::TICK, 1)], // prob=1 normal period
19        sub_type::RT => vec![(sbit::STOCK_STATE, 0), (sbit::TIME_SHARING, 0)],
20        sub_type::BROKER => vec![(sbit::HK_BROKER_QUEUE, 0)],
21        sub_type::KL_1MIN => vec![(sbit::KLINE_1MIN, 1)], // prob=1 no rehab
22        sub_type::KL_3MIN => vec![(sbit::KLINE_3MIN, 1)],
23        sub_type::KL_5MIN => vec![(sbit::KLINE_5MIN, 1)],
24        sub_type::KL_10MIN => vec![(sbit::KLINE_10MIN, 1)],
25        sub_type::KL_15MIN => vec![(sbit::KLINE_15MIN, 1)],
26        sub_type::KL_30MIN => vec![(sbit::KLINE_30MIN, 1)],
27        sub_type::KL_60MIN => vec![(sbit::KLINE_60MIN, 1)],
28        sub_type::KL_120MIN => vec![(sbit::KLINE_120MIN, 1)],
29        sub_type::KL_180MIN => vec![(sbit::KLINE_180MIN, 1)],
30        sub_type::KL_240MIN => vec![(sbit::KLINE_240MIN, 1)],
31        sub_type::KL_DAY => vec![(sbit::KLINE_DAY, 1)],
32        sub_type::KL_WEEK => vec![(sbit::KLINE_WEEK, 1)],
33        sub_type::KL_MONTH => vec![(sbit::KLINE_MONTH, 1)],
34        sub_type::KL_QUARTER => vec![(sbit::KLINE_QUARTER, 1)],
35        sub_type::KL_YEAR => vec![(sbit::KLINE_YEAR, 1)],
36        _ => vec![],
37    }
38}
39
40/// **v1.4.106 codex 1131 F6 [P2]**: 单个 (security, sub_type) 的扩展参数.
41///
42/// 决定 backend req body 里的 prob / detail flag / session 选择. 由 SubHandler
43/// 从 c2s + SubscriptionManager 的 detail/session aggregator 聚合产出.
44#[derive(Debug, Clone, Default)]
45pub struct SubBitOptions {
46    /// session: 1=RTH / 2=ETH / 3=ALL / 4=OVERNIGHT (rejected by C++)
47    /// 仅 Ticker / KLRT 类有 session 语义, 其他 sub_type 应填 RTH 默认.
48    pub session: i32,
49    /// OrderBook detail (有 conn 要 detail → 全局走 detail bit).
50    pub orderbook_detail: bool,
51    /// HKSF orderbook uses C++ `OrderBook_All` / `OrderBook_All_WithDetail`
52    /// backend prob so the server pushes every available side level, not only
53    /// the default 40-level snapshot.
54    pub orderbook_full_depth: bool,
55    /// Broker detail (HK broker queue 时有 conn 要 detail → 走 detail bit).
56    pub broker_detail: bool,
57    /// US Basic/MOST quote needs SBIT_US_PREMARKET_AFTERHOURS_DETAIL so
58    /// BasicQot can expose preMarket / afterMarket / overnight.
59    pub us_pre_after_detail: bool,
60    /// 扩展 session 含 ETH / Overnight / pre-after / etc — 影响 prob / time_seg.
61    /// 对齐 C++ APIServer_Qot_Sub.cpp:218-257.
62    pub extended_time: bool,
63    /// C++ `SBIT_MEGER_LV2_ORDER` 的 `prob2` USLV2OrderSubProb type bitmask.
64    /// Ref: `MktQotSubInstance.cpp:114-180`.
65    pub merged_lv2_order_types: u32,
66    /// C++ `SBIT_MEGER_LV2_ORDER` 的 `prob2_v2` USLV2OrderSubProb type bitmask.
67    /// Overnight / US futures 会产生非 UTF-8 protobuf bytes, 必须走 bytes field.
68    pub merged_lv2_order_types_v2: u32,
69    /// Same `prob2_v2` encoding, but C++ SG Level2 stock orderbook caps level
70    /// at 40 instead of the US / US-future 60-level path.
71    pub merged_lv2_order_types_v2_level40: u32,
72    /// Generic C++ `SBIT_MEGER_LV2_ORDER` descriptors. This is the preferred
73    /// path for markets whose level is not one of the older bucket fields
74    /// (JP Lv2 uses level=10, JP Lv3 uses level=40). Keep the legacy masks
75    /// above for direct callers/tests that still express US/SG as bitsets.
76    pub merged_lv2_order_subs: Vec<futu_cache::qot_right::Lv2OrderSubDescriptor>,
77    /// Backend NN_QuoteMktType / internal subscribe market.
78    ///
79    /// C++ SG/MY odd-lot orderbook prob is market-specific:
80    /// `MktQotSubInstance.cpp:392-417` (SG) and `:439-449` (MY).
81    /// Keep this on the per-security options rather than guessing from public
82    /// symbol strings in the bit mapper.
83    pub backend_market: i32,
84}
85
86#[derive(Debug, Clone, PartialEq, Eq)]
87pub struct SubscribeBitInfo {
88    pub bit: u32,
89    pub prob: i64,
90    pub prob2: Option<String>,
91    pub prob2_v2: Option<Vec<u8>>,
92}
93
94// Ref: FutuOpenD/Src/NNProtoCenter/Quote/SubBitUtil.cpp:119-126.
95const ORDER_BOOK_40_PROB: i64 = 0;
96const ORDER_BOOK_ALL_PROB: i64 = 1;
97const ORDER_BOOK_ALL_WITH_DETAIL_PROB: i64 = 2;
98const ORDER_BOOK_SIMPLE_LV2_PROB: i64 = 8 | 16;
99const ORDER_BOOK_SGX_STOCK_PROB: i64 = 1;
100const ORDER_BOOK_SGX_STOCK_ODD_LOT_PROB: i64 = 2;
101const ORDER_BOOK_MYX_STOCK_PROB: i64 = 1;
102const ORDER_BOOK_MYX_STOCK_ODD_LOT_PROB: i64 = 16;
103
104fn sg_my_order_book_prob(backend_market: i32) -> Option<i64> {
105    match backend_market {
106        // Ref: FutuOpenD/Src/NNProtoCenter/Quote/MktQotSubInstance.cpp:392-449.
107        // SG/MY stock orderbook always subscribes normal+odd-lot backend prob
108        // through SBIT_ORDER_BOOK, regardless of whether the public SubType is
109        // OrderBook(2) or OrderBook_Odd(22).
110        15 => Some(ORDER_BOOK_SGX_STOCK_PROB | ORDER_BOOK_SGX_STOCK_ODD_LOT_PROB),
111        27 => Some(ORDER_BOOK_MYX_STOCK_PROB | ORDER_BOOK_MYX_STOCK_ODD_LOT_PROB),
112        _ => None,
113    }
114}
115
116fn encode_us_lv2_order_sub_prob(lv2_type: u32, level: u32) -> Vec<u8> {
117    ft_cmd_stock_quote_sub_data::Uslv2OrderSubProb {
118        sub_items: vec![ft_cmd_stock_quote_sub_data::Uslv2OrderSubItem {
119            us_lv2_order_type: Some(lv2_type),
120            us_lv2_order_level: Some(level),
121        }],
122    }
123    .encode_to_vec()
124}
125
126fn for_each_type_mask_bit(mut mask: u32, mut f: impl FnMut(u32)) {
127    while mask != 0 {
128        let bit = mask & (!mask + 1);
129        f(bit);
130        mask &= !bit;
131    }
132}
133
134fn for_each_us_lv2_prob2_type(mask: u32, mut f: impl FnMut(u32)) {
135    // Ref: FutuOpenD/Src/NNSymbol/MktQotSubInstance.cpp:150-168.
136    // US stock LV2 orderbook uses prob2 order: NASDAQ TotalView first, ARCA
137    // second. Keep any future unknown bit after the known C++ order.
138    for known in [
139        futu_cache::qot_right::US_LV2_ORDER_NASDAQ_TV,
140        futu_cache::qot_right::US_LV2_ORDER_ARCA,
141    ] {
142        if mask & known != 0 {
143            f(known);
144        }
145    }
146    let remaining = mask
147        & !(futu_cache::qot_right::US_LV2_ORDER_NASDAQ_TV
148            | futu_cache::qot_right::US_LV2_ORDER_ARCA);
149    for_each_type_mask_bit(remaining, f);
150}
151
152fn push_lv2_order_sub_info(
153    infos: &mut Vec<SubscribeBitInfo>,
154    sub: futu_cache::qot_right::Lv2OrderSubDescriptor,
155) {
156    let bytes = encode_us_lv2_order_sub_prob(sub.lv2_type, sub.level);
157    if sub.prob2_v2 {
158        infos.push(SubscribeBitInfo {
159            bit: sbit::MEGER_LV2_ORDER,
160            prob: 0,
161            prob2: None,
162            prob2_v2: Some(bytes),
163        });
164    } else {
165        let (prob2, prob2_v2) = encode_legacy_or_bytes_prob2(bytes);
166        infos.push(SubscribeBitInfo {
167            bit: sbit::MEGER_LV2_ORDER,
168            prob: 0,
169            prob2,
170            prob2_v2,
171        });
172    }
173}
174
175/// 按 SubBitOptions 计算实际 (bit, prob) 列表 — 取代 sub_type_to_bits 简版.
176///
177/// **v1.4.106 codex 1131 F6**: prob 不是死值. detail 切替 bit.
178/// orderbook_detail=true → ORDER_BOOK_DETAIL bit (4) 替代 ORDER_BOOK bit (3).
179pub fn sub_type_to_bits_with_options(sub_type: i32, opts: SubBitOptions) -> Vec<(u32, i64)> {
180    sub_type_to_bit_infos_with_options(sub_type, opts)
181        .into_iter()
182        .map(|info| (info.bit, info.prob))
183        .collect()
184}
185
186pub fn sub_type_to_bit_infos_with_options(
187    sub_type: i32,
188    opts: SubBitOptions,
189) -> Vec<SubscribeBitInfo> {
190    // Ticker session prob uses backend FTCmdStockQuoteSubData::BitProbTick
191    // bitset, after C++ converts NN_Prob_Type_* through
192    // `SubBitUtil::SubProb_NNToSvr`:
193    // - `QotSubscribe.cpp:1254-1268`: RTH=Normal, ETH=Normal|Before|After,
194    //   ALL=Normal|Before|After|OverNight.
195    // - `SubBitUtil.cpp:119-129`: Normal=1 / Before=2 / After=4 /
196    //   OverNight=8.
197    let ticker_session_prob = match opts.session {
198        2 => 1 | 2 | 4,
199        3 => 1 | 2 | 4 | 8,
200        _ => 1,
201    };
202    // KLine probabilities are pre-existing values for this CMD6211 path. Do
203    // not reuse ticker bitset here: C++ `QotSubscribe.cpp:1280-1294` sends KLRT
204    // through INNBiz_Qot_KLRT, not the ticker SubBit path.
205    let kline_session_prob = match opts.session {
206        2 => 2,
207        3 => 3,
208        _ => 1,
209    };
210    match sub_type {
211        sub_type::BASIC => {
212            let mut infos = vec![
213                SubscribeBitInfo::new(sbit::PRICE, 0),
214                SubscribeBitInfo::new(sbit::STOCK_STATE, 0),
215                SubscribeBitInfo::new(sbit::STOCK_TYPE_SPECIFIC, 0),
216                SubscribeBitInfo::new(sbit::DEAL_STATISTICS, 0),
217            ];
218            if opts.us_pre_after_detail {
219                // C++ SubBitUtil keeps this bit for US / US_OPTIONS / US_FUT
220                // Basic/MOST subscriptions and erases it for non-US markets.
221                infos.push(SubscribeBitInfo::new(sbit::US_PREMARKET_AFTERHOURS, 0));
222            }
223            infos
224        }
225        sub_type::ORDER_BOOK => {
226            let sg_my_prob = sg_my_order_book_prob(opts.backend_market);
227            let bit = if sg_my_prob.is_some() {
228                sbit::ORDER_BOOK
229            } else if opts.orderbook_detail {
230                sbit::ORDER_BOOK_DETAIL
231            } else {
232                sbit::ORDER_BOOK
233            };
234            let mut infos = Vec::new();
235            for sub in &opts.merged_lv2_order_subs {
236                push_lv2_order_sub_info(&mut infos, *sub);
237            }
238            for_each_type_mask_bit(opts.merged_lv2_order_types_v2_level40, |lv2_type| {
239                infos.push(SubscribeBitInfo {
240                    bit: sbit::MEGER_LV2_ORDER,
241                    prob: 0,
242                    prob2: None,
243                    prob2_v2: Some(encode_us_lv2_order_sub_prob(lv2_type, 40)),
244                });
245            });
246            for_each_type_mask_bit(opts.merged_lv2_order_types_v2, |lv2_type| {
247                infos.push(SubscribeBitInfo {
248                    bit: sbit::MEGER_LV2_ORDER,
249                    prob: 0,
250                    prob2: None,
251                    prob2_v2: Some(encode_us_lv2_order_sub_prob(lv2_type, 60)),
252                });
253            });
254            for_each_us_lv2_prob2_type(opts.merged_lv2_order_types, |lv2_type| {
255                let bytes = encode_us_lv2_order_sub_prob(lv2_type, 60);
256                let (prob2, prob2_v2) = encode_legacy_or_bytes_prob2(bytes);
257                infos.push(SubscribeBitInfo {
258                    bit: sbit::MEGER_LV2_ORDER,
259                    prob: 0,
260                    prob2,
261                    prob2_v2,
262                });
263            });
264            let prob = if let Some(prob) = sg_my_prob {
265                prob
266            } else if opts.orderbook_full_depth {
267                if opts.orderbook_detail {
268                    ORDER_BOOK_ALL_WITH_DETAIL_PROB
269                } else {
270                    ORDER_BOOK_ALL_PROB
271                }
272            } else if opts.merged_lv2_order_types != 0
273                || opts.merged_lv2_order_types_v2 != 0
274                || opts.merged_lv2_order_types_v2_level40 != 0
275                || !opts.merged_lv2_order_subs.is_empty()
276            {
277                ORDER_BOOK_SIMPLE_LV2_PROB
278            } else {
279                ORDER_BOOK_40_PROB
280            };
281            infos.push(SubscribeBitInfo::new(bit, prob));
282            infos
283        }
284        sub_type::ORDER_BOOK_ODD => {
285            let prob = sg_my_order_book_prob(opts.backend_market).unwrap_or(0);
286            vec![SubscribeBitInfo::new(sbit::ORDER_BOOK, prob)]
287        }
288        sub_type::TICKER => vec![SubscribeBitInfo::new(sbit::TICK, ticker_session_prob)],
289        sub_type::RT => vec![
290            SubscribeBitInfo::new(sbit::STOCK_STATE, 0),
291            SubscribeBitInfo::new(sbit::TIME_SHARING, 0),
292        ],
293        sub_type::BROKER => {
294            let bit = if opts.broker_detail {
295                sbit::HK_BROKER_DETAIL
296            } else {
297                sbit::HK_BROKER_QUEUE
298            };
299            vec![SubscribeBitInfo::new(bit, 0)]
300        }
301        sub_type::KL_1MIN => vec![SubscribeBitInfo::new(sbit::KLINE_1MIN, kline_session_prob)],
302        sub_type::KL_3MIN => vec![SubscribeBitInfo::new(sbit::KLINE_3MIN, kline_session_prob)],
303        sub_type::KL_5MIN => vec![SubscribeBitInfo::new(sbit::KLINE_5MIN, kline_session_prob)],
304        sub_type::KL_10MIN => vec![SubscribeBitInfo::new(sbit::KLINE_10MIN, kline_session_prob)],
305        sub_type::KL_15MIN => vec![SubscribeBitInfo::new(sbit::KLINE_15MIN, kline_session_prob)],
306        sub_type::KL_30MIN => vec![SubscribeBitInfo::new(sbit::KLINE_30MIN, kline_session_prob)],
307        sub_type::KL_60MIN => vec![SubscribeBitInfo::new(sbit::KLINE_60MIN, kline_session_prob)],
308        sub_type::KL_120MIN => {
309            vec![SubscribeBitInfo::new(
310                sbit::KLINE_120MIN,
311                kline_session_prob,
312            )]
313        }
314        sub_type::KL_180MIN => {
315            vec![SubscribeBitInfo::new(
316                sbit::KLINE_180MIN,
317                kline_session_prob,
318            )]
319        }
320        sub_type::KL_240MIN => {
321            vec![SubscribeBitInfo::new(
322                sbit::KLINE_240MIN,
323                kline_session_prob,
324            )]
325        }
326        sub_type::KL_DAY => vec![SubscribeBitInfo::new(sbit::KLINE_DAY, kline_session_prob)],
327        sub_type::KL_WEEK => vec![SubscribeBitInfo::new(sbit::KLINE_WEEK, kline_session_prob)],
328        sub_type::KL_MONTH => vec![SubscribeBitInfo::new(sbit::KLINE_MONTH, kline_session_prob)],
329        sub_type::KL_QUARTER => vec![SubscribeBitInfo::new(
330            sbit::KLINE_QUARTER,
331            kline_session_prob,
332        )],
333        sub_type::KL_YEAR => vec![SubscribeBitInfo::new(sbit::KLINE_YEAR, kline_session_prob)],
334        _ => vec![],
335    }
336}
337
338impl SubscribeBitInfo {
339    pub(crate) fn new(bit: u32, prob: i64) -> Self {
340        Self {
341            bit,
342            prob,
343            prob2: None,
344            prob2_v2: None,
345        }
346    }
347}
348
349fn encode_legacy_or_bytes_prob2(bytes: Vec<u8>) -> (Option<String>, Option<Vec<u8>>) {
350    match String::from_utf8(bytes) {
351        Ok(prob2) => (Some(prob2), None),
352        Err(err) => (None, Some(err.into_bytes())),
353    }
354}