ek-data 0.2.7

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
//! flows_order —— F3 下单闸门 + F4 委托生命周期 + F5 成交流。
//!
//! 键形(原作 gateway.py:122-128):委托流 = 裸键 `eOrder.` + `eOrder.<en_orderid>` 双投,
//! 成交流 = 裸键 `eTrade.` + `eTrade.<en_tradeid>` 双投;订单键用 en_orderid(非 symbol)。

mod common;

use common::{Capture, fixture, wait_until};
use ek_data::{
    AccountData, Direction, EVENT_ACCOUNT, EVENT_ORDER, EVENT_TRADE, Exchange, Offset, OrderData,
    OrderRequest, OrderType, Status, TradeData,
};
use ek_event::Event;
use std::sync::Arc;
use std::time::Duration;

fn fx_req() -> OrderRequest {
    OrderRequest {
        symbol: "IF2506".into(),
        exchange: Exchange::CFFEX,
        direction: Direction::LONG,
        order_type: OrderType::LIMIT,
        volume: 2.0,
        price: 3520.2,
        offset: Offset::OPEN,
        reference: "demo".into(),
        old_order_id: String::new(),
    }
}

/// F3 交易前闸门四连:min/max/range/beishu(委托流时的风控语境)。
#[test]
fn f3_gates_on_request() {
    let req = fx_req();
    // 下限量(2 手)对合约最小 1 手 / 最大 50 手 / 倍数 0.5 的判定
    assert!(req.is_min_volume_ok(1.0), "2 >= 1 满 min");
    assert!(req.is_min_volume_ok(2.0), "闭口径 >=(object.py:559)");
    assert!(!req.is_min_volume_ok(3.0));
    assert!(req.is_max_volume_ok(50.0));
    assert!(!req.is_max_volume_ok(1.0));
    assert!(
        req.is_volume_range_ok(1.0, 50.0),
        "闭区间口径(object.py:575)"
    );
    assert!(!req.is_volume_range_ok(3.0, 50.0));
    assert!(req.is_volume_beishu_ok(0.5));
    assert!(!req.is_volume_beishu_ok(1.5), "2 不是 1.5 的整数倍");
    // 价格对齐:合约 price_tick=0.2 而请求价 3520.3 应被撮合侧对齐
    // (0.2 在 f64 上非精确可表,round_to 的整数比实现保证正确)
    assert_eq!(ek_data::round_to(3520.3, 0.2), 3520.4);
}

/// F4 委托生命周期全经真总线:键互斥 + 状态机 + 派生 ID + 终态出撤销单。
/// (en_orderid = gateway.orderid(object.py:180);订单流组合键无 symbol 段。)
#[test]
fn f4_order_lifecycle_over_bus() {
    let eng = ek_event::EventEngine::new();
    let live = Capture::<OrderData>::new(); // eOrder.xt.xt.1(本单定向)
    let bare = Capture::<OrderData>::new(); // eOrder.(全市场)
    let other = Capture::<OrderData>::new(); // eOrder.xt.xt.2(异单,不串台)
    live.register_on(&eng, format!("{EVENT_ORDER}xt.xt.1"));
    bare.register_on(&eng, EVENT_ORDER);
    other.register_on(&eng, format!("{EVENT_ORDER}xt.xt.2"));
    eng.start();

    // create_order_data:请求 → 委托(状态 SUBMITTING,reference 拷贝)
    let req = fx_req();
    let o = req.create_order_data("xt.1", "xt");
    assert_eq!(o.status, Status::SUBMITTING);
    assert_eq!(o.reference, "demo");
    // en_orderid = gateway.orderid(object.py:180,无 symbol 段)
    assert_eq!(o.en_orderid(), "xt.xt.1");
    assert!(o.is_active());
    let o_key = format!("{EVENT_ORDER}{}", o.en_orderid());

    // 时间轴:提交 → 全部成交(两帧推送同一条键)
    let mut submitted = o.clone();
    submitted.status = Status::ALLTRADED;
    submitted.traded = 2.0;
    submitted.datetime = Some(fixture::t0());
    eng.put(Event::with_data(o_key.clone(), Arc::new(o.clone())))
        .unwrap();
    eng.put(Event::with_data(o_key, Arc::new(submitted.clone())))
        .unwrap();
    // 异单只走自己的键(不串台)
    let mut other_o = o.clone();
    other_o.orderid = "xt.2".into();
    eng.put(Event::with_data(
        format!("{EVENT_ORDER}{}", other_o.en_orderid()),
        Arc::new(other_o),
    ))
    .unwrap();

    assert!(wait_until(Duration::from_secs(2), || live.len() == 2));
    eng.stop();
    assert_eq!(bare.len(), 0, "裸键未授权(本场景只发组合键)");
    assert_eq!(other.len(), 1, "异单不进本单的键");
    let timeline = live.all();
    assert_eq!(timeline[0].status, Status::SUBMITTING);
    assert_eq!(timeline[1].status, Status::ALLTRADED);
    assert_eq!(timeline[1].traded, 2.0);
    assert!(!timeline[1].is_active(), "终态即非活跃");
    assert_eq!(timeline[1].datetime, Some(fixture::t0()));
    // 终态出撤销请求:字段拷贝(委托流的直接消费)
    let cxl = timeline[1].create_cancel_request();
    assert_eq!(cxl.orderid, "xt.1");
    assert_eq!(cxl.symbol, "IF2506");
    assert_eq!(cxl.exchange, Exchange::CFFEX);
}

