Add futures order execution model

This commit is contained in:
boris
2026-04-23 20:29:14 -07:00
parent 68adc6b25c
commit 2669350154
4 changed files with 472 additions and 7 deletions

View File

@@ -1,5 +1,9 @@
use std::collections::BTreeMap;
use chrono::NaiveDate;
use crate::events::{AccountEvent, FillEvent, OrderEvent, OrderSide, OrderStatus, PositionEvent};
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
pub enum FuturesDirection {
Long,
@@ -20,6 +24,39 @@ impl FuturesDirection {
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)]
@@ -29,6 +66,72 @@ pub struct FuturesContractSpec {
pub short_margin_rate: f64,
}
#[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 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,
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,
reason: reason.into(),
}
}
}
#[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 diagnostics: Vec<String>,
}
impl FuturesContractSpec {
pub fn new(contract_multiplier: f64, long_margin_rate: f64, short_margin_rate: f64) -> Self {
Self {
@@ -146,6 +249,24 @@ impl FuturesPosition {
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 {} {}",
@@ -158,6 +279,37 @@ impl FuturesPosition {
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
@@ -194,6 +346,7 @@ impl FuturesPosition {
let cash_delta = self.equity();
self.avg_price = self.last_price;
self.prev_close = self.last_price;
self.old_quantity = self.quantity;
cash_delta
}
}
@@ -306,13 +459,32 @@ impl FuturesAccountState {
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(quantity, price, transaction_cost)?;
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);
@@ -320,6 +492,183 @@ impl FuturesAccountState {
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 = if intent.effect == FuturesPositionEffect::Open {
intent.direction.open_side()
} else {
intent.direction.close_side()
};
if intent.quantity == 0 || !intent.price.is_finite() || intent.price <= 0.0 {
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()
),
});
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) => {
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 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);