ek-data 0.1.0

Trading data contracts for the enkeep ek ecosystem: venue enums, zero-dep epoch-nanosecond time, vt-symbol convention, bus event dictionary, tick/bar/ord/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());
}

/// 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);
}