Skip to main content

futu_qot/
types.rs

1// 行情域通用类型
2// 从 proto 结构体转换为 Rust 友好的类型。
3
4/// 股票标识
5#[derive(Debug, Clone, PartialEq, Eq, Hash)]
6pub struct Security {
7    /// 所属市场(HK / US / SH / SZ / SG / JP / ...)
8    pub market: QotMarket,
9    /// 市场内的原始代码(如 HK `"00700"` / US `"NVDA"` / 期权 `"NVDA261219C150000"`)
10    pub code: String,
11}
12
13impl Security {
14    /// 构造 Security,`code` 支持 `&str` / `String` / 任何 `Into<String>`。
15    pub fn new(market: QotMarket, code: impl Into<String>) -> Self {
16        Self {
17            market,
18            code: code.into(),
19        }
20    }
21
22    /// 从 proto Security 转换
23    pub fn from_proto(s: &futu_proto::qot_common::Security) -> Self {
24        Self {
25            market: QotMarket::from_i32(s.market),
26            code: s.code.clone(),
27        }
28    }
29
30    /// 转换为 proto Security
31    pub fn to_proto(&self) -> futu_proto::qot_common::Security {
32        futu_proto::qot_common::Security {
33            market: self.market as i32,
34            code: self.code.clone(),
35        }
36    }
37}
38
39/// 市场类型
40#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
41#[repr(i32)]
42#[non_exhaustive]
43pub enum QotMarket {
44    Unknown = 0,
45    HkSecurity = 1,
46    HkFuture = 2,
47    UsSecurity = 11,
48    CnshSecurity = 21,
49    CnszSecurity = 22,
50    SgSecurity = 31,
51    JpSecurity = 41,
52    AuSecurity = 51,
53    MySecurity = 61,
54    CaSecurity = 71,
55    FxSecurity = 81,
56    // Ref: proto/Qot_Common.proto:22 `QotMarket_CC_Security = 91`.
57    // Public QOT crypto market; crypto trade still requires account/broker
58    // context and static-cache metadata before write-path routing.
59    Crypto = 91,
60}
61
62impl QotMarket {
63    /// 从 proto i32 值还原;未知值返 [`Self::Unknown`]。
64    pub fn from_i32(v: i32) -> Self {
65        match v {
66            1 => Self::HkSecurity,
67            2 => Self::HkFuture,
68            11 => Self::UsSecurity,
69            21 => Self::CnshSecurity,
70            22 => Self::CnszSecurity,
71            31 => Self::SgSecurity,
72            41 => Self::JpSecurity,
73            51 => Self::AuSecurity,
74            61 => Self::MySecurity,
75            71 => Self::CaSecurity,
76            81 => Self::FxSecurity,
77            91 => Self::Crypto,
78            _ => Self::Unknown,
79        }
80    }
81}
82
83/// 订阅类型
84#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
85#[repr(i32)]
86#[non_exhaustive]
87pub enum SubType {
88    None = 0,
89    Basic = 1,
90    OrderBook = 2,
91    OrderBookOdd = 22,
92    Ticker = 4,
93    RT = 5,
94    KLDay = 6,
95    KL5Min = 7,
96    KL15Min = 8,
97    KL30Min = 9,
98    KL60Min = 10,
99    KL1Min = 11,
100    KLWeek = 12,
101    KLMonth = 13,
102    Broker = 14,
103    KLQuarter = 15,
104    KLYear = 16,
105    KL3Min = 17,
106    KL10Min = 18,
107    KL120Min = 19,
108    KL180Min = 20,
109    KL240Min = 21,
110    /// Deprecated compatibility shim. `OrderDetail` is a separate API
111    /// (`Qot_GetOrderDetail`), not a `Qot_Common.SubType`. Keep this variant
112    /// outside the valid subtype range so old source references fail closed
113    /// instead of silently subscribing `KL_10Min`.
114    ///
115    /// Removal trigger: delete this shim after two consecutive minor releases
116    /// have no downstream compile reports referencing `SubType::OrderDetail`.
117    #[deprecated(
118        since = "1.4.113",
119        note = "OrderDetail is not a Qot_Common.SubType; use the order-detail API surface"
120    )]
121    OrderDetail = -3016,
122}
123
124impl SubType {
125    /// Valid public `Qot_Common.SubType` values accepted by subscribe surfaces.
126    ///
127    /// `None=0` is intentionally excluded: C++ treats it as an unsupported
128    /// request value, while some option-support checks still need to name it.
129    pub const VALID_PUBLIC: &'static [Self] = &[
130        Self::Basic,
131        Self::OrderBook,
132        Self::Ticker,
133        Self::RT,
134        Self::KLDay,
135        Self::KL5Min,
136        Self::KL15Min,
137        Self::KL30Min,
138        Self::KL60Min,
139        Self::KL1Min,
140        Self::KLWeek,
141        Self::KLMonth,
142        Self::Broker,
143        Self::KLQuarter,
144        Self::KLYear,
145        Self::KL3Min,
146        Self::KL10Min,
147        Self::KL120Min,
148        Self::KL180Min,
149        Self::KL240Min,
150        Self::OrderBookOdd,
151    ];
152
153    pub const VALID_PUBLIC_INT_VALUES: &'static [i32] = &[
154        Self::Basic as i32,
155        Self::OrderBook as i32,
156        Self::Ticker as i32,
157        Self::RT as i32,
158        Self::KLDay as i32,
159        Self::KL5Min as i32,
160        Self::KL15Min as i32,
161        Self::KL30Min as i32,
162        Self::KL60Min as i32,
163        Self::KL1Min as i32,
164        Self::KLWeek as i32,
165        Self::KLMonth as i32,
166        Self::Broker as i32,
167        Self::KLQuarter as i32,
168        Self::KLYear as i32,
169        Self::KL3Min as i32,
170        Self::KL10Min as i32,
171        Self::KL120Min as i32,
172        Self::KL180Min as i32,
173        Self::KL240Min as i32,
174        Self::OrderBookOdd as i32,
175    ];
176
177    pub const fn as_i32(self) -> i32 {
178        self as i32
179    }
180
181    pub const fn from_i32(v: i32) -> Option<Self> {
182        Some(match v {
183            0 => Self::None,
184            1 => Self::Basic,
185            2 => Self::OrderBook,
186            4 => Self::Ticker,
187            5 => Self::RT,
188            6 => Self::KLDay,
189            7 => Self::KL5Min,
190            8 => Self::KL15Min,
191            9 => Self::KL30Min,
192            10 => Self::KL60Min,
193            11 => Self::KL1Min,
194            12 => Self::KLWeek,
195            13 => Self::KLMonth,
196            14 => Self::Broker,
197            15 => Self::KLQuarter,
198            16 => Self::KLYear,
199            17 => Self::KL3Min,
200            18 => Self::KL10Min,
201            19 => Self::KL120Min,
202            20 => Self::KL180Min,
203            21 => Self::KL240Min,
204            22 => Self::OrderBookOdd,
205            _ => return None,
206        })
207    }
208
209    pub const fn from_public_i32(v: i32) -> Option<Self> {
210        match Self::from_i32(v) {
211            Some(Self::None) | None => None,
212            Some(sub_type) => Some(sub_type),
213        }
214    }
215
216    pub fn from_str_alias(s: &str) -> Option<Self> {
217        Some(match s.trim().to_ascii_lowercase().as_str() {
218            "basic" => Self::Basic,
219            "orderbook" | "order_book" => Self::OrderBook,
220            "orderbookodd" | "orderbook_odd" | "order-book-odd" | "order_book_odd"
221            | "odd_orderbook" | "odd-lot-orderbook" | "odd_lot_orderbook" => Self::OrderBookOdd,
222            "ticker" => Self::Ticker,
223            "rt" => Self::RT,
224            "kl_day" | "kl-day" | "day" => Self::KLDay,
225            "kl_1min" | "kl-1min" | "1min" => Self::KL1Min,
226            "kl_3min" | "kl-3min" | "3min" => Self::KL3Min,
227            "kl_5min" | "kl-5min" | "5min" => Self::KL5Min,
228            "kl_10min" | "kl-10min" | "10min" => Self::KL10Min,
229            "kl_15min" | "kl-15min" | "15min" => Self::KL15Min,
230            "kl_30min" | "kl-30min" | "30min" => Self::KL30Min,
231            "kl_60min" | "kl-60min" | "60min" => Self::KL60Min,
232            "kl_120min" | "kl-120min" | "120min" => Self::KL120Min,
233            "kl_180min" | "kl-180min" | "180min" => Self::KL180Min,
234            "kl_240min" | "kl-240min" | "240min" => Self::KL240Min,
235            "kl_week" | "kl-week" | "week" => Self::KLWeek,
236            "kl_month" | "kl-month" | "month" => Self::KLMonth,
237            "kl_quarter" | "kl-quarter" | "quarter" => Self::KLQuarter,
238            "kl_year" | "kl-year" | "year" => Self::KLYear,
239            "broker" => Self::Broker,
240            _ => return None,
241        })
242    }
243
244    pub const fn is_kline(self) -> bool {
245        matches!(
246            self,
247            Self::KLDay
248                | Self::KL5Min
249                | Self::KL15Min
250                | Self::KL30Min
251                | Self::KL60Min
252                | Self::KL1Min
253                | Self::KLWeek
254                | Self::KLMonth
255                | Self::KLQuarter
256                | Self::KLYear
257                | Self::KL3Min
258                | Self::KL10Min
259                | Self::KL120Min
260                | Self::KL180Min
261                | Self::KL240Min
262        )
263    }
264
265    pub const fn kl_type(self) -> Option<KLType> {
266        Some(match self {
267            Self::KL1Min => KLType::Min1,
268            Self::KLDay => KLType::Day,
269            Self::KLWeek => KLType::Week,
270            Self::KLMonth => KLType::Month,
271            Self::KLYear => KLType::Year,
272            Self::KL5Min => KLType::Min5,
273            Self::KL15Min => KLType::Min15,
274            Self::KL30Min => KLType::Min30,
275            Self::KL60Min => KLType::Min60,
276            Self::KL3Min => KLType::Min3,
277            Self::KLQuarter => KLType::Quarter,
278            Self::KL10Min => KLType::Min10,
279            Self::KL120Min => KLType::Min120,
280            Self::KL180Min => KLType::Min180,
281            Self::KL240Min => KLType::Min240,
282            _ => return None,
283        })
284    }
285
286    pub const fn short_label(self) -> Option<&'static str> {
287        Some(match self {
288            Self::Basic => "basic",
289            Self::OrderBook => "orderbook",
290            Self::OrderBookOdd => "orderbook_odd",
291            Self::Ticker => "ticker",
292            Self::RT => "rt",
293            Self::KLDay => "kl_day",
294            Self::KL1Min => "kl_1min",
295            Self::KL3Min => "kl_3min",
296            Self::KL5Min => "kl_5min",
297            Self::KL10Min => "kl_10min",
298            Self::KL15Min => "kl_15min",
299            Self::KL30Min => "kl_30min",
300            Self::KL60Min => "kl_60min",
301            Self::KL120Min => "kl_120min",
302            Self::KL180Min => "kl_180min",
303            Self::KL240Min => "kl_240min",
304            Self::KLWeek => "kl_week",
305            Self::KLMonth => "kl_month",
306            Self::KLQuarter => "kl_quarter",
307            Self::KLYear => "kl_year",
308            Self::Broker => "broker",
309            _ => return None,
310        })
311    }
312}
313
314/// K 线类型
315#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
316#[repr(i32)]
317#[non_exhaustive]
318pub enum KLType {
319    Unknown = 0,
320    Min1 = 1,
321    Day = 2,
322    Week = 3,
323    Month = 4,
324    Year = 5,
325    Min5 = 6,
326    Min15 = 7,
327    Min30 = 8,
328    Min60 = 9,
329    Min3 = 10,
330    Quarter = 11,
331    Min10 = 12,
332    Min120 = 13,
333    Min180 = 14,
334    Min240 = 15,
335}
336
337impl KLType {
338    pub const VALID_PUBLIC: &'static [Self] = &[
339        Self::Min1,
340        Self::Day,
341        Self::Week,
342        Self::Month,
343        Self::Year,
344        Self::Min5,
345        Self::Min15,
346        Self::Min30,
347        Self::Min60,
348        Self::Min3,
349        Self::Quarter,
350        Self::Min10,
351        Self::Min120,
352        Self::Min180,
353        Self::Min240,
354    ];
355
356    pub const fn as_i32(self) -> i32 {
357        self as i32
358    }
359
360    pub const fn from_i32(v: i32) -> Option<Self> {
361        Some(match v {
362            0 => Self::Unknown,
363            1 => Self::Min1,
364            2 => Self::Day,
365            3 => Self::Week,
366            4 => Self::Month,
367            5 => Self::Year,
368            6 => Self::Min5,
369            7 => Self::Min15,
370            8 => Self::Min30,
371            9 => Self::Min60,
372            10 => Self::Min3,
373            11 => Self::Quarter,
374            12 => Self::Min10,
375            13 => Self::Min120,
376            14 => Self::Min180,
377            15 => Self::Min240,
378            _ => return None,
379        })
380    }
381
382    pub const fn from_public_i32(v: i32) -> Option<Self> {
383        match Self::from_i32(v) {
384            Some(Self::Unknown) | None => None,
385            Some(kl_type) => Some(kl_type),
386        }
387    }
388
389    pub fn from_str_alias(s: &str) -> Option<Self> {
390        Some(match s.trim().to_ascii_lowercase().as_str() {
391            "day" => Self::Day,
392            "week" => Self::Week,
393            "month" => Self::Month,
394            "quarter" => Self::Quarter,
395            "year" => Self::Year,
396            "1min" => Self::Min1,
397            "3min" => Self::Min3,
398            "5min" => Self::Min5,
399            "10min" => Self::Min10,
400            "15min" => Self::Min15,
401            "30min" => Self::Min30,
402            "60min" => Self::Min60,
403            "120min" => Self::Min120,
404            "180min" => Self::Min180,
405            "240min" => Self::Min240,
406            _ => return None,
407        })
408    }
409
410    pub const fn is_minute(self) -> bool {
411        matches!(
412            self,
413            Self::Min1
414                | Self::Min3
415                | Self::Min5
416                | Self::Min10
417                | Self::Min15
418                | Self::Min30
419                | Self::Min60
420                | Self::Min120
421                | Self::Min180
422                | Self::Min240
423        )
424    }
425
426    pub const fn sub_type(self) -> Option<SubType> {
427        Some(match self {
428            Self::Min1 => SubType::KL1Min,
429            Self::Day => SubType::KLDay,
430            Self::Week => SubType::KLWeek,
431            Self::Month => SubType::KLMonth,
432            Self::Year => SubType::KLYear,
433            Self::Min5 => SubType::KL5Min,
434            Self::Min15 => SubType::KL15Min,
435            Self::Min30 => SubType::KL30Min,
436            Self::Min60 => SubType::KL60Min,
437            Self::Min3 => SubType::KL3Min,
438            Self::Quarter => SubType::KLQuarter,
439            Self::Min10 => SubType::KL10Min,
440            Self::Min120 => SubType::KL120Min,
441            Self::Min180 => SubType::KL180Min,
442            Self::Min240 => SubType::KL240Min,
443            Self::Unknown => return None,
444        })
445    }
446
447    /// Backend `FTCmdKline.KlineType` value for this public FTAPI KLType.
448    ///
449    /// Most values are identical, but 10/120/180/240 minute cycles are
450    /// non-contiguous in backend protobuf.
451    pub const fn backend_kline_type(self) -> Option<u32> {
452        Some(match self {
453            Self::Min1 => 1,
454            Self::Day => 2,
455            Self::Week => 3,
456            Self::Month => 4,
457            Self::Year => 5,
458            Self::Min5 => 6,
459            Self::Min15 => 7,
460            Self::Min30 => 8,
461            Self::Min60 => 9,
462            Self::Min3 => 10,
463            Self::Quarter => 11,
464            Self::Min10 => 26,
465            Self::Min120 => 14,
466            Self::Min180 => 29,
467            Self::Min240 => 15,
468            Self::Unknown => return None,
469        })
470    }
471}
472
473/// 复权类型
474#[derive(Debug, Clone, Copy, PartialEq, Eq)]
475#[repr(i32)]
476#[non_exhaustive]
477pub enum RehabType {
478    None = 0,
479    Forward = 1,
480    Backward = 2,
481}
482
483/// K 线数据点
484#[derive(Debug, Clone)]
485pub struct KLine {
486    /// 所属 K 线时间(字符串表示,如 `"2026-04-21 09:30:00"`)
487    pub time: String,
488    /// 是否为空白 K 线(该时间点无成交)
489    pub is_blank: bool,
490    /// 最高价
491    pub high_price: f64,
492    /// 开盘价
493    pub open_price: f64,
494    /// 最低价
495    pub low_price: f64,
496    /// 收盘价
497    pub close_price: f64,
498    /// 昨收价
499    pub last_close_price: f64,
500    /// 成交量(股 / 手 / 合约数,视产品而定)
501    pub volume: i64,
502    /// 成交额
503    pub turnover: f64,
504    /// 换手率(百分比)
505    pub turnover_rate: f64,
506    /// 市盈率
507    pub pe: f64,
508    /// 涨跌幅(百分比,含正负)
509    pub change_rate: f64,
510    /// Unix 秒时间戳(用于排序 / 对齐 tz)
511    pub timestamp: f64,
512}
513
514impl KLine {
515    pub fn from_proto(k: &futu_proto::qot_common::KLine) -> Self {
516        Self {
517            time: k.time.clone(),
518            is_blank: k.is_blank,
519            high_price: k.high_price.unwrap_or(0.0),
520            open_price: k.open_price.unwrap_or(0.0),
521            low_price: k.low_price.unwrap_or(0.0),
522            close_price: k.close_price.unwrap_or(0.0),
523            last_close_price: k.last_close_price.unwrap_or(0.0),
524            volume: k.volume.unwrap_or(0),
525            turnover: k.turnover.unwrap_or(0.0),
526            turnover_rate: k.turnover_rate.unwrap_or(0.0),
527            pe: k.pe.unwrap_or(0.0),
528            change_rate: k.change_rate.unwrap_or(0.0),
529            timestamp: k.timestamp.unwrap_or(0.0),
530        }
531    }
532}
533
534/// 基本行情数据
535#[derive(Debug, Clone)]
536pub struct BasicQot {
537    /// 对应证券
538    pub security: Security,
539    /// 是否停牌
540    pub is_suspended: bool,
541    /// 上市日期
542    pub list_time: String,
543    /// 价位(最小变动价,spread)
544    pub price_spread: f64,
545    /// 最新报价更新时间(`"YYYY-MM-DD HH:MM:SS"`)
546    pub update_time: String,
547    /// 今日最高价
548    pub high_price: f64,
549    /// 今日开盘价
550    pub open_price: f64,
551    /// 今日最低价
552    pub low_price: f64,
553    /// 现价(最新成交价)
554    pub cur_price: f64,
555    /// 昨收价
556    pub last_close_price: f64,
557    /// 成交量
558    pub volume: i64,
559    /// 成交额
560    pub turnover: f64,
561    /// 换手率(百分比)
562    pub turnover_rate: f64,
563    /// 振幅(`(high - low) / last_close`,百分比)
564    pub amplitude: f64,
565}
566
567impl BasicQot {
568    pub fn from_proto(q: &futu_proto::qot_common::BasicQot) -> Self {
569        Self {
570            security: Security::from_proto(&q.security),
571            is_suspended: q.is_suspended,
572            list_time: q.list_time.clone(),
573            price_spread: q.price_spread,
574            update_time: q.update_time.clone(),
575            high_price: q.high_price,
576            open_price: q.open_price,
577            low_price: q.low_price,
578            cur_price: q.cur_price,
579            last_close_price: q.last_close_price,
580            volume: q.volume,
581            turnover: q.turnover,
582            turnover_rate: q.turnover_rate,
583            amplitude: q.amplitude,
584        }
585    }
586}
587
588/// 摆盘数据项
589#[derive(Debug, Clone)]
590pub struct OrderBookEntry {
591    /// 档位价格
592    pub price: f64,
593    /// 该档位合计挂单量
594    pub volume: i64,
595    /// 该档位挂单笔数
596    pub order_count: i32,
597}
598
599impl OrderBookEntry {
600    pub fn from_proto(ob: &futu_proto::qot_common::OrderBook) -> Self {
601        Self {
602            price: ob.price,
603            volume: ob.volume,
604            order_count: ob.oreder_count, // proto 中拼写为 oreder_count
605        }
606    }
607}
608
609/// 摆盘数据
610#[derive(Debug, Clone)]
611pub struct OrderBookData {
612    /// 对应证券
613    pub security: Security,
614    /// 卖档列表(升序,index 0 为卖一)
615    pub ask_list: Vec<OrderBookEntry>,
616    /// 买档列表(降序,index 0 为买一)
617    pub bid_list: Vec<OrderBookEntry>,
618}
619
620/// `Qot_Common.QotMarketState` enum → 人读 label。
621///
622/// 严格对齐 `proto/Qot_Common.proto:83-124` 的 QotMarketState 枚举(含夜市 /
623/// 期货日市 / HkCas 港股收盘竞价 / 美股夜盘等)。
624///
625/// 历史:v1.4.25 加 `market-state` 命令时把 label 写错了(`Closed=6` 被叫
626/// 成 `ClosedToday` 等),v1.4.30 严格按 proto 重写。v1.4.31 从 futucli 抽
627/// 到这里统一维护,避免两处拷贝再次漂移。
628pub fn market_state_label(s: i32) -> &'static str {
629    match s {
630        0 => "None",                  // 无交易
631        1 => "Auction",               // 竞价
632        2 => "WaitingOpen",           // 早盘前等待开盘
633        3 => "Morning",               // 早盘
634        4 => "Rest",                  // 午间休市
635        5 => "Afternoon",             // 午盘
636        6 => "Closed",                // 收盘
637        8 => "PreMarketBegin",        // 盘前
638        9 => "PreMarketEnd",          // 盘前结束
639        10 => "AfterHoursBegin",      // 盘后
640        11 => "AfterHoursEnd",        // 盘后结束
641        12 => "FutuSwitchDate",       // 切换日
642        13 => "NightOpen",            // 夜市开盘
643        14 => "NightEnd",             // 夜市收盘
644        15 => "FutureDayOpen",        // 期货日市开盘
645        16 => "FutureDayBreak",       // 期货日市休市
646        17 => "FutureDayClose",       // 期货日市收盘
647        18 => "FutureDayWaitForOpen", // 期货日市等待开盘
648        19 => "HkCas",                // 港股收盘竞价
649        20 => "FutureNightWait",      // 夜市等待开盘(已废弃)
650        21 => "FutureAfternoon",      // 期货下午开盘(已废弃)
651        22 => "FutureSwitchDate",     // 期货切交易日(已废弃)
652        23 => "FutureOpen",           // 期货开盘
653        24 => "FutureBreak",          // 期货中盘休息
654        25 => "FutureBreakOver",      // 期货休息后开盘
655        26 => "FutureClose",          // 期货收盘
656        27 => "StibAfterHoursWait",   // 科创板盘后撮合等待(已废弃)
657        28 => "StibAfterHoursBegin",  // 科创板盘后交易开始(已废弃)
658        29 => "StibAfterHoursEnd",    // 科创板盘后交易结束(已废弃)
659        30 => "CloseAuction",         // 收市竞价
660        31 => "AfternoonEnd",         // 已收盘
661        32 => "Night",                // 交易中
662        33 => "OvernightBegin",       // 夜盘开始
663        34 => "OvernightEnd",         // 夜盘结束
664        35 => "TradeAtLast",          // 收盘前成交
665        36 => "TradeAuction",         // 收盘前竞价
666        37 => "Overnight",            // 美股夜盘交易时段
667        _ => "Unknown",
668    }
669}
670
671#[cfg(test)]
672mod tests;