将期货现金账本切换为定点并修正日度盈亏
This commit is contained in:
@@ -208,3 +208,134 @@ fn futures_expiration_settlement_closes_all_contract_directions() {
|
||||
);
|
||||
assert!((account.total_cash() - 1_003_000.0).abs() < 1e-6);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn futures_full_close_preserves_closed_position_daily_metrics() {
|
||||
let spec = FuturesContractSpec::new(10.0, 0.1, 0.1);
|
||||
let mut account = FuturesAccountState::new(100_000.0);
|
||||
account.open("IF2506.CCFX", FuturesDirection::Long, spec, 1, 100.0, 1.0);
|
||||
account.begin_trading_day();
|
||||
|
||||
let realized = account
|
||||
.close("IF2506.CCFX", FuturesDirection::Long, 1, 110.0, 2.0)
|
||||
.expect("close overnight position");
|
||||
|
||||
assert!(account.positions().is_empty());
|
||||
assert!((realized - 98.0).abs() < 1e-12);
|
||||
assert!((account.position_pnl() - 100.0).abs() < 1e-12);
|
||||
assert!(account.trading_pnl().abs() < 1e-12);
|
||||
assert!((account.transaction_cost() - 2.0).abs() < 1e-12);
|
||||
assert!((account.daily_pnl() - 98.0).abs() < 1e-12);
|
||||
assert!((account.total_cash() - 100_097.0).abs() < 1e-12);
|
||||
|
||||
account.begin_trading_day();
|
||||
assert!(account.daily_pnl().abs() < 1e-12);
|
||||
assert!(account.transaction_cost().abs() < 1e-12);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn futures_intraday_roundtrip_preserves_closed_trading_pnl() {
|
||||
let spec = FuturesContractSpec::new(10.0, 0.1, 0.1);
|
||||
let mut account = FuturesAccountState::new(100_000.0);
|
||||
account.begin_trading_day();
|
||||
account.open("IF2506.CCFX", FuturesDirection::Long, spec, 1, 100.0, 1.0);
|
||||
account
|
||||
.close("IF2506.CCFX", FuturesDirection::Long, 1, 110.0, 2.0)
|
||||
.expect("close intraday position");
|
||||
|
||||
assert!(account.positions().is_empty());
|
||||
assert!((account.trading_pnl() - 100.0).abs() < 1e-12);
|
||||
assert!(account.position_pnl().abs() < 1e-12);
|
||||
assert!((account.transaction_cost() - 3.0).abs() < 1e-12);
|
||||
assert!((account.daily_pnl() - 97.0).abs() < 1e-12);
|
||||
assert!((account.total_cash() - 100_097.0).abs() < 1e-12);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn futures_partial_close_offsets_later_mark_with_trading_pnl() {
|
||||
let spec = FuturesContractSpec::new(10.0, 0.1, 0.1);
|
||||
let mut account = FuturesAccountState::new(100_000.0);
|
||||
account.open("IF2506.CCFX", FuturesDirection::Long, spec, 2, 100.0, 0.0);
|
||||
account.begin_trading_day();
|
||||
account
|
||||
.close("IF2506.CCFX", FuturesDirection::Long, 1, 110.0, 0.0)
|
||||
.expect("partially close overnight position");
|
||||
account.mark_price("IF2506.CCFX", FuturesDirection::Long, 120.0);
|
||||
|
||||
assert!((account.position_pnl() - 400.0).abs() < 1e-12);
|
||||
assert!((account.trading_pnl() + 100.0).abs() < 1e-12);
|
||||
assert!((account.daily_pnl() - 300.0).abs() < 1e-12);
|
||||
assert!((account.total_value() - 100_300.0).abs() < 1e-12);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn futures_settlement_keeps_same_day_pnl_visible_until_next_day() {
|
||||
let spec = FuturesContractSpec::new(10.0, 0.1, 0.1);
|
||||
let mut account = FuturesAccountState::new(100_000.0);
|
||||
account.open("IF2506.CCFX", FuturesDirection::Long, spec, 1, 100.0, 0.0);
|
||||
account.begin_trading_day();
|
||||
account.mark_price("IF2506.CCFX", FuturesDirection::Long, 110.0);
|
||||
|
||||
let settled = account.settle(&BTreeMap::from([("IF2506.CCFX".to_string(), 110.0)]));
|
||||
|
||||
assert!((settled - 100.0).abs() < 1e-12);
|
||||
assert!((account.daily_pnl() - 100.0).abs() < 1e-12);
|
||||
assert!((account.total_cash() - 100_100.0).abs() < 1e-12);
|
||||
assert!((account.total_value() - 100_100.0).abs() < 1e-12);
|
||||
|
||||
account.begin_trading_day();
|
||||
assert!(account.daily_pnl().abs() < 1e-12);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn futures_cash_and_closed_cost_accumulate_micro_yuan_exactly() {
|
||||
let spec = FuturesContractSpec::new(1.0, 0.0, 0.0);
|
||||
let mut account = FuturesAccountState::new(1_000_000.0);
|
||||
account.begin_trading_day();
|
||||
for _ in 0..10_000 {
|
||||
account.open(
|
||||
"IF2506.CCFX",
|
||||
FuturesDirection::Long,
|
||||
spec,
|
||||
1,
|
||||
100.0,
|
||||
0.000001,
|
||||
);
|
||||
account
|
||||
.close("IF2506.CCFX", FuturesDirection::Long, 1, 100.0, 0.000001)
|
||||
.expect("close micro-cost position");
|
||||
}
|
||||
|
||||
assert!((account.total_cash() - 999_999.98).abs() < 1e-12);
|
||||
assert!((account.transaction_cost() - 0.02).abs() < 1e-12);
|
||||
assert!((account.daily_pnl() + 0.02).abs() < 1e-12);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn futures_margin_gate_and_fill_cash_use_exact_micro_yuan() {
|
||||
let date = d(2025, 1, 2);
|
||||
let spec = FuturesContractSpec::new(1.0, 1.0, 1.0);
|
||||
let intent = FuturesOrderIntent::open(
|
||||
"IF2506.CCFX",
|
||||
FuturesDirection::Long,
|
||||
spec,
|
||||
1,
|
||||
100.0,
|
||||
0.000001,
|
||||
"micro margin boundary",
|
||||
);
|
||||
|
||||
let mut insufficient = FuturesAccountState::new(100.0);
|
||||
let rejected = insufficient.execute_order(date, Some(1), intent.clone());
|
||||
assert_eq!(rejected.order_events[0].status, OrderStatus::Rejected);
|
||||
assert!((insufficient.total_cash() - 100.0).abs() < 1e-12);
|
||||
|
||||
let mut exact = FuturesAccountState::new(100.000001);
|
||||
let filled = exact.execute_order(date, Some(2), intent);
|
||||
assert_eq!(filled.order_events[0].status, OrderStatus::Filled);
|
||||
assert_eq!(filled.fill_events.len(), 1);
|
||||
assert!((filled.fill_events[0].gross_amount - 100.0).abs() < 1e-12);
|
||||
assert!((filled.fill_events[0].commission - 0.000001).abs() < 1e-12);
|
||||
assert!((filled.fill_events[0].net_cash_flow + 0.000001).abs() < 1e-12);
|
||||
assert!(exact.cash().abs() < 1e-12);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user