Skip to main content

futu_core/
qot_subscription.rs

1use std::num::NonZeroU32;
2
3use crate::qot_stock_key::QotSecurityKey;
4
5pub const SECURITY_TYPE_DRVT: i32 = 8;
6pub const SECURITY_TYPE_FUTURE: i32 = 10;
7
8pub const BACKEND_MARKET_HK_OPTION: i32 = 9;
9pub const BACKEND_MARKET_US_OPTION: i32 = 15;
10
11pub const US_LV2_ORDER_ARCA: u32 = 1;
12pub const US_LV2_ORDER_NASDAQ_TV: u32 = 4;
13pub const US_LV2_ORDER_OVERNIGHT: u32 = 128;
14pub const LV2_ORDER_US_FUTURE: u32 = 1;
15pub const SG_LV2_ORDER_STOCK: u32 = 1;
16pub const SG_LV2_ORDER_STOCK_ODD_LOT: u32 = 2;
17pub const JP_LV2_ORDER_STOCK: u32 = 1;
18pub const JP_LV2_ORDER_STOCK_FULL: u32 = 2;
19
20pub const SUB_TYPE_NONE: i32 = 0;
21pub const SUB_TYPE_BASIC: i32 = 1;
22pub const SUB_TYPE_ORDER_BOOK: i32 = 2;
23pub const SUB_TYPE_TICKER: i32 = 4;
24pub const SUB_TYPE_RT: i32 = 5;
25pub const SUB_TYPE_KL_DAY: i32 = 6;
26pub const SUB_TYPE_KL_5MIN: i32 = 7;
27pub const SUB_TYPE_KL_15MIN: i32 = 8;
28pub const SUB_TYPE_KL_30MIN: i32 = 9;
29pub const SUB_TYPE_KL_60MIN: i32 = 10;
30pub const SUB_TYPE_KL_1MIN: i32 = 11;
31pub const SUB_TYPE_KL_WEEK: i32 = 12;
32pub const SUB_TYPE_KL_MONTH: i32 = 13;
33pub const SUB_TYPE_BROKER: i32 = 14;
34pub const SUB_TYPE_KL_QUARTER: i32 = 15;
35pub const SUB_TYPE_KL_YEAR: i32 = 16;
36pub const SUB_TYPE_KL_3MIN: i32 = 17;
37pub const SUB_TYPE_KL_10MIN: i32 = 18;
38pub const SUB_TYPE_KL_120MIN: i32 = 19;
39pub const SUB_TYPE_KL_180MIN: i32 = 20;
40pub const SUB_TYPE_KL_240MIN: i32 = 21;
41pub const SUB_TYPE_ORDER_BOOK_ODD: i32 = 22;
42
43pub const VALID_QOT_SUB_TYPES: &[i32] = &[
44    SUB_TYPE_BASIC,
45    SUB_TYPE_ORDER_BOOK,
46    SUB_TYPE_TICKER,
47    SUB_TYPE_RT,
48    SUB_TYPE_KL_DAY,
49    SUB_TYPE_KL_5MIN,
50    SUB_TYPE_KL_15MIN,
51    SUB_TYPE_KL_30MIN,
52    SUB_TYPE_KL_60MIN,
53    SUB_TYPE_KL_1MIN,
54    SUB_TYPE_KL_WEEK,
55    SUB_TYPE_KL_MONTH,
56    SUB_TYPE_BROKER,
57    SUB_TYPE_KL_QUARTER,
58    SUB_TYPE_KL_YEAR,
59    SUB_TYPE_KL_3MIN,
60    SUB_TYPE_KL_10MIN,
61    SUB_TYPE_KL_120MIN,
62    SUB_TYPE_KL_180MIN,
63    SUB_TYPE_KL_240MIN,
64    SUB_TYPE_ORDER_BOOK_ODD,
65];
66
67#[derive(Debug, Clone, Copy, PartialEq, Eq)]
68pub enum EventContractWireRight {
69    Unknown,
70    Level1,
71}
72
73#[derive(Debug, Clone, Copy, PartialEq, Eq)]
74pub struct Lv2OrderGroupPlan {
75    pub lv2_type: u32,
76    pub level: u32,
77    pub prob2_v2: bool,
78}
79
80#[derive(Debug, Clone, PartialEq, Eq)]
81pub enum OrderBookWireProfile {
82    Generic,
83    Hk { sf: bool },
84    UsStock { lv2_groups: Vec<Lv2OrderGroupPlan> },
85    UsFuture { level2: bool },
86    JpStock { depth: Option<u32> },
87    SgStock { level2: bool },
88    MyStock,
89    Crypto { dynamic_prob: Option<u32> },
90    EventContract { right: EventContractWireRight },
91}
92
93#[derive(Debug, Clone, Default, PartialEq, Eq)]
94pub enum SubscriptionWireProfile {
95    #[default]
96    NonOrderBook,
97    OrderBook(OrderBookWireProfile),
98}
99
100#[derive(Clone, Debug, PartialEq, Eq)]
101pub struct BitInfoPlan {
102    pub bit: u32,
103    pub prob: Option<i64>,
104    pub payload: BitInfoPayloadPlan,
105}
106
107impl BitInfoPlan {
108    #[must_use]
109    pub const fn scalar(bit: u32, prob: Option<i64>) -> Self {
110        Self {
111            bit,
112            prob,
113            payload: BitInfoPayloadPlan::None,
114        }
115    }
116
117    #[must_use]
118    pub fn lv2(
119        bit: u32,
120        encoding: Lv2OrderEncoding,
121        first: Lv2OrderItemPlan,
122        additional: impl IntoIterator<Item = Lv2OrderItemPlan>,
123    ) -> Self {
124        let items = std::iter::once(first).chain(additional).collect();
125        Self {
126            bit,
127            prob: None,
128            payload: BitInfoPayloadPlan::Lv2Order { encoding, items },
129        }
130    }
131}
132
133#[derive(Clone, Debug, PartialEq, Eq)]
134pub enum BitInfoPayloadPlan {
135    None,
136    Lv2Order {
137        encoding: Lv2OrderEncoding,
138        items: Vec<Lv2OrderItemPlan>,
139    },
140}
141
142#[derive(Clone, Copy, Debug, PartialEq, Eq)]
143pub enum Lv2OrderEncoding {
144    LegacyProb2,
145    BytesV2,
146}
147
148#[derive(Clone, Copy, Debug, PartialEq, Eq)]
149pub struct Lv2OrderItemPlan {
150    pub lv2_type: u32,
151    pub level: u32,
152}
153
154#[derive(Debug, Clone, Default, PartialEq, Eq)]
155pub struct SubBitOptions {
156    pub session: i32,
157    /// The enclosing CMD6211 market command targets C++ `NN_QuoteMktType_US`.
158    /// KLine subscription probabilities need this frozen routing fact because
159    /// US minute KLines add pre/after-hours and overnight rehab groups.
160    pub kline_us_market: bool,
161    pub orderbook_detail: bool,
162    pub orderbook_full_depth: bool,
163    pub broker_detail: bool,
164    pub us_pre_after_detail: bool,
165    pub extended_time: bool,
166    /// Frozen market-specific planner input derived from one facts snapshot.
167    pub wire_profile: SubscriptionWireProfile,
168}
169
170#[derive(Debug, Clone, PartialEq, Eq)]
171pub struct SecurityWithOpts {
172    pub stock_id: u64,
173    pub ftapi_market: i32,
174    pub sub_types_with_opts: Vec<(i32, SubBitOptions)>,
175    pub broker_id: Option<NonZeroU32>,
176}
177
178impl SecurityWithOpts {
179    #[must_use]
180    pub fn new(
181        stock_id: u64,
182        ftapi_market: i32,
183        sub_types_with_opts: Vec<(i32, SubBitOptions)>,
184    ) -> Self {
185        Self {
186            stock_id,
187            ftapi_market,
188            sub_types_with_opts,
189            broker_id: None,
190        }
191    }
192
193    #[must_use]
194    pub fn with_broker(
195        stock_id: u64,
196        ftapi_market: i32,
197        sub_types_with_opts: Vec<(i32, SubBitOptions)>,
198        broker_id: u32,
199    ) -> Self {
200        Self {
201            stock_id,
202            ftapi_market,
203            sub_types_with_opts,
204            broker_id: NonZeroU32::new(broker_id),
205        }
206    }
207}
208
209#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
210pub struct EmptyDesiredMarket {
211    pub mkt_type: u8,
212    pub is_depth: bool,
213}
214
215#[derive(Debug, Clone, Copy, PartialEq, Eq)]
216pub struct SubscriptionSecurityFacts {
217    pub stock_id: u64,
218    pub sec_type: i32,
219    pub mkt_id: u32,
220    pub market: i32,
221}
222
223#[must_use]
224pub fn is_kl_sub_type(sub_type: i32) -> bool {
225    matches!(
226        sub_type,
227        SUB_TYPE_KL_DAY
228            | SUB_TYPE_KL_5MIN
229            | SUB_TYPE_KL_15MIN
230            | SUB_TYPE_KL_30MIN
231            | SUB_TYPE_KL_60MIN
232            | SUB_TYPE_KL_1MIN
233            | SUB_TYPE_KL_WEEK
234            | SUB_TYPE_KL_MONTH
235            | SUB_TYPE_KL_QUARTER
236            | SUB_TYPE_KL_YEAR
237            | SUB_TYPE_KL_3MIN
238            | SUB_TYPE_KL_10MIN
239            | SUB_TYPE_KL_120MIN
240            | SUB_TYPE_KL_180MIN
241            | SUB_TYPE_KL_240MIN
242    )
243}
244
245#[must_use]
246pub fn is_valid_sub_type(sub_type: i32) -> bool {
247    VALID_QOT_SUB_TYPES.contains(&sub_type)
248}
249
250#[must_use]
251pub fn qot_sub_type_from_str_alias(s: &str) -> Option<i32> {
252    Some(match s.trim().to_ascii_lowercase().as_str() {
253        "basic" => SUB_TYPE_BASIC,
254        "orderbook" | "order_book" => SUB_TYPE_ORDER_BOOK,
255        "orderbookodd" | "orderbook_odd" | "order-book-odd" | "order_book_odd"
256        | "odd_orderbook" | "odd-lot-orderbook" | "odd_lot_orderbook" => SUB_TYPE_ORDER_BOOK_ODD,
257        "ticker" => SUB_TYPE_TICKER,
258        "rt" => SUB_TYPE_RT,
259        "kl_day" | "kl-day" | "day" => SUB_TYPE_KL_DAY,
260        "kl_1min" | "kl-1min" | "1min" => SUB_TYPE_KL_1MIN,
261        "kl_3min" | "kl-3min" | "3min" => SUB_TYPE_KL_3MIN,
262        "kl_5min" | "kl-5min" | "5min" => SUB_TYPE_KL_5MIN,
263        "kl_10min" | "kl-10min" | "10min" => SUB_TYPE_KL_10MIN,
264        "kl_15min" | "kl-15min" | "15min" => SUB_TYPE_KL_15MIN,
265        "kl_30min" | "kl-30min" | "30min" => SUB_TYPE_KL_30MIN,
266        "kl_60min" | "kl-60min" | "60min" => SUB_TYPE_KL_60MIN,
267        "kl_120min" | "kl-120min" | "120min" => SUB_TYPE_KL_120MIN,
268        "kl_180min" | "kl-180min" | "180min" => SUB_TYPE_KL_180MIN,
269        "kl_240min" | "kl-240min" | "240min" => SUB_TYPE_KL_240MIN,
270        "kl_week" | "kl-week" | "week" => SUB_TYPE_KL_WEEK,
271        "kl_month" | "kl-month" | "month" => SUB_TYPE_KL_MONTH,
272        "kl_quarter" | "kl-quarter" | "quarter" => SUB_TYPE_KL_QUARTER,
273        "kl_year" | "kl-year" | "year" => SUB_TYPE_KL_YEAR,
274        "broker" => SUB_TYPE_BROKER,
275        _ => return None,
276    })
277}
278
279#[must_use]
280pub const fn qot_sub_type_short_label(sub_type: i32) -> Option<&'static str> {
281    Some(match sub_type {
282        SUB_TYPE_BASIC => "basic",
283        SUB_TYPE_ORDER_BOOK => "orderbook",
284        SUB_TYPE_ORDER_BOOK_ODD => "orderbook_odd",
285        SUB_TYPE_TICKER => "ticker",
286        SUB_TYPE_RT => "rt",
287        SUB_TYPE_KL_DAY => "kl_day",
288        SUB_TYPE_KL_1MIN => "kl_1min",
289        SUB_TYPE_KL_3MIN => "kl_3min",
290        SUB_TYPE_KL_5MIN => "kl_5min",
291        SUB_TYPE_KL_10MIN => "kl_10min",
292        SUB_TYPE_KL_15MIN => "kl_15min",
293        SUB_TYPE_KL_30MIN => "kl_30min",
294        SUB_TYPE_KL_60MIN => "kl_60min",
295        SUB_TYPE_KL_120MIN => "kl_120min",
296        SUB_TYPE_KL_180MIN => "kl_180min",
297        SUB_TYPE_KL_240MIN => "kl_240min",
298        SUB_TYPE_KL_WEEK => "kl_week",
299        SUB_TYPE_KL_MONTH => "kl_month",
300        SUB_TYPE_KL_QUARTER => "kl_quarter",
301        SUB_TYPE_KL_YEAR => "kl_year",
302        SUB_TYPE_BROKER => "broker",
303        _ => return None,
304    })
305}
306
307#[must_use]
308pub fn qot_kl_type_from_str_alias(s: &str) -> Option<i32> {
309    Some(match s.trim().to_ascii_lowercase().as_str() {
310        "day" => 2,
311        "week" => 3,
312        "month" => 4,
313        "quarter" => 11,
314        "year" => 5,
315        "1min" => 1,
316        "3min" => 10,
317        "5min" => 6,
318        "10min" => 12,
319        "15min" => 7,
320        "30min" => 8,
321        "60min" => 9,
322        "120min" => 13,
323        "180min" => 14,
324        "240min" => 15,
325        _ => return None,
326    })
327}
328
329/// Backend `FTCmdKline.KlineType` value for public FTAPI KLType.
330///
331/// Most values are identical, but 10/120/180/240 minute cycles are
332/// non-contiguous in backend protobuf.
333#[must_use]
334pub const fn qot_kl_type_backend_kline_type(kl_type: i32) -> Option<u32> {
335    Some(match kl_type {
336        1 => 1,
337        2 => 2,
338        3 => 3,
339        4 => 4,
340        5 => 5,
341        6 => 6,
342        7 => 7,
343        8 => 8,
344        9 => 9,
345        10 => 10,
346        11 => 11,
347        12 => 26,
348        13 => 14,
349        14 => 29,
350        15 => 15,
351        _ => return None,
352    })
353}
354
355#[must_use]
356pub fn unsupported_option_sub_type_name(sub_type: i32) -> Option<&'static str> {
357    match sub_type {
358        SUB_TYPE_NONE => Some("None"),
359        SUB_TYPE_KL_30MIN => Some("KL_30Min"),
360        SUB_TYPE_KL_WEEK => Some("KL_Week"),
361        SUB_TYPE_KL_MONTH => Some("KL_Month"),
362        SUB_TYPE_KL_QUARTER => Some("KL_Quarter"),
363        SUB_TYPE_KL_YEAR => Some("KL_Year"),
364        SUB_TYPE_KL_3MIN => Some("KL_3Min"),
365        _ => None,
366    }
367}
368
369#[must_use]
370pub const fn kl_type_for_sub_type(sub_type: i32) -> Option<i32> {
371    Some(match sub_type {
372        SUB_TYPE_KL_1MIN => 1,
373        SUB_TYPE_KL_DAY => 2,
374        SUB_TYPE_KL_WEEK => 3,
375        SUB_TYPE_KL_MONTH => 4,
376        SUB_TYPE_KL_YEAR => 5,
377        SUB_TYPE_KL_5MIN => 6,
378        SUB_TYPE_KL_15MIN => 7,
379        SUB_TYPE_KL_30MIN => 8,
380        SUB_TYPE_KL_60MIN => 9,
381        SUB_TYPE_KL_3MIN => 10,
382        SUB_TYPE_KL_QUARTER => 11,
383        SUB_TYPE_KL_10MIN => 12,
384        SUB_TYPE_KL_120MIN => 13,
385        SUB_TYPE_KL_180MIN => 14,
386        SUB_TYPE_KL_240MIN => 15,
387        _ => return None,
388    })
389}
390
391#[must_use]
392pub const fn sub_type_for_kl_type(kl_type: i32) -> Option<i32> {
393    Some(match kl_type {
394        1 => SUB_TYPE_KL_1MIN,
395        2 => SUB_TYPE_KL_DAY,
396        3 => SUB_TYPE_KL_WEEK,
397        4 => SUB_TYPE_KL_MONTH,
398        5 => SUB_TYPE_KL_YEAR,
399        6 => SUB_TYPE_KL_5MIN,
400        7 => SUB_TYPE_KL_15MIN,
401        8 => SUB_TYPE_KL_30MIN,
402        9 => SUB_TYPE_KL_60MIN,
403        10 => SUB_TYPE_KL_3MIN,
404        11 => SUB_TYPE_KL_QUARTER,
405        12 => SUB_TYPE_KL_10MIN,
406        13 => SUB_TYPE_KL_120MIN,
407        14 => SUB_TYPE_KL_180MIN,
408        15 => SUB_TYPE_KL_240MIN,
409        _ => return None,
410    })
411}
412
413#[must_use]
414pub const fn is_minute_kl_type(kl_type: i32) -> bool {
415    matches!(kl_type, 1 | 6 | 7 | 8 | 9 | 10 | 12 | 13 | 14 | 15)
416}
417
418#[must_use]
419pub fn is_valid_qot_market(market: i32) -> bool {
420    matches!(
421        market,
422        1 | 11 | 21 | 22 | 31 | 41 | 51 | 61 | 71 | 81 | 91 | 101
423    )
424}
425
426#[must_use]
427pub fn canonical_public_qot_market(market: i32) -> Option<i32> {
428    // Ref: FutuOpenD/proto/Qot_Common.proto:8-23 and
429    // APIServer_Inner_API.cpp:5513-5524. Public QotMarket=2 is the deprecated
430    // HK_Future alias and must be canonicalized before any legacy TrdMarket
431    // compatibility mapping can observe the same numeric value.
432    if market == 2 {
433        return Some(1);
434    }
435    if is_valid_qot_market(market) {
436        return Some(market);
437    }
438
439    match market {
440        3 => Some(21),
441        4 => Some(1),
442        5 => Some(1),
443        6 => Some(31),
444        7 => Some(91),
445        8 => Some(51),
446        15 => Some(41),
447        111 => Some(61),
448        112 => Some(71),
449        _ => None,
450    }
451}
452
453/// Explicit legacy TrdMarket compatibility mapping. This helper must never be
454/// used for a public QotMarket field because both namespaces assign value 2 to
455/// different markets (US versus HK_Future).
456#[must_use]
457pub const fn legacy_trd_market_to_qot_market(market: i32) -> Option<i32> {
458    match market {
459        2 => Some(11),
460        3 => Some(21),
461        4 | 5 => Some(1),
462        6 => Some(31),
463        7 => Some(91),
464        8 => Some(51),
465        15 => Some(41),
466        111 => Some(61),
467        112 => Some(71),
468        _ => None,
469    }
470}
471
472#[must_use]
473pub fn ftapi_market_to_quote_mkt(market: i32) -> u8 {
474    match market {
475        1 => 1,
476        11 => 2,
477        21 => 3,
478        22 => 4,
479        5 => 5,
480        6 => 6,
481        9 => 9,
482        13 => 13,
483        31 => 15,
484        15 => 7,
485        14 => 8,
486        16 => 16,
487        41 => 25,
488        61 => 27,
489        23 => 10,
490        91 => 17,
491        // Ref: NNBase_Define_Enum.h:542 NN_QuoteMktType_EventContract=35.
492        101 => 35,
493        _ => 0,
494    }
495}
496
497#[must_use]
498pub fn is_depth_sub_type(sub_type: i32) -> bool {
499    matches!(
500        sub_type,
501        SUB_TYPE_ORDER_BOOK | SUB_TYPE_ORDER_BOOK_ODD | SUB_TYPE_BROKER
502    )
503}
504
505#[must_use]
506pub fn empty_desired_market_for_sub(
507    ftapi_market: i32,
508    sub_type: i32,
509) -> Option<EmptyDesiredMarket> {
510    let mkt_type = ftapi_market_to_quote_mkt(ftapi_market);
511    if mkt_type == 0 {
512        return None;
513    }
514    Some(EmptyDesiredMarket {
515        mkt_type,
516        is_depth: is_depth_sub_type(sub_type),
517    })
518}
519
520#[must_use]
521pub fn backend_subscribe_market_for_info(
522    requested_market: i32,
523    info: &SubscriptionSecurityFacts,
524) -> i32 {
525    backend_subscribe_market_for_facts(requested_market, info.sec_type, info.mkt_id)
526}
527
528#[must_use]
529pub fn backend_subscribe_market_for_facts(
530    requested_market: i32,
531    sec_type: i32,
532    mkt_id: u32,
533) -> i32 {
534    backend_subscribe_market_for_security(requested_market, sec_type, mkt_id)
535}
536
537#[must_use]
538pub fn backend_desired_key_for_info(
539    sec_key: &str,
540    info: &SubscriptionSecurityFacts,
541) -> Option<(u64, i32)> {
542    backend_desired_key_for_sec_key(sec_key, info.stock_id, info.sec_type, info.mkt_id)
543}
544
545#[must_use]
546pub fn backend_subscribe_market_for_security(
547    requested_market: i32,
548    sec_type: i32,
549    mkt_id: u32,
550) -> i32 {
551    if sec_type == SECURITY_TYPE_DRVT {
552        return match mkt_id {
553            7 | 8 | 570..=579 => BACKEND_MARKET_HK_OPTION,
554            41..=45 => BACKEND_MARKET_US_OPTION,
555            _ => match requested_market {
556                1 => BACKEND_MARKET_HK_OPTION,
557                11 => BACKEND_MARKET_US_OPTION,
558                _ => requested_market,
559            },
560        };
561    }
562
563    if sec_type != SECURITY_TYPE_FUTURE {
564        return requested_market;
565    }
566
567    match mkt_id {
568        5 => 5,
569        6 | 110..=119 => 6,
570        60..=109 => 14,
571        160..=179 => 13,
572        185..=194 => 16,
573        1400..=1449 => requested_market,
574        _ => match requested_market {
575            1 | 2 => 6,
576            11 => 14,
577            31 => 13,
578            41 => 16,
579            _ => requested_market,
580        },
581    }
582}
583
584#[must_use]
585pub fn public_market_from_sec_key(sec_key: &str) -> Option<i32> {
586    QotSecurityKey::parse_public_sec_key(sec_key).map(|key| key.market)
587}
588
589#[must_use]
590pub fn backend_desired_key_for_sec_key(
591    sec_key: &str,
592    stock_id: u64,
593    sec_type: i32,
594    mkt_id: u32,
595) -> Option<(u64, i32)> {
596    if stock_id == 0 {
597        return None;
598    }
599    let public_market = public_market_from_sec_key(sec_key)?;
600    Some((
601        stock_id,
602        backend_subscribe_market_for_security(public_market, sec_type, mkt_id),
603    ))
604}