Skip to main content

ek_data/
tick.rs

1//! 行情逐笔快照(`object.py:37-119`,字段逐值复刻 + 方法三层)。
2
3use crate::constant::Exchange;
4use crate::time::DtNs;
5
6/// Tick 数据:最后成交、盘口快照、日内统计。
7///
8/// 字段与 Python `TickData` 同名同义(snake_case);缺省值同 Python(0 / "")。
9#[derive(Debug, Clone, PartialEq, Default)]
10#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
11pub struct TickData {
12    /// 上游网关名(`BaseData.gateway_name`,`object.py:31`)
13    pub gateway_name: String,
14    /// 合约代码
15    pub symbol: String,
16    /// 交易所
17    pub exchange: Exchange,
18    /// 时间戳(UTC 纪元纳秒;Python 为本地 naive datetime)
19    ///
20    /// ⚠ 只在 [`Self::datetime_invalid`] 为假时可信;为真时它是 `DtNs` 的缺省值
21    /// (纪元零点),拿它参与排序/绘图会把这条行情甩到 1970 年。
22    pub datetime: DtNs,
23    /// 时间戳**是否已知不可用**(0.3.0,边界零丢弃)。
24    ///
25    /// `true` = 柜台没给时间,或给的时间戳畸形(`"00000000"` / `"99:99:99"`)。
26    /// 这种行情**仍然上岸**:量价盘口都是真事实,丢掉的只是一条时间轴。
27    ///
28    /// **为什么是「无效」而不是「有效」**:这个字段的引入目的是让**知道**时间
29    /// 不可用的生产方(目前只有 ek-ctp)主动标记,而不是让所有生产方先失效再
30    /// 逐个适配。反过来命名(`datetime_ok` + `derive(Default)` 给 `false`)会让
31    /// **每一个尚未适配的网关**(ek-tq 等)的 tick 时间轴静默消失 —— 那正是这份
32    /// 改造要消灭的那类静默故障,不能自己再造一个。
33    ///
34    /// 缺省 `false` = 「没人说它坏」= 沿今天的行为(时间有效)。消费侧要判就读
35    /// [`Self::datetime_if_ok`],不要在别处手判这个布尔位。
36    pub datetime_invalid: bool,
37
38    /// 交易日(缺省 None;Python 缺省空串)
39    pub trading_day: Option<i32>,
40    /// 行动日/自然日(缺省 None;Python 缺省空串)
41    pub action_day: Option<i32>,
42    /// 原始 HH:MM:SS 文本(上游原样)
43    pub update_time: String,
44    /// 毫秒段
45    pub update_millisec: i64,
46
47    /// 合约中文名
48    pub name: String,
49    /// 当日累计成交量
50    pub volume: f64,
51    /// 当日累计成交额
52    pub turnover: f64,
53    /// 持仓量/流通量
54    pub open_interest: f64,
55    /// 最新价
56    pub last_price: f64,
57    /// 本笔成交量
58    pub last_volume: f64,
59    /// 涨停价
60    pub limit_up: f64,
61    /// 跌停价
62    pub limit_down: f64,
63
64    /// 今开
65    pub open_price: f64,
66    /// 最高
67    pub high_price: f64,
68    /// 最低
69    pub low_price: f64,
70    /// 昨收
71    pub pre_close: f64,
72    /// 涨跌幅(%)
73    pub pct_change: f64,
74
75    /// 买一价
76    pub bid_price_1: f64,
77    /// 买二价
78    pub bid_price_2: f64,
79    /// 买三价
80    pub bid_price_3: f64,
81    /// 买四价
82    pub bid_price_4: f64,
83    /// 买五价
84    pub bid_price_5: f64,
85
86    /// 卖一价
87    pub ask_price_1: f64,
88    /// 卖二价
89    pub ask_price_2: f64,
90    /// 卖三价
91    pub ask_price_3: f64,
92    /// 卖四价
93    pub ask_price_4: f64,
94    /// 卖五价
95    pub ask_price_5: f64,
96
97    /// 买一量
98    pub bid_volume_1: f64,
99    /// 买二量
100    pub bid_volume_2: f64,
101    /// 买三量
102    pub bid_volume_3: f64,
103    /// 买四量
104    pub bid_volume_4: f64,
105    /// 买五量
106    pub bid_volume_5: f64,
107
108    /// 卖一量
109    pub ask_volume_1: f64,
110    /// 卖二量
111    pub ask_volume_2: f64,
112    /// 卖三量
113    pub ask_volume_3: f64,
114    /// 卖四量
115    pub ask_volume_4: f64,
116    /// 卖五量
117    pub ask_volume_5: f64,
118
119    /// 本地收包时间(UTC 纪元纳秒;Python 缺省 None)
120    pub localtime: Option<DtNs>,
121}
122
123impl TickData {
124    /// **唯一推荐的时间戳取值口**:已知时间不可用时返回 `None`。
125    ///
126    /// 直接读 `self.datetime` 会把"时间不可得"当成 1970-01-01,而那个值在排序、
127    /// 去重、画 K 线时**看不出任何异常** —— 静默错比显式缺更危险。
128    #[must_use]
129    pub fn datetime_if_ok(&self) -> Option<DtNs> {
130        if self.datetime_invalid {
131            None
132        } else {
133            Some(self.datetime)
134        }
135    }
136
137    /// `en_symbol`(Python `__post_init__` 暂存字段的派生方法化;分配一次)。
138    pub fn en_symbol(&self) -> String {
139        let mut buf = String::with_capacity(self.exchange.as_str().len() + 1 + self.symbol.len());
140        self.en_symbol_into(&mut buf);
141        buf
142    }
143
144    /// 零分配版本:追加到复用缓冲(逐 tick 热路径;每笔行情少一次堆分配)。
145    pub fn en_symbol_into(&self, buf: &mut String) {
146        crate::symbol::write_vt_symbol(buf, &self.symbol, self.exchange);
147    }
148
149    /// tick 是否触及涨停(`object.py:99`;diff 为容差,非负向给)。
150    ///
151    /// 注意原作怪癖(字节级保留):tick 缺省字段(0.0/0.0)时
152    /// `bid_price_1(0) + 0 >= limit_up(0)` 成立 → 返回 true;
153    /// 使用方须先落真实价格再作判,或自行先校验 limit_up>0。
154    pub fn reach_limit_up(&self, diff_price: f64) -> bool {
155        self.bid_price_1 + diff_price >= self.limit_up
156    }
157
158    /// tick 是否触及跌停(`object.py:105`)。
159    pub fn reach_limit_down(&self, diff_price: f64) -> bool {
160        self.ask_price_1 - diff_price <= self.limit_down
161    }
162
163    /// 触及涨停或跌停(`object.py:111`)。
164    pub fn reach_limit(&self, diff_price: f64) -> bool {
165        self.reach_limit_up(diff_price) || self.reach_limit_down(diff_price)
166    }
167}