use crate::{Fill, Side}; use chrono::{DateTime, Utc}; use dashmap::DashMap; use serde::{Deserialize, Serialize}; use std::sync::Arc; use parking_lot::RwLock; #[derive(Debug, Clone, Serialize, Deserialize)] pub struct Position { pub symbol: String, pub quantity: f64, pub average_price: f64, pub realized_pnl: f64, pub unrealized_pnl: f64, pub total_cost: f64, pub last_update: DateTime, } #[derive(Debug, Clone, Serialize, Deserialize)] pub struct PositionUpdate { pub symbol: String, pub fill: Fill, pub resulting_position: Position, } #[derive(Debug, Clone, Serialize, Deserialize)] pub struct TradeRecord { pub id: String, pub symbol: String, pub side: Side, pub quantity: f64, pub price: f64, pub timestamp: DateTime, pub commission: f64, pub order_id: Option, pub strategy_id: Option, } #[derive(Debug, Clone, Serialize, Deserialize)] pub struct ClosedTrade { pub id: String, pub symbol: String, pub entry_time: DateTime, pub exit_time: DateTime, pub entry_price: f64, pub exit_price: f64, pub quantity: f64, pub side: Side, // Side of the opening trade pub pnl: f64, pub pnl_percent: f64, pub commission: f64, pub duration_ms: i64, pub entry_fill_id: String, pub exit_fill_id: String, } pub struct PositionTracker { positions: DashMap, trade_history: Arc>>, closed_trades: Arc>>, open_trades: DashMap>, // Track open trades by symbol next_trade_id: Arc>, } impl PositionTracker { pub fn new() -> Self { Self { positions: DashMap::new(), trade_history: Arc::new(RwLock::new(Vec::new())), closed_trades: Arc::new(RwLock::new(Vec::new())), open_trades: DashMap::new(), next_trade_id: Arc::new(RwLock::new(1)), } } fn generate_trade_id(&self) -> String { let mut id = self.next_trade_id.write(); let current_id = *id; *id += 1; format!("T{:08}", current_id) } pub fn process_fill_with_tracking( &self, symbol: &str, fill: &Fill, side: Side, order_id: Option, strategy_id: Option ) -> PositionUpdate { // First process the fill normally let update = self.process_fill(symbol, fill, side); // Create trade record let trade_record = TradeRecord { id: self.generate_trade_id(), symbol: symbol.to_string(), side, quantity: fill.quantity, price: fill.price, timestamp: fill.timestamp, commission: fill.commission, order_id, strategy_id, }; // Add to trade history self.trade_history.write().push(trade_record.clone()); // Handle trade matching for closed trades match side { Side::Buy => { // For buy orders, try to match with open sell trades (closing shorts) if let Some(mut open_trades) = self.open_trades.get_mut(symbol) { let mut remaining_quantity = fill.quantity; let mut trades_to_remove = Vec::new(); // FIFO matching against short positions for (idx, open_trade) in open_trades.iter_mut().enumerate() { if open_trade.side == Side::Sell && remaining_quantity > 0.0 { let close_quantity = remaining_quantity.min(open_trade.quantity); // Create closed trade record for short position let closed_trade = ClosedTrade { id: format!("CT{}", self.generate_trade_id()), symbol: symbol.to_string(), entry_time: open_trade.timestamp, exit_time: fill.timestamp, entry_price: open_trade.price, exit_price: fill.price, quantity: close_quantity, side: Side::Sell, // Opening side (short) pnl: close_quantity * (open_trade.price - fill.price) - (open_trade.commission + fill.commission * close_quantity / fill.quantity), pnl_percent: ((open_trade.price - fill.price) / open_trade.price) * 100.0, commission: open_trade.commission + fill.commission * close_quantity / fill.quantity, duration_ms: (fill.timestamp - open_trade.timestamp).num_milliseconds(), entry_fill_id: open_trade.id.clone(), exit_fill_id: trade_record.id.clone(), }; self.closed_trades.write().push(closed_trade); // Update quantities remaining_quantity -= close_quantity; open_trade.quantity -= close_quantity; if open_trade.quantity <= 0.0 { trades_to_remove.push(idx); } } } // Remove fully closed trades for idx in trades_to_remove.into_iter().rev() { open_trades.remove(idx); } // If we still have quantity left, it's a new long position if remaining_quantity > 0.0 { let long_trade = TradeRecord { quantity: remaining_quantity, ..trade_record.clone() }; open_trades.push(long_trade); } } else { // No open trades, start a new long position self.open_trades.entry(symbol.to_string()) .or_insert_with(Vec::new) .push(trade_record); } } Side::Sell => { // For sell orders, try to match with open buy trades if let Some(mut open_trades) = self.open_trades.get_mut(symbol) { let mut remaining_quantity = fill.quantity; let mut trades_to_remove = Vec::new(); // FIFO matching for (idx, open_trade) in open_trades.iter_mut().enumerate() { if open_trade.side == Side::Buy && remaining_quantity > 0.0 { let close_quantity = remaining_quantity.min(open_trade.quantity); // Create closed trade record let closed_trade = ClosedTrade { id: format!("CT{}", self.generate_trade_id()), symbol: symbol.to_string(), entry_time: open_trade.timestamp, exit_time: fill.timestamp, entry_price: open_trade.price, exit_price: fill.price, quantity: close_quantity, side: Side::Buy, // Opening side pnl: close_quantity * (fill.price - open_trade.price) - (open_trade.commission + fill.commission * close_quantity / fill.quantity), pnl_percent: ((fill.price - open_trade.price) / open_trade.price) * 100.0, commission: open_trade.commission + fill.commission * close_quantity / fill.quantity, duration_ms: (fill.timestamp - open_trade.timestamp).num_milliseconds(), entry_fill_id: open_trade.id.clone(), exit_fill_id: trade_record.id.clone(), }; self.closed_trades.write().push(closed_trade); // Update quantities remaining_quantity -= close_quantity; open_trade.quantity -= close_quantity; if open_trade.quantity <= 0.0 { trades_to_remove.push(idx); } } } // Remove fully closed trades for idx in trades_to_remove.into_iter().rev() { open_trades.remove(idx); } // If we still have quantity left, it's a short position if remaining_quantity > 0.0 { let short_trade = TradeRecord { quantity: remaining_quantity, ..trade_record.clone() }; open_trades.push(short_trade); } } else { // No open trades, start a new short position self.open_trades.entry(symbol.to_string()) .or_insert_with(Vec::new) .push(trade_record); } } } update } pub fn process_fill(&self, symbol: &str, fill: &Fill, side: Side) -> PositionUpdate { let mut entry = self.positions.entry(symbol.to_string()).or_insert_with(|| { Position { symbol: symbol.to_string(), quantity: 0.0, average_price: 0.0, realized_pnl: 0.0, unrealized_pnl: 0.0, total_cost: 0.0, last_update: fill.timestamp, } }); let position = entry.value_mut(); let old_quantity = position.quantity; let old_avg_price = position.average_price; // Calculate new position match side { Side::Buy => { // Adding to position position.quantity += fill.quantity; if old_quantity >= 0.0 { // Already long or flat, average up/down position.total_cost += fill.price * fill.quantity; position.average_price = if position.quantity > 0.0 { position.total_cost / position.quantity } else { 0.0 }; } else { // Was short, closing or flipping let close_quantity = fill.quantity.min(-old_quantity); let open_quantity = fill.quantity - close_quantity; // Realize P&L on closed portion position.realized_pnl += close_quantity * (old_avg_price - fill.price); // Update position for remaining if open_quantity > 0.0 { position.total_cost = open_quantity * fill.price; position.average_price = fill.price; } else { position.total_cost = (position.quantity.abs()) * old_avg_price; } } } Side::Sell => { // Reducing position position.quantity -= fill.quantity; if old_quantity <= 0.0 { // Already short or flat, average up/down position.total_cost += fill.price * fill.quantity; position.average_price = if position.quantity < 0.0 { position.total_cost / position.quantity.abs() } else { 0.0 }; } else { // Was long, closing or flipping let close_quantity = fill.quantity.min(old_quantity); let open_quantity = fill.quantity - close_quantity; // Realize P&L on closed portion position.realized_pnl += close_quantity * (fill.price - old_avg_price); // Update position for remaining if open_quantity > 0.0 { position.total_cost = open_quantity * fill.price; position.average_price = fill.price; } else { position.total_cost = (position.quantity.abs()) * old_avg_price; } } } } // Subtract commission from realized P&L position.realized_pnl -= fill.commission; position.last_update = fill.timestamp; PositionUpdate { symbol: symbol.to_string(), fill: fill.clone(), resulting_position: position.clone(), } } pub fn get_position(&self, symbol: &str) -> Option { self.positions.get(symbol).map(|p| p.clone()) } pub fn get_all_positions(&self) -> Vec { self.positions.iter().map(|entry| entry.value().clone()).collect() } pub fn get_open_positions(&self) -> Vec { self.positions .iter() .filter(|entry| entry.value().quantity.abs() > 0.0001) .map(|entry| entry.value().clone()) .collect() } pub fn update_unrealized_pnl(&self, symbol: &str, current_price: f64) { if let Some(mut position) = self.positions.get_mut(symbol) { if position.quantity > 0.0 { position.unrealized_pnl = position.quantity * (current_price - position.average_price); } else if position.quantity < 0.0 { position.unrealized_pnl = position.quantity * (current_price - position.average_price); } else { position.unrealized_pnl = 0.0; } } } pub fn get_total_pnl(&self) -> (f64, f64) { let mut realized = 0.0; let mut unrealized = 0.0; for position in self.positions.iter() { realized += position.realized_pnl; unrealized += position.unrealized_pnl; } (realized, unrealized) } pub fn reset(&self) { self.positions.clear(); self.trade_history.write().clear(); self.closed_trades.write().clear(); self.open_trades.clear(); *self.next_trade_id.write() = 1; } pub fn get_trade_history(&self) -> Vec { self.trade_history.read().clone() } pub fn get_closed_trades(&self) -> Vec { self.closed_trades.read().clone() } pub fn get_open_trades(&self) -> Vec { let mut all_open_trades = Vec::new(); for entry in self.open_trades.iter() { all_open_trades.extend(entry.value().clone()); } all_open_trades } pub fn get_trade_count(&self) -> usize { self.trade_history.read().len() } pub fn get_closed_trade_count(&self) -> usize { self.closed_trades.read().len() } }