use std::collections::BTreeMap; use chrono::NaiveDate; use serde::{Deserialize, Serialize}; use crate::events::{ AccountEvent, FillEvent, OrderEvent, OrderSide, OrderStatus, PositionEvent, ProcessEvent, ProcessEventKind, }; #[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 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, } } } #[derive(Debug, Clone)] pub struct FuturesPosition { pub symbol: String, pub direction: FuturesDirection, pub old_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, pub transaction_cost: f64, trade_quantity_delta: i32, trade_cost: f64, } 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, 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: 0.0, trade_quantity_delta: 0, trade_cost: 0.0, } } pub fn today_quantity(&self) -> u32 { self.quantity.saturating_sub(self.old_quantity) } pub fn market_value(&self) -> f64 { self.quantity as f64 * self.last_price * self.contract_multiplier } pub fn margin(&self) -> f64 { self.market_value() * self.margin_rate } pub fn equity(&self) -> f64 { (self.last_price - self.avg_price) * self.quantity as f64 * self.contract_multiplier * self.direction.factor() } pub fn pnl(&self) -> f64 { self.equity() } pub fn trading_pnl(&self) -> f64 { (self.trade_quantity_delta as f64 * self.last_price - self.trade_cost) * self.contract_multiplier * self.direction.factor() } pub fn position_pnl(&self) -> f64 { if self.old_quantity == 0 { 0.0 } else { self.old_quantity as f64 * (self.last_price - self.prev_close) * self.contract_multiplier * self.direction.factor() } } 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; self.transaction_cost += transaction_cost.max(0.0); self.trade_quantity_delta += quantity as i32; self.trade_cost += price * quantity as f64; } 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 { 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(0.0); } 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 realized = (price - self.avg_price) * quantity as f64 * self.contract_multiplier * self.direction.factor() - transaction_cost.max(0.0); self.quantity -= quantity; if self.quantity == 0 { self.avg_price = 0.0; } self.last_price = price; self.transaction_cost += transaction_cost.max(0.0); self.trade_quantity_delta -= quantity as i32; self.trade_cost -= price * quantity as f64; 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.prev_close = self.last_price; self.transaction_cost = 0.0; self.trade_quantity_delta = 0; self.trade_cost = 0.0; } pub fn settlement(&mut self, settlement_price: f64) -> f64 { self.mark_price(settlement_price); let cash_delta = self.equity(); self.avg_price = self.last_price; self.prev_close = self.last_price; self.old_quantity = self.quantity; cash_delta } } #[derive(Debug, Clone)] pub struct FuturesAccountState { starting_cash: f64, total_cash: f64, frozen_cash: f64, positions: BTreeMap<(String, FuturesDirection), FuturesPosition>, } impl FuturesAccountState { pub fn new(total_cash: f64) -> Self { Self { starting_cash: total_cash, total_cash, frozen_cash: 0.0, positions: BTreeMap::new(), } } pub fn starting_cash(&self) -> f64 { self.starting_cash } pub fn total_cash(&self) -> f64 { self.total_cash } pub fn frozen_cash(&self) -> f64 { self.frozen_cash } pub fn cash(&self) -> f64 { self.total_cash - self.margin() - self.frozen_cash } pub fn margin(&self) -> f64 { self.positions.values().map(FuturesPosition::margin).sum() } pub fn market_value(&self) -> f64 { self.positions .values() .map(FuturesPosition::market_value) .sum() } pub fn position_equity(&self) -> f64 { self.positions.values().map(FuturesPosition::equity).sum() } pub fn total_value(&self) -> f64 { self.total_cash + self.position_equity() } pub fn daily_pnl(&self) -> f64 { self.trading_pnl() + self.position_pnl() - self.transaction_cost() } pub fn trading_pnl(&self) -> f64 { self.positions .values() .map(FuturesPosition::trading_pnl) .sum() } pub fn position_pnl(&self) -> f64 { self.positions .values() .map(FuturesPosition::position_pnl) .sum() } pub fn transaction_cost(&self) -> f64 { self.positions .values() .map(|position| position.transaction_cost) .sum() } 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 -= transaction_cost.max(0.0); } 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(quantity, price, transaction_cost, effect)?; self.total_cash += cash_delta; if position.quantity == 0 { self.positions.remove(&key); } Ok(cash_delta) } 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, 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() < -1e-8 { 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(-intent.transaction_cost.max(0.0)) } } 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 = intent.price * intent.quantity as f64 * intent.spec.contract_multiplier; report.fill_events.push(FillEvent { date, order_id, symbol: intent.symbol.clone(), side, quantity: intent.quantity, price: intent.price, gross_amount: notional, commission: intent.transaction_cost.max(0.0), stamp_tax: 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, 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, 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) { for position in self.positions.values_mut() { position.begin_trading_day(); } } pub fn settle(&mut self, settlement_prices: &BTreeMap) -> f64 { let mut cash_delta = 0.0; for position in self.positions.values_mut() { let price = settlement_prices .get(&position.symbol) .copied() .unwrap_or(position.last_price); cash_delta += position.settlement(price); } self.total_cash += cash_delta; cash_delta } } 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(), }); }