Add futures account model

This commit is contained in:
boris
2026-04-23 20:25:50 -07:00
parent 8e53c995cd
commit 68adc6b25c
4 changed files with 409 additions and 3 deletions

View File

@@ -0,0 +1,347 @@
use std::collections::BTreeMap;
#[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,
}
}
}
#[derive(Debug, Clone, Copy)]
pub struct FuturesContractSpec {
pub contract_multiplier: f64,
pub long_margin_rate: f64,
pub short_margin_rate: f64,
}
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> {
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);
}
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;
cash_delta
}
}
#[derive(Debug, Clone)]
pub struct FuturesAccountState {
total_cash: f64,
frozen_cash: f64,
positions: BTreeMap<(String, FuturesDirection), FuturesPosition>,
}
impl FuturesAccountState {
pub fn new(total_cash: f64) -> Self {
Self {
total_cash,
frozen_cash: 0.0,
positions: BTreeMap::new(),
}
}
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> {
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)?;
self.total_cash += cash_delta;
if position.quantity == 0 {
self.positions.remove(&key);
}
Ok(cash_delta)
}
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
}
}