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
//! 枚举 Display/FromStr 回路:英文线值 + 中文旧值别名双通道。

use ek_data::EnumParseError;
use ek_data::constant::*;
use std::fmt::{Debug, Display};
use std::str::FromStr;

/// 单变体三通道校验:Display==线值、FromStr 认线值、FromStr 认中文别名。
fn wire<T>(v: T, wit: &str, aliases: &[&str], name: &'static str)
where
    T: Copy + Display + FromStr + PartialEq + Debug,
    T::Err: Debug,
{
    assert_eq!(v.to_string(), wit, "{name} Display");
    assert_eq!(T::from_str(wit).unwrap(), v, "{name} from wire");
    for a in aliases {
        assert_eq!(T::from_str(a).unwrap(), v, "{name} from alias {a:?}");
    }
}

#[test]
fn direction_roundtrip() {
    wire(Direction::LONG, "LONG", &["多"], "Direction");
    wire(Direction::SHORT, "SHORT", &["空"], "Direction");
    wire(Direction::NET, "NET", &["净"], "Direction");
    assert_eq!(Direction::ALL.len(), 3);
    // 线值大小写敏感(与 Python .value 同口径)
    assert!(Direction::from_str("long").is_err());
    assert!(matches!(
        Direction::from_str("bull"),
        Err(EnumParseError {
            type_name: "Direction",
            ..
        })
    ));
}

#[test]
fn offset_roundtrip() {
    wire(Offset::NONE, "NONE", &[""], "Offset");
    wire(Offset::OPEN, "OPEN", &["开"], "Offset");
    wire(Offset::CLOSE, "CLOSE", &["平"], "Offset");
    wire(Offset::CLOSETODAY, "CLOSETODAY", &["平今"], "Offset");
    wire(
        Offset::CLOSEYESTERDAY,
        "CLOSEYESTERDAY",
        &["平昨"],
        "Offset",
    );
    assert_eq!(Offset::ALL.len(), 5);
}

#[test]
fn status_roundtrip() {
    wire(Status::SUBMITTING, "SUBMITTING", &["提交中"], "Status");
    wire(Status::NOTTRADED, "NOTTRADED", &["未成交"], "Status");
    wire(Status::PARTTRADED, "PARTTRADED", &["部分成交"], "Status");
    wire(Status::ALLTRADED, "ALLTRADED", &["全部成交"], "Status");
    wire(Status::CANCELLED, "CANCELLED", &["已撤销"], "Status");
    wire(Status::REJECTED, "REJECTED", &["拒单"], "Status");
    // 0.3.0(边界零丢弃):新增 UNKNOWN —— 契约变更,断言随之 +1(唯一允许的改动)。
    wire(Status::UNKNOWN, "UNKNOWN", &["未知"], "Status");
    assert_eq!(Status::ALL.len(), 7);
    // UNKNOWN 刻意不进活跃态:在途判定是 ek-oms 的业务决策,网关只如实说"不认识"。
    assert!(!Status::UNKNOWN.is_active());
    assert_eq!(
        ACTIVE_STATUSES,
        [Status::SUBMITTING, Status::NOTTRADED, Status::PARTTRADED]
    );
}

#[test]
fn asset_and_instrument_types() {
    wire(Asset::EQUITY, "EQUITY", &["股票"], "Asset");
    wire(Asset::BOND, "BOND", &["债券"], "Asset");
    wire(Asset::FUTURES, "FUTURES", &["期货"], "Asset");
    wire(Asset::OPTION, "OPTION", &["期权"], "Asset");
    wire(Asset::FOREX, "FOREX", &["外汇"], "Asset");
    // 0.3.0(边界零丢弃):新增 UNKNOWN —— 仅在勾选非标准产品类码时触发。
    wire(Asset::UNKNOWN, "UNKNOWN", &["未知"], "Asset");
    assert_eq!(Asset::ALL.len(), 6);

    wire(InstrumentType::STOCK, "STOCK", &["股票"], "InstrumentType");
    wire(InstrumentType::ETF, "ETF", &["ETF"], "InstrumentType");
    wire(InstrumentType::INDEX, "INDEX", &["指数"], "InstrumentType");
    wire(
        InstrumentType::FUTURES_CONTRACT,
        "FUTURES_CONTRACT",
        &["期货合约"],
        "InstrumentType",
    );
    wire(
        InstrumentType::MAIN_CONTRACT,
        "MAIN_CONTRACT",
        &["主力合约"],
        "InstrumentType",
    );
    wire(
        InstrumentType::CONTINUOUS_CONTRACT,
        "CONTINUOUS_CONTRACT",
        &["连续合约"],
        "InstrumentType",
    );
    wire(
        InstrumentType::OPTION_CONTRACT,
        "OPTION_CONTRACT",
        &["期权合约"],
        "InstrumentType",
    );
    wire(InstrumentType::BOND, "BOND", &["债券"], "InstrumentType");
    wire(
        InstrumentType::CONVERTIBLE_BOND,
        "CONVERTIBLE_BOND",
        &["可转债"],
        "InstrumentType",
    );
    wire(
        InstrumentType::SPREAD,
        "SPREAD",
        &["价差组合"],
        "InstrumentType",
    );
    wire(
        InstrumentType::HEDGE,
        "HEDGE",
        &["对冲组合"],
        "InstrumentType",
    );
    assert_eq!(InstrumentType::ALL.len(), 11);
}

#[test]
fn order_option_types() {
    wire(OrderType::LIMIT, "LIMIT", &["限价"], "OrderType");
    wire(OrderType::MARKET, "MARKET", &["市价"], "OrderType");
    wire(OrderType::STOP, "STOP", &[], "OrderType");
    wire(OrderType::FAK, "FAK", &[], "OrderType");
    wire(OrderType::FOK, "FOK", &[], "OrderType");
    wire(OrderType::RFQ, "RFQ", &["询价"], "OrderType");
    // 0.3.0(边界零丢弃):新增 UNKNOWN —— 不借 LIMIT 顶替。
    wire(OrderType::UNKNOWN, "UNKNOWN", &["未知"], "OrderType");
    assert_eq!(OrderType::ALL.len(), 7);

    wire(OptionType::CALL, "CALL", &["看涨期权"], "OptionType");
    wire(OptionType::PUT, "PUT", &["看跌期权"], "OptionType");
}

#[test]
fn exchange_full_replica_and_special_wires() {
    // 全量 50 变体(constant.py:128-181 逐名)+ 0.3.0 新增的 UNKNOWN = 51
    assert_eq!(Exchange::ALL.len(), 51);
    // UNKNOWN 与 LOCAL 语义不同:前者"来自交易所但不认识",后者"不来自任何交易所"。
    assert_eq!(Exchange::UNKNOWN.to_string(), "UNKNOWN");
    assert_eq!(
        Exchange::from_str("未知").unwrap(),
        Exchange::UNKNOWN,
        "UNKNOWN 的中文别名必须可解析"
    );
    assert_ne!(Exchange::UNKNOWN, Exchange::LOCAL);
    // 特例:显示名沿 Python .value(字节级)
    assert_eq!(Exchange::CBOT.to_string(), "CBT");
    assert_eq!(Exchange::EUREX.to_string(), "EUX");
    assert_eq!(Exchange::ETF_INDEX.to_string(), "ETF_index");
    assert_eq!(Exchange::from_str("CBT").unwrap(), Exchange::CBOT);
    assert_eq!(Exchange::from_str("EUX").unwrap(), Exchange::EUREX);
    assert_eq!(
        Exchange::from_str("ETF_index").unwrap(),
        Exchange::ETF_INDEX
    );
    // 全枚举两通道抽查(中文交易所无别名,A 股列举对照 constant.py)
    wire(Exchange::CFFEX, "CFFEX", &[], "Exchange");
    wire(Exchange::SHFE, "SHFE", &[], "Exchange");
    wire(Exchange::SZSE, "SZSE", &[], "Exchange");
    wire(Exchange::LOCAL, "LOCAL", &[], "Exchange");
    assert!(Exchange::from_str("NOPE").is_err());
}

#[test]
fn currency_and_interval() {
    wire(Currency::USD, "USD", &[], "Currency");
    wire(Currency::HKD, "HKD", &[], "Currency");
    wire(Currency::CNY, "CNY", &[], "Currency");
    wire(Currency::CAD, "CAD", &[], "Currency");

    wire(Interval::TICK, "TICK", &[], "Interval");
    wire(Interval::MINUTE, "MINUTE", &[], "Interval");
    wire(Interval::MINUTE_5, "MINUTE_5", &[], "Interval");
    wire(Interval::MINUTE_15, "MINUTE_15", &[], "Interval");
    wire(Interval::MINUTE_30, "MINUTE_30", &[], "Interval");
    wire(Interval::HOUR, "HOUR", &[], "Interval");
    wire(Interval::DAILY, "DAILY", &[], "Interval");
    wire(Interval::WEEKLY, "WEEKLY", &[], "Interval");
    // Python .value 秒值(constant.py:200-207)
    assert_eq!(Interval::TICK.seconds(), 0);
    assert_eq!(Interval::MINUTE.seconds(), 60);
    assert_eq!(Interval::MINUTE_5.seconds(), 300);
    assert_eq!(Interval::MINUTE_15.seconds(), 900);
    assert_eq!(Interval::MINUTE_30.seconds(), 1800);
    assert_eq!(Interval::HOUR.seconds(), 3600);
    assert_eq!(Interval::DAILY.seconds(), 86400);
    assert_eq!(Interval::WEEKLY.seconds(), 604800);
}

/// 审计级不变量:每个枚举的线值两两互异、别名两两互异且不与线值冲突。
///
/// 若未来新增变体撞了线值/别名,`FromStr` 会静默偏向先命中者 —— 本用例把它变成红灯。
#[test]
fn wire_values_and_aliases_are_collision_free() {
    fn check<T: Display + FromStr + Copy + Debug + PartialEq>(
        all: &[T],
        aliases_of: &dyn Fn(&T) -> &'static [&'static str],
        label: &str,
    ) where
        T::Err: Debug,
    {
        let mut wires: Vec<String> = Vec::new();
        let mut aliases: Vec<String> = Vec::new();
        for v in all {
            let w = v.to_string();
            assert_eq!(T::from_str(&w).unwrap(), *v, "{label} 线值自回路失败: {w}");
            assert!(!wires.contains(&w), "{label} 线值冲突: {w}");
            wires.push(w);
            for a in aliases_of(v) {
                assert!(T::from_str(a).unwrap() == *v, "{label} 别名解析异常: {a:?}");
                assert!(!wires.contains(&a.to_string()), "{label} 别名 {a:?} 撞线值");
                assert!(!aliases.contains(&a.to_string()), "{label} 别名冲突: {a:?}");
                aliases.push(a.to_string());
            }
        }
    }

    check(Direction::ALL, &|v| v.chinese_alias(), "Direction");
    check(Offset::ALL, &|v| v.chinese_alias(), "Offset");
    check(Status::ALL, &|v| v.chinese_alias(), "Status");
    check(Asset::ALL, &|v| v.chinese_alias(), "Asset");
    check(
        InstrumentType::ALL,
        &|v| v.chinese_alias(),
        "InstrumentType",
    );
    check(OrderType::ALL, &|v| v.chinese_alias(), "OrderType");
    check(OptionType::ALL, &|v| v.chinese_alias(), "OptionType");
    check(Exchange::ALL, &|v| v.chinese_alias(), "Exchange");
    check(Currency::ALL, &|v| v.chinese_alias(), "Currency");
    check(Interval::ALL, &|v| v.chinese_alias(), "Interval");
}