Files
foxhunt/backtesting/src/lib.rs
jgrusewski d98b967adf refactor: Major type system fixes with parallel agent deployment
Deployed 12 parallel agents to fix compilation errors using common type system:

 Successfully Fixed:
- Symbol type SQLx database traits implementation
- u64 to i64 conversions for PostgreSQL compatibility
- rust_decimal::Decimal ToPrimitive trait imports
- Order struct field naming (order_id→id, timestamp→created_at)
- Execution struct gross_value/net_value field initialization
- TimeInForce::GoodTillCancelled → GoodTillCancel
- Position struct field mappings
- Database feature flags in Cargo.toml files
- Storage crate common type system integration
- TLI pure client architecture compliance
- Services compilation issues

Current Status:
- Initial errors: 86
- Current errors: 3710 (increased due to import cascading)
- Main issue: Import path resolution problems
- 5 crates failing compilation

Next Steps:
- Fix import paths and module resolutions
- Resolve duplicate Position definition
- Fix async_trait and model_cache imports

🤖 Generated with Claude Code

Co-Authored-By: Claude <noreply@anthropic.com>
2025-09-27 10:16:45 +02:00

1056 lines
36 KiB
Rust

#![warn(missing_docs)]
#![warn(clippy::all)]
#![warn(clippy::pedantic)]
#![allow(clippy::module_name_repetitions)]
#![deny(clippy::unwrap_used, clippy::expect_used, clippy::panic)]
#![allow(clippy::too_many_arguments)]
#![allow(clippy::too_many_lines)]
//! Historical Market Replay System for Backtesting
//!
//! This crate provides a comprehensive backtesting framework for trading strategies
//! with tick-by-tick historical market data replay, strategy execution, and performance analytics.
//!
//! # Features
//!
//! - **Market Data Replay**: Replay historical market data with configurable speed and filtering
//! - **Strategy Testing**: Execute trading strategies against historical data with realistic execution
//! - **Performance Analytics**: Comprehensive performance metrics including risk, return, and drawdown analysis
//! - **Tick-by-tick Precision**: Support for high-frequency tick-level backtesting
//! - **Multiple Data Sources**: CSV, Parquet, and database support for historical data
//! - **Risk Management**: Built-in position sizing, stop losses, and risk controls
//!
//! # Quick Start
//!
//! ```rust,no_run
//! use backtesting::{BacktestEngine, BacktestConfig, replay_engine::ReplayConfig};
//! use chrono::Utc;
//! use core::types::prelude::*;
//
// #[tokio::main]
// async fn main() -> anyhow::Result<()> {
// let config = BacktestConfig {
// initial_capital: Decimal::from(100000),
// replay_config: ReplayConfig {
// start_time: Utc::now() - chrono::Duration::days(30),
// end_time: Utc::now(),
// tick_by_tick: true,
// ..Default::default()
// },
// ..Default::default()
// };
//
// let mut engine = BacktestEngine::new(config).await?;
// let results = engine.run().await?;
//
// info!("Total Return: {:.2}%", results.strategy_result.total_return * Decimal::from(100));
// info!("Sharpe Ratio: {:.2}", results.strategy_result.sharpe_ratio);
// info!("Max Drawdown: {:.2}%", results.strategy_result.max_drawdown * Decimal::from(100));
//
// Ok(())
// }
/// ```
// Re-export std modules that might be shadowed by local crate names
use std as stdlib;
use std::{collections::HashMap, sync::Arc, time::Instant};
use anyhow::{Context, Result};
use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
use tokio::sync::{mpsc, RwLock};
use tracing::{error, info, warn};
use common::*;
use rust_decimal::prelude::ToPrimitive;
// mod types; // Removed - using core::prelude types instead
pub mod metrics;
pub mod replay_engine;
pub mod strategy_tester;
pub mod strategy_runner;
pub use replay_engine::{MarketReplay, ReplayConfig, ReplayEvent};
pub use strategy_tester::{
PerformanceSnapshot, SignalType, Strategy, StrategyConfig, StrategyContext, StrategyResult,
StrategyTester, TradeRecord, TradingSignal,
};
pub use metrics::{
DrawdownMetrics, MetricsCalculator, PerformanceAnalytics, PortfolioMetrics, ReturnMetrics,
RiskMetrics, TimeAnalysis, TradeStatistics,
};
pub use strategy_runner::{
create_adaptive_strategy, create_adaptive_strategy_with_config, AdaptiveStrategyConfig,
AdaptiveStrategyRunner, FeatureSettings, RiskSettings,
};
// Import Side directly (no alias needed)
use common::Side;
use trading_engine::types::events::MarketEvent;
/// Main backtesting engine configuration
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct BacktestConfig {
/// Initial capital for backtesting
pub initial_capital: Decimal,
/// Market data replay configuration
pub replay_config: ReplayConfig,
/// Strategy configuration
pub strategy_config: StrategyConfig,
/// Risk-free rate for Sharpe ratio calculation
pub risk_free_rate: Decimal,
/// Enable detailed logging
pub enable_logging: bool,
/// Performance snapshot interval (seconds)
pub snapshot_interval: u64,
/// Maximum memory usage (bytes)
pub max_memory_usage: usize,
}
impl Default for BacktestConfig {
fn default() -> Self {
Self {
initial_capital: Decimal::from(100000),
replay_config: ReplayConfig::default(),
strategy_config: StrategyConfig::default(),
risk_free_rate: Decimal::new(2, 2), // 2% annually
enable_logging: true,
snapshot_interval: 3600, // 1 hour
max_memory_usage: 1024 * 1024 * 1024, // 1GB
}
}
}
/// Main backtesting engine that orchestrates market replay and strategy execution
pub struct BacktestEngine {
/// Configuration
config: BacktestConfig,
/// Market data replay engine
market_replay: Arc<MarketReplay>,
/// Strategy being tested
strategy: Option<Box<dyn Strategy>>,
/// Strategy tester
strategy_tester: Option<StrategyTester>,
/// Performance metrics calculator
metrics_calculator: Arc<RwLock<MetricsCalculator>>,
/// Current execution state
state: Arc<RwLock<BacktestState>>,
/// Performance monitoring
performance_monitor: Arc<PerformanceMonitor>,
}
/// Current state of backtesting engine
#[derive(Debug, Clone)]
pub struct BacktestState {
/// Is backtest running
pub is_running: bool,
/// Is backtest paused
pub is_paused: bool,
/// Backtest start time (wall clock)
pub start_time: Option<Instant>,
/// Current simulation time
pub current_sim_time: Option<DateTime<Utc>>,
/// Events processed
pub events_processed: u64,
/// Current portfolio value
pub portfolio_value: Decimal,
/// Total trades executed
pub trades_executed: u64,
/// Last performance snapshot time
pub last_snapshot: Option<DateTime<Utc>>,
}
impl Default for BacktestState {
fn default() -> Self {
Self {
is_running: false,
is_paused: false,
start_time: None,
current_sim_time: None,
events_processed: 0,
portfolio_value: Decimal::ZERO,
trades_executed: 0,
last_snapshot: None,
}
}
}
/// Performance monitoring for the backtesting engine
#[derive(Debug)]
pub struct PerformanceMonitor {
/// Memory usage tracking
memory_usage: Arc<std::sync::atomic::AtomicUsize>,
/// CPU usage tracking
cpu_usage: Arc<std::sync::atomic::AtomicU64>,
/// Event processing rate
events_per_second: Arc<std::sync::atomic::AtomicU64>,
/// Last performance check
last_check: Arc<RwLock<Option<Instant>>>,
}
impl Default for PerformanceMonitor {
fn default() -> Self {
Self {
memory_usage: Arc::new(std::sync::atomic::AtomicUsize::new(0)),
cpu_usage: Arc::new(std::sync::atomic::AtomicU64::new(0)),
events_per_second: Arc::new(std::sync::atomic::AtomicU64::new(0)),
last_check: Arc::new(RwLock::new(None)),
}
}
}
impl BacktestEngine {
/// Create a new backtesting engine
pub async fn new(config: BacktestConfig) -> Result<Self> {
info!(
"Initializing backtesting engine with initial capital: {}",
config.initial_capital
);
let market_replay = Arc::new(MarketReplay::new(config.replay_config.clone()));
let metrics_calculator =
Arc::new(RwLock::new(MetricsCalculator::new(config.risk_free_rate)));
let performance_monitor = Arc::new(PerformanceMonitor::default());
Ok(Self {
config,
market_replay,
strategy: None,
strategy_tester: None,
metrics_calculator,
state: Arc::new(RwLock::new(BacktestState::default())),
performance_monitor,
})
}
/// Set the trading strategy to test
pub async fn set_strategy(&mut self, strategy: Box<dyn Strategy>) -> Result<()> {
info!("Setting strategy: {}", strategy.name());
let strategy_tester = StrategyTester::new(
strategy,
self.config.strategy_config.clone(),
Arc::clone(&self.market_replay),
self.config.initial_capital,
);
self.strategy_tester = Some(strategy_tester);
Ok(())
}
/// Run the complete backtesting process
pub async fn run(&mut self) -> Result<BacktestResult> {
if self.strategy_tester.is_none() {
return Err(anyhow::anyhow!(
"No strategy set. Call set_strategy() first."
));
}
info!("Starting backtesting run");
// Update state
{
let mut state = self.state.write().await;
state.is_running = true;
state.start_time = Some(Instant::now());
state.current_sim_time = Some(self.config.replay_config.start_time);
}
// Start performance monitoring
let monitor_handle = self.start_performance_monitoring().await;
// Run the strategy test
let strategy_result = match self.strategy_tester.as_mut() {
Some(tester) => tester.run_test().await?,
None => return Err(anyhow::anyhow!("Strategy tester not initialized")),
};
// Calculate comprehensive analytics
let analytics = {
let calculator = self.metrics_calculator.read().await;
calculator.calculate_analytics()?
};
// Stop performance monitoring
monitor_handle.abort();
// Update final state
{
let mut state = self.state.write().await;
state.is_running = false;
state.portfolio_value = strategy_result.final_value;
state.trades_executed = strategy_result.total_trades;
}
let backtest_result = BacktestResult {
strategy_result,
analytics,
execution_stats: self.get_execution_stats().await,
config: self.config.clone(),
};
info!(
"Backtesting completed. Total return: {:.2}%, Sharpe ratio: {:.2}",
backtest_result.strategy_result.total_return * Decimal::from(100),
backtest_result.strategy_result.sharpe_ratio
);
Ok(backtest_result)
}
/// Run backtesting with real-time monitoring
pub async fn run_with_monitoring(
&mut self,
) -> Result<(BacktestResult, mpsc::UnboundedReceiver<MonitoringUpdate>)> {
let (update_sender, update_receiver) = mpsc::unbounded_channel();
// Clone necessary data for monitoring task
let state = Arc::clone(&self.state);
let performance_monitor = Arc::clone(&self.performance_monitor);
// Start monitoring task
let monitoring_handle = tokio::spawn(async move {
let mut interval = tokio::time::interval(tokio::time::Duration::from_secs(1));
loop {
interval.tick().await;
let current_state = state.read().await.clone();
if !current_state.is_running {
break;
}
let update = MonitoringUpdate {
timestamp: Utc::now(),
events_processed: current_state.events_processed,
portfolio_value: current_state.portfolio_value,
trades_executed: current_state.trades_executed,
memory_usage: performance_monitor
.memory_usage
.load(std::sync::atomic::Ordering::Relaxed),
events_per_second: performance_monitor
.events_per_second
.load(std::sync::atomic::Ordering::Relaxed),
current_sim_time: current_state.current_sim_time,
};
if update_sender.send(update).is_err() {
break; // Receiver dropped
}
}
});
// Run the backtest
let result = self.run().await;
// Clean up monitoring
monitoring_handle.abort();
match result {
Ok(backtest_result) => Ok((backtest_result, update_receiver)),
Err(e) => Err(e),
}
}
/// Pause the backtesting process
pub async fn pause(&self) -> Result<()> {
{
let mut state = self.state.write().await;
state.is_paused = true;
}
self.market_replay.pause().await;
info!("Backtesting paused");
Ok(())
}
/// Resume the backtesting process
pub async fn resume(&self) -> Result<()> {
{
let mut state = self.state.write().await;
state.is_paused = false;
}
self.market_replay.resume().await;
info!("Backtesting resumed");
Ok(())
}
/// Stop the backtesting process
pub async fn stop(&self) -> Result<()> {
{
let mut state = self.state.write().await;
state.is_running = false;
state.is_paused = false;
}
self.market_replay.stop().await;
info!("Backtesting stopped");
Ok(())
}
/// Get current backtesting state
pub async fn get_state(&self) -> BacktestState {
self.state.read().await.clone()
}
/// Get current performance metrics
pub async fn get_current_analytics(&self) -> Result<PerformanceAnalytics> {
let calculator = self.metrics_calculator.read().await;
calculator.calculate_analytics()
}
/// Add market data for replay
pub async fn add_market_data(&self, symbol: Symbol, data: Vec<MarketEvent>) -> Result<()> {
// This would integrate with the market replay engine to add data
// Implementation depends on the specific data loading mechanism
warn!("add_market_data not yet implemented - use ReplayConfig data sources instead");
Ok(())
}
/// Run backtesting with adaptive strategy using real ML models
pub async fn run_with_adaptive_strategy(
&mut self,
adaptive_config: AdaptiveStrategyConfig,
) -> Result<BacktestResult> {
info!("Starting backtesting with adaptive ML strategy");
// Create adaptive strategy
let adaptive_strategy = Box::new(create_adaptive_strategy_with_config(adaptive_config));
// Set the strategy
self.set_strategy(adaptive_strategy).await?;
// Run the backtest
let result = self.run().await?;
info!(
"Adaptive ML backtesting completed. Models used: {:?}",
result.config.strategy_config
);
Ok(result)
}
/// Run backtesting with parallel model evaluation
pub async fn run_with_parallel_models(
&mut self,
model_names: Vec<String>,
) -> Result<Vec<BacktestResult>> {
info!(
"Starting parallel backtesting with {} models",
model_names.len()
);
let mut results = Vec::new();
for model_name in model_names {
info!("Running backtest with model: {}", model_name);
let adaptive_config = AdaptiveStrategyConfig {
active_models: vec![model_name.clone()],
..AdaptiveStrategyConfig::default()
};
// Clone the engine configuration for each model test
let mut model_engine = BacktestEngine::new(self.config.clone()).await?;
let result = model_engine
.run_with_adaptive_strategy(adaptive_config)
.await?;
results.push(result);
}
info!(
"Parallel model backtesting completed for {} models",
results.len()
);
Ok(results)
}
/// Run ensemble backtesting comparing individual models vs ensemble
pub async fn run_ensemble_comparison(
&mut self,
model_names: Vec<String>,
) -> Result<EnsembleComparisonResult> {
info!(
"Starting ensemble comparison with {} models",
model_names.len()
);
// Test individual models
let individual_results = self.run_with_parallel_models(model_names.clone()).await?;
// Test ensemble
let ensemble_config = AdaptiveStrategyConfig {
active_models: model_names.clone(),
..AdaptiveStrategyConfig::default()
};
let mut ensemble_engine = BacktestEngine::new(self.config.clone()).await?;
let ensemble_result = ensemble_engine
.run_with_adaptive_strategy(ensemble_config)
.await?;
// Calculate comparison metrics before moving individual_results
let comparison_metrics = self
.calculate_comparison_metrics(&individual_results, &ensemble_result)
.await;
let comparison = EnsembleComparisonResult {
individual_results,
ensemble_result,
models_tested: model_names,
comparison_metrics,
};
info!(
"Ensemble comparison completed. Ensemble Sharpe: {:.3}, Best Individual: {:.3}",
comparison.ensemble_result.strategy_result.sharpe_ratio,
comparison.comparison_metrics.best_individual_sharpe
);
Ok(comparison)
}
/// Start performance monitoring task
async fn start_performance_monitoring(&self) -> tokio::task::JoinHandle<()> {
let performance_monitor = Arc::clone(&self.performance_monitor);
tokio::spawn(async move {
let mut interval = tokio::time::interval(tokio::time::Duration::from_secs(5));
loop {
interval.tick().await;
// Update memory usage
if let Ok(info) = sys_info::mem_info() {
let used_memory = (info.total - info.free) * 1024; // Convert to bytes
performance_monitor
.memory_usage
.store(used_memory as usize, std::sync::atomic::Ordering::Relaxed);
}
// Update last check time
{
let mut last_check = performance_monitor.last_check.write().await;
*last_check = Some(Instant::now());
}
}
})
}
/// Calculate comparison metrics between individual models and ensemble
async fn calculate_comparison_metrics(
&self,
individual_results: &[BacktestResult],
ensemble_result: &BacktestResult,
) -> ComparisonMetrics {
let individual_sharpes: Vec<Decimal> = individual_results
.iter()
.map(|r| r.strategy_result.sharpe_ratio)
.collect();
let best_individual_sharpe = individual_sharpes
.iter()
.max()
.copied()
.unwrap_or(Decimal::ZERO);
let avg_individual_sharpe = if !individual_sharpes.is_empty() {
individual_sharpes.iter().sum::<Decimal>() / Decimal::from(individual_sharpes.len())
} else {
Decimal::ZERO
};
let ensemble_sharpe = ensemble_result.strategy_result.sharpe_ratio;
ComparisonMetrics {
best_individual_sharpe,
avg_individual_sharpe,
ensemble_sharpe,
ensemble_improvement: ensemble_sharpe - best_individual_sharpe,
diversification_benefit: ensemble_sharpe - avg_individual_sharpe,
}
}
/// Get execution statistics
async fn get_execution_stats(&self) -> ExecutionStats {
let state = self.state.read().await;
let wall_time = state
.start_time
.map(|start| start.elapsed())
.unwrap_or_default();
ExecutionStats {
wall_time_seconds: wall_time.as_secs(),
events_processed: state.events_processed,
trades_executed: state.trades_executed,
memory_peak_mb: self
.performance_monitor
.memory_usage
.load(std::sync::atomic::Ordering::Relaxed)
/ (1024 * 1024),
events_per_second: if wall_time.as_secs() > 0 {
state.events_processed / wall_time.as_secs()
} else {
0
},
}
}
}
/// Complete backtesting result
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct BacktestResult {
/// Strategy execution results
pub strategy_result: StrategyResult,
/// Comprehensive performance analytics
pub analytics: PerformanceAnalytics,
/// Execution statistics
pub execution_stats: ExecutionStats,
/// Configuration used
pub config: BacktestConfig,
}
/// Execution performance statistics
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ExecutionStats {
/// Wall clock time in seconds
pub wall_time_seconds: u64,
/// Total events processed
pub events_processed: u64,
/// Total trades executed
pub trades_executed: u64,
/// Peak memory usage in MB
pub memory_peak_mb: usize,
/// Average events per second
pub events_per_second: u64,
}
/// Real-time monitoring update
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct MonitoringUpdate {
/// Update timestamp
pub timestamp: DateTime<Utc>,
/// Events processed so far
pub events_processed: u64,
/// Current portfolio value
pub portfolio_value: Decimal,
/// Trades executed so far
pub trades_executed: u64,
/// Current memory usage in bytes
pub memory_usage: usize,
/// Current events per second
pub events_per_second: u64,
/// Current simulation time
pub current_sim_time: Option<DateTime<Utc>>,
}
/// Result of ensemble vs individual model comparison
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct EnsembleComparisonResult {
/// Results from individual model backtests
pub individual_results: Vec<BacktestResult>,
/// Result from ensemble backtest
pub ensemble_result: BacktestResult,
/// Names of models tested
pub models_tested: Vec<String>,
/// Comparison metrics
pub comparison_metrics: ComparisonMetrics,
}
/// Metrics comparing ensemble vs individual model performance
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ComparisonMetrics {
/// Best individual model Sharpe ratio
pub best_individual_sharpe: Decimal,
/// Average individual model Sharpe ratio
pub avg_individual_sharpe: Decimal,
/// Ensemble Sharpe ratio
pub ensemble_sharpe: Decimal,
/// Improvement of ensemble over best individual
pub ensemble_improvement: Decimal,
/// Diversification benefit (ensemble vs average)
pub diversification_benefit: Decimal,
}
// Re-export commonly used types
// Note: DateTime, Utc, and Decimal are already imported above, no need to re-export
#[cfg(test)]
mod tests {
use super::*;
use std::io::Write;
use tempfile::NamedTempFile;
#[tokio::test]
async fn test_backtest_engine_creation() {
let config = BacktestConfig::default();
let engine = BacktestEngine::new(config).await;
assert!(
engine.is_ok(),
"BacktestEngine creation should not fail in test: {:?}",
engine.err()
);
let engine = engine.unwrap();
let state = engine.get_state().await;
assert!(!state.is_running);
assert!(!state.is_paused);
}
#[tokio::test]
async fn test_backtest_config_default() {
let config = BacktestConfig::default();
assert_eq!(config.initial_capital, Decimal::from(100000));
assert_eq!(config.risk_free_rate, Decimal::new(2, 2));
assert!(config.enable_logging);
}
// Real Mean Reversion Strategy for Testing
struct MeanReversionStrategy {
lookback_period: usize,
price_history: Vec<Decimal>,
position_size: Decimal,
entry_threshold: Decimal,
exit_threshold: Decimal,
current_position: Option<Position>,
position_side: Option<Side>,
trades_executed: usize,
total_pnl: Decimal,
max_drawdown: Decimal,
peak_value: Decimal,
initial_capital: Decimal,
}
impl MeanReversionStrategy {
fn new() -> Self {
Self {
lookback_period: 20,
price_history: Vec::new(),
position_size: dec!(0.02), // 2% position size
entry_threshold: dec!(2.0), // 2 standard deviations
exit_threshold: dec!(0.5), // 0.5 standard deviations
current_position: None,
position_side: None,
trades_executed: 0,
total_pnl: Decimal::ZERO,
max_drawdown: Decimal::ZERO,
peak_value: Decimal::ZERO,
initial_capital: Decimal::ZERO,
}
}
fn calculate_z_score(&self, current_price: Decimal) -> Option<Decimal> {
if self.price_history.len() < self.lookback_period {
return None;
}
let recent_prices =
&self.price_history[self.price_history.len() - self.lookback_period..];
let mean = recent_prices.iter().sum::<Decimal>() / Decimal::from(recent_prices.len());
let variance = recent_prices
.iter()
.map(|price| {
let diff = *price - mean;
diff * diff
})
.sum::<Decimal>()
/ Decimal::from(recent_prices.len());
// Calculate standard deviation using f64 for sqrt operation
let variance_f64 = variance.to_f64().unwrap_or(0.0);
let std_dev = Decimal::from_f64(variance_f64.sqrt()).unwrap_or(Decimal::ZERO);
if std_dev > Decimal::ZERO {
Some((current_price - mean) / std_dev)
} else {
None
}
}
fn should_enter_long(&self, z_score: Decimal) -> bool {
z_score < -self.entry_threshold && self.current_position.is_none()
}
fn should_enter_short(&self, z_score: Decimal) -> bool {
z_score > self.entry_threshold && self.current_position.is_none()
}
fn should_exit_position(&self, z_score: Decimal) -> bool {
if let Some(ref _position) = self.current_position {
if let Some(ref side) = self.position_side {
match side {
Side::Buy => z_score > -self.exit_threshold, // Long position
Side::Sell => z_score < self.exit_threshold, // Short position
}
} else {
false
}
} else {
false
}
}
}
#[async_trait::async_trait(?Send)]
impl Strategy for MeanReversionStrategy {
fn name(&self) -> &str {
"mean_reversion_strategy"
}
async fn initialize(
&mut self,
initial_capital: Decimal,
_config: StrategyConfig,
) -> Result<()> {
self.initial_capital = initial_capital;
self.peak_value = initial_capital;
info!(
"Mean Reversion Strategy initialized with capital: {}",
initial_capital
);
Ok(())
}
async fn on_market_event(
&mut self,
event: &MarketEvent,
context: &StrategyContext,
) -> Result<Vec<TradingSignal>> {
let mut signals = Vec::new();
if let MarketEvent::Trade { symbol, price, .. } = event {
self.price_history.push((*price).into());
// Keep only recent price history
if self.price_history.len() > self.lookback_period * 2 {
self.price_history.drain(0..self.lookback_period);
}
if let Some(z_score) = self.calculate_z_score((*price).into()) {
// Generate trading signals based on mean reversion logic
if self.should_enter_long(z_score) {
let price_decimal: Decimal = (*price).into();
let quantity = (context.account_balance * self.position_size
/ price_decimal)
.round_dp(0);
let mut metadata = HashMap::new();
metadata
.insert("strategy".to_string(), serde_json::json!("mean_reversion"));
metadata.insert("z_score".to_string(), serde_json::json!(z_score));
metadata.insert("signal_type".to_string(), serde_json::json!("enter_long"));
signals.push(TradingSignal {
symbol: symbol.clone(),
signal_type: SignalType::Buy,
quantity: Quantity::from_f64(quantity.to_f64().unwrap_or(0.0))
.unwrap_or(Quantity::ZERO),
target_price: Some(*price),
stop_loss: None,
take_profit: None,
confidence: dec!(0.8),
metadata,
});
} else if self.should_enter_short(z_score) {
let price_decimal: Decimal = (*price).into();
let quantity = (context.account_balance * self.position_size
/ price_decimal)
.round_dp(0);
let mut metadata = HashMap::new();
metadata
.insert("strategy".to_string(), serde_json::json!("mean_reversion"));
metadata.insert("z_score".to_string(), serde_json::json!(z_score));
metadata
.insert("signal_type".to_string(), serde_json::json!("enter_short"));
signals.push(TradingSignal {
symbol: symbol.clone(),
signal_type: SignalType::Sell,
quantity: Quantity::from_f64(quantity.to_f64().unwrap_or(0.0))
.unwrap_or(Quantity::ZERO),
target_price: Some(*price),
stop_loss: None,
take_profit: None,
confidence: dec!(0.8),
metadata,
});
} else if self.should_exit_position(z_score) {
if let Some(ref position) = self.current_position {
if let Some(ref side) = self.position_side {
let exit_signal_type = match side {
Side::Buy => SignalType::Sell, // Exit long position
Side::Sell => SignalType::Cover, // Exit short position
};
let mut metadata = HashMap::new();
metadata.insert(
"strategy".to_string(),
serde_json::json!("mean_reversion"),
);
metadata.insert("z_score".to_string(), serde_json::json!(z_score));
metadata.insert(
"signal_type".to_string(),
serde_json::json!("exit_position"),
);
signals.push(TradingSignal {
symbol: symbol.clone(),
signal_type: exit_signal_type,
quantity: Quantity::from_f64(position.quantity.to_f64())
.unwrap_or(Quantity::ZERO),
target_price: Some(*price),
stop_loss: None,
take_profit: None,
confidence: dec!(0.8),
metadata,
});
}
}
}
}
}
Ok(signals)
}
async fn on_order_update(
&mut self,
order: &Order,
_context: &StrategyContext,
) -> anyhow::Result<()> {
if order.status == OrderStatus::Filled {
self.trades_executed += 1;
let display_price = order
.average_price
.unwrap_or(order.price.unwrap_or(Price::ZERO));
info!(
"Order filled: {} {} @ {}",
order.side, order.quantity, display_price
);
}
Ok(())
}
async fn on_position_update(
&mut self,
position: &Position,
context: &StrategyContext,
) -> anyhow::Result<()> {
self.current_position = Some(position.clone());
// Determine position side based on quantity sign
if position.quantity.to_f64() > 0.0 {
self.position_side = Some(Side::Buy); // Long position
} else if position.quantity.to_f64() < 0.0 {
self.position_side = Some(Side::Sell); // Short position
} else {
self.position_side = None; // No position
}
// Update P&L tracking
let current_value = context.account_balance;
if current_value > self.peak_value {
self.peak_value = current_value;
}
let current_drawdown = (self.peak_value - current_value) / self.peak_value;
if current_drawdown > self.max_drawdown {
self.max_drawdown = current_drawdown;
}
self.total_pnl = current_value - self.initial_capital;
Ok(())
}
async fn finalize(&mut self, context: &StrategyContext) -> Result<StrategyResult> {
let total_return = if self.initial_capital > Decimal::ZERO {
self.total_pnl / self.initial_capital
} else {
Decimal::ZERO
};
let annualized_return = total_return; // Simplified for test
let win_rate = if self.trades_executed > 0 {
// Simplified calculation - in reality would track individual trade outcomes
if self.total_pnl > Decimal::ZERO {
dec!(0.6)
} else {
dec!(0.4)
}
} else {
Decimal::ZERO
};
let avg_trade_return = if self.trades_executed > 0 {
self.total_pnl / Decimal::from(self.trades_executed)
} else {
Decimal::ZERO
};
let sharpe_ratio = if self.max_drawdown > Decimal::ZERO {
annualized_return / self.max_drawdown // Simplified Sharpe calculation
} else {
Decimal::ZERO
};
Ok(StrategyResult {
strategy_name: "mean_reversion_strategy".to_string(),
total_return,
annualized_return,
max_drawdown: self.max_drawdown,
sharpe_ratio,
total_trades: self.trades_executed as u64,
win_rate,
avg_trade_return,
final_value: context.account_balance,
trades: vec![], // Would be populated with actual trade records
performance_timeline: vec![], // Would be populated with performance snapshots
})
}
async fn get_state(&self) -> Result<serde_json::Value> {
Ok(serde_json::json!({
"name": "mean_reversion_strategy",
"lookback_period": self.lookback_period,
"position_size": self.position_size,
"entry_threshold": self.entry_threshold,
"exit_threshold": self.exit_threshold,
"trades_executed": self.trades_executed,
"total_pnl": self.total_pnl,
"max_drawdown": self.max_drawdown,
"current_position": self.current_position
}))
}
}
#[tokio::test]
async fn test_strategy_setting() {
let config = BacktestConfig::default();
let engine_result = BacktestEngine::new(config).await;
assert!(
engine_result.is_ok(),
"BacktestEngine creation should not fail in test: {:?}",
engine_result.err()
);
let mut engine = engine_result.unwrap();
let strategy = Box::new(MeanReversionStrategy::new());
let result = engine.set_strategy(strategy).await;
assert!(
result.is_ok(),
"Strategy setting should not fail in test: {:?}",
result.err()
);
assert!(engine.strategy_tester.is_some());
}
}