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_contract_log —— F8 合约流(单投,parity 约定) + F9 eLog 数据侧(单形态)。
//!
//! 0.3.0 决议落地后的形态:业务日志只有一种载荷 —— ek-data::LogData 于 "eLog"(无点键);
//! 宿主(ek-core 0.3.0 零日志)全程不在场,本文件验证「单一真载荷」契约本身。

mod common;

use common::{Capture, wait_until};
use ek_data::{
    ContractData, Direction, EVENT_CONTRACT, EVENT_LOG, Exchange, LEVEL_ERROR, LEVEL_INFO,
    LEVEL_WARNING, LogData, Offset, OrderRequest, OrderType,
};
use ek_event::Event;
use std::sync::Arc;
use std::time::Duration;

/// F8 合约流:原作约定为【单投】(gateway.py:160-164 无双投),订阅裸键即收;
/// 载荷先于行情到,订阅请求与价签刻度从合约字典读出。
#[test]
fn f8_contract_flow_single_put() {
    let eng = ek_event::EventEngine::new();
    let cap = Capture::<ContractData>::new();
    // 原作形态:合约只有裸键事件(eContract.),不存在 per-symbol 组合键
    cap.register_on(&eng, EVENT_CONTRACT);
    eng.start();

    let c = ContractData {
        gateway_name: "xt".into(),
        symbol: "IF2506".into(),
        exchange: Exchange::CFFEX,
        name: "沪深300股指期货06".into(),
        ..Default::default()
    };
    // 缺省契约字典:size=1,price_tick=0.01(object.py:356 一致性保留)
    assert_eq!(c.size, 1.0);
    assert_eq!(c.price_tick, 0.01);
    eng.put(Event::with_data(EVENT_CONTRACT, Arc::new(c)))
        .unwrap();

    assert!(wait_until(Duration::from_secs(2), || cap.len() == 1));
    eng.stop();
    let dict = cap.last().unwrap();
    assert_eq!(dict.en_symbol(), "CFFEX.IF2506");
    // 下游人口:订阅请求 + 订单请求价签均按字典出
    let sub = OrderRequest {
        symbol: dict.symbol.clone(),
        exchange: dict.exchange,
        direction: Direction::LONG,
        order_type: OrderType::LIMIT,
        volume: 1.0,
        price: ek_data::round_to(3520.28, dict.price_tick),
        offset: Offset::OPEN,
        reference: "demo".into(),
        old_order_id: String::new(),
    };
    assert_eq!(sub.price, 3520.28, "0.01 tick 下 3520.28 即合法价签");
}

/// F8-2 合约流 parity 反证:发 per-symbol 组合键无人接收(约定就是单投)。
#[test]
fn f8_contract_no_composed_key() {
    let eng = ek_event::EventEngine::new();
    let bare = Capture::<ContractData>::new();
    let composed = Capture::<ContractData>::new();
    bare.register_on(&eng, EVENT_CONTRACT);
    composed.register_on(&eng, format!("{EVENT_CONTRACT}CFFEX.IF2506"));
    eng.start();

    let c = ContractData {
        gateway_name: "xt".into(),
        symbol: "IF2506".into(),
        exchange: Exchange::CFFEX,
        ..Default::default()
    };
    // parity:原作合约只发裸键(无双投);哪怕有人订阅字面组合键也永远空收
    eng.put(Event::with_data(EVENT_CONTRACT, Arc::new(c)))
        .unwrap();
    // 排空哨兵:与裸键事件同为 FIFO,哨兵到齐 ⇒ 负面窗口已关
    let settle = common::drained(&eng, 1);
    assert!(wait_until(Duration::from_secs(2), || settle.len() == 1));
    eng.stop();
    assert_eq!(bare.len(), 1);
    assert_eq!(
        composed.len(),
        0,
        "发的是字面键 eContract.CFFEX.IF2506,字典前缀订阅(eContract.)精确匹配收不到 —— 原作同样只发裸键"
    );
}

/// F9(数据侧单形态):eLog 上只有 ek-data::LogData 一种载荷;级别常量 20/30/40。
#[test]
fn f9_elog_single_shape() {
    let eng = ek_event::EventEngine::new();
    let logs = Capture::<LogData>::new();
    logs.register_on(&eng, EVENT_LOG);
    eng.start();

    let conn = LogData::info("xt", "连接成功");
    assert_eq!(conn.gateway_name, "xt");
    assert!(!conn.is_warning_or_above());
    let burst = LogData {
        gateway_name: "xt".into(),
        msg: "重连退避(3)".into(),
        level: LEVEL_WARNING,
    };
    let fatal = LogData {
        gateway_name: "xt".into(),
        msg: "交易前置不可达".into(),
        level: LEVEL_ERROR,
    };
    assert!(burst.is_warning_or_above());
    assert!(fatal.is_warning_or_above());
    assert_eq!(LEVEL_INFO, 20);
    assert_eq!(LEVEL_WARNING, 30);
    assert_eq!(LEVEL_ERROR, 40);

    // 三条日志按序吸附在同一条 eLog(全局流,订阅即裸键本身)
    eng.put(Event::with_data(EVENT_LOG, Arc::new(conn)))
        .unwrap();
    eng.put(Event::with_data(EVENT_LOG, Arc::new(burst)))
        .unwrap();
    eng.put(Event::with_data(EVENT_LOG, Arc::new(fatal)))
        .unwrap();

    assert!(wait_until(Duration::from_secs(2), || logs.len() == 3));
    eng.stop();
    let all = logs.all();
    assert!(all.iter().all(|l| l.gateway_name == "xt"));
    assert!(all[1].is_warning_or_above());
    assert_eq!(all[2].level, LEVEL_ERROR);
}