//! Barrier Parameter Optimizer Example //! //! Uses Monte-Carlo simulations to find optimal triple-barrier parameters //! for ES.FUT, NQ.FUT, ZN.FUT, and 6E.FUT futures contracts. //! //! ## Usage //! ```bash //! cargo run -p ml --example optimize_barriers --release //! ``` //! //! ## Expected Output //! - Optimal profit_target_bps, stop_loss_bps, max_holding_period //! - Sharpe ratio, win rate, max drawdown for each configuration //! - CSV file with full grid search results use anyhow::{Context, Result}; use rand::Rng; use std::collections::HashMap; use std::fs::File; use std::io::Write; use std::path::PathBuf; use tracing::{error, info, warn}; use ml::labeling::triple_barrier::{BarrierConfig, BarrierResult}; use ml::real_data_loader::DbnDataSource; /// Optimal parameters for a specific market regime #[derive(Debug, Clone)] pub struct OptimalParameters { pub symbol: String, pub profit_target_bps: u32, pub stop_loss_bps: u32, pub max_holding_hours: f64, pub sharpe_ratio: f64, pub win_rate: f64, pub avg_return_pct: f64, pub max_drawdown_pct: f64, pub avg_bars_held: usize, } /// Simulation metrics for a parameter set #[derive(Debug, Clone)] struct SimulationMetrics { sharpe: f64, win_rate: f64, avg_return: f64, max_drawdown: f64, avg_bars_held: f64, trade_count: usize, } /// Monte-Carlo barrier optimizer pub struct BarrierOptimizer { pub symbol: String, pub historical_prices: Vec, pub daily_volatility: f64, pub n_simulations: usize, } impl BarrierOptimizer { pub fn new(symbol: String, historical_prices: Vec, n_simulations: usize) -> Self { let daily_volatility = Self::compute_daily_volatility(&historical_prices); info!( "Initialized optimizer for {} with {} bars, volatility={:.2}%", symbol, historical_prices.len(), daily_volatility * 100.0 ); Self { symbol, historical_prices, daily_volatility, n_simulations, } } /// Compute daily volatility from price series fn compute_daily_volatility(prices: &[f64]) -> f64 { if prices.len() < 2 { return 0.02; // Default 2% } let returns: Vec = prices.windows(2).map(|w| (w[1] / w[0]).ln()).collect(); let mean = returns.iter().sum::() / returns.len() as f64; let variance = returns.iter().map(|r| (r - mean).powi(2)).sum::() / returns.len() as f64; variance.sqrt() } /// Run grid search over parameter space pub fn optimize(&self) -> Result { info!("Starting grid search optimization..."); let mut best_score = f64::NEG_INFINITY; let mut best_params: Option = None; let mut all_results = Vec::new(); // Grid search parameters let profit_targets = (50..=500).step_by(25).collect::>(); let stop_losses = (50..=500).step_by(25).collect::>(); let holding_periods = vec![0.25, 0.5, 1.0, 2.0, 4.0, 8.0]; // Hours let total_combinations = profit_targets.len() * stop_losses.len() * holding_periods.len(); info!("Testing {} parameter combinations", total_combinations); for (idx, profit_bps) in profit_targets.iter().enumerate() { for stop_bps in &stop_losses { for holding_hours in &holding_periods { let config = BarrierConfig { profit_target_bps: *profit_bps, stop_loss_bps: *stop_bps, max_holding_period_ns: (*holding_hours * 3600.0 * 1e9) as u64, }; // Run Monte-Carlo simulations let metrics = self.simulate(&config)?; let score = self.compute_score(&metrics); all_results.push((config.clone(), metrics.clone(), score)); if score > best_score { best_score = score; best_params = Some(OptimalParameters { symbol: self.symbol.clone(), profit_target_bps: *profit_bps, stop_loss_bps: *stop_bps, max_holding_hours: *holding_hours, sharpe_ratio: metrics.sharpe, win_rate: metrics.win_rate, avg_return_pct: metrics.avg_return * 100.0, max_drawdown_pct: metrics.max_drawdown * 100.0, avg_bars_held: metrics.avg_bars_held as usize, }); info!( "New best: profit={}bps, stop={}bps, hold={:.1}h → Sharpe={:.2}, WR={:.1}%, score={:.4}", profit_bps, stop_bps, holding_hours, metrics.sharpe, metrics.win_rate * 100.0, score ); } } } if idx % 5 == 0 { info!( "Progress: {}/{} profit targets tested", idx, profit_targets.len() ); } } // Save full results to CSV self.save_results_csv(&all_results)?; best_params.context("No optimal parameters found (all simulations failed)") } /// Run Monte-Carlo simulations for a parameter set fn simulate(&self, config: &BarrierConfig) -> Result { let mut wins = 0; let mut losses = 0; let mut neutrals = 0; let mut returns = Vec::new(); let mut bars_held = Vec::new(); for _ in 0..self.n_simulations { // Generate synthetic price path using Geometric Brownian Motion let path_length = (config.max_holding_period_ns / 1e9 / 3600.0 * 24.0) as usize; // Approximate bars let path = self.generate_gbm_path(path_length.max(10)); // Apply triple-barrier method let outcome = self.apply_barriers(&path, config); match outcome.result { BarrierResult::ProfitTarget => wins += 1, BarrierResult::StopLoss => losses += 1, BarrierResult::TimeExpiry => neutrals += 1, } returns.push(outcome.return_pct); bars_held.push(outcome.bars_held); } let total_trades = wins + losses + neutrals; let win_rate = if total_trades > 0 { wins as f64 / total_trades as f64 } else { 0.0 }; Ok(SimulationMetrics { sharpe: Self::compute_sharpe(&returns), win_rate, avg_return: returns.iter().sum::() / returns.len() as f64, max_drawdown: Self::compute_max_drawdown(&returns), avg_bars_held: bars_held.iter().sum::() as f64 / bars_held.len() as f64, trade_count: total_trades, }) } /// Generate synthetic price path using Geometric Brownian Motion fn generate_gbm_path(&self, n_steps: usize) -> Vec { let mut rng = rand::thread_rng(); let mut path = vec![self.historical_prices[0]]; // Start at first historical price let dt = 1.0 / 252.0 / 6.5; // Assume 1 step = 1 hour (6.5 hours/day, 252 days/year) let drift = 0.0; // Neutral drift for conservative estimation let diffusion = self.daily_volatility * dt.sqrt(); for _ in 0..n_steps { let z: f64 = rng.sample(rand::distributions::StandardNormal); let new_price = path.last().unwrap() * ((drift - 0.5 * diffusion.powi(2)) * dt + diffusion * z).exp(); path.push(new_price); } path } /// Apply triple-barrier method to a price path fn apply_barriers(&self, path: &[f64], config: &BarrierConfig) -> BarrierOutcome { let entry_price = path[0]; let profit_level = entry_price * (1.0 + config.profit_target_bps as f64 / 10000.0); let stop_level = entry_price * (1.0 - config.stop_loss_bps as f64 / 10000.0); for (i, &price) in path.iter().enumerate().skip(1) { if price >= profit_level { return BarrierOutcome { result: BarrierResult::ProfitTarget, return_pct: config.profit_target_bps as f64 / 10000.0, bars_held: i, }; } if price <= stop_level { return BarrierOutcome { result: BarrierResult::StopLoss, return_pct: -(config.stop_loss_bps as f64 / 10000.0), bars_held: i, }; } } // Time expiry let final_price = *path.last().unwrap(); let final_return = (final_price - entry_price) / entry_price; BarrierOutcome { result: BarrierResult::TimeExpiry, return_pct: final_return, bars_held: path.len() - 1, } } /// Compute objective score (multi-objective optimization) fn compute_score(&self, metrics: &SimulationMetrics) -> f64 { // Weighted scoring function optimized for HFT let sharpe_weight = 0.4; let win_rate_weight = 0.3; let drawdown_weight = 0.2; let return_vol_weight = 0.1; let sharpe_score = metrics.sharpe.max(0.0).min(3.0) / 3.0; // Normalize to [0,1] let win_rate_score = metrics.win_rate; let drawdown_score = (1.0 - metrics.max_drawdown).max(0.0).min(1.0); let return_vol_score = (metrics.avg_return / self.daily_volatility) .max(-1.0) .min(1.0) * 0.5 + 0.5; sharpe_weight * sharpe_score + win_rate_weight * win_rate_score + drawdown_weight * drawdown_score + return_vol_weight * return_vol_score } /// Compute annualized Sharpe ratio fn compute_sharpe(returns: &[f64]) -> f64 { if returns.is_empty() { return 0.0; } let mean = returns.iter().sum::() / returns.len() as f64; let variance = returns.iter().map(|r| (r - mean).powi(2)).sum::() / returns.len() as f64; let std = variance.sqrt(); if std > 1e-10 { mean / std * (252.0_f64).sqrt() // Annualized (252 trading days) } else { 0.0 } } /// Compute maximum drawdown fn compute_max_drawdown(returns: &[f64]) -> f64 { if returns.is_empty() { return 0.0; } let mut cumulative = 0.0; let mut peak = 0.0; let mut max_dd = 0.0; for &ret in returns { cumulative += ret; if cumulative > peak { peak = cumulative; } let drawdown = if peak > 1e-10 { (peak - cumulative) / peak } else { 0.0 }; max_dd = max_dd.max(drawdown); } max_dd } /// Save grid search results to CSV fn save_results_csv(&self, results: &[(BarrierConfig, SimulationMetrics, f64)]) -> Result<()> { let filename = format!("ml/checkpoints/barrier_optimization_{}.csv", self.symbol); let mut file = File::create(&filename)?; // Write header writeln!( file, "profit_bps,stop_bps,holding_hours,sharpe,win_rate,avg_return,max_drawdown,avg_bars,score" )?; // Write data for (config, metrics, score) in results { writeln!( file, "{},{},{:.2},{:.3},{:.3},{:.4},{:.4},{:.1},{:.4}", config.profit_target_bps, config.stop_loss_bps, config.max_holding_period_ns as f64 / 3600.0 / 1e9, metrics.sharpe, metrics.win_rate, metrics.avg_return, metrics.max_drawdown, metrics.avg_bars_held, score )?; } info!("Saved results to {}", filename); Ok(()) } } /// Outcome of applying triple-barrier to a price path struct BarrierOutcome { result: BarrierResult, return_pct: f64, bars_held: usize, } #[tokio::main] async fn main() -> Result<()> { tracing_subscriber::fmt::init(); info!("=== Triple-Barrier Parameter Optimization ==="); // Load historical ES.FUT data let data_dir = PathBuf::from("test_data/real/databento/ml_training_small"); let mut file_mapping = HashMap::new(); file_mapping.insert("ES.FUT".to_string(), data_dir.join("ESH5.dbn.zst")); let data_source = DbnDataSource::new(file_mapping) .await .context("Failed to create DBN data source")?; let bars = data_source .load_ohlcv_bars("ES.FUT") .await .context("Failed to load ES.FUT bars")?; info!("Loaded {} ES.FUT bars", bars.len()); // Extract closing prices let prices: Vec = bars.iter().map(|bar| bar.close).collect(); // Run optimization (1000 Monte-Carlo simulations per parameter set) let optimizer = BarrierOptimizer::new("ES.FUT".to_string(), prices, 1000); let optimal = optimizer.optimize()?; // Print results info!("\n=== OPTIMAL PARAMETERS FOR {} ===", optimal.symbol); info!( "Profit Target: {} bps ({:.2}%)", optimal.profit_target_bps, optimal.profit_target_bps as f64 / 100.0 ); info!( "Stop Loss: {} bps ({:.2}%)", optimal.stop_loss_bps, optimal.stop_loss_bps as f64 / 100.0 ); info!("Max Holding: {:.1} hours", optimal.max_holding_hours); info!("\n=== PERFORMANCE METRICS ==="); info!("Sharpe Ratio: {:.2}", optimal.sharpe_ratio); info!("Win Rate: {:.1}%", optimal.win_rate * 100.0); info!("Avg Return: {:.2}%", optimal.avg_return_pct); info!("Max Drawdown: {:.2}%", optimal.max_drawdown_pct); info!("Avg Bars Held: {}", optimal.avg_bars_held); // Save optimal parameters to config file let config_content = format!( r#"# Optimal Triple-Barrier Parameters for {} # Generated: {} [barrier_config] profit_target_bps = {} stop_loss_bps = {} max_holding_period_ns = {} # {:.1} hours # Performance Metrics (from {} Monte-Carlo simulations) # Sharpe Ratio: {:.2} # Win Rate: {:.1}% # Avg Return: {:.2}% # Max Drawdown: {:.2}% "#, optimal.symbol, chrono::Utc::now().format("%Y-%m-%d %H:%M:%S UTC"), optimal.profit_target_bps, optimal.stop_loss_bps, (optimal.max_holding_hours * 3600.0 * 1e9) as u64, optimal.max_holding_hours, optimizer.n_simulations, optimal.sharpe_ratio, optimal.win_rate * 100.0, optimal.avg_return_pct, optimal.max_drawdown_pct, ); let config_path = format!("ml/checkpoints/optimal_barriers_{}.toml", optimal.symbol); std::fs::write(&config_path, config_content)?; info!("\nSaved optimal config to {}", config_path); Ok(()) } #[cfg(test)] mod tests { use super::*; #[test] fn test_volatility_calculation() { let prices = vec![100.0, 101.0, 99.5, 102.0, 101.5]; let vol = BarrierOptimizer::compute_daily_volatility(&prices); assert!(vol > 0.0); assert!(vol < 0.1); // Reasonable daily volatility } #[test] fn test_sharpe_calculation() { let returns = vec![0.01, -0.005, 0.02, 0.015, -0.01]; let sharpe = BarrierOptimizer::compute_sharpe(&returns); assert!(sharpe.is_finite()); } #[test] fn test_max_drawdown() { let returns = vec![0.1, 0.05, -0.2, -0.1, 0.15]; let dd = BarrierOptimizer::compute_max_drawdown(&returns); assert!(dd >= 0.0); assert!(dd <= 1.0); } }