use std::collections::BTreeMap; use chrono::NaiveDate; use serde::{Deserialize, Serialize}; use crate::events::{ AccountEvent, FillEvent, OrderEvent, OrderSide, OrderStatus, PositionEvent, ProcessEvent, ProcessEventKind, }; use crate::fixed_point::FixedMoney; fn futures_money(value: f64, label: &str) -> Result { FixedMoney::from_f64(value) .ok_or_else(|| format!("{label} is not representable as fixed-point money: {value}")) } fn futures_money_or_panic(value: f64, label: &str) -> FixedMoney { futures_money(value, label).unwrap_or_else(|error| panic!("{error}")) } fn sum_futures_money(values: impl IntoIterator, label: &str) -> FixedMoney { values.into_iter().fold(FixedMoney::ZERO, |total, value| { total .checked_add(value) .unwrap_or_else(|| panic!("fixed-point {label} overflow")) }) } #[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)] pub enum FuturesDirection { Long, Short, } impl FuturesDirection { pub fn as_str(&self) -> &'static str { match self { Self::Long => "long", Self::Short => "short", } } fn factor(&self) -> f64 { match self { Self::Long => 1.0, Self::Short => -1.0, } } fn open_side(&self) -> OrderSide { match self { Self::Long => OrderSide::Buy, Self::Short => OrderSide::Sell, } } fn close_side(&self) -> OrderSide { match self { Self::Long => OrderSide::Sell, Self::Short => OrderSide::Buy, } } } #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum FuturesPositionEffect { Open, Close, CloseToday, CloseYesterday, } impl FuturesPositionEffect { pub fn as_str(&self) -> &'static str { match self { Self::Open => "open", Self::Close => "close", Self::CloseToday => "close_today", Self::CloseYesterday => "close_yesterday", } } } #[derive(Debug, Clone, Copy)] pub struct FuturesContractSpec { pub contract_multiplier: f64, pub long_margin_rate: f64, pub short_margin_rate: f64, } #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] pub enum FuturesCommissionType { ByMoney, ByVolume, } impl FuturesCommissionType { pub fn parse(value: &str) -> Self { match value.trim().to_ascii_lowercase().as_str() { "by_volume" | "volume" | "byvolume" => Self::ByVolume, _ => Self::ByMoney, } } pub fn as_str(&self) -> &'static str { match self { Self::ByMoney => "by_money", Self::ByVolume => "by_volume", } } } #[derive(Debug, Clone, Serialize, Deserialize)] pub struct FuturesTradingParameter { pub symbol: String, pub effective_date: Option, pub contract_multiplier: f64, pub long_margin_rate: f64, pub short_margin_rate: f64, pub commission_type: FuturesCommissionType, pub open_commission_ratio: f64, pub close_commission_ratio: f64, pub close_today_commission_ratio: f64, pub price_tick: f64, } impl FuturesTradingParameter { pub fn spec(&self) -> FuturesContractSpec { FuturesContractSpec::new( self.contract_multiplier, self.long_margin_rate, self.short_margin_rate, ) } } #[derive(Debug, Clone, Copy)] pub struct FuturesTransactionCostModel { pub commission_multiplier: f64, } impl Default for FuturesTransactionCostModel { fn default() -> Self { Self { commission_multiplier: 1.0, } } } impl FuturesTransactionCostModel { pub fn calculate( &self, params: &FuturesTradingParameter, effect: FuturesPositionEffect, price: f64, quantity: u32, close_today_quantity: u32, ) -> f64 { if quantity == 0 || !price.is_finite() || price <= 0.0 { return 0.0; } let quantity = quantity as f64; let close_today_quantity = close_today_quantity.min(quantity as u32) as f64; let close_yesterday_quantity = (quantity - close_today_quantity).max(0.0); let raw = match params.commission_type { FuturesCommissionType::ByMoney => match effect { FuturesPositionEffect::Open => { price * quantity * params.contract_multiplier * params.open_commission_ratio } FuturesPositionEffect::Close | FuturesPositionEffect::CloseToday | FuturesPositionEffect::CloseYesterday => { price * params.contract_multiplier * (close_yesterday_quantity * params.close_commission_ratio + close_today_quantity * params.close_today_commission_ratio) } }, FuturesCommissionType::ByVolume => match effect { FuturesPositionEffect::Open => quantity * params.open_commission_ratio, FuturesPositionEffect::Close | FuturesPositionEffect::CloseToday | FuturesPositionEffect::CloseYesterday => { close_yesterday_quantity * params.close_commission_ratio + close_today_quantity * params.close_today_commission_ratio } }, }; raw.max(0.0) * self.commission_multiplier.max(0.0) } } #[derive(Debug, Clone)] pub struct FuturesOrderIntent { pub symbol: String, pub direction: FuturesDirection, pub effect: FuturesPositionEffect, pub spec: FuturesContractSpec, pub quantity: u32, pub price: f64, pub transaction_cost: f64, pub limit_price: Option, pub allow_pending: bool, pub reason: String, } impl FuturesOrderIntent { pub fn open( symbol: impl Into, direction: FuturesDirection, spec: FuturesContractSpec, quantity: u32, price: f64, transaction_cost: f64, reason: impl Into, ) -> Self { Self { symbol: symbol.into(), direction, effect: FuturesPositionEffect::Open, spec, quantity, price, transaction_cost, limit_price: None, allow_pending: false, reason: reason.into(), } } pub fn close( symbol: impl Into, direction: FuturesDirection, effect: FuturesPositionEffect, spec: FuturesContractSpec, quantity: u32, price: f64, transaction_cost: f64, reason: impl Into, ) -> Self { Self { symbol: symbol.into(), direction, effect, spec, quantity, price, transaction_cost, limit_price: None, allow_pending: false, reason: reason.into(), } } pub fn limit_open( symbol: impl Into, direction: FuturesDirection, spec: FuturesContractSpec, quantity: u32, limit_price: f64, transaction_cost: f64, reason: impl Into, ) -> Self { Self::open( symbol, direction, spec, quantity, limit_price, transaction_cost, reason, ) .with_limit_price(limit_price) } pub fn limit_close( symbol: impl Into, direction: FuturesDirection, effect: FuturesPositionEffect, spec: FuturesContractSpec, quantity: u32, limit_price: f64, transaction_cost: f64, reason: impl Into, ) -> Self { Self::close( symbol, direction, effect, spec, quantity, limit_price, transaction_cost, reason, ) .with_limit_price(limit_price) } pub fn with_limit_price(mut self, limit_price: f64) -> Self { self.limit_price = limit_price .is_finite() .then_some(limit_price) .filter(|v| *v > 0.0); self.allow_pending = self.limit_price.is_some(); self } pub fn with_allow_pending(mut self, allow_pending: bool) -> Self { self.allow_pending = allow_pending; self } pub fn with_price(mut self, price: f64) -> Self { self.price = price; self } pub fn with_transaction_cost(mut self, transaction_cost: f64) -> Self { self.transaction_cost = transaction_cost; self } pub fn side(&self) -> OrderSide { if self.effect == FuturesPositionEffect::Open { self.direction.open_side() } else { self.direction.close_side() } } pub fn with_trading_parameter( mut self, params: &FuturesTradingParameter, cost_model: FuturesTransactionCostModel, ) -> Self { self.spec = params.spec(); if self.transaction_cost <= 0.0 { let close_today_quantity = if self.effect == FuturesPositionEffect::CloseToday { self.quantity } else { 0 }; self.transaction_cost = cost_model.calculate( params, self.effect, self.price, self.quantity, close_today_quantity, ); } self } } #[derive(Debug, Clone, Default)] pub struct FuturesExecutionReport { pub order_events: Vec, pub fill_events: Vec, pub position_events: Vec, pub account_events: Vec, pub process_events: Vec, pub diagnostics: Vec, } impl FuturesContractSpec { pub fn unresolved() -> Self { Self { contract_multiplier: f64::NAN, long_margin_rate: f64::NAN, short_margin_rate: f64::NAN, } } pub fn new(contract_multiplier: f64, long_margin_rate: f64, short_margin_rate: f64) -> Self { Self { contract_multiplier: contract_multiplier.max(1.0), long_margin_rate: long_margin_rate.max(0.0), short_margin_rate: short_margin_rate.max(0.0), } } pub fn margin_rate(&self, direction: FuturesDirection) -> f64 { match direction { FuturesDirection::Long => self.long_margin_rate, FuturesDirection::Short => self.short_margin_rate, } } pub fn is_resolved(&self) -> bool { self.contract_multiplier.is_finite() && self.contract_multiplier > 0.0 && self.long_margin_rate.is_finite() && self.long_margin_rate >= 0.0 && self.short_margin_rate.is_finite() && self.short_margin_rate >= 0.0 } } #[derive(Debug, Clone)] pub struct FuturesPosition { pub symbol: String, pub direction: FuturesDirection, pub old_quantity: u32, day_start_quantity: u32, pub quantity: u32, pub avg_price: f64, pub last_price: f64, pub prev_close: f64, pub contract_multiplier: f64, pub margin_rate: f64, transaction_cost: FixedMoney, trade_quantity_delta: i32, trade_value: FixedMoney, } impl FuturesPosition { pub fn new( symbol: impl Into, direction: FuturesDirection, spec: FuturesContractSpec, init_quantity: u32, init_price: f64, ) -> Self { let margin_rate = spec.margin_rate(direction); Self { symbol: symbol.into(), direction, old_quantity: init_quantity, day_start_quantity: init_quantity, quantity: init_quantity, avg_price: init_price.max(0.0), last_price: init_price.max(0.0), prev_close: init_price.max(0.0), contract_multiplier: spec.contract_multiplier, margin_rate, transaction_cost: FixedMoney::ZERO, trade_quantity_delta: 0, trade_value: FixedMoney::ZERO, } } pub fn today_quantity(&self) -> u32 { self.quantity.saturating_sub(self.old_quantity) } pub fn market_value(&self) -> f64 { self.market_value_money().to_f64() } fn market_value_money(&self) -> FixedMoney { futures_money_or_panic( self.quantity as f64 * self.last_price * self.contract_multiplier, "futures position market value", ) } pub fn margin(&self) -> f64 { self.margin_money().to_f64() } fn margin_money(&self) -> FixedMoney { futures_money_or_panic( self.market_value_money().to_f64() * self.margin_rate, "futures position margin", ) } pub fn equity(&self) -> f64 { self.equity_money().to_f64() } fn equity_money(&self) -> FixedMoney { futures_money_or_panic( (self.last_price - self.avg_price) * self.quantity as f64 * self.contract_multiplier * self.direction.factor(), "futures position equity", ) } pub fn pnl(&self) -> f64 { self.equity() } pub fn trading_pnl(&self) -> f64 { self.trading_pnl_money().to_f64() } fn trading_pnl_money(&self) -> FixedMoney { let marked_trade_value = futures_money_or_panic( self.trade_quantity_delta as f64 * self.last_price * self.contract_multiplier, "futures marked trade value", ); let pnl = marked_trade_value .checked_sub(self.trade_value) .expect("fixed-point futures trading PnL overflow"); if self.direction == FuturesDirection::Short { pnl.checked_neg() .expect("fixed-point futures short trading PnL overflow") } else { pnl } } pub fn position_pnl(&self) -> f64 { self.position_pnl_money().to_f64() } fn position_pnl_money(&self) -> FixedMoney { if self.day_start_quantity == 0 { FixedMoney::ZERO } else { futures_money_or_panic( self.day_start_quantity as f64 * (self.last_price - self.prev_close) * self.contract_multiplier * self.direction.factor(), "futures position daily PnL", ) } } pub fn transaction_cost(&self) -> f64 { self.transaction_cost.to_f64() } pub fn open(&mut self, quantity: u32, price: f64, transaction_cost: f64) { if quantity == 0 { return; } let old_value = self.avg_price * self.quantity as f64; self.quantity += quantity; self.avg_price = (old_value + price * quantity as f64) / self.quantity as f64; self.last_price = price; let transaction_cost = futures_money_or_panic(transaction_cost.max(0.0), "futures open transaction cost"); self.transaction_cost = self .transaction_cost .checked_add(transaction_cost) .expect("fixed-point futures transaction cost overflow"); self.trade_quantity_delta += quantity as i32; self.trade_value = self .trade_value .checked_add(futures_money_or_panic( price * quantity as f64 * self.contract_multiplier, "futures open trade value", )) .expect("fixed-point futures trade value overflow"); } pub fn close( &mut self, quantity: u32, price: f64, transaction_cost: f64, ) -> Result { self.close_with_effect( quantity, price, transaction_cost, FuturesPositionEffect::Close, ) } pub fn close_with_effect( &mut self, quantity: u32, price: f64, transaction_cost: f64, effect: FuturesPositionEffect, ) -> Result { self.close_with_effect_money(quantity, price, transaction_cost, effect) .map(FixedMoney::to_f64) } fn close_with_effect_money( &mut self, quantity: u32, price: f64, transaction_cost: f64, effect: FuturesPositionEffect, ) -> Result { if effect == FuturesPositionEffect::Open { return Err("close_with_effect does not accept open effect".to_string()); } if quantity > self.quantity { return Err(format!( "close quantity {} exceeds current quantity {} for {} {}", quantity, self.quantity, self.symbol, self.direction.as_str() )); } if quantity == 0 { return Ok(FixedMoney::ZERO); } match effect { FuturesPositionEffect::Open => unreachable!(), FuturesPositionEffect::Close => { let old_closed = quantity.min(self.old_quantity); self.old_quantity -= old_closed; } FuturesPositionEffect::CloseToday => { let today_quantity = self.today_quantity(); if quantity > today_quantity { return Err(format!( "close today quantity {} exceeds today quantity {} for {} {}", quantity, today_quantity, self.symbol, self.direction.as_str() )); } } FuturesPositionEffect::CloseYesterday => { if quantity > self.old_quantity { return Err(format!( "close yesterday quantity {} exceeds old quantity {} for {} {}", quantity, self.old_quantity, self.symbol, self.direction.as_str() )); } self.old_quantity -= quantity; } } let transaction_cost = futures_money(transaction_cost.max(0.0), "futures close transaction cost")?; let realized = futures_money( (price - self.avg_price) * quantity as f64 * self.contract_multiplier * self.direction.factor(), "futures realized PnL", )? .checked_sub(transaction_cost) .ok_or_else(|| "fixed-point futures realized PnL overflow".to_string())?; self.quantity -= quantity; if self.quantity == 0 { self.avg_price = 0.0; } self.last_price = price; self.transaction_cost = self .transaction_cost .checked_add(transaction_cost) .ok_or_else(|| "fixed-point futures transaction cost overflow".to_string())?; self.trade_quantity_delta -= quantity as i32; self.trade_value = self .trade_value .checked_sub(futures_money( price * quantity as f64 * self.contract_multiplier, "futures close trade value", )?) .ok_or_else(|| "fixed-point futures trade value overflow".to_string())?; Ok(realized) } pub fn mark_price(&mut self, price: f64) { if price.is_finite() && price > 0.0 { self.last_price = price; } } pub fn begin_trading_day(&mut self) { self.old_quantity = self.quantity; self.day_start_quantity = self.quantity; self.prev_close = self.last_price; self.transaction_cost = FixedMoney::ZERO; self.trade_quantity_delta = 0; self.trade_value = FixedMoney::ZERO; } pub fn settlement(&mut self, settlement_price: f64) -> f64 { self.settlement_money(settlement_price).to_f64() } fn settlement_money(&mut self, settlement_price: f64) -> FixedMoney { self.mark_price(settlement_price); let cash_delta = self.equity_money(); self.avg_price = self.last_price; cash_delta } } #[derive(Debug, Clone)] pub struct FuturesAccountState { starting_cash: FixedMoney, total_cash: FixedMoney, frozen_cash: FixedMoney, closed_day_trading_pnl: FixedMoney, closed_day_position_pnl: FixedMoney, closed_day_transaction_cost: FixedMoney, positions: BTreeMap<(String, FuturesDirection), FuturesPosition>, } impl FuturesAccountState { pub fn new(total_cash: f64) -> Self { let total_cash = futures_money_or_panic(total_cash, "futures starting cash"); Self { starting_cash: total_cash, total_cash, frozen_cash: FixedMoney::ZERO, closed_day_trading_pnl: FixedMoney::ZERO, closed_day_position_pnl: FixedMoney::ZERO, closed_day_transaction_cost: FixedMoney::ZERO, positions: BTreeMap::new(), } } pub fn starting_cash(&self) -> f64 { self.starting_cash.to_f64() } pub fn total_cash(&self) -> f64 { self.total_cash.to_f64() } pub fn frozen_cash(&self) -> f64 { self.frozen_cash.to_f64() } pub fn cash(&self) -> f64 { self.cash_money().to_f64() } fn cash_money(&self) -> FixedMoney { self.total_cash .checked_sub(self.margin_money()) .and_then(|cash| cash.checked_sub(self.frozen_cash)) .expect("fixed-point futures available cash overflow") } pub fn margin(&self) -> f64 { self.margin_money().to_f64() } fn margin_money(&self) -> FixedMoney { sum_futures_money( self.positions.values().map(FuturesPosition::margin_money), "futures account margin", ) } pub fn market_value(&self) -> f64 { sum_futures_money( self.positions .values() .map(FuturesPosition::market_value_money), "futures account market value", ) .to_f64() } pub fn position_equity(&self) -> f64 { self.position_equity_money().to_f64() } fn position_equity_money(&self) -> FixedMoney { sum_futures_money( self.positions.values().map(FuturesPosition::equity_money), "futures account position equity", ) } pub fn total_value(&self) -> f64 { self.total_cash .checked_add(self.position_equity_money()) .expect("fixed-point futures total value overflow") .to_f64() } pub fn daily_pnl(&self) -> f64 { self.trading_pnl_money() .checked_add(self.position_pnl_money()) .and_then(|pnl| pnl.checked_sub(self.transaction_cost_money())) .expect("fixed-point futures daily PnL overflow") .to_f64() } pub fn trading_pnl(&self) -> f64 { self.trading_pnl_money().to_f64() } fn trading_pnl_money(&self) -> FixedMoney { sum_futures_money( std::iter::once(self.closed_day_trading_pnl).chain( self.positions .values() .map(FuturesPosition::trading_pnl_money), ), "futures account trading PnL", ) } pub fn position_pnl(&self) -> f64 { self.position_pnl_money().to_f64() } fn position_pnl_money(&self) -> FixedMoney { sum_futures_money( std::iter::once(self.closed_day_position_pnl).chain( self.positions .values() .map(FuturesPosition::position_pnl_money), ), "futures account position PnL", ) } pub fn transaction_cost(&self) -> f64 { self.transaction_cost_money().to_f64() } fn transaction_cost_money(&self) -> FixedMoney { sum_futures_money( std::iter::once(self.closed_day_transaction_cost).chain( self.positions .values() .map(|position| position.transaction_cost), ), "futures account transaction cost", ) } pub fn positions(&self) -> &BTreeMap<(String, FuturesDirection), FuturesPosition> { &self.positions } pub fn position(&self, symbol: &str, direction: FuturesDirection) -> Option<&FuturesPosition> { self.positions.get(&(symbol.to_string(), direction)) } pub fn open( &mut self, symbol: impl Into, direction: FuturesDirection, spec: FuturesContractSpec, quantity: u32, price: f64, transaction_cost: f64, ) { if quantity == 0 { return; } let symbol = symbol.into(); let position = self .positions .entry((symbol.clone(), direction)) .or_insert_with(|| FuturesPosition::new(symbol, direction, spec, 0, price)); position.open(quantity, price, transaction_cost); self.total_cash = self .total_cash .checked_sub(futures_money_or_panic( transaction_cost.max(0.0), "futures open transaction cost", )) .expect("fixed-point futures cash overflow"); } pub fn close( &mut self, symbol: &str, direction: FuturesDirection, quantity: u32, price: f64, transaction_cost: f64, ) -> Result { self.close_with_effect( symbol, direction, quantity, price, transaction_cost, FuturesPositionEffect::Close, ) } pub fn close_with_effect( &mut self, symbol: &str, direction: FuturesDirection, quantity: u32, price: f64, transaction_cost: f64, effect: FuturesPositionEffect, ) -> Result { let key = (symbol.to_string(), direction); let position = self .positions .get_mut(&key) .ok_or_else(|| format!("missing futures position {symbol} {}", direction.as_str()))?; let cash_delta = position.close_with_effect_money(quantity, price, transaction_cost, effect)?; self.total_cash = self .total_cash .checked_add(cash_delta) .ok_or_else(|| "fixed-point futures cash overflow".to_string())?; if position.quantity == 0 { self.closed_day_trading_pnl = self .closed_day_trading_pnl .checked_add(position.trading_pnl_money()) .ok_or_else(|| "fixed-point closed futures trading PnL overflow".to_string())?; self.closed_day_position_pnl = self .closed_day_position_pnl .checked_add(position.position_pnl_money()) .ok_or_else(|| "fixed-point closed futures position PnL overflow".to_string())?; self.closed_day_transaction_cost = self .closed_day_transaction_cost .checked_add(position.transaction_cost) .ok_or_else(|| { "fixed-point closed futures transaction cost overflow".to_string() })?; self.positions.remove(&key); } Ok(cash_delta.to_f64()) } pub fn execute_order( &mut self, date: NaiveDate, order_id: Option, intent: FuturesOrderIntent, ) -> FuturesExecutionReport { let mut report = FuturesExecutionReport::default(); let side = intent.side(); push_futures_process_event( &mut report, date, ProcessEventKind::OrderPendingNew, order_id, &intent.symbol, side, format!( "requested_quantity={} direction={} effect={} reason={}", intent.quantity, intent.direction.as_str(), intent.effect.as_str(), intent.reason ), ); if intent.quantity == 0 || !intent.price.is_finite() || intent.price <= 0.0 { push_futures_process_event( &mut report, date, ProcessEventKind::OrderCreationReject, order_id, &intent.symbol, side, "invalid futures order", ); report.order_events.push(OrderEvent { date, decision_date: None, order_created_date: None, execution_date: None, order_id, symbol: intent.symbol, side, requested_quantity: intent.quantity, filled_quantity: 0, status: OrderStatus::Rejected, reason: format!( "{}: invalid futures order effect={} price={} quantity={}", intent.reason, intent.effect.as_str(), intent.price, intent.quantity ), }); return report; } let cash_before = self.total_cash(); let position_before = self .position(&intent.symbol, intent.direction) .map(|position| position.quantity) .unwrap_or(0); let result = match intent.effect { FuturesPositionEffect::Open => { let mut projected = self.clone(); projected.open( intent.symbol.clone(), intent.direction, intent.spec, intent.quantity, intent.price, intent.transaction_cost, ); if projected.cash_money().raw() < 0 { Err(format!( "insufficient futures margin available_cash={:.2} required_margin_after={:.2}", self.cash(), projected.margin() )) } else { self.open( intent.symbol.clone(), intent.direction, intent.spec, intent.quantity, intent.price, intent.transaction_cost, ); Ok(futures_money_or_panic( intent.transaction_cost.max(0.0), "futures open transaction cost", ) .checked_neg() .expect("fixed-point futures open cash delta overflow") .to_f64()) } } FuturesPositionEffect::Close | FuturesPositionEffect::CloseToday | FuturesPositionEffect::CloseYesterday => self.close_with_effect( &intent.symbol, intent.direction, intent.quantity, intent.price, intent.transaction_cost, intent.effect, ), }; match result { Ok(cash_delta) => { let position_after = self .position(&intent.symbol, intent.direction) .map(|position| position.quantity) .unwrap_or(0); let avg_price_after = self .position(&intent.symbol, intent.direction) .map(|position| position.avg_price) .unwrap_or(0.0); let notional = futures_money_or_panic( intent.price * intent.quantity as f64 * intent.spec.contract_multiplier, "futures fill notional", ) .to_f64(); let transaction_cost = futures_money_or_panic( intent.transaction_cost.max(0.0), "futures fill transaction cost", ) .to_f64(); report.fill_events.push(FillEvent { date, decision_date: None, order_created_date: None, execution_date: None, execution_start_timestamp: None, execution_timestamp: None, order_id, symbol: intent.symbol.clone(), side, quantity: intent.quantity, price: intent.price, gross_amount: notional, commission: transaction_cost, stamp_tax: 0.0, transfer_fee: 0.0, net_cash_flow: cash_delta, reason: format!( "{} direction={} effect={}", intent.reason, intent.direction.as_str(), intent.effect.as_str() ), }); push_futures_process_event( &mut report, date, ProcessEventKind::OrderCreationPass, order_id, &intent.symbol, side, "futures order passed account checks", ); push_futures_process_event( &mut report, date, ProcessEventKind::Trade, order_id, &intent.symbol, side, format!("filled_quantity={} price={}", intent.quantity, intent.price), ); report.position_events.push(PositionEvent { date, symbol: intent.symbol.clone(), delta_quantity: position_after as i32 - position_before as i32, quantity_after: position_after, average_cost: avg_price_after, realized_pnl_delta: if intent.effect == FuturesPositionEffect::Open { 0.0 } else { cash_delta }, reason: format!( "{} direction={} effect={}", intent.reason, intent.direction.as_str(), intent.effect.as_str() ), }); report.account_events.push(AccountEvent { date, cash_before, cash_after: self.total_cash(), total_equity: self.total_value(), note: format!( "futures {} {} {}", intent.symbol, intent.direction.as_str(), intent.effect.as_str() ), }); report.order_events.push(OrderEvent { date, decision_date: None, order_created_date: None, execution_date: None, order_id, symbol: intent.symbol, side, requested_quantity: intent.quantity, filled_quantity: intent.quantity, status: OrderStatus::Filled, reason: format!( "{} direction={} effect={}", intent.reason, intent.direction.as_str(), intent.effect.as_str() ), }); } Err(reason) => { push_futures_process_event( &mut report, date, ProcessEventKind::OrderCreationReject, order_id, &intent.symbol, side, reason.clone(), ); report.order_events.push(OrderEvent { date, decision_date: None, order_created_date: None, execution_date: None, order_id, symbol: intent.symbol, side, requested_quantity: intent.quantity, filled_quantity: 0, status: OrderStatus::Rejected, reason: format!( "{}: {} direction={} effect={}", intent.reason, reason, intent.direction.as_str(), intent.effect.as_str() ), }); } } 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.process_events.extend(report.process_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); } } pub fn begin_trading_day(&mut self) { self.closed_day_trading_pnl = FixedMoney::ZERO; self.closed_day_position_pnl = FixedMoney::ZERO; self.closed_day_transaction_cost = FixedMoney::ZERO; for position in self.positions.values_mut() { position.begin_trading_day(); } } pub fn settle(&mut self, settlement_prices: &BTreeMap) -> f64 { let mut cash_delta = FixedMoney::ZERO; for position in self.positions.values_mut() { let price = settlement_prices .get(&position.symbol) .copied() .unwrap_or(position.last_price); cash_delta = cash_delta .checked_add(position.settlement_money(price)) .expect("fixed-point futures settlement overflow"); } self.total_cash = self .total_cash .checked_add(cash_delta) .expect("fixed-point futures cash settlement overflow"); cash_delta.to_f64() } } fn push_futures_process_event( report: &mut FuturesExecutionReport, date: NaiveDate, kind: ProcessEventKind, order_id: Option, symbol: &str, side: OrderSide, detail: impl Into, ) { report.process_events.push(ProcessEvent { date, kind, order_id, symbol: Some(symbol.to_string()), side: Some(side), detail: detail.into(), }); }