use crate::backtesting::metrics::{
AdvancedRiskMetrics, GeneralPerformanceMetrics, MarketConditionMetrics, OptionsSpecificMetrics,
};
use crate::backtesting::types::{
CapitalUtilization, DrawdownAnalysis, TimeSeriesData, TradeRecord, TradeStatistics,
VolatilityData,
};
use crate::pnl::PnL;
use crate::risk::RiskMetricsSimulation;
use crate::simulation::ExitPolicy;
use chrono::{DateTime, Utc};
use pretty_simple_display::{DebugPretty, DisplaySimple};
use rust_decimal::Decimal;
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use utoipa::ToSchema;
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub struct BacktestResult {
pub general_performance: GeneralPerformanceMetrics,
pub options_metrics: OptionsSpecificMetrics,
pub trade_statistics: TradeStatistics,
pub drawdown_analysis: DrawdownAnalysis,
pub capital_utilization: CapitalUtilization,
pub time_series: TimeSeriesData,
pub trades: Vec<TradeRecord>,
pub market_conditions: Option<MarketConditionMetrics>,
pub volatility_data: Option<VolatilityData>,
pub risk_metrics: Option<AdvancedRiskMetrics>,
pub monte_carlo_simulation: Option<SimulationResult>,
pub strategy_name: String,
pub test_period_start: DateTime<Utc>,
pub test_period_end: DateTime<Utc>,
pub initial_capital: Decimal,
pub final_capital: Decimal,
pub custom_metrics: HashMap<String, Decimal>,
}
#[derive(DebugPretty, DisplaySimple, Clone, Serialize, Deserialize, Default, ToSchema)]
pub struct SimulationResult {
pub simulation_count: usize,
pub risk_metrics: Option<RiskMetricsSimulation>,
pub final_equity_percentiles: HashMap<u8, Decimal>,
pub max_premium: Decimal,
pub min_premium: Decimal,
pub avg_premium: Decimal,
pub hit_take_profit: bool,
pub hit_stop_loss: bool,
pub expired: bool,
pub expiration_premium: Option<Decimal>,
pub pnl: PnL,
pub holding_period: usize,
pub exit_reason: ExitPolicy,
}
#[derive(DebugPretty, DisplaySimple, Clone, Serialize, Deserialize, Default)]
pub struct SimulationStatsResult {
pub results: Vec<SimulationResult>,
pub total_simulations: usize,
pub profitable_count: usize,
pub loss_count: usize,
pub average_pnl: Decimal,
pub median_pnl: Decimal,
pub std_dev_pnl: Decimal,
pub best_pnl: Decimal,
pub worst_pnl: Decimal,
pub win_rate: Decimal,
pub average_holding_period: Decimal,
}
impl SimulationStatsResult {
pub fn print_summary(&self) {
use prettytable::{Cell, Row, Table, color, format};
use rust_decimal_macros::dec;
use tracing::info;
info!("\n========== SIMULATION SUMMARY ==========");
let mut general_table = Table::new();
general_table.set_format(*format::consts::FORMAT_BOX_CHARS);
general_table.set_titles(Row::new(vec![
Cell::new("Metric").with_style(prettytable::Attr::ForegroundColor(color::BLUE)),
Cell::new("Value").with_style(prettytable::Attr::ForegroundColor(color::BLUE)),
]));
general_table.add_row(Row::new(vec![
Cell::new("Total Simulations"),
Cell::new(&self.total_simulations.to_string()),
]));
general_table.printstd();
info!("\n--- Trade Outcomes ---");
let mut outcomes_table = Table::new();
outcomes_table.set_format(*format::consts::FORMAT_BOX_CHARS);
outcomes_table.set_titles(Row::new(vec![
Cell::new("Outcome").with_style(prettytable::Attr::ForegroundColor(color::BLUE)),
Cell::new("Count").with_style(prettytable::Attr::ForegroundColor(color::BLUE)),
Cell::new("Percentage").with_style(prettytable::Attr::ForegroundColor(color::BLUE)),
]));
let expired_count = self.results.iter().filter(|r| r.expired).count();
outcomes_table.add_row(Row::new(vec![
Cell::new("Profitable Trades"),
Cell::new(&self.profitable_count.to_string()),
Cell::new(&format!("{:.2}%", self.win_rate)),
]));
outcomes_table.add_row(Row::new(vec![
Cell::new("Loss Trades"),
Cell::new(&self.loss_count.to_string()),
Cell::new(&format!(
"{:.2}%",
(self.loss_count as f64 / self.total_simulations as f64) * 100.0
)),
]));
outcomes_table.add_row(Row::new(vec![
Cell::new("Expired Trades"),
Cell::new(&expired_count.to_string()),
Cell::new(&format!(
"{:.2}%",
(expired_count as f64 / self.total_simulations as f64) * 100.0
)),
]));
outcomes_table.printstd();
info!("\n--- Profit/Loss Statistics ---");
let mut pnl_table = Table::new();
pnl_table.set_format(*format::consts::FORMAT_BOX_CHARS);
pnl_table.set_titles(Row::new(vec![
Cell::new("Metric").with_style(prettytable::Attr::ForegroundColor(color::BLUE)),
Cell::new("Amount").with_style(prettytable::Attr::ForegroundColor(color::BLUE)),
]));
let total_pnl: Decimal = self.results.iter().filter_map(|r| r.pnl.total_pnl()).sum();
let color_pnl = |value: Decimal| -> Cell {
let text = format!("${:.2}", value);
if value < dec!(0.0) {
Cell::new(&text).with_style(prettytable::Attr::ForegroundColor(color::RED))
} else if value > dec!(0.0) {
Cell::new(&text).with_style(prettytable::Attr::ForegroundColor(color::GREEN))
} else {
Cell::new(&text)
}
};
pnl_table.add_row(Row::new(vec![Cell::new("Total P&L"), color_pnl(total_pnl)]));
pnl_table.add_row(Row::new(vec![
Cell::new("Average P&L per Trade"),
color_pnl(self.average_pnl),
]));
pnl_table.add_row(Row::new(vec![
Cell::new("Median P&L"),
color_pnl(self.median_pnl),
]));
pnl_table.add_row(Row::new(vec![
Cell::new("Std Dev P&L"),
Cell::new(&format!("${:.2}", self.std_dev_pnl)),
]));
pnl_table.add_row(Row::new(vec![
Cell::new("Maximum Profit"),
color_pnl(self.best_pnl),
]));
pnl_table.add_row(Row::new(vec![
Cell::new("Maximum Loss"),
color_pnl(self.worst_pnl),
]));
pnl_table.printstd();
info!("\n--- Holding Period ---");
let mut holding_table = Table::new();
holding_table.set_format(*format::consts::FORMAT_BOX_CHARS);
holding_table.set_titles(Row::new(vec![
Cell::new("Metric").with_style(prettytable::Attr::ForegroundColor(color::BLUE)),
Cell::new("Value").with_style(prettytable::Attr::ForegroundColor(color::BLUE)),
]));
holding_table.add_row(Row::new(vec![
Cell::new("Average Holding Period"),
Cell::new(&format!("{:.2} steps", self.average_holding_period)),
]));
holding_table.printstd();
info!("\n--- Exit Reasons ---");
let mut exit_reasons: HashMap<String, usize> = HashMap::new();
for result in &self.results {
*exit_reasons
.entry(result.exit_reason.to_string())
.or_insert(0) += 1;
}
let mut exit_table = Table::new();
exit_table.set_format(*format::consts::FORMAT_BOX_CHARS);
exit_table.set_titles(Row::new(vec![
Cell::new("Exit Reason").with_style(prettytable::Attr::ForegroundColor(color::BLUE)),
Cell::new("Count").with_style(prettytable::Attr::ForegroundColor(color::BLUE)),
Cell::new("Percentage").with_style(prettytable::Attr::ForegroundColor(color::BLUE)),
]));
for (reason, count) in exit_reasons.iter() {
exit_table.add_row(Row::new(vec![
Cell::new(reason),
Cell::new(&count.to_string()),
Cell::new(&format!(
"{:.2}%",
(*count as f64 / self.total_simulations as f64) * 100.0
)),
]));
}
exit_table.printstd();
}
pub fn print_individual_results(&self) {
use prettytable::{Cell, Row, Table, color, format};
use rust_decimal_macros::dec;
use tracing::info;
info!("\n========== INDIVIDUAL SIMULATION RESULTS ==========");
let mut table = Table::new();
table.set_format(*format::consts::FORMAT_BOX_CHARS);
table.set_titles(Row::new(vec![
Cell::new("Sim").with_style(prettytable::Attr::ForegroundColor(color::BLUE)),
Cell::new("Max\nPremium").with_style(prettytable::Attr::ForegroundColor(color::BLUE)),
Cell::new("Min\nPremium").with_style(prettytable::Attr::ForegroundColor(color::BLUE)),
Cell::new("Avg\nPremium").with_style(prettytable::Attr::ForegroundColor(color::BLUE)),
Cell::new("Final\nP&L").with_style(prettytable::Attr::ForegroundColor(color::BLUE)),
Cell::new("Holding\nPeriod")
.with_style(prettytable::Attr::ForegroundColor(color::BLUE)),
Cell::new("Exit\nReason").with_style(prettytable::Attr::ForegroundColor(color::BLUE)),
]));
for result in &self.results {
let pnl = result.pnl.total_pnl().unwrap_or_default();
let pnl_cell = if pnl < dec!(0.0) {
Cell::new(&format!("${:.2}", pnl))
.with_style(prettytable::Attr::ForegroundColor(color::RED))
} else if pnl > dec!(0.0) {
Cell::new(&format!("${:.2}", pnl))
.with_style(prettytable::Attr::ForegroundColor(color::GREEN))
} else {
Cell::new(&format!("${:.2}", pnl))
};
table.add_row(Row::new(vec![
Cell::new(&result.simulation_count.to_string()),
Cell::new(&format!("${:.2}", result.max_premium)),
Cell::new(&format!("${:.2}", result.min_premium)),
Cell::new(&format!("${:.2}", result.avg_premium)),
pnl_cell,
Cell::new(&result.holding_period.to_string()),
Cell::new(&result.exit_reason.to_string()),
]));
}
table.printstd();
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::pnl::PnL;
use positive::pos_or_panic;
use crate::simulation::ExitPolicy;
use rust_decimal_macros::dec;
fn create_test_simulation_result(
sim_count: usize,
pnl_value: Decimal,
holding_period: usize,
expired: bool,
) -> SimulationResult {
SimulationResult {
simulation_count: sim_count,
risk_metrics: None,
final_equity_percentiles: HashMap::new(),
max_premium: dec!(100.0),
min_premium: dec!(50.0),
avg_premium: dec!(75.0),
hit_take_profit: pnl_value > dec!(0.0),
hit_stop_loss: pnl_value < dec!(0.0),
expired,
expiration_premium: if expired { Some(dec!(50.0)) } else { None },
pnl: PnL::new(
Some(pnl_value),
None,
pos_or_panic!(10.0),
pos_or_panic!(5.0),
Utc::now(),
),
holding_period,
exit_reason: ExitPolicy::Expiration,
}
}
#[test]
fn test_simulation_stats_creation() {
let results = vec![
create_test_simulation_result(1, dec!(100.0), 10, false),
create_test_simulation_result(2, dec!(-50.0), 15, false),
create_test_simulation_result(3, dec!(75.0), 12, true),
];
let stats = SimulationStatsResult {
results: results.clone(),
total_simulations: 3,
profitable_count: 2,
loss_count: 1,
average_pnl: dec!(41.67),
median_pnl: dec!(75.0),
std_dev_pnl: dec!(62.92),
best_pnl: dec!(100.0),
worst_pnl: dec!(-50.0),
win_rate: dec!(66.67),
average_holding_period: dec!(12.33),
};
assert_eq!(stats.total_simulations, 3);
assert_eq!(stats.profitable_count, 2);
assert_eq!(stats.loss_count, 1);
assert_eq!(stats.results.len(), 3);
}
#[test]
fn test_simulation_stats_print_summary() {
let results = vec![
create_test_simulation_result(1, dec!(100.0), 10, false),
create_test_simulation_result(2, dec!(-50.0), 15, true),
];
let stats = SimulationStatsResult {
results,
total_simulations: 2,
profitable_count: 1,
loss_count: 1,
average_pnl: dec!(25.0),
median_pnl: dec!(25.0),
std_dev_pnl: dec!(75.0),
best_pnl: dec!(100.0),
worst_pnl: dec!(-50.0),
win_rate: dec!(50.0),
average_holding_period: dec!(12.5),
};
stats.print_summary();
}
#[test]
fn test_simulation_stats_print_individual_results() {
let results = vec![
create_test_simulation_result(1, dec!(100.0), 10, false),
create_test_simulation_result(2, dec!(-50.0), 15, false),
create_test_simulation_result(3, dec!(75.0), 12, true),
];
let stats = SimulationStatsResult {
results,
total_simulations: 3,
profitable_count: 2,
loss_count: 1,
average_pnl: dec!(41.67),
median_pnl: dec!(75.0),
std_dev_pnl: dec!(62.92),
best_pnl: dec!(100.0),
worst_pnl: dec!(-50.0),
win_rate: dec!(66.67),
average_holding_period: dec!(12.33),
};
stats.print_individual_results();
}
#[test]
fn test_simulation_stats_empty_results() {
let stats = SimulationStatsResult {
results: vec![],
total_simulations: 0,
profitable_count: 0,
loss_count: 0,
average_pnl: dec!(0.0),
median_pnl: dec!(0.0),
std_dev_pnl: dec!(0.0),
best_pnl: dec!(0.0),
worst_pnl: dec!(0.0),
win_rate: dec!(0.0),
average_holding_period: dec!(0.0),
};
stats.print_summary();
stats.print_individual_results();
}
#[test]
fn test_simulation_stats_all_profitable() {
let results = vec![
create_test_simulation_result(1, dec!(100.0), 10, false),
create_test_simulation_result(2, dec!(50.0), 15, false),
create_test_simulation_result(3, dec!(75.0), 12, false),
];
let stats = SimulationStatsResult {
results,
total_simulations: 3,
profitable_count: 3,
loss_count: 0,
average_pnl: dec!(75.0),
median_pnl: dec!(75.0),
std_dev_pnl: dec!(20.41),
best_pnl: dec!(100.0),
worst_pnl: dec!(50.0),
win_rate: dec!(100.0),
average_holding_period: dec!(12.33),
};
stats.print_summary();
assert_eq!(stats.win_rate, dec!(100.0));
}
#[test]
fn test_simulation_stats_all_losses() {
let results = vec![
create_test_simulation_result(1, dec!(-100.0), 10, false),
create_test_simulation_result(2, dec!(-50.0), 15, false),
];
let stats = SimulationStatsResult {
results,
total_simulations: 2,
profitable_count: 0,
loss_count: 2,
average_pnl: dec!(-75.0),
median_pnl: dec!(-75.0),
std_dev_pnl: dec!(25.0),
best_pnl: dec!(-50.0),
worst_pnl: dec!(-100.0),
win_rate: dec!(0.0),
average_holding_period: dec!(12.5),
};
stats.print_summary();
assert_eq!(stats.win_rate, dec!(0.0));
}
}