use ek_data::constant::*;
use ek_data::time::*;
use ek_data::*;
fn tick(limit_up: f64, limit_down: f64) -> TickData {
TickData {
limit_up,
limit_down,
..TickData::default()
}
}
#[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));
assert!(t.reach_limit_up(0.7));
}
#[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));
assert!(t.reach_limit_down(0.7));
}
#[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));
}
fn order(status: Status) -> OrderData {
OrderData {
gateway_name: "xt".into(),
symbol: "IF2506".into(),
exchange: Exchange::CFFEX,
orderid: "1".into(),
status,
..OrderData::default()
}
}
#[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());
assert_eq!(Status::SUBMITTING.is_active(), order(Status::SUBMITTING).is_active());
assert!(!Status::REJECTED.is_active());
}
#[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);
}
#[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);
}
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() {
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() {
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));
assert!(!req_volume(10.0).is_volume_beishu_ok(0.0));
}
#[test]
fn time_civil_roundtrip_known_days() {
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));
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
);
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)
);
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); let _ = format_civil(civil_to_nanos(Civil { year: 2024, month: 1, day: 6, hour: 8, minute: 3, second: 9, nanos: 12 }));
}
#[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));
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()))
);
}
#[test]
fn contract_defaults_match_python() {
let c = ContractData::default();
assert_eq!(c.size, 1.0); assert_eq!(c.price_tick, 0.01); assert!(c.instrument_type.is_none());
assert!(c.product_id.is_none());
}
#[test]
fn log_data_default_level_is_info_20() {
let l = LogData { msg: "hello".into(), ..LogData::default() };
assert_eq!(l.level, 20); 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);
}
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)));
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() {
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 年超出下界");
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 })
);
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 }))
);
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);
}
#[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");
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();
}