//! Backtesting Dashboard - Strategy Testing and Historical Analysis //! //! This dashboard provides comprehensive backtesting functionality including: //! - Active backtest monitoring with real-time progress //! - Historical backtest results and performance analysis //! - Strategy configuration and parameter management //! - Performance metrics visualization (returns, Sharpe ratio, drawdown) //! - Trade execution analysis and order flow use super::{Dashboard, DashboardEvent}; use anyhow::Result; use crossterm::event::KeyEvent; use ratatui::{ layout::{Constraint, Direction, Layout, Rect}, style::{Color, Modifier, Style}, widgets::{Block, Borders, Cell, List, ListItem, ListState, Paragraph, Row, Table}, Frame, }; use std::collections::HashMap; use tokio::sync::mpsc; /// Backtesting Dashboard for strategy testing and historical analysis pub struct BacktestingDashboard { /// Event sender for dashboard communications event_sender: mpsc::Sender, /// Active backtest status active_backtests: Vec, /// Historical backtest results historical_results: Vec, /// Selected backtest in the list selected_backtest: ListState, /// Current view mode view_mode: BacktestViewMode, /// Performance metrics cache metrics_cache: HashMap, /// Redraw flag needs_redraw: bool, } /// View modes for the backtesting dashboard #[derive(Debug, Clone, Copy, PartialEq)] pub enum BacktestViewMode { /// Show active running backtests ActiveBacktests, /// Show historical backtest results HistoricalResults, /// Show detailed performance analysis PerformanceAnalysis, /// Show strategy configuration StrategyConfig, } /// Active backtest entry #[derive(Debug, Clone)] pub struct BacktestEntry { /// Backtest ID pub id: String, /// Strategy name pub strategy: String, /// Symbols being tested pub symbols: Vec, /// Progress percentage pub progress: f64, /// Current status pub status: String, /// Start time pub started_at: String, /// Estimated completion time pub eta: Option, /// Current PnL pub current_pnl: f64, /// Trade count pub trade_count: u64, } /// Historical backtest result #[derive(Debug, Clone)] pub struct BacktestResult { /// Backtest ID pub id: String, /// Strategy name pub strategy: String, /// Symbols tested pub symbols: Vec, /// Test period pub period: String, /// Final return pub total_return: f64, /// Sharpe ratio pub sharpe_ratio: f64, /// Maximum drawdown pub max_drawdown: f64, /// Win rate pub win_rate: f64, /// Total trades pub total_trades: u64, /// Completion date pub completed_at: String, } /// Performance metrics for detailed analysis #[derive(Debug, Clone)] pub struct PerformanceMetrics { /// Daily returns pub daily_returns: Vec, /// Cumulative returns pub cumulative_returns: Vec, /// Rolling Sharpe ratio pub rolling_sharpe: Vec, /// Drawdown series pub drawdown_series: Vec, /// Trade analysis pub trade_metrics: TradeMetrics, } /// Trade execution metrics #[derive(Debug, Clone)] pub struct TradeMetrics { /// Average trade duration (hours) pub avg_duration: f64, /// Average win amount pub avg_win: f64, /// Average loss amount pub avg_loss: f64, /// Profit factor pub profit_factor: f64, /// Maximum consecutive wins pub max_consecutive_wins: u32, /// Maximum consecutive losses pub max_consecutive_losses: u32, } impl BacktestingDashboard { /// Create a new backtesting dashboard pub fn new(event_sender: mpsc::Sender) -> Self { let mut dashboard = Self { event_sender, active_backtests: Vec::new(), historical_results: Vec::new(), selected_backtest: ListState::default(), view_mode: BacktestViewMode::ActiveBacktests, metrics_cache: HashMap::new(), needs_redraw: true, }; // Initialize with sample data dashboard.load_sample_data(); dashboard } /// Load sample data for demonstration fn load_sample_data(&mut self) { // Sample active backtests self.active_backtests = vec![ BacktestEntry { id: "bt_001".to_string(), strategy: "MeanReversion_v2.1".to_string(), symbols: vec!["SPY".to_string(), "QQQ".to_string()], progress: 73.5, status: "Running".to_string(), started_at: "2025-01-23 14:30:15".to_string(), eta: Some("2025-01-23 16:45:00".to_string()), current_pnl: 12_450.75, trade_count: 127, }, BacktestEntry { id: "bt_002".to_string(), strategy: "Momentum_ML_v1.3".to_string(), symbols: vec!["AAPL".to_string(), "MSFT".to_string(), "GOOGL".to_string()], progress: 28.2, status: "Running".to_string(), started_at: "2025-01-23 15:15:30".to_string(), eta: Some("2025-01-23 18:20:00".to_string()), current_pnl: -2_100.25, trade_count: 43, }, ]; // Sample historical results self.historical_results = vec![ BacktestResult { id: "bt_hist_001".to_string(), strategy: "MeanReversion_v2.0".to_string(), symbols: vec!["SPY".to_string(), "QQQ".to_string(), "IWM".to_string()], period: "2024-01-01 to 2024-12-31".to_string(), total_return: 18.75, sharpe_ratio: 1.42, max_drawdown: -8.3, win_rate: 64.2, total_trades: 284, completed_at: "2025-01-22 18:45:12".to_string(), }, BacktestResult { id: "bt_hist_002".to_string(), strategy: "Arbitrage_v3.1".to_string(), symbols: vec!["AAPL".to_string(), "MSFT".to_string()], period: "2024-06-01 to 2024-12-31".to_string(), total_return: 12.34, sharpe_ratio: 2.18, max_drawdown: -3.7, win_rate: 71.8, total_trades: 156, completed_at: "2025-01-21 22:15:45".to_string(), }, BacktestResult { id: "bt_hist_003".to_string(), strategy: "Momentum_ML_v1.2".to_string(), symbols: vec!["QQQ".to_string(), "XLK".to_string(), "TQQQ".to_string()], period: "2024-03-01 to 2024-09-30".to_string(), total_return: 24.67, sharpe_ratio: 1.89, max_drawdown: -12.1, win_rate: 58.9, total_trades: 412, completed_at: "2025-01-20 16:30:22".to_string(), }, ]; // Select first item by default self.selected_backtest.select(Some(0)); } /// Render active backtests view fn render_active_backtests(&mut self, frame: &mut Frame, area: Rect) -> Result<()> { let chunks = Layout::default() .direction(Direction::Vertical) .constraints([Constraint::Length(3), Constraint::Min(10)].as_ref()) .split(area); // Header with summary let summary = format!( "Active Backtests: {} | Total Progress: {:.1}%", self.active_backtests.len(), self.active_backtests .iter() .map(|bt| bt.progress) .sum::() / self.active_backtests.len() as f64 ); let header = Paragraph::new(summary).block( Block::default() .borders(Borders::ALL) .title("Active Backtests Overview") .style(Style::default().fg(Color::Green)), ); frame.render_widget(header, chunks[0]); // Active backtests list with progress bars let items: Vec = self .active_backtests .iter() .map(|bt| { let pnl_color = if bt.current_pnl >= 0.0 { Color::Green } else { Color::Red }; let content = format!( "{} | {} | {:.1}% | PnL: ${:.2} | Trades: {}", bt.strategy, bt.symbols.join(","), bt.progress, bt.current_pnl, bt.trade_count ); ListItem::new(content).style(Style::default().fg(pnl_color)) }) .collect(); let list = List::new(items) .block( Block::default() .borders(Borders::ALL) .title("Running Backtests (↑↓ to navigate, Enter for details)") .style(Style::default().fg(Color::White)), ) .highlight_style( Style::default() .fg(Color::Yellow) .add_modifier(Modifier::BOLD), ) .highlight_symbol("► "); frame.render_stateful_widget(list, chunks[1], &mut self.selected_backtest); Ok(()) } /// Render historical results view fn render_historical_results(&mut self, frame: &mut Frame, area: Rect) -> Result<()> { let chunks = Layout::default() .direction(Direction::Vertical) .constraints([Constraint::Length(3), Constraint::Min(10)].as_ref()) .split(area); // Summary stats let avg_return = self .historical_results .iter() .map(|r| r.total_return) .sum::() / self.historical_results.len() as f64; let avg_sharpe = self .historical_results .iter() .map(|r| r.sharpe_ratio) .sum::() / self.historical_results.len() as f64; let summary = format!( "Historical Results: {} | Avg Return: {:.2}% | Avg Sharpe: {:.2}", self.historical_results.len(), avg_return, avg_sharpe ); let header = Paragraph::new(summary).block( Block::default() .borders(Borders::ALL) .title("Historical Performance Summary") .style(Style::default().fg(Color::Cyan)), ); frame.render_widget(header, chunks[0]); // Results table let headers = vec![ "Strategy", "Period", "Return%", "Sharpe", "MaxDD%", "WinRate%", "Trades", ]; let header_cells = headers .iter() .map(|h| Cell::from(*h).style(Style::default().fg(Color::Yellow))); let header_row = Row::new(header_cells).style(Style::default().bg(Color::DarkGray)); let rows: Vec = self .historical_results .iter() .map(|result| { let return_color = if result.total_return >= 0.0 { Color::Green } else { Color::Red }; Row::new(vec![ Cell::from(result.strategy.as_str()), Cell::from(result.period.as_str()), Cell::from(format!("{:.2}", result.total_return)) .style(Style::default().fg(return_color)), Cell::from(format!("{:.2}", result.sharpe_ratio)), Cell::from(format!("{:.1}", result.max_drawdown)) .style(Style::default().fg(Color::Red)), Cell::from(format!("{:.1}", result.win_rate)), Cell::from(format!("{}", result.total_trades)), ]) }) .collect(); let table = Table::new( rows, [ Constraint::Length(18), // Strategy Constraint::Length(22), // Period Constraint::Length(8), // Return% Constraint::Length(7), // Sharpe Constraint::Length(8), // MaxDD% Constraint::Length(9), // WinRate% Constraint::Length(7), // Trades ], ) .header(header_row) .block( Block::default() .borders(Borders::ALL) .title("Historical Backtest Results") .style(Style::default().fg(Color::White)), ) .column_spacing(1); frame.render_widget(table, chunks[1]); Ok(()) } /// Switch to next view mode fn next_view_mode(&mut self) { self.view_mode = match self.view_mode { BacktestViewMode::ActiveBacktests => BacktestViewMode::HistoricalResults, BacktestViewMode::HistoricalResults => BacktestViewMode::PerformanceAnalysis, BacktestViewMode::PerformanceAnalysis => BacktestViewMode::StrategyConfig, BacktestViewMode::StrategyConfig => BacktestViewMode::ActiveBacktests, }; self.needs_redraw = true; } /// Switch to previous view mode fn previous_view_mode(&mut self) { self.view_mode = match self.view_mode { BacktestViewMode::ActiveBacktests => BacktestViewMode::StrategyConfig, BacktestViewMode::HistoricalResults => BacktestViewMode::ActiveBacktests, BacktestViewMode::PerformanceAnalysis => BacktestViewMode::HistoricalResults, BacktestViewMode::StrategyConfig => BacktestViewMode::PerformanceAnalysis, }; self.needs_redraw = true; } } impl Dashboard for BacktestingDashboard { fn render(&mut self, frame: &mut Frame, area: Rect) -> Result<()> { // Create main layout with tabs let chunks = Layout::default() .direction(Direction::Vertical) .constraints([Constraint::Length(3), Constraint::Min(10)].as_ref()) .split(area); // Render view mode tabs let tab_title = match self.view_mode { BacktestViewMode::ActiveBacktests => "Active Backtests [Tab: Historical]", BacktestViewMode::HistoricalResults => "Historical Results [Tab: Performance]", BacktestViewMode::PerformanceAnalysis => "Performance Analysis [Tab: Strategy Config]", BacktestViewMode::StrategyConfig => "Strategy Configuration [Tab: Active]", }; let tab_block = Block::default() .borders(Borders::ALL) .title(format!("Backtesting Dashboard - {}", tab_title)) .style(Style::default().fg(Color::Magenta)); frame.render_widget(tab_block, chunks[0]); // Render current view match self.view_mode { BacktestViewMode::ActiveBacktests => self.render_active_backtests(frame, chunks[1])?, BacktestViewMode::HistoricalResults => { self.render_historical_results(frame, chunks[1])? } BacktestViewMode::PerformanceAnalysis => { // Placeholder for performance analysis view let content = Paragraph::new( "Performance Analysis View\n\n\ • Cumulative returns chart\n\ • Rolling Sharpe ratio\n\ • Drawdown analysis\n\ • Trade distribution metrics\n\ • Risk-adjusted returns\n\n\ [Implementation in progress...]", ) .block( Block::default() .borders(Borders::ALL) .title("Performance Analysis") .style(Style::default().fg(Color::Green)), ); frame.render_widget(content, chunks[1]); } BacktestViewMode::StrategyConfig => { // Placeholder for strategy configuration view let content = Paragraph::new( "Strategy Configuration View\n\n\ • Parameter settings\n\ • Optimization ranges\n\ • Risk constraints\n\ • Market data settings\n\ • Execution parameters\n\n\ [Implementation in progress...]", ) .block( Block::default() .borders(Borders::ALL) .title("Strategy Configuration") .style(Style::default().fg(Color::Yellow)), ); frame.render_widget(content, chunks[1]); } } self.needs_redraw = false; Ok(()) } fn handle_input(&mut self, key: KeyEvent) -> Result> { use crossterm::event::KeyCode; match key.code { KeyCode::Tab => { self.next_view_mode(); Ok(None) } KeyCode::BackTab => { self.previous_view_mode(); Ok(None) } KeyCode::Up => { if let Some(selected) = self.selected_backtest.selected() { let max_items = match self.view_mode { BacktestViewMode::ActiveBacktests => self.active_backtests.len(), BacktestViewMode::HistoricalResults => self.historical_results.len(), _ => 0, }; if max_items > 0 { let next = if selected > 0 { selected - 1 } else { max_items - 1 }; self.selected_backtest.select(Some(next)); self.needs_redraw = true; } } Ok(None) } KeyCode::Down => { let max_items = match self.view_mode { BacktestViewMode::ActiveBacktests => self.active_backtests.len(), BacktestViewMode::HistoricalResults => self.historical_results.len(), _ => 0, }; if max_items > 0 { let selected = self.selected_backtest.selected().unwrap_or(0); let next = if selected >= max_items - 1 { 0 } else { selected + 1 }; self.selected_backtest.select(Some(next)); self.needs_redraw = true; } Ok(None) } KeyCode::Enter => { // Handle selection - would show details or start actions self.needs_redraw = true; Ok(None) } KeyCode::Char('r') => { // Refresh data self.load_sample_data(); self.needs_redraw = true; Ok(None) } _ => Ok(None), } } fn update(&mut self, _event: DashboardEvent) -> Result<()> { // Handle backtest-related events self.needs_redraw = true; Ok(()) } fn title(&self) -> &str { "Backtesting" } fn shortcut_key(&self) -> char { 'b' } fn needs_redraw(&self) -> bool { self.needs_redraw } fn mark_drawn(&mut self) { self.needs_redraw = false; } }