Add futures order execution model

This commit is contained in:
boris
2026-04-23 20:29:14 -07:00
parent 68adc6b25c
commit 2669350154
4 changed files with 472 additions and 7 deletions

View File

@@ -1,6 +1,14 @@
use std::collections::BTreeMap;
use fidc_core::{FuturesAccountState, FuturesContractSpec, FuturesDirection};
use chrono::NaiveDate;
use fidc_core::{
FuturesAccountState, FuturesContractSpec, FuturesDirection, FuturesOrderIntent,
FuturesPositionEffect, OrderStatus,
};
fn d(year: i32, month: u32, day: u32) -> NaiveDate {
NaiveDate::from_ymd_opt(year, month, day).expect("valid date")
}
#[test]
fn futures_account_tracks_long_margin_pnl_and_settlement() {
@@ -50,3 +58,106 @@ fn futures_account_tracks_short_close_cash_delta() {
assert_eq!(position.quantity, 3);
assert!((position.equity() - 900.0).abs() < 1e-6);
}
#[test]
fn futures_order_execution_splits_close_between_old_and_today_quantity() {
let spec = FuturesContractSpec::new(300.0, 0.12, 0.12);
let mut account = FuturesAccountState::new(1_000_000.0);
account.open("IF2501", FuturesDirection::Long, spec, 3, 4000.0, 0.0);
account.begin_trading_day();
account.open("IF2501", FuturesDirection::Long, spec, 2, 4010.0, 0.0);
let report = account.execute_order(
d(2025, 1, 2),
Some(10),
FuturesOrderIntent::close(
"IF2501",
FuturesDirection::Long,
FuturesPositionEffect::Close,
spec,
4,
4020.0,
4.0,
"auto close",
),
);
assert_eq!(report.order_events.len(), 1);
assert_eq!(report.order_events[0].status, OrderStatus::Filled);
assert_eq!(report.fill_events[0].quantity, 4);
assert!((report.fill_events[0].net_cash_flow - 19_196.0).abs() < 1e-6);
let position = account
.position("IF2501", FuturesDirection::Long)
.expect("remaining long position");
assert_eq!(position.quantity, 1);
assert_eq!(position.old_quantity, 0);
assert_eq!(position.today_quantity(), 1);
}
#[test]
fn futures_close_today_rejects_when_today_quantity_is_insufficient() {
let spec = FuturesContractSpec::new(10.0, 0.1, 0.1);
let mut account = FuturesAccountState::new(100_000.0);
account.open("RB2501", FuturesDirection::Short, spec, 2, 3500.0, 0.0);
account.begin_trading_day();
let report = account.execute_order(
d(2025, 1, 3),
Some(11),
FuturesOrderIntent::close(
"RB2501",
FuturesDirection::Short,
FuturesPositionEffect::CloseToday,
spec,
1,
3490.0,
1.0,
"close today without today position",
),
);
assert_eq!(report.order_events.len(), 1);
assert_eq!(report.order_events[0].status, OrderStatus::Rejected);
assert!(
report.order_events[0]
.reason
.contains("close today quantity")
);
let position = account
.position("RB2501", FuturesDirection::Short)
.expect("short position unchanged");
assert_eq!(position.quantity, 2);
assert_eq!(position.old_quantity, 2);
}
#[test]
fn futures_open_order_rejects_when_margin_is_insufficient() {
let spec = FuturesContractSpec::new(300.0, 0.2, 0.2);
let mut account = FuturesAccountState::new(10_000.0);
let report = account.execute_order(
d(2025, 1, 6),
Some(12),
FuturesOrderIntent::open(
"IF2501",
FuturesDirection::Long,
spec,
1,
4000.0,
2.0,
"oversized open",
),
);
assert_eq!(report.order_events.len(), 1);
assert_eq!(report.order_events[0].status, OrderStatus::Rejected);
assert!(
report.order_events[0]
.reason
.contains("insufficient futures margin")
);
assert!(account.position("IF2501", FuturesDirection::Long).is_none());
assert!((account.total_cash() - 10_000.0).abs() < 1e-6);
}