From dfd39fd8a3fed8a495bdf8902012f1443b01d48f Mon Sep 17 00:00:00 2001 From: boris Date: Thu, 23 Apr 2026 20:39:40 -0700 Subject: [PATCH] Add futures expiration settlement --- crates/fidc-core/src/futures.rs | 55 +++++++++++++++++++++++ crates/fidc-core/tests/futures_account.rs | 27 +++++++++++ docs/rqalpha-gap-roadmap.md | 6 ++- 3 files changed, 86 insertions(+), 2 deletions(-) diff --git a/crates/fidc-core/src/futures.rs b/crates/fidc-core/src/futures.rs index d49ade6..613488f 100644 --- a/crates/fidc-core/src/futures.rs +++ b/crates/fidc-core/src/futures.rs @@ -679,6 +679,61 @@ impl FuturesAccountState { report } + pub fn expire_contract( + &mut self, + date: NaiveDate, + symbol: &str, + settlement_price: f64, + reason: impl Into, + ) -> FuturesExecutionReport { + let reason = reason.into(); + let keys = self + .positions + .keys() + .filter(|(position_symbol, _)| position_symbol == symbol) + .cloned() + .collect::>(); + let mut combined = FuturesExecutionReport::default(); + for (position_symbol, direction) in keys { + let Some(position) = self.position(&position_symbol, direction) else { + continue; + }; + if position.quantity == 0 { + continue; + } + let price = if settlement_price.is_finite() && settlement_price > 0.0 { + settlement_price + } else { + position.last_price + }; + let intent = FuturesOrderIntent::close( + position_symbol.clone(), + direction, + FuturesPositionEffect::Close, + FuturesContractSpec::new( + position.contract_multiplier, + position.margin_rate, + position.margin_rate, + ), + position.quantity, + price, + 0.0, + format!("{reason}: futures_expiration_settlement"), + ); + let report = self.execute_order(date, None, intent); + combined.order_events.extend(report.order_events); + combined.fill_events.extend(report.fill_events); + combined.position_events.extend(report.position_events); + combined.account_events.extend(report.account_events); + combined.diagnostics.extend(report.diagnostics); + } + combined.diagnostics.push(format!( + "futures_expiration_settlement symbol={symbol} closed_orders={}", + combined.order_events.len() + )); + combined + } + pub fn mark_price(&mut self, symbol: &str, direction: FuturesDirection, price: f64) { if let Some(position) = self.positions.get_mut(&(symbol.to_string(), direction)) { position.mark_price(price); diff --git a/crates/fidc-core/tests/futures_account.rs b/crates/fidc-core/tests/futures_account.rs index 821ab85..4554453 100644 --- a/crates/fidc-core/tests/futures_account.rs +++ b/crates/fidc-core/tests/futures_account.rs @@ -161,3 +161,30 @@ fn futures_open_order_rejects_when_margin_is_insufficient() { assert!(account.position("IF2501", FuturesDirection::Long).is_none()); assert!((account.total_cash() - 10_000.0).abs() < 1e-6); } + +#[test] +fn futures_expiration_settlement_closes_all_contract_directions() { + let spec = FuturesContractSpec::new(300.0, 0.12, 0.14); + let mut account = FuturesAccountState::new(1_000_000.0); + + account.open("IF2501", FuturesDirection::Long, spec, 2, 4000.0, 0.0); + account.open("IF2501", FuturesDirection::Short, spec, 1, 4000.0, 0.0); + + let report = account.expire_contract(d(2025, 1, 17), "IF2501", 4010.0, "contract expired"); + + assert_eq!(report.order_events.len(), 2); + assert_eq!(report.fill_events.len(), 2); + assert!( + report + .order_events + .iter() + .all(|event| event.status == OrderStatus::Filled) + ); + assert!(account.position("IF2501", FuturesDirection::Long).is_none()); + assert!( + account + .position("IF2501", FuturesDirection::Short) + .is_none() + ); + assert!((account.total_cash() - 1_003_000.0).abs() < 1e-6); +} diff --git a/docs/rqalpha-gap-roadmap.md b/docs/rqalpha-gap-roadmap.md index 9cede8a..2c1fac4 100644 --- a/docs/rqalpha-gap-roadmap.md +++ b/docs/rqalpha-gap-roadmap.md @@ -94,7 +94,9 @@ current alignment pass. `accounts`) - [x] wire futures order intents into the generic `BacktestEngine` execution loop for account-level open/close execution -- [ ] futures intraday matching integration and expiration settlement +- [x] standalone futures expiration settlement closes all long/short contract + positions at settlement price +- [ ] futures intraday matching integration and data-driven expiration schedule ## Execution Order @@ -115,4 +117,4 @@ account runtime view, core Portfolio fields, deposit/withdraw, financing liability APIs, management-fee callbacks, stock account accessors, and the standalone futures account/order execution model plus generic engine runtime account visibility and account-level futures order intents; next gap is adding -futures intraday matching and expiration settlement semantics. +futures intraday matching and a data-driven expiration schedule.