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