ek-data 0.2.8

Trading data contracts for the enkeep ek ecosystem: venue enums, zero-dep epoch-nanosecond time, decimal-exact price rounding, vt-symbol convention, bus event dictionary, tick/bar/order/trade/position/account/contract objects and request objects (Python enkeep_trade trader/object.py + constant.py, faithfully ported).
Documentation
//! 行情逐笔快照(`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)
    }
}