optionstratlib_backtest/backtesting/
adapters.rs1use crate::backtesting::results::{SimulationResult, SimulationStatsResult};
13use chrono::Utc;
14use optionstratlib_analytics::pnl::PnL;
15use optionstratlib_core::model::Positive;
16use optionstratlib_simulation::error::SimulationError;
17use optionstratlib_simulation::simulation::{PathOutcome, PathStatistics};
18use std::collections::HashMap;
19
20impl From<PathOutcome> for SimulationResult {
21 fn from(outcome: PathOutcome) -> Self {
28 Self {
29 simulation_count: 1,
30 risk_metrics: None,
31 final_equity_percentiles: HashMap::new(),
32 max_premium: outcome.max_premium,
33 min_premium: outcome.min_premium,
34 avg_premium: outcome.avg_premium,
35 hit_take_profit: outcome.hit_take_profit,
36 hit_stop_loss: outcome.hit_stop_loss,
37 expired: outcome.expired,
38 expiration_premium: outcome.expiration_premium,
39 pnl: PnL {
40 realized: outcome.pnl,
41 unrealized: None,
42 initial_costs: Positive::ZERO,
43 initial_income: Positive::ZERO,
44 date_time: Utc::now(),
45 },
46 holding_period: outcome.holding_period,
47 exit_reason: outcome.exit_reason,
48 }
49 }
50}
51
52impl From<&SimulationResult> for PathOutcome {
53 fn from(result: &SimulationResult) -> Self {
58 Self {
59 pnl: result.pnl.total_pnl(),
60 holding_period: result.holding_period,
61 exit_reason: result.exit_reason.clone(),
62 hit_take_profit: result.hit_take_profit,
63 hit_stop_loss: result.hit_stop_loss,
64 expired: result.expired,
65 max_premium: result.max_premium,
66 min_premium: result.min_premium,
67 avg_premium: result.avg_premium,
68 expiration_premium: result.expiration_premium,
69 }
70 }
71}
72
73impl SimulationStatsResult {
74 #[must_use = "the aggregate is the only product of this call"]
84 pub fn from_results(results: Vec<SimulationResult>) -> Result<Self, SimulationError> {
85 let outcomes: Vec<PathOutcome> = results.iter().map(PathOutcome::from).collect();
86 let stats = PathStatistics::from_outcomes(&outcomes)?;
87 Ok(Self::from_parts(results, stats))
88 }
89
90 #[must_use = "the aggregate is the only product of this call"]
101 pub fn from_outcomes(outcomes: Vec<PathOutcome>) -> Result<Self, SimulationError> {
102 let stats = PathStatistics::from_outcomes(&outcomes)?;
103 let results = outcomes.into_iter().map(SimulationResult::from).collect();
104 Ok(Self::from_parts(results, stats))
105 }
106
107 #[must_use]
109 fn from_parts(results: Vec<SimulationResult>, stats: PathStatistics) -> Self {
110 Self {
111 results,
112 total_simulations: stats.total_paths,
113 profitable_count: stats.profitable_count,
114 loss_count: stats.loss_count,
115 average_pnl: stats.average_pnl,
116 median_pnl: stats.median_pnl,
117 std_dev_pnl: stats.std_dev_pnl,
118 best_pnl: stats.best_pnl,
119 worst_pnl: stats.worst_pnl,
120 win_rate: stats.win_rate,
121 average_holding_period: stats.average_holding_period,
122 }
123 }
124}
125
126#[cfg(test)]
127mod tests {
128 use super::*;
129 use optionstratlib_simulation::simulation::ExitPolicy;
130 use rust_decimal::Decimal;
131 use rust_decimal_macros::dec;
132
133 fn outcome(pnl: Option<Decimal>, holding_period: usize) -> PathOutcome {
134 PathOutcome {
135 pnl,
136 holding_period,
137 exit_reason: ExitPolicy::ProfitPercent(dec!(0.5)),
138 hit_take_profit: pnl.is_some_and(|p| p > Decimal::ZERO),
139 hit_stop_loss: pnl.is_some_and(|p| p < Decimal::ZERO),
140 expired: false,
141 max_premium: dec!(9.0),
142 min_premium: dec!(4.0),
143 avg_premium: dec!(6.0),
144 expiration_premium: Some(dec!(4.5)),
145 }
146 }
147
148 #[test]
149 fn test_path_outcome_round_trips_through_simulation_result() {
150 let original = outcome(Some(dec!(-12.0)), 7);
151 let result = SimulationResult::from(original.clone());
152 assert_eq!(result.simulation_count, 1);
153 assert!(result.risk_metrics.is_none());
154 assert!(result.final_equity_percentiles.is_empty());
155 assert_eq!(result.pnl.realized, Some(dec!(-12.0)));
156 assert_eq!(result.pnl.unrealized, None);
157 assert_eq!(result.pnl.initial_costs, Positive::ZERO);
158 assert_eq!(result.pnl.initial_income, Positive::ZERO);
159 assert_eq!(PathOutcome::from(&result), original);
160 }
161
162 #[test]
163 fn test_path_outcome_from_result_uses_total_pnl() {
164 let mut result = SimulationResult::from(outcome(Some(dec!(10.0)), 1));
165 result.pnl.unrealized = Some(dec!(2.5));
166 assert_eq!(PathOutcome::from(&result).pnl, Some(dec!(12.5)));
167 }
168
169 #[test]
170 fn test_from_outcomes_matches_path_statistics() {
171 let outcomes = vec![
172 outcome(Some(dec!(50.0)), 10),
173 outcome(Some(dec!(-100.0)), 20),
174 outcome(Some(dec!(25.0)), 15),
175 outcome(None, 5),
176 ];
177 let expected = PathStatistics::from_outcomes(&outcomes).unwrap();
178 let stats = SimulationStatsResult::from_outcomes(outcomes).unwrap();
179
180 assert_eq!(stats.results.len(), 4);
181 assert_eq!(stats.total_simulations, expected.total_paths);
182 assert_eq!(stats.profitable_count, expected.profitable_count);
183 assert_eq!(stats.loss_count, expected.loss_count);
184 assert_eq!(stats.average_pnl, expected.average_pnl);
185 assert_eq!(stats.median_pnl, expected.median_pnl);
186 assert_eq!(stats.std_dev_pnl, expected.std_dev_pnl);
187 assert_eq!(stats.best_pnl, expected.best_pnl);
188 assert_eq!(stats.worst_pnl, expected.worst_pnl);
189 assert_eq!(stats.win_rate, expected.win_rate);
190 assert_eq!(
191 stats.average_holding_period,
192 expected.average_holding_period
193 );
194 assert_eq!(stats.average_pnl, dec!(-6.25));
195 assert_eq!(stats.median_pnl, dec!(12.5));
196 assert_eq!(stats.win_rate, dec!(50.0));
197 }
198
199 #[test]
200 fn test_from_results_and_from_outcomes_agree() {
201 let outcomes = vec![
202 outcome(Some(dec!(3.0)), 1),
203 outcome(Some(dec!(-1.0)), 3),
204 outcome(Some(dec!(7.0)), 5),
205 ];
206 let via_outcomes = SimulationStatsResult::from_outcomes(outcomes.clone()).unwrap();
207 let results: Vec<SimulationResult> =
208 outcomes.into_iter().map(SimulationResult::from).collect();
209 let via_results = SimulationStatsResult::from_results(results).unwrap();
210
211 assert_eq!(via_results.total_simulations, 3);
212 assert_eq!(via_results.average_pnl, via_outcomes.average_pnl);
213 assert_eq!(via_results.median_pnl, dec!(3.0));
214 assert_eq!(via_results.std_dev_pnl, dec!(4.0));
215 assert_eq!(via_results.best_pnl, dec!(7.0));
216 assert_eq!(via_results.worst_pnl, dec!(-1.0));
217 assert_eq!(via_results.average_holding_period, dec!(3));
218 }
219
220 #[test]
221 fn test_from_outcomes_empty_run_is_all_zero() {
222 let stats = SimulationStatsResult::from_outcomes(Vec::new()).unwrap();
223 assert!(stats.results.is_empty());
224 assert_eq!(stats.total_simulations, 0);
225 assert_eq!(stats.average_pnl, Decimal::ZERO);
226 assert_eq!(stats.win_rate, Decimal::ZERO);
227 }
228
229 #[test]
230 fn test_from_outcomes_reports_overflow() {
231 let outcomes = vec![
232 outcome(Some(Decimal::MAX), 1),
233 outcome(Some(Decimal::MAX), 1),
234 ];
235 assert!(matches!(
236 SimulationStatsResult::from_outcomes(outcomes),
237 Err(SimulationError::Decimal(_))
238 ));
239 }
240}