ek-data 0.2.2

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
//! 对象方法行为用例:与 Python 用例一一对抄。

use ek_data::constant::*;
use ek_data::time::*;
use ek_data::*;

// —— TickData reach_limit 三层(object.py:99-119)——

fn tick(limit_up: f64, limit_down: f64) -> TickData {
    TickData {
        limit_up,
        limit_down,
        ..TickData::default()
    }
}

/// Python `reach_limit_up`: bid_price_1 + diff >= limit_up
#[test]
fn tick_reach_limit_up() {
    let mut t = tick(10.0, 8.0);
    t.bid_price_1 = 10.0;
    assert!(t.reach_limit_up(0.0));
    assert!(t.reach_limit_up(0.5));
    t.bid_price_1 = 9.4;
    assert!(!t.reach_limit_up(0.0));
    // 加宽 diff 后触及
    assert!(t.reach_limit_up(0.7));
}

/// Python `reach_limit_down`: ask_price_1 - diff <= limit_down
#[test]
fn tick_reach_limit_down() {
    let mut t = tick(100.0, 92.0);
    t.ask_price_1 = 92.0;
    assert!(t.reach_limit_down(0.0));
    assert!(t.reach_limit_down(0.5));
    t.ask_price_1 = 92.6;
    assert!(!t.reach_limit_down(0.0));
    // 加宽 diff 后触及
    assert!(t.reach_limit_down(0.7));
}

/// Python `reach_limit`: up 或 down 命其一
#[test]
fn tick_reach_limit_combined() {
    let mut t = tick(10.0, 8.0);
    t.bid_price_1 = 10.0;
    assert!(t.reach_limit(0.0));
    t.bid_price_1 = 0.0;
    t.ask_price_1 = 8.0;
    assert!(t.reach_limit(0.0));
    t.ask_price_1 = 9.0;
    assert!(!t.reach_limit(0.0));
}

// —— OrderData(object.py:21, 182, 188)——

fn order(status: Status) -> OrderData {
    OrderData {
        gateway_name: "xt".into(),
        symbol: "IF2506".into(),
        exchange: Exchange::CFFEX,
        orderid: "1".into(),
        status,
        ..OrderData::default()
    }
}

/// Python `is_active`: SUBMITTING/NOTTRADED/PARTTRADED 活跃;其余非
#[test]
fn order_is_active_matrix() {
    assert!(order(Status::SUBMITTING).is_active());
    assert!(order(Status::NOTTRADED).is_active());
    assert!(order(Status::PARTTRADED).is_active());
    assert!(!order(Status::ALLTRADED).is_active());
    assert!(!order(Status::CANCELLED).is_active());
    assert!(!order(Status::REJECTED).is_active());
    // 0.2.0:判定真源收敛到 Status::is_active
    assert_eq!(Status::SUBMITTING.is_active(), order(Status::SUBMITTING).is_active());
    assert!(!Status::REJECTED.is_active());
}

/// Python `create_cancel_request`: 只搬 orderid/symbol/exchange
#[test]
fn order_create_cancel_request_copies_fields() {
    let req = order(Status::NOTTRADED).create_cancel_request();
    assert_eq!(req.orderid, "1");
    assert_eq!(req.symbol, "IF2506");
    assert_eq!(req.exchange, Exchange::CFFEX);
}

// —— AccountData(object.py:281)——

#[test]
fn account_available_is_balance_minus_frozen() {
    let a = AccountData {
        balance: 1000.0,
        frozen: 300.0,
        ..AccountData::default()
    };
    assert_eq!(a.available(), 700.0);
    let zero = AccountData::default();
    assert_eq!(zero.available(), 0.0);
}

// —— OrderRequest 量域四件套(object.py:559-588)——

fn req_volume(v: f64) -> OrderRequest {
    OrderRequest {
        volume: v,
        ..OrderRequest::default()
    }
}

#[test]
fn request_min_volume_check() {
    assert!(!req_volume(0.99).is_min_volume_ok(1.0));
    assert!(req_volume(1.0).is_min_volume_ok(1.0));
    assert!(req_volume(5.0).is_min_volume_ok(1.0));
}

#[test]
fn request_max_volume_check() {
    assert!(req_volume(1000.0).is_max_volume_ok(1000.0));
    assert!(!req_volume(1000.1).is_max_volume_ok(1000.0));
    assert!(!req_volume(1001.0).is_max_volume_ok(1000.0));
}

