398 lines
No EOL
16 KiB
Rust
398 lines
No EOL
16 KiB
Rust
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<Utc>,
|
|
}
|
|
|
|
#[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<Utc>,
|
|
pub commission: f64,
|
|
pub order_id: Option<String>,
|
|
pub strategy_id: Option<String>,
|
|
}
|
|
|
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
pub struct ClosedTrade {
|
|
pub id: String,
|
|
pub symbol: String,
|
|
pub entry_time: DateTime<Utc>,
|
|
pub exit_time: DateTime<Utc>,
|
|
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<String, Position>,
|
|
trade_history: Arc<RwLock<Vec<TradeRecord>>>,
|
|
closed_trades: Arc<RwLock<Vec<ClosedTrade>>>,
|
|
open_trades: DashMap<String, Vec<TradeRecord>>, // Track open trades by symbol
|
|
next_trade_id: Arc<RwLock<u64>>,
|
|
}
|
|
|
|
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<String>,
|
|
strategy_id: Option<String>
|
|
) -> 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<Position> {
|
|
self.positions.get(symbol).map(|p| p.clone())
|
|
}
|
|
|
|
pub fn get_all_positions(&self) -> Vec<Position> {
|
|
self.positions.iter().map(|entry| entry.value().clone()).collect()
|
|
}
|
|
|
|
pub fn get_open_positions(&self) -> Vec<Position> {
|
|
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<TradeRecord> {
|
|
self.trade_history.read().clone()
|
|
}
|
|
|
|
pub fn get_closed_trades(&self) -> Vec<ClosedTrade> {
|
|
self.closed_trades.read().clone()
|
|
}
|
|
|
|
pub fn get_open_trades(&self) -> Vec<TradeRecord> {
|
|
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()
|
|
}
|
|
} |