finished initial backtest / engine

This commit is contained in:
Boki 2025-07-03 12:49:22 -04:00
parent 55b4ca78c9
commit c106a719e8
18 changed files with 1571 additions and 180 deletions

View file

@ -2,6 +2,8 @@ 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 {
@ -21,17 +23,159 @@ pub struct PositionUpdate {
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, just add to open trades
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);
}
}
}
}
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 {
@ -162,5 +306,33 @@ impl PositionTracker {
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()
}
}