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    // 0.2.8:`#[serde(default)]` 是**必须**的 —— 0.2.6 加上这个字段时漏了它,
37    // 导致 0.2.6 之前序列化出来的 JSON(不带该键)反序列化直接失败。
38    // 实测:ek-tq 把 ek-data 从 0.2.5 抬到 0.2.7 后,`usage_tq` 的 snapshot
39    // 测试里 Python 传回的 tick 缺这个键 → `tick` 变 None,一直 PASS 的用例挂掉。
40    // 布尔位的缺省本来就该是"没人说它坏",`serde(default)` 正好给 false。
41    #[cfg_attr(feature = "serde", serde(default))]
42    pub datetime_invalid: bool,
43
44    /// 交易日(缺省 None;Python 缺省空串)
45    pub trading_day: Option<i32>,
46    /// 行动日/自然日(缺省 None;Python 缺省空串)
47    pub action_day: Option<i32>,
48    /// 原始 HH:MM:SS 文本(上游原样)
49    pub update_time: String,
50    /// 毫秒段
51    pub update_millisec: i64,
52
53    /// 合约中文名
54    pub name: String,
55    /// 当日累计成交量
56    pub volume: f64,
57    /// 当日累计成交额
58    pub turnover: f64,
59    /// 持仓量/流通量
60    pub open_interest: f64,
61    /// 最新价
62    pub last_price: f64,
63    /// 本笔成交量
64    pub last_volume: f64,
65    /// 涨停价
66    pub limit_up: f64,
67    /// 跌停价
68    pub limit_down: f64,
69
70    /// 今开
71    pub open_price: f64,
72    /// 最高
73    pub high_price: f64,
74    /// 最低
75    pub low_price: f64,
76    /// 昨收
77    pub pre_close: f64,
78    /// 涨跌幅(%)
79    pub pct_change: f64,
80
81    /// 买一价
82    pub bid_price_1: f64,
83    /// 买二价
84    pub bid_price_2: f64,
85    /// 买三价
86    pub bid_price_3: f64,
87    /// 买四价
88    pub bid_price_4: f64,
89    /// 买五价
90    pub bid_price_5: f64,
91
92    /// 卖一价
93    pub ask_price_1: f64,
94    /// 卖二价
95    pub ask_price_2: f64,
96    /// 卖三价
97    pub ask_price_3: f64,
98    /// 卖四价
99    pub ask_price_4: f64,
100    /// 卖五价
101    pub ask_price_5: f64,
102
103    /// 买一量
104    pub bid_volume_1: f64,
105    /// 买二量
106    pub bid_volume_2: f64,
107    /// 买三量
108    pub bid_volume_3: f64,
109    /// 买四量
110    pub bid_volume_4: f64,
111    /// 买五量
112    pub bid_volume_5: f64,
113
114    /// 卖一量
115    pub ask_volume_1: f64,
116    /// 卖二量
117    pub ask_volume_2: f64,
118    /// 卖三量
119    pub ask_volume_3: f64,
120    /// 卖四量
121    pub ask_volume_4: f64,
122    /// 卖五量
123    pub ask_volume_5: f64,
124
125    /// 本地收包时间(UTC 纪元纳秒;Python 缺省 None)
126    pub localtime: Option<DtNs>,
127}
128
129impl TickData {
130    /// **唯一推荐的时间戳取值口**:已知时间不可用时返回 `None`。
131    ///
132    /// 直接读 `self.datetime` 会把"时间不可得"当成 1970-01-01,而那个值在排序、
133    /// 去重、画 K 线时**看不出任何异常** —— 静默错比显式缺更危险。
134    #[must_use]
135    pub fn datetime_if_ok(&self) -> Option<DtNs> {
136        if self.datetime_invalid {
137            None
138        } else {
139            Some(self.datetime)
140        }
141    }
142
143    /// `en_symbol`(Python `__post_init__` 暂存字段的派生方法化;分配一次)。
144    pub fn en_symbol(&self) -> String {
145        let mut buf = String::with_capacity(self.exchange.as_str().len() + 1 + self.symbol.len());
146        self.en_symbol_into(&mut buf);
147        buf
148    }
149
150    /// 零分配版本:追加到复用缓冲(逐 tick 热路径;每笔行情少一次堆分配)。
151    pub fn en_symbol_into(&self, buf: &mut String) {
152        crate::symbol::write_vt_symbol(buf, &self.symbol, self.exchange);
153    }
154
155    /// tick 是否触及涨停(`object.py:99`;diff 为容差,非负向给)。
156    ///
157    /// 注意原作怪癖(字节级保留):tick 缺省字段(0.0/0.0)时
158    /// `bid_price_1(0) + 0 >= limit_up(0)` 成立 → 返回 true;
159    /// 使用方须先落真实价格再作判,或自行先校验 limit_up>0。
160    pub fn reach_limit_up(&self, diff_price: f64) -> bool {
161        self.bid_price_1 + diff_price >= self.limit_up
162    }
163
164    /// tick 是否触及跌停(`object.py:105`)。
165    pub fn reach_limit_down(&self, diff_price: f64) -> bool {
166        self.ask_price_1 - diff_price <= self.limit_down
167    }
168
169    /// 触及涨停或跌停(`object.py:111`)。
170    pub fn reach_limit(&self, diff_price: f64) -> bool {
171        self.reach_limit_up(diff_price) || self.reach_limit_down(diff_price)
172    }
173}