Files
fidc-backtest-engine/crates/fidc-core/src/futures.rs
2026-04-23 21:07:59 -07:00

1037 lines
31 KiB
Rust

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<NaiveDate>,
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<f64>,
pub allow_pending: bool,
pub reason: String,
}
impl FuturesOrderIntent {
pub fn open(
symbol: impl Into<String>,
direction: FuturesDirection,
spec: FuturesContractSpec,
quantity: u32,
price: f64,
transaction_cost: f64,
reason: impl Into<String>,
) -> 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<String>,
direction: FuturesDirection,
effect: FuturesPositionEffect,
spec: FuturesContractSpec,
quantity: u32,
price: f64,
transaction_cost: f64,
reason: impl Into<String>,
) -> 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<String>,
direction: FuturesDirection,
spec: FuturesContractSpec,
quantity: u32,
limit_price: f64,
transaction_cost: f64,
reason: impl Into<String>,
) -> Self {
Self::open(
symbol,
direction,
spec,
quantity,
limit_price,
transaction_cost,
reason,
)
.with_limit_price(limit_price)
}
pub fn limit_close(
symbol: impl Into<String>,
direction: FuturesDirection,
effect: FuturesPositionEffect,
spec: FuturesContractSpec,
quantity: u32,
limit_price: f64,
transaction_cost: f64,
reason: impl Into<String>,
) -> 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<OrderEvent>,
pub fill_events: Vec<FillEvent>,
pub position_events: Vec<PositionEvent>,
pub account_events: Vec<AccountEvent>,
pub process_events: Vec<ProcessEvent>,
pub diagnostics: Vec<String>,
}
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<String>,
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<f64, String> {
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<f64, String> {
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<String>,
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<f64, String> {
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<f64, String> {
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<u64>,
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<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.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<String, f64>) -> 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<u64>,
symbol: &str,
side: OrderSide,
detail: impl Into<String>,
) {
report.process_events.push(ProcessEvent {
date,
kind,
order_id,
symbol: Some(symbol.to_string()),
side: Some(side),
detail: detail.into(),
});
}