#[test]
fn request_volume_range_check() {
    // object.py:575: volume < min 或 > max → False(闭区间)
    assert!(!req_volume(0.0).is_volume_range_ok(1.0, 1000.0));
    assert!(req_volume(1.0).is_volume_range_ok(1.0, 1000.0));
    assert!(req_volume(1000.0).is_volume_range_ok(1.0, 1000.0));
    assert!(!req_volume(1000.5).is_volume_range_ok(1.0, 1000.0));
}

#[test]
fn request_volume_beishu_check() {
    // object.py:582: volume % sub_beishu == 0 → True
    assert!(req_volume(10.0).is_volume_beishu_ok(5.0));
    assert!(!req_volume(10.5).is_volume_beishu_ok(5.0));
    assert!(req_volume(3.0).is_volume_beishu_ok(1.0));
    assert!(req_volume(0.0).is_volume_beishu_ok(5.0));
    // 除零:Python 抛 ZeroDivisionError,Rust 版约定 false
    assert!(!req_volume(10.0).is_volume_beishu_ok(0.0));
}

// —— time.rs:Hinnant 算法钉死值 ——
#[test]
fn time_civil_roundtrip_known_days() {
    // 已知基准:1970-01-01 → 0;1970-01-02 → 1;2000-03-01 → 11017(Hinnant 论断)
    assert_eq!(days_from_civil(1970, 1, 1), 0);
    assert_eq!(days_from_civil(1970, 1, 2), 1);
    assert_eq!(days_from_civil(2000, 3, 1), 11017);
    assert_eq!(civil_from_days(0), (1970, 1, 1));
    assert_eq!(civil_from_days(11017), (2000, 3, 1));
    // 闰日
    let leap = days_from_civil(2024, 2, 29);
    let (y, m, d) = civil_from_days(leap);
    assert_eq!((y, m, d), (2024, 2, 29));
    // 闰年 2 月底衔接
    let next = days_from_civil(2024, 3, 1);
    assert_eq!(next - leap, 1);
}

#[test]
fn time_nanos_roundtrip_and_negative() {
    let c = Civil { year: 2024, month: 12, day: 25, hour: 9, minute: 30, second: 1, nanos: 123 };
    let t = civil_to_nanos(c);
    assert_eq!(civil_from_nanos(t), c);
    // 纪元本身
    assert_eq!(
        civil_to_nanos(Civil { year: 1970, month: 1, day: 1, hour: 0, minute: 0, second: 0, nanos: 0 }),
        0
    );
    // 负纳秒(epoch 前)floor 语义:-86400s-1ns = 1969-12-30T23:59:59.999999999
    let neg = -86_400 * NANOS_PER_SEC - 1;
    let c_neg = civil_from_nanos(neg);
    assert_eq!((c_neg.year, c_neg.month, c_neg.day), (1969, 12, 30));
    assert_eq!(
        (c_neg.hour, c_neg.minute, c_neg.second, c_neg.nanos),
        (23, 59, 59, 999_999_999)
    );
    // 而 1969-12-31T23:59:59.999999999 落在 day -1(floor)
    let last_of_m1 = civil_to_nanos(Civil { year: 1969, month: 12, day: 31, hour: 23, minute: 59, second: 59, nanos: 999_999_999 });
    assert_eq!(trading_day(last_of_m1), -1);
}

#[test]
fn time_from_system_time_and_now() {
    let epoch = std::time::UNIX_EPOCH;
    assert_eq!(from_system_time(epoch), 0);
    assert!(now_nanos() > 1_600_000_000_000_000_000); // 2020-09 之后
    let _ = format_civil(civil_to_nanos(Civil { year: 2024, month: 1, day: 6, hour: 8, minute: 3, second: 9, nanos: 12 }));
}

