Files
foxhunt/crates/backtesting/src/strategy_tester.rs
jgrusewski 9c3d741a08 refactor: restructure repo — crates/, bin/, testing/ layout
Move 17 library crates into crates/, CLI binary into bin/fxt,
consolidate 10 test crates into testing/, split config crate
from deployment config files.

Root directory reduced from 38+ to ~17 directories.
All Cargo.toml paths and build.rs proto refs updated.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-25 11:56:00 +01:00

1314 lines
44 KiB
Rust

//! Strategy testing framework for backtesting
//!
//! Provides infrastructure for executing trading strategies against historical data,
//! managing positions, tracking performance, and handling risk management.
// Import everything async_trait needs - use fully qualified paths to avoid shadowing
use std::{
collections::{HashMap, VecDeque},
sync::Arc,
};
use anyhow::{Context, Result};
use async_trait::async_trait;
use chrono::{DateTime, Utc};
use common::Order;
use common::OrderId;
use common::OrderSide;
use common::OrderStatus;
use common::OrderType;
use common::Position;
use common::Price;
use common::Quantity;
use common::Symbol;
use common::TimeInForce;
use dashmap::DashMap;
use rust_decimal::Decimal;
use serde::{Deserialize, Serialize};
use serde_json;
use tokio::sync::RwLock;
use tracing::{error, info};
use trading_engine::types::events::MarketEvent;
use uuid::Uuid;
// TECHNICAL DEBT ELIMINATED - Use String and DateTime<Utc> directly
use crate::replay_engine::{MarketReplay, ReplayEvent};
/// Trading strategy trait that backtesting strategies must implement
#[async_trait(?Send)]
pub trait Strategy: Send + Sync {
/// Strategy name for identification
fn name(&self) -> &str;
/// Initialize strategy with initial capital and configuration
async fn initialize(&mut self, initial_capital: Decimal, config: StrategyConfig) -> Result<()>;
/// Process market event and generate trading signals
async fn on_market_event(
&mut self,
event: &MarketEvent,
context: &StrategyContext,
) -> Result<Vec<TradingSignal>>;
/// Handle order execution updates
async fn on_order_update(&mut self, order: &Order, context: &StrategyContext) -> Result<()>;
/// Handle position updates
async fn on_position_update(
&mut self,
position: &Position,
context: &StrategyContext,
) -> Result<()>;
/// Strategy cleanup and final calculations
async fn finalize(&mut self, context: &StrategyContext) -> Result<StrategyResult>;
/// Get current strategy state for debugging
async fn get_state(&self) -> Result<serde_json::Value>;
}
/// Strategy configuration parameters
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct StrategyConfig {
/// Maximum position size per symbol
pub max_position_size: Decimal,
/// Risk per trade as percentage of capital
pub risk_per_trade: Decimal,
/// Maximum number of open positions
pub max_open_positions: u32,
/// Stop loss percentage
pub stop_loss_pct: Option<Decimal>,
/// Take profit percentage
pub take_profit_pct: Option<Decimal>,
/// Strategy-specific parameters
pub parameters: HashMap<String, serde_json::Value>,
/// Enable position sizing
pub position_sizing_enabled: bool,
/// Commission rate per trade
pub commission_rate: Decimal,
/// Slippage factor
pub slippage_factor: Decimal,
}
impl Default for StrategyConfig {
fn default() -> Self {
Self {
max_position_size: Decimal::from(100000),
risk_per_trade: Decimal::new(2, 2), // 2%
max_open_positions: 10,
stop_loss_pct: Some(Decimal::new(5, 2)), // 5%
take_profit_pct: Some(Decimal::new(10, 2)), // 10%
parameters: HashMap::new(),
position_sizing_enabled: true,
commission_rate: Decimal::new(1, 4), // 0.01%
slippage_factor: Decimal::new(5, 5), // 0.005%
}
}
}
/// Context provided to strategy during execution
#[derive(Debug, Clone)]
pub struct StrategyContext {
/// Current timestamp
pub current_time: DateTime<Utc>,
/// Current account balance
pub account_balance: Decimal,
/// Available buying power
pub buying_power: Decimal,
/// Current positions
pub positions: HashMap<Symbol, Position>,
/// Open orders
pub open_orders: HashMap<OrderId, Order>,
/// Current market prices
pub market_prices: HashMap<Symbol, Price>,
/// Performance metrics
pub performance: PerformanceMetrics,
}
/// Trading signal generated by strategy
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct TradingSignal {
/// Symbol to trade
pub symbol: Symbol,
/// Signal type
pub signal_type: SignalType,
/// Suggested quantity
pub quantity: Quantity,
/// Target price (if limit order)
pub target_price: Option<Price>,
/// Stop loss price
pub stop_loss: Option<Price>,
/// Take profit price
pub take_profit: Option<Price>,
/// Signal confidence (0.0 - 1.0)
pub confidence: Decimal,
/// Additional metadata
pub metadata: HashMap<String, serde_json::Value>,
}
/// Types of trading signals
#[derive(Debug, Clone, Serialize, Deserialize)]
pub enum SignalType {
Buy,
Sell,
Short,
Cover,
CloseLong,
CloseShort,
CloseAll,
}
/// Strategy execution result
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct StrategyResult {
/// Strategy name
pub strategy_name: String,
/// Total return
pub total_return: Decimal,
/// Annualized return
pub annualized_return: Decimal,
/// Maximum drawdown
pub max_drawdown: Decimal,
/// Sharpe ratio
pub sharpe_ratio: Decimal,
/// Number of trades
pub total_trades: u64,
/// Win rate
pub win_rate: Decimal,
/// Average trade return
pub avg_trade_return: Decimal,
/// Final portfolio value
pub final_value: Decimal,
/// Detailed trade history
pub trades: Vec<TradeRecord>,
/// Performance timeline
pub performance_timeline: Vec<PerformanceSnapshot>,
}
/// Individual trade record
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct TradeRecord {
/// Trade ID
pub trade_id: String,
/// Symbol traded
pub symbol: Symbol,
/// Trade side
pub side: OrderSide,
/// Entry price
pub entry_price: Price,
/// Exit price
pub exit_price: Price,
/// Quantity traded
pub quantity: Quantity,
/// Entry timestamp
pub entry_time: DateTime<Utc>,
/// Exit timestamp
pub exit_time: DateTime<Utc>,
/// Profit/loss
pub pnl: Decimal,
/// Return percentage
pub return_pct: Decimal,
/// Commission paid
pub commission: Decimal,
}
/// Performance snapshot at a point in time
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PerformanceSnapshot {
/// Timestamp
pub timestamp: DateTime<Utc>,
/// Portfolio value
pub portfolio_value: Decimal,
/// Cash balance
pub cash_balance: Decimal,
/// Unrealized PnL
pub unrealized_pnl: Decimal,
/// Realized PnL
pub realized_pnl: Decimal,
/// Number of open positions
pub open_positions: u32,
/// Current drawdown
pub drawdown: Decimal,
}
/// Performance metrics tracked during execution
#[derive(Debug, Clone, Default)]
pub struct PerformanceMetrics {
/// Total realized PnL
pub total_realized_pnl: Decimal,
/// Total unrealized PnL
pub total_unrealized_pnl: Decimal,
/// Peak portfolio value
pub peak_value: Decimal,
/// Current drawdown
pub current_drawdown: Decimal,
/// Maximum drawdown
pub max_drawdown: Decimal,
/// Total trades executed
pub total_trades: u64,
/// Winning trades
pub winning_trades: u64,
/// Total commission paid
pub total_commission: Decimal,
/// Returns history
pub daily_returns: VecDeque<Decimal>,
}
/// Strategy tester engine
pub struct StrategyTester {
/// Strategy being tested
strategy: Box<dyn Strategy>,
/// Strategy configuration
config: StrategyConfig,
/// Market replay engine
market_replay: Arc<MarketReplay>,
/// Current account state
account: Arc<RwLock<Account>>,
/// Order management system
order_manager: Arc<OrderManager>,
/// Position tracker
position_tracker: Arc<PositionTracker>,
/// Performance tracker
performance_tracker: Arc<RwLock<PerformanceTracker>>,
/// Current market data
market_data: Arc<DashMap<Symbol, MarketEvent>>,
}
/// Account state for backtesting
#[derive(Debug, Clone)]
pub struct Account {
/// Initial capital
pub initial_capital: Decimal,
/// Current cash balance
pub cash_balance: Decimal,
/// Total portfolio value
pub portfolio_value: Decimal,
/// Account creation time
pub created_at: DateTime<Utc>,
/// Last update time
pub last_updated: DateTime<Utc>,
}
/// Order management for backtesting
pub struct OrderManager {
/// Open orders
orders: DashMap<OrderId, Order>,
/// Next order ID
next_order_id: std::sync::atomic::AtomicU64,
}
/// Position tracking for backtesting
pub struct PositionTracker {
/// Current positions
positions: DashMap<Symbol, Position>,
/// Position history
position_history: RwLock<Vec<Position>>,
/// Trade records
trade_records: RwLock<Vec<TradeRecord>>,
}
/// Performance tracking
pub struct PerformanceTracker {
/// Performance metrics
metrics: PerformanceMetrics,
/// Performance snapshots
snapshots: Vec<PerformanceSnapshot>,
/// Last snapshot time
last_snapshot: Option<DateTime<Utc>>,
}
impl StrategyTester {
/// Create new strategy tester
pub fn new(
strategy: Box<dyn Strategy>,
config: StrategyConfig,
market_replay: Arc<MarketReplay>,
initial_capital: Decimal,
) -> Self {
let account = Account {
initial_capital,
cash_balance: initial_capital,
portfolio_value: initial_capital,
created_at: Utc::now(),
last_updated: Utc::now(),
};
Self {
strategy,
config,
market_replay,
account: Arc::new(RwLock::new(account)),
order_manager: Arc::new(OrderManager::new()),
position_tracker: Arc::new(PositionTracker::new()),
performance_tracker: Arc::new(RwLock::new(PerformanceTracker::new())),
market_data: Arc::new(DashMap::new()),
}
}
/// Run the strategy test
pub async fn run_test(&mut self) -> Result<StrategyResult> {
info!("Starting strategy test for: {}", self.strategy.name());
// Initialize strategy
self.strategy
.initialize(
self.account.read().await.initial_capital,
self.config.clone(),
)
.await?;
// Get market data receiver
let mut event_receiver = self
.market_replay
.take_receiver()
.await
.context("Failed to get market data receiver")?;
// Start market replay
let replay_handle = {
let replay = Arc::clone(&self.market_replay);
tokio::spawn(async move {
if let Err(e) = replay.start_replay().await {
error!("Market replay failed: {}", e);
}
})
};
// Process market events
while let Some(replay_event) = event_receiver.recv().await {
if let Err(e) = self.process_market_event(replay_event).await {
error!("Failed to process market event: {}", e);
}
}
// Wait for replay to complete
replay_handle.await?;
// Finalize strategy and generate results
let context = self.build_strategy_context().await?;
let result = self.strategy.finalize(&context).await?;
info!(
"Strategy test completed. Total return: {:.2}%",
result.total_return * Decimal::from(100)
);
Ok(result)
}
/// Process a single market event
async fn process_market_event(&mut self, replay_event: ReplayEvent) -> Result<()> {
let event = &replay_event.event;
// Update market data
self.update_market_data(event).await;
// Update account and positions with current market prices
self.update_valuations().await?;
// Process pending orders
self.process_pending_orders().await?;
// Build strategy context
let context = self.build_strategy_context().await?;
// Get trading signals from strategy
let signals = self.strategy.on_market_event(event, &context).await?;
// Execute trading signals
for signal in signals {
self.execute_trading_signal(signal).await?;
}
// Take performance snapshot periodically
self.take_performance_snapshot(&context).await?;
Ok(())
}
/// Update market data cache
async fn update_market_data(&self, event: &MarketEvent) {
let symbol = match event {
MarketEvent::Trade { symbol, .. } => symbol,
MarketEvent::Quote { symbol, .. } => symbol,
MarketEvent::OrderBookUpdate { symbol, .. } => symbol,
MarketEvent::Bar { symbol, .. } => symbol,
MarketEvent::OrderBook { symbol, .. } => symbol,
MarketEvent::Sentiment { .. } => return, // Skip sentiment events for now
MarketEvent::Control { .. } => return, // Skip control events for now
};
self.market_data.insert(symbol.clone(), event.clone());
}
/// Update portfolio valuations
async fn update_valuations(&self) -> Result<()> {
let mut account = self.account.write().await;
let positions = self.position_tracker.get_all_positions().await;
let mut total_value = account.cash_balance;
for (symbol, position) in positions {
if let Some(market_event) = self.market_data.get(&symbol) {
let current_price = self.extract_price_from_event(&market_event)?;
let position_decimal = position.quantity;
let price_decimal = current_price.to_decimal().unwrap_or(Decimal::ZERO);
let position_value = position_decimal * price_decimal;
if position_decimal >= Decimal::ZERO {
total_value += position_value;
} else {
// Short position
total_value -= position_value;
}
}
}
account.portfolio_value = total_value;
account.last_updated = Utc::now();
Ok(())
}
/// Process pending orders for execution
async fn process_pending_orders(&mut self) -> Result<()> {
let pending_orders = self.order_manager.get_pending_orders().await;
for order in pending_orders {
// Extract the current price first to avoid borrowing conflicts
let current_price = if let Some(market_event) = self.market_data.get(&order.symbol) {
self.extract_price_from_event(&market_event)?
} else {
continue; // Skip this order if no market data available
};
// Now check if we should execute and execute if needed
if self.should_execute_order(&order, current_price) {
self.execute_order(order).await?;
}
}
Ok(())
}
/// Check if order should be executed
fn should_execute_order(&self, order: &Order, current_price: Price) -> bool {
match order.order_type {
OrderType::Market => true,
OrderType::Iceberg => {
// For backtesting, treat Iceberg orders as market orders
true
},
OrderType::Limit => {
if let Some(order_price) = order.price {
match order.side {
OrderSide::Buy => current_price <= order_price,
OrderSide::Sell => current_price >= order_price,
}
} else {
false
}
},
OrderType::Stop => {
if let Some(order_price) = order.price {
match order.side {
OrderSide::Buy => current_price >= order_price,
OrderSide::Sell => current_price <= order_price,
}
} else {
false
}
},
OrderType::StopLimit => {
// Simplified logic - would need stop price tracking
if let Some(order_price) = order.price {
match order.side {
OrderSide::Buy => current_price >= order_price,
OrderSide::Sell => current_price <= order_price,
}
} else {
false
}
},
OrderType::TrailingStop => {
// For backtesting, treat as stop order
if let Some(order_price) = order.price {
match order.side {
OrderSide::Buy => current_price >= order_price,
OrderSide::Sell => current_price <= order_price,
}
} else {
false
}
},
OrderType::Hidden => {
// For backtesting, treat as market order
true
},
_ => {
// Default case for any other order types
false
},
}
}
/// Execute an order
async fn execute_order(&mut self, mut order: Order) -> Result<()> {
let current_price = if let Some(market_event) = self.market_data.get(&order.symbol) {
self.extract_price_from_event(&market_event)?
} else {
return Err(anyhow::anyhow!(
"No market data for symbol: {}",
order.symbol
));
};
// Apply slippage
let execution_price = self.apply_slippage(current_price, &order);
// Calculate commission
let commission = self.calculate_commission(&order, execution_price);
// Update order
order.status = OrderStatus::Filled;
order.filled_quantity = order.quantity;
order.average_price = Some(execution_price);
// Update account
let mut account = self.account.write().await;
let trade_value = order.quantity.to_decimal().unwrap_or(Decimal::ZERO)
* execution_price.to_decimal().unwrap_or(Decimal::ZERO);
match order.side {
OrderSide::Buy => {
account.cash_balance -= trade_value + commission;
},
OrderSide::Sell => {
account.cash_balance += trade_value - commission;
},
}
// Update positions
self.position_tracker
.update_position(&order.symbol, &order, execution_price)
.await?;
// Record trade
self.position_tracker
.record_trade(&order, execution_price, commission)
.await;
// Notify strategy of order update
let context = self.build_strategy_context().await?;
self.strategy.on_order_update(&order, &context).await?;
info!(
"Executed order: {:?} {} {} @ {} (commission: {})",
order.side, order.quantity, order.symbol, execution_price, commission
);
Ok(())
}
/// Execute a trading signal
async fn execute_trading_signal(&mut self, signal: TradingSignal) -> Result<()> {
let order = self.convert_signal_to_order(signal).await?;
self.order_manager.place_order(order).await?;
Ok(())
}
/// Convert trading signal to order
async fn convert_signal_to_order(&self, signal: TradingSignal) -> Result<Order> {
let order_id = self.order_manager.generate_order_id();
let (side, order_type, price) = match signal.signal_type {
SignalType::Buy => (OrderSide::Buy, OrderType::Market, Price::zero()),
SignalType::Sell => (OrderSide::Sell, OrderType::Market, Price::zero()),
SignalType::Short => (OrderSide::Sell, OrderType::Market, Price::zero()),
SignalType::Cover => (OrderSide::Buy, OrderType::Market, Price::zero()),
_ => {
return Err(anyhow::anyhow!(
"Unsupported signal type: {:?}",
signal.signal_type
))
},
};
Ok(Order {
id: order_id.clone(),
client_order_id: Some(format!("client_{}", Uuid::new_v4())),
broker_order_id: None,
account_id: Some("default".to_string()),
symbol: signal.symbol,
side,
order_type,
status: OrderStatus::Pending,
time_in_force: TimeInForce::Day,
quantity: signal.quantity,
price: Some(price),
stop_price: None,
filled_quantity: Quantity::zero(),
remaining_quantity: signal.quantity,
average_price: None,
avg_fill_price: None,
average_fill_price: None,
exchange_order_id: None,
parent_id: None,
execution_algorithm: None,
execution_params: serde_json::json!({}),
stop_loss: None,
take_profit: None,
created_at: common::HftTimestamp::now_or_zero(),
updated_at: None,
expires_at: None,
metadata: serde_json::json!({}),
})
}
/// Apply slippage to execution price
fn apply_slippage(&self, price: Price, order: &Order) -> Price {
let slippage = price.to_decimal().unwrap_or(Decimal::ZERO) * self.config.slippage_factor;
match order.side {
OrderSide::Buy => Price::from_f64(
(price.to_decimal().unwrap_or(Decimal::ZERO) + slippage)
.try_into()
.unwrap_or(0.0),
)
.unwrap_or(Price::zero()),
OrderSide::Sell => Price::from_f64(
(price.to_decimal().unwrap_or(Decimal::ZERO) - slippage)
.try_into()
.unwrap_or(0.0),
)
.unwrap_or(Price::zero()),
}
}
/// Calculate commission for trade
fn calculate_commission(&self, order: &Order, price: Price) -> Decimal {
let trade_value = order.quantity.to_decimal().unwrap_or(Decimal::ZERO)
* price.to_decimal().unwrap_or(Decimal::ZERO);
trade_value * self.config.commission_rate
}
/// Extract price from market event
fn extract_price_from_event(&self, event: &MarketEvent) -> Result<Price> {
match event {
MarketEvent::Trade { price, .. } => Ok(*price),
MarketEvent::Quote {
bid_price,
ask_price,
..
} => {
let avg_price = (bid_price.to_decimal().unwrap_or(Decimal::ZERO)
+ ask_price.to_decimal().unwrap_or(Decimal::ZERO))
/ Decimal::from(2);
Ok(Price::from_f64(avg_price.try_into().unwrap_or(0.0)).unwrap_or(Price::ZERO))
},
MarketEvent::Bar { close, .. } => Ok(*close),
_ => Err(anyhow::anyhow!(
"Cannot extract price from event: {:?}",
event
)),
}
}
/// Build strategy context
async fn build_strategy_context(&self) -> Result<StrategyContext> {
let account = self.account.read().await;
let positions = self.position_tracker.get_all_positions().await;
let open_orders = self.order_manager.get_all_orders().await;
let performance = self.performance_tracker.read().await.metrics.clone();
let mut market_prices = HashMap::new();
for item in self.market_data.iter() {
let symbol = item.key();
let event = item.value();
if let Ok(price) = self.extract_price_from_event(event) {
market_prices.insert(symbol.clone(), price);
}
}
Ok(StrategyContext {
current_time: Utc::now(),
account_balance: account.cash_balance,
buying_power: account.cash_balance, // Simplified
positions,
open_orders,
market_prices,
performance,
})
}
/// Take performance snapshot
async fn take_performance_snapshot(&self, context: &StrategyContext) -> Result<()> {
let mut tracker = self.performance_tracker.write().await;
let snapshot = PerformanceSnapshot {
timestamp: context.current_time,
portfolio_value: context.account_balance,
cash_balance: context.account_balance,
unrealized_pnl: context.performance.total_unrealized_pnl,
realized_pnl: context.performance.total_realized_pnl,
open_positions: context.positions.len() as u32,
drawdown: context.performance.current_drawdown,
};
tracker.snapshots.push(snapshot);
tracker.last_snapshot = Some(context.current_time);
Ok(())
}
}
impl OrderManager {
pub fn new() -> Self {
Self {
orders: DashMap::new(),
next_order_id: std::sync::atomic::AtomicU64::new(1),
}
}
pub fn generate_order_id(&self) -> OrderId {
format!(
"order_{}",
self.next_order_id
.fetch_add(1, std::sync::atomic::Ordering::Relaxed)
)
.into()
}
pub async fn place_order(&self, order: Order) -> Result<()> {
let order_id = order.id.clone();
self.orders.insert(order_id, order);
Ok(())
}
pub async fn get_pending_orders(&self) -> Vec<Order> {
self.orders
.iter()
.filter(|entry| entry.value().status == OrderStatus::Pending)
.map(|entry| entry.value().clone())
.collect()
}
pub async fn get_all_orders(&self) -> HashMap<OrderId, Order> {
self.orders
.iter()
.map(|entry| (entry.key().clone(), entry.value().clone()))
.collect()
}
}
impl PositionTracker {
pub fn new() -> Self {
Self {
positions: DashMap::new(),
position_history: RwLock::new(Vec::new()),
trade_records: RwLock::new(Vec::new()),
}
}
pub async fn get_all_positions(&self) -> HashMap<Symbol, Position> {
self.positions
.iter()
.map(|entry| (entry.key().clone(), entry.value().clone()))
.collect()
}
/// Update position accounting for a filled order.
///
/// Handles opening new positions, adding to existing ones, reducing positions,
/// closing positions (generating a TradeRecord with realized PnL), and flipping
/// from long to short or vice-versa.
pub async fn update_position(
&self,
symbol: &Symbol,
order: &Order,
execution_price: Price,
) -> Result<()> {
let exec_price_dec = execution_price.to_decimal().unwrap_or(Decimal::ZERO);
let order_qty_dec = order.quantity.to_decimal().unwrap_or(Decimal::ZERO);
// Signed fill quantity: positive for Buy, negative for Sell
let signed_fill_qty = match order.side {
OrderSide::Buy => order_qty_dec,
OrderSide::Sell => -order_qty_dec,
};
let now = Utc::now();
if let Some(mut pos_ref) = self.positions.get_mut(symbol) {
let old_qty = pos_ref.quantity;
let old_avg = pos_ref.avg_price;
let new_qty = old_qty + signed_fill_qty;
// Determine if this fill reduces / closes the position
// A reduction happens when old_qty and signed_fill_qty have opposite signs
let is_reducing = (old_qty > Decimal::ZERO && signed_fill_qty < Decimal::ZERO)
|| (old_qty < Decimal::ZERO && signed_fill_qty > Decimal::ZERO);
if is_reducing {
// Quantity being closed is the smaller of |old_qty| and |signed_fill_qty|
let closed_qty = old_qty.abs().min(order_qty_dec);
// PnL: for a long close: (exit - entry) * qty
// for a short close: (entry - exit) * qty
let pnl = if old_qty > Decimal::ZERO {
(exec_price_dec - old_avg) * closed_qty
} else {
(old_avg - exec_price_dec) * closed_qty
};
let entry_side = if old_qty > Decimal::ZERO {
OrderSide::Buy
} else {
OrderSide::Sell
};
// Record the round-trip trade
let trade = TradeRecord {
trade_id: Uuid::new_v4().to_string(),
symbol: symbol.clone(),
side: entry_side,
entry_price: Price::from_decimal(old_avg),
exit_price: execution_price,
quantity: Quantity::from_f64(
closed_qty.try_into().unwrap_or(0.0),
)
.unwrap_or(Quantity::ZERO),
entry_time: pos_ref.created_at,
exit_time: now,
pnl,
return_pct: if old_avg != Decimal::ZERO {
pnl / (old_avg * closed_qty) * Decimal::from(100)
} else {
Decimal::ZERO
},
commission: Decimal::ZERO, // Commission tracked separately in record_trade
};
let mut records = self.trade_records.write().await;
records.push(trade);
if new_qty == Decimal::ZERO {
// Position fully closed -- archive and remove
let closed_pos = pos_ref.clone();
drop(pos_ref);
let mut history = self.position_history.write().await;
history.push(closed_pos);
self.positions.remove(symbol);
} else if (new_qty > Decimal::ZERO) != (old_qty > Decimal::ZERO) {
// Position flipped direction -- close old fully, open new with remainder
let remainder_qty = new_qty.abs();
// Archive the closed position
let closed_pos = pos_ref.clone();
// Update to the new flipped position
pos_ref.quantity = new_qty;
pos_ref.avg_price = exec_price_dec;
pos_ref.avg_cost = exec_price_dec;
pos_ref.average_price = exec_price_dec;
pos_ref.basis = remainder_qty * exec_price_dec;
pos_ref.unrealized_pnl = Decimal::ZERO;
pos_ref.realized_pnl += pnl;
pos_ref.updated_at = now;
pos_ref.last_updated = now;
drop(pos_ref);
let mut history = self.position_history.write().await;
history.push(closed_pos);
} else {
// Partial reduction, same direction
pos_ref.quantity = new_qty;
pos_ref.realized_pnl += pnl;
pos_ref.updated_at = now;
pos_ref.last_updated = now;
}
} else {
// Adding to existing position (same direction) -- compute weighted avg price
let total_cost = old_avg * old_qty.abs() + exec_price_dec * order_qty_dec;
let total_qty = old_qty.abs() + order_qty_dec;
let new_avg = if total_qty != Decimal::ZERO {
total_cost / total_qty
} else {
exec_price_dec
};
pos_ref.quantity = new_qty;
pos_ref.avg_price = new_avg;
pos_ref.avg_cost = new_avg;
pos_ref.average_price = new_avg;
pos_ref.basis = total_qty * new_avg;
pos_ref.updated_at = now;
pos_ref.last_updated = now;
}
} else {
// No existing position -- open a new one
let pos = Position::new(symbol.as_str().to_owned(), signed_fill_qty, exec_price_dec);
self.positions.insert(symbol.clone(), pos);
}
Ok(())
}
/// Record a trade fill for audit / reporting.
///
/// This creates a TradeRecord from the raw fill data and appends it to the
/// internal trade log. For round-trip PnL records, see `update_position`.
pub async fn record_trade(
&self,
order: &Order,
execution_price: Price,
commission: Decimal,
) {
let now = Utc::now();
let trade = TradeRecord {
trade_id: Uuid::new_v4().to_string(),
symbol: order.symbol.clone(),
side: order.side.clone(),
entry_price: execution_price,
exit_price: execution_price,
quantity: order.quantity,
entry_time: order.created_at.to_datetime(),
exit_time: now,
pnl: Decimal::ZERO, // Raw fill; PnL computed in update_position
return_pct: Decimal::ZERO,
commission,
};
let mut records = self.trade_records.write().await;
records.push(trade);
}
/// Get a snapshot of all recorded trades.
pub async fn get_trades(&self) -> Vec<TradeRecord> {
let records = self.trade_records.read().await;
records.clone()
}
/// Sum of realized PnL across all recorded round-trip trades.
pub async fn get_realized_pnl(&self) -> Decimal {
let records = self.trade_records.read().await;
records.iter().map(|t| t.pnl).sum()
}
}
impl PerformanceTracker {
pub fn new() -> Self {
Self {
metrics: PerformanceMetrics::default(),
snapshots: Vec::new(),
last_snapshot: None,
}
}
}
#[cfg(test)]
mod tests {
use super::*;
struct TestStrategy {
name: String,
}
#[async_trait(?Send)]
impl Strategy for TestStrategy {
fn name(&self) -> &str {
&self.name
}
async fn initialize(
&mut self,
_initial_capital: Decimal,
_config: StrategyConfig,
) -> Result<()> {
Ok(())
}
async fn on_market_event(
&mut self,
_event: &MarketEvent,
_context: &StrategyContext,
) -> Result<Vec<TradingSignal>> {
Ok(vec![])
}
async fn on_order_update(
&mut self,
_order: &Order,
_context: &StrategyContext,
) -> Result<()> {
Ok(())
}
async fn on_position_update(
&mut self,
_position: &Position,
_context: &StrategyContext,
) -> Result<()> {
Ok(())
}
async fn finalize(&mut self, _context: &StrategyContext) -> Result<StrategyResult> {
Ok(StrategyResult {
strategy_name: self.name.clone(),
total_return: Decimal::ZERO,
annualized_return: Decimal::ZERO,
max_drawdown: Decimal::ZERO,
sharpe_ratio: Decimal::ZERO,
total_trades: 0,
win_rate: Decimal::ZERO,
avg_trade_return: Decimal::ZERO,
final_value: Decimal::from(100000),
trades: vec![],
performance_timeline: vec![],
})
}
async fn get_state(&self) -> Result<serde_json::Value> {
Ok(serde_json::json!({"name": self.name}))
}
}
#[tokio::test]
async fn test_strategy_tester_creation() {
use crate::replay_engine::{MarketReplay, ReplayConfig};
let strategy = Box::new(TestStrategy {
name: "test_strategy".to_string(),
});
let config = StrategyConfig::default();
let replay_config = ReplayConfig::default();
let market_replay = Arc::new(MarketReplay::new(replay_config));
let initial_capital = Decimal::from(100000);
let tester = StrategyTester::new(strategy, config, market_replay, initial_capital);
assert_eq!(tester.strategy.name(), "test_strategy");
}
/// Helper: build a minimal Order for testing position tracker logic.
fn make_test_order(symbol: &str, side: OrderSide, qty_f64: f64) -> Order {
Order {
id: format!("test_{}", Uuid::new_v4()).into(),
client_order_id: None,
broker_order_id: None,
account_id: None,
symbol: Symbol::from(symbol),
side,
order_type: OrderType::Market,
status: OrderStatus::Filled,
time_in_force: TimeInForce::Day,
quantity: Quantity::from_f64(qty_f64).unwrap_or(Quantity::ZERO),
price: None,
stop_price: None,
filled_quantity: Quantity::from_f64(qty_f64).unwrap_or(Quantity::ZERO),
remaining_quantity: Quantity::ZERO,
average_price: None,
avg_fill_price: None,
average_fill_price: None,
exchange_order_id: None,
parent_id: None,
execution_algorithm: None,
execution_params: serde_json::json!({}),
stop_loss: None,
take_profit: None,
created_at: common::HftTimestamp::now_or_zero(),
updated_at: None,
expires_at: None,
metadata: serde_json::json!({}),
}
}
#[tokio::test]
async fn test_position_tracker_records_trade() {
let tracker = PositionTracker::new();
let order = make_test_order("AAPL", OrderSide::Buy, 10.0);
let price = Price::from_f64(150.0).unwrap();
let commission = Decimal::new(15, 1); // 1.5
tracker.record_trade(&order, price, commission).await;
let trades = tracker.get_trades().await;
assert_eq!(trades.len(), 1);
let trade = &trades[0];
assert_eq!(trade.symbol, Symbol::from("AAPL"));
assert_eq!(trade.side, OrderSide::Buy);
assert_eq!(trade.commission, Decimal::new(15, 1));
assert_eq!(trade.quantity, order.quantity);
}
#[tokio::test]
async fn test_position_tracker_update_position_open_and_close() {
let tracker = PositionTracker::new();
let sym = Symbol::from("AAPL");
// Step 1: Buy 10 shares at $100
let buy_order = make_test_order("AAPL", OrderSide::Buy, 10.0);
let buy_price = Price::from_f64(100.0).unwrap();
tracker
.update_position(&sym, &buy_order, buy_price)
.await
.unwrap();
// Verify the position exists with correct quantity
let positions = tracker.get_all_positions().await;
assert_eq!(positions.len(), 1);
let pos = positions.get(&sym).unwrap();
assert!(pos.quantity > Decimal::ZERO, "Position should be long");
// Step 2: Sell 10 shares at $110 -- closes the position
let sell_order = make_test_order("AAPL", OrderSide::Sell, 10.0);
let sell_price = Price::from_f64(110.0).unwrap();
tracker
.update_position(&sym, &sell_order, sell_price)
.await
.unwrap();
// Position should be removed after full close
let positions = tracker.get_all_positions().await;
assert!(
positions.is_empty(),
"Position should be removed after full close"
);
// A round-trip trade record should have been created with correct PnL
let trades = tracker.get_trades().await;
assert_eq!(trades.len(), 1, "One round-trip trade expected");
let trade = &trades[0];
// PnL = (110 - 100) * 10 = 100
assert_eq!(trade.pnl, Decimal::from(100));
assert_eq!(trade.side, OrderSide::Buy); // entry side was Buy
}
#[tokio::test]
async fn test_position_tracker_short_position_pnl() {
let tracker = PositionTracker::new();
let sym = Symbol::from("TSLA");
// Sell short 5 shares at $200
let sell_order = make_test_order("TSLA", OrderSide::Sell, 5.0);
let sell_price = Price::from_f64(200.0).unwrap();
tracker
.update_position(&sym, &sell_order, sell_price)
.await
.unwrap();
let positions = tracker.get_all_positions().await;
let pos = positions.get(&sym).unwrap();
assert!(pos.quantity < Decimal::ZERO, "Position should be short");
// Cover (buy) 5 shares at $180 -- profit on short
let buy_order = make_test_order("TSLA", OrderSide::Buy, 5.0);
let buy_price = Price::from_f64(180.0).unwrap();
tracker
.update_position(&sym, &buy_order, buy_price)
.await
.unwrap();
let positions = tracker.get_all_positions().await;
assert!(positions.is_empty(), "Short position should be closed");
let trades = tracker.get_trades().await;
assert_eq!(trades.len(), 1);
// Short PnL = (entry 200 - exit 180) * 5 = 100
assert_eq!(trades[0].pnl, Decimal::from(100));
}
#[tokio::test]
async fn test_position_tracker_get_realized_pnl() {
let tracker = PositionTracker::new();
// Trade 1: Buy 10 AAPL at $100, sell at $110 => PnL = +100
let sym1 = Symbol::from("AAPL");
let buy1 = make_test_order("AAPL", OrderSide::Buy, 10.0);
let sell1 = make_test_order("AAPL", OrderSide::Sell, 10.0);
tracker
.update_position(&sym1, &buy1, Price::from_f64(100.0).unwrap())
.await
.unwrap();
tracker
.update_position(&sym1, &sell1, Price::from_f64(110.0).unwrap())
.await
.unwrap();
// Trade 2: Buy 5 MSFT at $300, sell at $290 => PnL = -50
let sym2 = Symbol::from("MSFT");
let buy2 = make_test_order("MSFT", OrderSide::Buy, 5.0);
let sell2 = make_test_order("MSFT", OrderSide::Sell, 5.0);
tracker
.update_position(&sym2, &buy2, Price::from_f64(300.0).unwrap())
.await
.unwrap();
tracker
.update_position(&sym2, &sell2, Price::from_f64(290.0).unwrap())
.await
.unwrap();
// Total realized PnL should be 100 + (-50) = 50
let total_pnl = tracker.get_realized_pnl().await;
assert_eq!(total_pnl, Decimal::from(50));
let trades = tracker.get_trades().await;
assert_eq!(trades.len(), 2, "Two round-trip trades expected");
}
#[tokio::test]
async fn test_position_tracker_partial_close() {
let tracker = PositionTracker::new();
let sym = Symbol::from("GOOG");
// Buy 20 at $50
let buy = make_test_order("GOOG", OrderSide::Buy, 20.0);
tracker
.update_position(&sym, &buy, Price::from_f64(50.0).unwrap())
.await
.unwrap();
// Sell 10 at $60 -- partial close
let sell = make_test_order("GOOG", OrderSide::Sell, 10.0);
tracker
.update_position(&sym, &sell, Price::from_f64(60.0).unwrap())
.await
.unwrap();
// 10 shares should remain
let positions = tracker.get_all_positions().await;
assert_eq!(positions.len(), 1);
let pos = positions.get(&sym).unwrap();
assert_eq!(pos.quantity, Decimal::from(10));
// Realized PnL from the closed 10 shares: (60-50)*10 = 100
let pnl = tracker.get_realized_pnl().await;
assert_eq!(pnl, Decimal::from(100));
}
#[tokio::test]
async fn test_position_tracker_add_to_position() {
let tracker = PositionTracker::new();
let sym = Symbol::from("AMZN");
// Buy 10 at $100
let buy1 = make_test_order("AMZN", OrderSide::Buy, 10.0);
tracker
.update_position(&sym, &buy1, Price::from_f64(100.0).unwrap())
.await
.unwrap();
// Buy 10 more at $120 -- adding to position
let buy2 = make_test_order("AMZN", OrderSide::Buy, 10.0);
tracker
.update_position(&sym, &buy2, Price::from_f64(120.0).unwrap())
.await
.unwrap();
let positions = tracker.get_all_positions().await;
let pos = positions.get(&sym).unwrap();
assert_eq!(pos.quantity, Decimal::from(20));
// Weighted avg: (100*10 + 120*10) / 20 = 110
assert_eq!(pos.avg_price, Decimal::from(110));
// No realized PnL yet (no close)
let pnl = tracker.get_realized_pnl().await;
assert_eq!(pnl, Decimal::ZERO);
}
}