Add futures expiration settlement

This commit is contained in:
boris
2026-04-23 20:39:40 -07:00
parent 3439b5d8d0
commit dfd39fd8a3
3 changed files with 86 additions and 2 deletions

View File

@@ -679,6 +679,61 @@ impl FuturesAccountState {
report
}
pub fn expire_contract(
&mut self,
date: NaiveDate,
symbol: &str,
settlement_price: f64,
reason: impl Into<String>,
) -> FuturesExecutionReport {
let reason = reason.into();
let keys = self
.positions
.keys()
.filter(|(position_symbol, _)| position_symbol == symbol)
.cloned()
.collect::<Vec<_>>();
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);

View File

@@ -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);
}

View File

@@ -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.