// —— symbol.rs ——
#[test]
fn vt_symbol_roundtrip_and_errors() {
    let vt = vt_symbol("IF2506", Exchange::CFFEX);
    assert_eq!(vt, "CFFEX.IF2506");
    let (sym, ex) = parse_vt_symbol(&vt).unwrap();
    assert_eq!((sym.as_str(), ex), ("IF2506", Exchange::CFFEX));
    // symbol 段可含点(extract 语义:首个 . 切分)
    let (sym2, ex2) = parse_vt_symbol("SZSE.000300.IDX").unwrap();
    assert_eq!(sym2, "000300.IDX");
    assert_eq!(ex2, Exchange::SZSE);
    // 错误面
    assert_eq!(parse_vt_symbol("NO_DOT"), Err(ek_data::SymbolError::Malformed));
    assert_eq!(parse_vt_symbol(".NOHEAD"), Err(ek_data::SymbolError::Malformed));
    assert_eq!(
        parse_vt_symbol("FAKE.IF2506"),
        Err(ek_data::SymbolError::UnknownExchange("FAKE".into()))
    );
}

// —— 契约默认值(Python object.py 字段缺省)——
#[test]
fn contract_defaults_match_python() {
    let c = ContractData::default();
    assert_eq!(c.size, 1.0); // object.py:347 size: float = 1
    assert_eq!(c.price_tick, 0.01); // object.py:346 price_tick: float = 0.01
    assert!(c.instrument_type.is_none());
    assert!(c.product_id.is_none());
}

// —— LogData(object.py:286-296)——
#[test]
fn log_data_default_level_is_info_20() {
    let l = LogData { msg: "hello".into(), ..LogData::default() };
    assert_eq!(l.level, 20); // logging.INFO
    assert!(!l.is_warning_or_above());
    assert!(LogData { level: 30, ..l.clone() }.is_warning_or_above());
    assert_eq!(LEVEL_INFO, 20);
    assert_eq!(LEVEL_WARNING, 30);
    assert_eq!(LEVEL_ERROR, 40);
}

// —— 时间严格性(0.2.0 加固):校验 + 解析 + 范围 ——

fn ok_zero() -> Civil {
    Civil { year: 2024, month: 1, day: 1, hour: 0, minute: 0, second: 0, nanos: 0 }
}

#[test]
fn civil_validation_is_strict_like_python_datetime() {
    let ok = Civil { year: 2024, month: 2, day: 29, hour: 0, minute: 0, second: 0, nanos: 0 };
    assert!(is_valid_civil(&ok));
    assert_eq!(try_civil_to_nanos(ok), Some(civil_to_nanos(ok)));
    // 闰年 2 月 29 合法,平年不合法(Python datetime 会抛 ValueError)
    let bad = Civil { year: 2023, day: 29, ..ok };
    assert!(!is_valid_civil(&bad));
    assert_eq!(try_civil_to_nanos(bad), None);
    // 月/日/时分秒/纳秒越界
    assert!(!is_valid_civil(&Civil { year: 2024, month: 13, day: 1, ..ok_zero() }));
    assert!(!is_valid_civil(&Civil { year: 2024, month: 4, day: 31, ..ok_zero() }));
    assert!(!is_valid_civil(&Civil { year: 2024, month: 1, day: 1, hour: 24, ..ok_zero() }));
    assert!(!is_valid_civil(&Civil { year: 2024, month: 1, day: 1, nanos: 1_000_000_000, ..ok_zero() }));
    // 闰年规则
    assert!(is_leap_year(2000));
    assert!(!is_leap_year(1900));
    assert!(is_leap_year(2024));
    assert_eq!(days_in_month(2024, 2), 29);
    assert_eq!(days_in_month(2023, 2), 28);
    assert_eq!(days_in_month(2024, 4), 30);
    assert_eq!(days_in_month(2024, 13), 0);
}

#[test]
fn civil_to_nanos_panics_on_invalid_input() {
    let bad = Civil { year: 2023, month: 2, day: 30, hour: 0, minute: 0, second: 0, nanos: 0 };
    let r = std::panic::catch_unwind(|| civil_to_nanos(bad));
    assert!(r.is_err(), "越界 civil 必须显式失败(Python ValueError 同面)");
}

