use crate::backtesting::metrics::{
AdvancedRiskMetrics, GeneralPerformanceMetrics, MarketConditionMetrics, OptionsSpecificMetrics,
};
use crate::backtesting::types::{
CapitalUtilization, DrawdownAnalysis, TimeSeriesData, TradeRecord, TradeStatistics,
VolatilityData,
};
use chrono::{DateTime, Utc};
use optionstratlib_analytics::pnl::PnL;
use optionstratlib_analytics::risk::RiskMetricsSimulation;
use optionstratlib_core::{impl_json_debug_pretty, impl_json_display};
use optionstratlib_simulation::simulation::ExitPolicy;
use rust_decimal::Decimal;
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
#[cfg(feature = "schema")]
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(Clone, Serialize, Deserialize, Default)]
#[cfg_attr(feature = "schema", derive(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,
}
impl_json_debug_pretty!(SimulationResult);
impl_json_display!(SimulationResult);
#[derive(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_json_debug_pretty!(SimulationStatsResult);
impl_json_display!(SimulationStatsResult);
#[cfg(test)]
mod tests {
use super::*;
use optionstratlib_analytics::pnl::PnL;
use optionstratlib_core::pos_or_panic;
use optionstratlib_simulation::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_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),
};
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),
};
assert_eq!(stats.win_rate, dec!(0.0));
}
}