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}