#[test]
fn dtns_range_is_documented_and_enforced() {
    // i64 纳秒只能表达 ~1677..2262;远日期必须走 Day 序数
    assert_eq!(DTNS_MIN, i64::MIN);
    assert_eq!(DTNS_MAX, i64::MAX);
    let far = Civil { year: 9999, month: 12, day: 31, hour: 23, minute: 59, second: 59, nanos: 0 };
    assert_eq!(try_civil_to_nanos(far), None, "9999 年超出 i64 纳秒范围");
    let far_past = Civil { year: 1600, month: 1, day: 1, hour: 0, minute: 0, second: 0, nanos: 0 };
    assert_eq!(try_civil_to_nanos(far_past), None, "1600 年超出下界");
    // 范围边缘内可取(2262-04-11T23:47:16.854775807Z == i64::MAX)
    let edge = Civil { year: 2262, month: 4, day: 11, hour: 23, minute: 47, second: 16, nanos: 854_775_807 };
    assert_eq!(try_civil_to_nanos(edge), Some(i64::MAX));
}

#[test]
fn parse_civil_accepts_gateway_and_replay_shapes() {
    let a = parse_civil("2024-12-25 09:30:01").unwrap();
    assert_eq!(
        a,
        civil_to_nanos(Civil { year: 2024, month: 12, day: 25, hour: 9, minute: 30, second: 1, nanos: 0 })
    );
    // T 分隔 + 省略秒 + 纯日期
    assert_eq!(parse_civil("2024-12-25T09:30"), Some(a - NANOS_PER_SEC));
    assert_eq!(
        parse_civil("2024-12-25"),
        Some(civil_to_nanos(Civil { year: 2024, month: 12, day: 25, hour: 0, minute: 0, second: 0, nanos: 0 }))
    );
    // 小数秒右补零:".5" = 500ms
    let half = parse_civil("2024-12-25 09:30:01.5").unwrap();
    assert_eq!(half - a, 500_000_000);
    // 与格式化回路
    assert_eq!(parse_civil(&format_civil(a)), Some(a));
    // 非法输入
    assert_eq!(parse_civil("2024-02-30 00:00:00"), None);
    assert_eq!(parse_civil("2024-13-01"), None);
    assert_eq!(parse_civil("not-a-date"), None);
    assert_eq!(parse_civil("2024-12-25 25:00:00"), None);
    assert_eq!(parse_civil("2024-12-25 09:30:01.1234567890"), None);
}

// —— 0.2.0:零分配拼装 ——

#[test]
fn write_vt_symbol_appends_without_realloc_semantics() {
    let mut buf = String::new();
    write_vt_symbol(&mut buf, "IF2506", Exchange::CFFEX);
    assert_eq!(buf, "CFFEX.IF2506");
    // 追加语义(可复用缓冲,逐 tick 清空后重用)
    buf.clear();
    write_vt_symbol(&mut buf, "600519", Exchange::SSE);
    assert_eq!(buf, "SSE.600519");
    // 与分配版一致
    assert_eq!(vt_symbol("600519", Exchange::SSE), buf);
}

#[test]
fn en_symbol_into_matches_en_symbol_on_every_object() {
    let mut buf = String::new();

    let tick = TickData { symbol: "IF2506".into(), exchange: Exchange::CFFEX, ..TickData::default() };
    tick.en_symbol_into(&mut buf);
    assert_eq_and_clear(&mut buf, &tick.en_symbol());

    let bar = BarData { symbol: "IF2506".into(), exchange: Exchange::CBOT, ..BarData::default() };
    bar.en_symbol_into(&mut buf);
    assert_eq!(buf, "CBT.IF2506");
    assert_eq_and_clear(&mut buf, &bar.en_symbol());

    let o = OrderData { symbol: "IF2506".into(), exchange: Exchange::CFFEX, ..OrderData::default() };
    o.en_symbol_into(&mut buf);
    assert_eq_and_clear(&mut buf, &o.en_symbol());

    let tr = TradeData { symbol: "IF2506".into(), exchange: Exchange::CFFEX, ..TradeData::default() };
    tr.en_symbol_into(&mut buf);
    assert_eq_and_clear(&mut buf, &tr.en_symbol());

    let pos = PositionData { symbol: "IF2506".into(), exchange: Exchange::CFFEX, ..PositionData::default() };
    pos.en_symbol_into(&mut buf);
    assert_eq_and_clear(&mut buf, &pos.en_symbol());

    let c = ContractData { symbol: "IF2506".into(), exchange: Exchange::CFFEX, ..ContractData::default() };
    c.en_symbol_into(&mut buf);
    assert_eq_and_clear(&mut buf, &c.en_symbol());
}

fn assert_eq_and_clear(buf: &mut String, expected: &str) {
    assert_eq!(buf.as_str(), expected);
    buf.clear();
}