Skip to main content

optionstratlib_backtest/backtesting/
results.rs

1/******************************************************************************
2   Author: Joaquín Béjar García
3   Email: jb@taunais.com
4   Date: 26/4/25
5******************************************************************************/
6use crate::backtesting::metrics::{
7    AdvancedRiskMetrics, GeneralPerformanceMetrics, MarketConditionMetrics, OptionsSpecificMetrics,
8};
9use crate::backtesting::types::{
10    CapitalUtilization, DrawdownAnalysis, TimeSeriesData, TradeRecord, TradeStatistics,
11    VolatilityData,
12};
13use chrono::{DateTime, Utc};
14use optionstratlib_analytics::pnl::PnL;
15use optionstratlib_analytics::risk::RiskMetricsSimulation;
16use optionstratlib_core::{impl_json_debug_pretty, impl_json_display};
17use optionstratlib_simulation::simulation::ExitPolicy;
18use rust_decimal::Decimal;
19use serde::{Deserialize, Serialize};
20use std::collections::HashMap;
21#[cfg(feature = "schema")]
22use utoipa::ToSchema;
23
24/// Comprehensive container for all results generated during a backtest simulation.
25///
26/// This struct aggregates various performance metrics, time-series data, trade details,
27/// risk analysis, and context related to a specific trading strategy simulation.
28#[derive(Debug, Clone, Serialize, Deserialize, Default)]
29pub struct BacktestResult {
30    /// Core performance metrics applicable to most trading strategies.
31    /// Includes returns, risk measures (like volatility), risk-adjusted returns (like Sharpe),
32    /// and win/loss statistics.
33    pub general_performance: GeneralPerformanceMetrics,
34    /// Metrics specifically relevant to options trading strategies.
35    /// Includes metrics like return on margin, premium capture, and average Greek exposures.
36    pub options_metrics: OptionsSpecificMetrics,
37    /// Statistical analysis of the trades executed during the backtest.
38    /// Contains counts, averages, and distributions related to trade outcomes.
39    pub trade_statistics: TradeStatistics,
40    /// Analysis of the portfolio's value declines (drawdowns) from peak equity.
41    /// Includes metrics like maximum drawdown and longest drawdown period.
42    pub drawdown_analysis: DrawdownAnalysis,
43    /// Metrics detailing how capital was deployed and managed throughout the backtest.
44    /// Includes average/max capital usage, margin metrics, and premium flows.
45    pub capital_utilization: CapitalUtilization,
46    /// Time-series data tracking the evolution of key portfolio metrics over the backtest period.
47    /// Includes equity curve, drawdown curve, margin usage, position counts, and potentially Greek exposures.
48    pub time_series: TimeSeriesData,
49    /// A detailed record of each individual trade executed during the backtest simulation.
50    pub trades: Vec<TradeRecord>,
51    /// Optional metrics describing the market environment during the backtest period.
52    /// Can include classifications like bull/bear market days or high/low volatility periods.
53    pub market_conditions: Option<MarketConditionMetrics>,
54    /// Optional data related to implied volatility observed during the backtest, often at trade execution times.
55    /// Includes metrics like average IV traded or IV rank/percentile.
56    pub volatility_data: Option<VolatilityData>,
57    /// Optional advanced risk analytics beyond standard deviation.
58    /// May include metrics like Value at Risk (VaR), Expected Shortfall (ES), or Ulcer Index.
59    pub risk_metrics: Option<AdvancedRiskMetrics>,
60    /// Optional results from Monte Carlo simulations performed on the strategy's outcomes.
61    /// Provides probabilistic insights into future performance potential and risk.
62    pub monte_carlo_simulation: Option<SimulationResult>,
63    /// The name identifier of the trading strategy being backtested.
64    pub strategy_name: String,
65    /// The starting date and time of the backtest period.
66    pub test_period_start: DateTime<Utc>,
67    /// The ending date and time of the backtest period.
68    pub test_period_end: DateTime<Utc>,
69    /// The initial amount of capital allocated at the beginning of the backtest.
70    pub initial_capital: Decimal,
71    /// The final amount of capital remaining at the end of the backtest.
72    pub final_capital: Decimal,
73    /// A flexible map allowing storage of custom, strategy-specific metrics.
74    /// Keys are metric names (strings), and values are the calculated metric values (Decimals).
75    pub custom_metrics: HashMap<String, Decimal>,
76}
77
78/// `SimulationResult` represents the outcome of a financial or trading simulation,
79/// capturing the details of the simulation's performance and metrics.
80///
81/// # Fields
82///
83/// * `simulation_count` - The total number of simulation runs performed.
84/// * `risk_metrics` - Optional risk metrics derived from the simulations.
85/// * `final_equity_percentiles` - A map containing the percentiles of the final equity
86///   distribution. The keys represent the percentile (e.g., 5, 50, 95), and the values
87///   are the corresponding equity values.
88/// * `max_premium` - The maximum premium value observed during the simulation.
89/// * `min_premium` - The minimum premium value observed during the simulation.
90/// * `avg_premium` - The average premium value observed during the simulation.
91/// * `hit_take_profit` - A boolean indicating if the take profit target was achieved,
92///   defined as a 50% reduction in premium.
93/// * `hit_stop_loss` - A boolean indicating if the stop loss condition was triggered,
94///   defined as a 100% increase in premium.
95/// * `expired` - A boolean indicating if the option expired without hitting the take
96///   profit or stop loss conditions.
97/// * `expiration_premium` - Optional final premium value at expiration. Only available
98///   if the option expired.
99/// * `pnl` - The final profit or loss (P&L) resulting from the simulation.
100/// * `holding_period` - The number of simulation steps during which the asset was held.
101/// * `exit_reason` - The reason or policy that triggered the exit from the position
102///   (e.g., take profit, stop loss, expiration).
103///
104/// # Notes
105///
106/// `SimulationResult` is a serializable and cloneable structure, making it convenient
107/// for storing, displaying, and transmitting simulation outcomes. It also provides
108/// a user-friendly debug and display interface through derived traits.
109#[derive(Clone, Serialize, Deserialize, Default)]
110#[cfg_attr(feature = "schema", derive(ToSchema))]
111pub struct SimulationResult {
112    /// Number of simulation runs
113    pub simulation_count: usize,
114
115    /// Risk metrics calculated from simulations
116    pub risk_metrics: Option<RiskMetricsSimulation>,
117
118    /// Percentiles of final equity distribution
119    pub final_equity_percentiles: HashMap<u8, Decimal>,
120
121    /// Maximum premium value during the simulation
122    pub max_premium: Decimal,
123
124    /// Minimum premium value during the simulation
125    pub min_premium: Decimal,
126
127    /// Average premium value during the simulation
128    pub avg_premium: Decimal,
129
130    /// Whether the exit policy's take-profit condition closed the run
131    pub hit_take_profit: bool,
132
133    /// Whether the exit policy's stop-loss condition closed the run
134    pub hit_stop_loss: bool,
135
136    /// Whether the option expired without hitting take profit or stop loss
137    pub expired: bool,
138
139    /// Final premium value at expiration (only if expired)
140    pub expiration_premium: Option<Decimal>,
141
142    /// Final P&L
143    pub pnl: PnL,
144
145    /// Holding period in steps
146    pub holding_period: usize,
147
148    /// Exit policy that triggered the exit
149    pub exit_reason: ExitPolicy,
150}
151
152impl_json_debug_pretty!(SimulationResult);
153impl_json_display!(SimulationResult);
154
155/// Statistics aggregated from multiple simulation runs.
156///
157/// This struct contains a vector of individual simulation results along with
158/// aggregate statistics computed across all simulations.
159#[derive(Clone, Serialize, Deserialize, Default)]
160pub struct SimulationStatsResult {
161    /// Individual results from each simulation run
162    pub results: Vec<SimulationResult>,
163
164    /// Total number of simulations performed
165    pub total_simulations: usize,
166
167    /// Number of profitable simulations
168    pub profitable_count: usize,
169
170    /// Number of loss-making simulations
171    pub loss_count: usize,
172
173    /// Average P&L across all simulations
174    pub average_pnl: Decimal,
175
176    /// Median P&L across all simulations
177    pub median_pnl: Decimal,
178
179    /// Standard deviation of P&L
180    pub std_dev_pnl: Decimal,
181
182    /// Best (maximum) P&L achieved
183    pub best_pnl: Decimal,
184
185    /// Worst (minimum) P&L achieved
186    pub worst_pnl: Decimal,
187
188    /// Win rate (percentage of profitable simulations)
189    pub win_rate: Decimal,
190
191    /// Average holding period across all simulations
192    pub average_holding_period: Decimal,
193}
194
195impl_json_debug_pretty!(SimulationStatsResult);
196impl_json_display!(SimulationStatsResult);
197
198#[cfg(test)]
199mod tests {
200    use super::*;
201    use optionstratlib_analytics::pnl::PnL;
202    use optionstratlib_core::pos_or_panic;
203
204    use optionstratlib_simulation::simulation::ExitPolicy;
205    use rust_decimal_macros::dec;
206
207    fn create_test_simulation_result(
208        sim_count: usize,
209        pnl_value: Decimal,
210        holding_period: usize,
211        expired: bool,
212    ) -> SimulationResult {
213        SimulationResult {
214            simulation_count: sim_count,
215            risk_metrics: None,
216            final_equity_percentiles: HashMap::new(),
217            max_premium: dec!(100.0),
218            min_premium: dec!(50.0),
219            avg_premium: dec!(75.0),
220            hit_take_profit: pnl_value > dec!(0.0),
221            hit_stop_loss: pnl_value < dec!(0.0),
222            expired,
223            expiration_premium: if expired { Some(dec!(50.0)) } else { None },
224            pnl: PnL::new(
225                Some(pnl_value),
226                None,
227                pos_or_panic!(10.0),
228                pos_or_panic!(5.0),
229                Utc::now(),
230            ),
231            holding_period,
232            exit_reason: ExitPolicy::Expiration,
233        }
234    }
235
236    #[test]
237    fn test_simulation_stats_creation() {
238        let results = vec![
239            create_test_simulation_result(1, dec!(100.0), 10, false),
240            create_test_simulation_result(2, dec!(-50.0), 15, false),
241            create_test_simulation_result(3, dec!(75.0), 12, true),
242        ];
243
244        let stats = SimulationStatsResult {
245            results: results.clone(),
246            total_simulations: 3,
247            profitable_count: 2,
248            loss_count: 1,
249            average_pnl: dec!(41.67),
250            median_pnl: dec!(75.0),
251            std_dev_pnl: dec!(62.92),
252            best_pnl: dec!(100.0),
253            worst_pnl: dec!(-50.0),
254            win_rate: dec!(66.67),
255            average_holding_period: dec!(12.33),
256        };
257
258        assert_eq!(stats.total_simulations, 3);
259        assert_eq!(stats.profitable_count, 2);
260        assert_eq!(stats.loss_count, 1);
261        assert_eq!(stats.results.len(), 3);
262    }
263
264    #[test]
265    fn test_simulation_stats_all_profitable() {
266        let results = vec![
267            create_test_simulation_result(1, dec!(100.0), 10, false),
268            create_test_simulation_result(2, dec!(50.0), 15, false),
269            create_test_simulation_result(3, dec!(75.0), 12, false),
270        ];
271
272        let stats = SimulationStatsResult {
273            results,
274            total_simulations: 3,
275            profitable_count: 3,
276            loss_count: 0,
277            average_pnl: dec!(75.0),
278            median_pnl: dec!(75.0),
279            std_dev_pnl: dec!(20.41),
280            best_pnl: dec!(100.0),
281            worst_pnl: dec!(50.0),
282            win_rate: dec!(100.0),
283            average_holding_period: dec!(12.33),
284        };
285
286        assert_eq!(stats.win_rate, dec!(100.0));
287    }
288
289    #[test]
290    fn test_simulation_stats_all_losses() {
291        let results = vec![
292            create_test_simulation_result(1, dec!(-100.0), 10, false),
293            create_test_simulation_result(2, dec!(-50.0), 15, false),
294        ];
295
296        let stats = SimulationStatsResult {
297            results,
298            total_simulations: 2,
299            profitable_count: 0,
300            loss_count: 2,
301            average_pnl: dec!(-75.0),
302            median_pnl: dec!(-75.0),
303            std_dev_pnl: dec!(25.0),
304            best_pnl: dec!(-50.0),
305            worst_pnl: dec!(-100.0),
306            win_rate: dec!(0.0),
307            average_holding_period: dec!(12.5),
308        };
309
310        assert_eq!(stats.win_rate, dec!(0.0));
311    }
312}