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