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    // Ref: proto/Qot_Common.proto:23 `QotMarket_EventContract = 101` and
61    // FutuOpenD/Src/APIServer/APIServer_Inner_API.cpp:5306-5307 (`EC.`).
62    // This is a public protocol value, not a runtime-configured market ID.
63    EventContract = 101,
64}
65
66impl QotMarket {
67    /// 从 proto i32 值还原;未知值返 [`Self::Unknown`]。
68    pub fn from_i32(v: i32) -> Self {
69        match v {
70            1 => Self::HkSecurity,
71            2 => Self::HkFuture,
72            11 => Self::UsSecurity,
73            21 => Self::CnshSecurity,
74            22 => Self::CnszSecurity,
75            31 => Self::SgSecurity,
76            41 => Self::JpSecurity,
77            51 => Self::AuSecurity,
78            61 => Self::MySecurity,
79            71 => Self::CaSecurity,
80            81 => Self::FxSecurity,
81            91 => Self::Crypto,
82            101 => Self::EventContract,
83            _ => Self::Unknown,
84        }
85    }
86}
87
88/// 订阅类型
89#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
90#[repr(i32)]
91#[non_exhaustive]
92pub enum SubType {
93    None = 0,
94    Basic = 1,
95    OrderBook = 2,
96    OrderBookOdd = 22,
97    Ticker = 4,
98    RT = 5,
99    KLDay = 6,
100    KL5Min = 7,
101    KL15Min = 8,
102    KL30Min = 9,
103    KL60Min = 10,
104    KL1Min = 11,
105    KLWeek = 12,
106    KLMonth = 13,
107    Broker = 14,
108    KLQuarter = 15,
109    KLYear = 16,
110    KL3Min = 17,
111    KL10Min = 18,
112    KL120Min = 19,
113    KL180Min = 20,
114    KL240Min = 21,
115    /// Deprecated compatibility shim. `OrderDetail` is a separate API
116    /// (`Qot_GetOrderDetail`), not a `Qot_Common.SubType`. Keep this variant
117    /// outside the valid subtype range so old source references fail closed
118    /// instead of silently subscribing `KL_10Min`.
119    ///
120    /// Removal trigger: delete this shim after two consecutive minor releases
121    /// have no downstream compile reports referencing `SubType::OrderDetail`.
122    #[deprecated(
123        since = "1.4.113",
124        note = "OrderDetail is not a Qot_Common.SubType; use the order-detail API surface"
125    )]
126    OrderDetail = -3016,
127}
128
129impl SubType {
130    /// Valid public `Qot_Common.SubType` values accepted by subscribe surfaces.
131    ///
132    /// `None=0` is intentionally excluded: C++ treats it as an unsupported
133    /// request value, while some option-support checks still need to name it.
134    pub const VALID_PUBLIC: &'static [Self] = &[
135        Self::Basic,
136        Self::OrderBook,
137        Self::Ticker,
138        Self::RT,
139        Self::KLDay,
140        Self::KL5Min,
141        Self::KL15Min,
142        Self::KL30Min,
143        Self::KL60Min,
144        Self::KL1Min,
145        Self::KLWeek,
146        Self::KLMonth,
147        Self::Broker,
148        Self::KLQuarter,
149        Self::KLYear,
150        Self::KL3Min,
151        Self::KL10Min,
152        Self::KL120Min,
153        Self::KL180Min,
154        Self::KL240Min,
155        Self::OrderBookOdd,
156    ];
157
158    pub const VALID_PUBLIC_INT_VALUES: &'static [i32] = &[
159        Self::Basic as i32,
160        Self::OrderBook as i32,
161        Self::Ticker as i32,
162        Self::RT as i32,
163        Self::KLDay as i32,
164        Self::KL5Min as i32,
165        Self::KL15Min as i32,
166        Self::KL30Min as i32,
167        Self::KL60Min as i32,
168        Self::KL1Min as i32,
169        Self::KLWeek as i32,
170        Self::KLMonth as i32,
171        Self::Broker as i32,
172        Self::KLQuarter as i32,
173        Self::KLYear as i32,
174        Self::KL3Min as i32,
175        Self::KL10Min as i32,
176        Self::KL120Min as i32,
177        Self::KL180Min as i32,
178        Self::KL240Min as i32,
179        Self::OrderBookOdd as i32,
180    ];
181
182    pub const fn as_i32(self) -> i32 {
183        self as i32
184    }
185
186    pub const fn from_i32(v: i32) -> Option<Self> {
187        Some(match v {
188            0 => Self::None,
189            1 => Self::Basic,
190            2 => Self::OrderBook,
191            4 => Self::Ticker,
192            5 => Self::RT,
193            6 => Self::KLDay,
194            7 => Self::KL5Min,
195            8 => Self::KL15Min,
196            9 => Self::KL30Min,
197            10 => Self::KL60Min,
198            11 => Self::KL1Min,
199            12 => Self::KLWeek,
200            13 => Self::KLMonth,
201            14 => Self::Broker,
202            15 => Self::KLQuarter,
203            16 => Self::KLYear,
204            17 => Self::KL3Min,
205            18 => Self::KL10Min,
206            19 => Self::KL120Min,
207            20 => Self::KL180Min,
208            21 => Self::KL240Min,
209            22 => Self::OrderBookOdd,
210            _ => return None,
211        })
212    }
213
214    pub const fn from_public_i32(v: i32) -> Option<Self> {
215        match Self::from_i32(v) {
216            Some(Self::None) | None => None,
217            Some(sub_type) => Some(sub_type),
218        }
219    }
220
221    pub fn from_str_alias(s: &str) -> Option<Self> {
222        futu_core::qot_subscription::qot_sub_type_from_str_alias(s).and_then(Self::from_i32)
223    }
224
225    pub const fn is_kline(self) -> bool {
226        matches!(
227            self,
228            Self::KLDay
229                | Self::KL5Min
230                | Self::KL15Min
231                | Self::KL30Min
232                | Self::KL60Min
233                | Self::KL1Min
234                | Self::KLWeek
235                | Self::KLMonth
236                | Self::KLQuarter
237                | Self::KLYear
238                | Self::KL3Min
239                | Self::KL10Min
240                | Self::KL120Min
241                | Self::KL180Min
242                | Self::KL240Min
243        )
244    }
245
246    pub const fn kl_type(self) -> Option<KLType> {
247        Some(match self {
248            Self::KL1Min => KLType::Min1,
249            Self::KLDay => KLType::Day,
250            Self::KLWeek => KLType::Week,
251            Self::KLMonth => KLType::Month,
252            Self::KLYear => KLType::Year,
253            Self::KL5Min => KLType::Min5,
254            Self::KL15Min => KLType::Min15,
255            Self::KL30Min => KLType::Min30,
256            Self::KL60Min => KLType::Min60,
257            Self::KL3Min => KLType::Min3,
258            Self::KLQuarter => KLType::Quarter,
259            Self::KL10Min => KLType::Min10,
260            Self::KL120Min => KLType::Min120,
261            Self::KL180Min => KLType::Min180,
262            Self::KL240Min => KLType::Min240,
263            _ => return None,
264        })
265    }
266
267    pub const fn short_label(self) -> Option<&'static str> {
268        futu_core::qot_subscription::qot_sub_type_short_label(self as i32)
269    }
270}
271
272/// K 线类型
273#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
274#[repr(i32)]
275#[non_exhaustive]
276pub enum KLType {
277    Unknown = 0,
278    Min1 = 1,
279    Day = 2,
280    Week = 3,
281    Month = 4,
282    Year = 5,
283    Min5 = 6,
284    Min15 = 7,
285    Min30 = 8,
286    Min60 = 9,
287    Min3 = 10,
288    Quarter = 11,
289    Min10 = 12,
290    Min120 = 13,
291    Min180 = 14,
292    Min240 = 15,
293}
294
295impl KLType {
296    pub const VALID_PUBLIC: &'static [Self] = &[
297        Self::Min1,
298        Self::Day,
299        Self::Week,
300        Self::Month,
301        Self::Year,
302        Self::Min5,
303        Self::Min15,
304        Self::Min30,
305        Self::Min60,
306        Self::Min3,
307        Self::Quarter,
308        Self::Min10,
309        Self::Min120,
310        Self::Min180,
311        Self::Min240,
312    ];
313
314    pub const fn as_i32(self) -> i32 {
315        self as i32
316    }
317
318    pub const fn from_i32(v: i32) -> Option<Self> {
319        Some(match v {
320            0 => Self::Unknown,
321            1 => Self::Min1,
322            2 => Self::Day,
323            3 => Self::Week,
324            4 => Self::Month,
325            5 => Self::Year,
326            6 => Self::Min5,
327            7 => Self::Min15,
328            8 => Self::Min30,
329            9 => Self::Min60,
330            10 => Self::Min3,
331            11 => Self::Quarter,
332            12 => Self::Min10,
333            13 => Self::Min120,
334            14 => Self::Min180,
335            15 => Self::Min240,
336            _ => return None,
337        })
338    }
339
340    pub const fn from_public_i32(v: i32) -> Option<Self> {
341        match Self::from_i32(v) {
342            Some(Self::Unknown) | None => None,
343            Some(kl_type) => Some(kl_type),
344        }
345    }
346
347    pub fn from_str_alias(s: &str) -> Option<Self> {
348        futu_core::qot_subscription::qot_kl_type_from_str_alias(s).and_then(Self::from_i32)
349    }
350
351    pub const fn is_minute(self) -> bool {
352        futu_core::qot_subscription::is_minute_kl_type(self as i32)
353    }
354
355    pub const fn sub_type(self) -> Option<SubType> {
356        match futu_core::qot_subscription::sub_type_for_kl_type(self as i32) {
357            Some(sub_type) => SubType::from_i32(sub_type),
358            None => None,
359        }
360    }
361
362    /// Backend `FTCmdKline.KlineType` value for this public FTAPI KLType.
363    ///
364    /// Most values are identical, but 10/120/180/240 minute cycles are
365    /// non-contiguous in backend protobuf.
366    pub const fn backend_kline_type(self) -> Option<u32> {
367        futu_core::qot_subscription::qot_kl_type_backend_kline_type(self as i32)
368    }
369}
370
371/// 复权类型
372#[derive(Debug, Clone, Copy, PartialEq, Eq)]
373#[repr(i32)]
374#[non_exhaustive]
375pub enum RehabType {
376    None = 0,
377    Forward = 1,
378    Backward = 2,
379}
380
381/// K 线数据点
382#[derive(Debug, Clone)]
383pub struct KLine {
384    /// 所属 K 线时间(字符串表示,如 `"2026-04-21 09:30:00"`)
385    pub time: String,
386    /// 是否为空白 K 线(该时间点无成交)
387    pub is_blank: bool,
388    /// 最高价
389    pub high_price: f64,
390    /// 开盘价
391    pub open_price: f64,
392    /// 最低价
393    pub low_price: f64,
394    /// 收盘价
395    pub close_price: f64,
396    /// 昨收价
397    pub last_close_price: f64,
398    /// 成交量(股 / 手 / 合约数,视产品而定)
399    pub volume: i64,
400    /// 成交额
401    pub turnover: f64,
402    /// 换手率(百分比)
403    pub turnover_rate: f64,
404    /// 市盈率
405    pub pe: f64,
406    /// 涨跌幅(百分比,含正负)
407    pub change_rate: f64,
408    /// Unix 秒时间戳(用于排序 / 对齐 tz)
409    pub timestamp: f64,
410}
411
412impl KLine {
413    pub fn from_proto(k: &futu_proto::qot_common::KLine) -> Self {
414        Self {
415            time: k.time.clone(),
416            is_blank: k.is_blank,
417            high_price: k.high_price.unwrap_or(0.0),
418            open_price: k.open_price.unwrap_or(0.0),
419            low_price: k.low_price.unwrap_or(0.0),
420            close_price: k.close_price.unwrap_or(0.0),
421            last_close_price: k.last_close_price.unwrap_or(0.0),
422            volume: k.volume.unwrap_or(0),
423            turnover: k.turnover.unwrap_or(0.0),
424            turnover_rate: k.turnover_rate.unwrap_or(0.0),
425            pe: k.pe.unwrap_or(0.0),
426            change_rate: k.change_rate.unwrap_or(0.0),
427            timestamp: k.timestamp.unwrap_or(0.0),
428        }
429    }
430}
431
432/// 基本行情数据
433#[derive(Debug, Clone)]
434pub struct BasicQot {
435    /// 对应证券
436    pub security: Security,
437    /// 是否停牌
438    pub is_suspended: bool,
439    /// 上市日期
440    pub list_time: String,
441    /// 价位(最小变动价,spread)
442    pub price_spread: f64,
443    /// 最新报价更新时间(`"YYYY-MM-DD HH:MM:SS"`)
444    pub update_time: String,
445    /// 今日最高价
446    pub high_price: f64,
447    /// 今日开盘价
448    pub open_price: f64,
449    /// 今日最低价
450    pub low_price: f64,
451    /// 现价(最新成交价)
452    pub cur_price: f64,
453    /// 昨收价
454    pub last_close_price: f64,
455    /// 成交量
456    pub volume: i64,
457    /// 成交额
458    pub turnover: f64,
459    /// 换手率(百分比)
460    pub turnover_rate: f64,
461    /// 振幅(`(high - low) / last_close`,百分比)
462    pub amplitude: f64,
463}
464
465impl BasicQot {
466    pub fn from_proto(q: &futu_proto::qot_common::BasicQot) -> Self {
467        Self {
468            security: Security::from_proto(&q.security),
469            is_suspended: q.is_suspended,
470            list_time: q.list_time.clone(),
471            price_spread: q.price_spread,
472            update_time: q.update_time.clone(),
473            high_price: q.high_price,
474            open_price: q.open_price,
475            low_price: q.low_price,
476            cur_price: q.cur_price,
477            last_close_price: q.last_close_price,
478            volume: q.volume,
479            turnover: q.turnover,
480            turnover_rate: q.turnover_rate,
481            amplitude: q.amplitude,
482        }
483    }
484}
485
486/// 摆盘数据项
487#[derive(Debug, Clone)]
488pub struct OrderBookEntry {
489    /// 档位价格
490    pub price: f64,
491    /// 该档位合计挂单量
492    pub volume: i64,
493    /// 该档位挂单笔数
494    pub order_count: i32,
495}
496
497impl OrderBookEntry {
498    pub fn from_proto(ob: &futu_proto::qot_common::OrderBook) -> Self {
499        Self {
500            price: ob.price,
501            volume: ob.volume,
502            order_count: ob.oreder_count, // proto 中拼写为 oreder_count
503        }
504    }
505}
506
507/// 摆盘数据
508#[derive(Debug, Clone)]
509pub struct OrderBookData {
510    /// 对应证券
511    pub security: Security,
512    /// 卖档列表(升序,index 0 为卖一)
513    pub ask_list: Vec<OrderBookEntry>,
514    /// 买档列表(降序,index 0 为买一)
515    pub bid_list: Vec<OrderBookEntry>,
516}
517
518/// `Qot_Common.QotMarketState` enum → 人读 label。
519///
520/// 严格对齐 `proto/Qot_Common.proto:83-124` 的 QotMarketState 枚举(含夜市 /
521/// 期货日市 / HkCas 港股收盘竞价 / 美股夜盘等)。
522///
523/// 历史:v1.4.25 加 `market-state` 命令时把 label 写错了(`Closed=6` 被叫
524/// 成 `ClosedToday` 等),v1.4.30 严格按 proto 重写。v1.4.31 从 futucli 抽
525/// 到这里统一维护,避免两处拷贝再次漂移。
526pub fn market_state_label(s: i32) -> &'static str {
527    futu_core::market::qot_market_state_label(s)
528}
529
530#[cfg(test)]
531mod tests;