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}