/// F5 成交流:trade 派生 ID + en_orderid 键回环 + 账户快照顺路(闸门语境的余额检查)。
#[test]
fn f5_trade_flow() {
    let eng = ek_event::EventEngine::new();
    let per = Capture::<TradeData>::new(); // eTrade.<en_tradeid>(方法造键,不写死字面)
    let acct = Capture::<AccountData>::new(); // eAccount.<en_accountid>    let acct = Capture::<AccountData>::new(); // eAccount.xt.demo
    eng.start();

    let req = fx_req();
    let o = req.create_order_data("xt.1", "xt");
    let trade = TradeData {
        gateway_name: "xt".into(),
        symbol: o.symbol.clone(),
        exchange: o.exchange,
        orderid: o.orderid.clone(),
        tradeid: "xt.90001".into(),
        direction: Some(Direction::LONG),
        offset: Offset::OPEN,
        price: 3520.2,
        volume: 2.0,
        datetime: Some(fixture::t0()),
        ..Default::default()
    };
    // 成交扣款后的账户快照(另一条流;可用资金 = balance - frozen)
    let acct0 = AccountData {
        gateway_name: "xt".into(),
        accountid: "demo".into(),
        balance: 100_000.0,
        frozen: 300.0,
    };
    // en_tradeid = gateway.tradeid(object.py:231);en_orderid = gateway.orderid(:230)
    assert_eq!(trade.en_tradeid(), "xt.xt.90001");
    assert_eq!(trade.en_orderid(), "xt.xt.1");
    let t_key = format!("{EVENT_TRADE}{}", trade.en_tradeid());
    let a_key = format!("{EVENT_ACCOUNT}{}", acct0.en_accountid());
    per.register_on(&eng, t_key.clone());
    acct.register_on(&eng, a_key.clone());
    eng.put(Event::with_data(t_key, Arc::new(trade.clone())))
        .unwrap();

    assert_eq!(acct0.available(), 99_700.0);
    assert_eq!(acct0.en_accountid(), "xt.demo");
    eng.put(Event::with_data(a_key, Arc::new(acct0))).unwrap();

    assert!(wait_until(Duration::from_secs(2), || per.len() == 1 && acct.len() == 1));
    eng.stop();
    let t = per.last().unwrap();
    assert_eq!(t.price, 3520.2);
    assert_eq!(t.volume, 2.0);
    let a = acct.last().unwrap();
    assert_eq!(a.available(), 99_700.0);
}