1use crate::backtesting::results::{SimulationResult, SimulationStatsResult};
18use crate::error::BacktestError;
19use optionstratlib_analytics::pnl::{PnL, PnLCalculator};
20use optionstratlib_core::model::Positive;
21use optionstratlib_core::model::decimal::{d_add, d_div, d_sub};
22use optionstratlib_core::model::{ExpirationDate, Options};
23use optionstratlib_core::utils::Len;
24use optionstratlib_pricing::pricing::OptionPricing;
25use optionstratlib_simulation::error::SimulationError;
26use optionstratlib_simulation::simulation::randomwalk::RandomWalk;
27use optionstratlib_simulation::simulation::simulator::Simulator;
28use optionstratlib_simulation::simulation::{ExitPolicy, PathEvaluator, check_exit_policy};
29use optionstratlib_strategies::strategies::base::Positionable;
30use optionstratlib_strategies::strategies::{LongCall, LongPut, ShortCall, ShortPut, Strategies};
31use rayon::prelude::*;
32use rust_decimal::Decimal;
33use rust_decimal_macros::dec;
34use std::collections::HashMap;
35use std::fmt::Display;
36use std::ops::AddAssign;
37
38pub trait Simulate<X, Y>
63where
64 X: Copy + TryInto<Positive> + AddAssign + Display,
65 Y: TryInto<Positive> + Display + Clone,
66{
67 fn simulate(
91 &self,
92 sim: &Simulator<X, Y>,
93 exit: ExitPolicy,
94 ) -> Result<SimulationStatsResult, BacktestError>;
95}
96
97const TAKE_PROFIT_FRACTION: Decimal = dec!(0.5);
100const STOP_LOSS_FRACTION: Decimal = dec!(-1.0);
103const EXPIRATION_MARK_DAYS: f64 = 0.001;
106
107pub trait SingleLegSimulation: PnLCalculator + Positionable {
113 const IS_LONG: bool;
116
117 fn mark_adjustment(&self) -> Result<Positive, BacktestError> {
124 Ok(Positive::ZERO)
125 }
126
127 fn simulated_option(&self) -> Result<&Options, SimulationError> {
134 let positions = self.get_positions().map_err(|e| {
135 SimulationError::invalid_parameters(&format!(
136 "single-leg simulation cannot read the strategy positions: {e}"
137 ))
138 })?;
139 match positions.as_slice() {
140 [position] => Ok(&position.option),
141 _ => Err(SimulationError::invalid_parameters(&format!(
142 "single-leg simulation expects exactly one position, found {}",
143 positions.len()
144 ))),
145 }
146 }
147}
148
149impl SingleLegSimulation for LongCall {
150 const IS_LONG: bool = true;
151}
152
153impl SingleLegSimulation for LongPut {
154 const IS_LONG: bool = true;
155}
156
157impl SingleLegSimulation for ShortCall {
158 const IS_LONG: bool = false;
159}
160
161impl SingleLegSimulation for ShortPut {
162 const IS_LONG: bool = false;
163
164 fn mark_adjustment(&self) -> Result<Positive, BacktestError> {
165 Ok(self.get_fees()?)
169 }
170}
171
172#[inline]
175fn div_unrounded(lhs: Decimal, rhs: Decimal, op: &'static str) -> Result<Decimal, BacktestError> {
176 if rhs.is_zero() {
177 return Err(SimulationError::invalid_parameters(&format!("{op}: division by zero")).into());
178 }
179 lhs.checked_div(rhs).ok_or_else(|| {
180 SimulationError::invalid_parameters(&format!("{op}: {lhs} / {rhs} overflowed")).into()
181 })
182}
183
184#[derive(Debug)]
189pub struct SingleLegPathEvaluator<'a, S> {
190 strategy: &'a S,
191 initial_premium: Decimal,
192}
193
194impl<'a, S> SingleLegPathEvaluator<'a, S>
195where
196 S: SingleLegSimulation,
197{
198 pub fn new(strategy: &'a S) -> Result<Self, BacktestError> {
205 let initial_premium = strategy
206 .simulated_option()?
207 .calculate_price_black_scholes()?
208 .abs();
209 Ok(Self {
210 strategy,
211 initial_premium,
212 })
213 }
214
215 #[must_use]
217 #[inline]
218 pub fn initial_premium(&self) -> Decimal {
219 self.initial_premium
220 }
221}
222
223impl<S, X, Y> PathEvaluator<X, Y> for SingleLegPathEvaluator<'_, S>
224where
225 S: SingleLegSimulation,
226 X: Copy + Into<Positive> + AddAssign + Display,
227 Y: Into<Positive> + Display + Clone,
228{
229 type Outcome = SimulationResult;
230 type Error = BacktestError;
231
232 fn evaluate_path(
233 &self,
234 random_walk: &RandomWalk<X, Y>,
235 exit: &ExitPolicy,
236 ) -> Result<SimulationResult, BacktestError> {
237 let option = self.strategy.simulated_option()?;
238 let initial_premium = self.initial_premium;
239 let mark_adjustment = self.strategy.mark_adjustment()?.to_dec();
240
241 let mut max_premium = initial_premium;
242 let mut min_premium = initial_premium;
243 let mut premium_sum = initial_premium;
244 let mut premium_count: usize = 1;
245 let mut hit_take_profit = false;
246 let mut hit_stop_loss = false;
247 let mut expired = false;
248 let mut expiration_premium = None;
249 let mut holding_period = 0;
250 let mut exit_reason = ExitPolicy::Expiration;
251 let mut final_pnl = None;
252
253 let mut current_option = option.clone();
257
258 for step in random_walk.get_steps().iter().skip(1) {
260 let days_left = match step.x.days_left() {
261 Ok(days) => days,
262 Err(_) => {
263 expired = true;
264 break;
265 }
266 };
267
268 current_option.underlying_price = step.y.positive()?;
270 current_option.expiration_date = ExpirationDate::Days(days_left);
271
272 let current_premium = d_add(
273 current_option.calculate_price_black_scholes()?.abs(),
274 mark_adjustment,
275 "backtesting::strategy_simulation/mark_adjustment",
276 )?;
277 let index = usize::try_from(*step.x.index()).map_err(|_| {
280 SimulationError::invalid_parameters(&format!(
281 "backtesting::strategy_simulation: negative step index {}",
282 step.x.index()
283 ))
284 })?;
285
286 max_premium = max_premium.max(current_premium);
288 min_premium = min_premium.min(current_premium);
289 premium_sum = d_add(
290 premium_sum,
291 current_premium,
292 "backtesting::strategy_simulation/premium_sum",
293 )?;
294 premium_count = premium_count.checked_add(1).ok_or_else(|| {
295 SimulationError::invalid_parameters(
296 "backtesting::strategy_simulation: premium count overflowed",
297 )
298 })?;
299 holding_period = index;
300
301 if let Some(reason) = check_exit_policy(
303 exit,
304 initial_premium,
305 current_premium,
306 index,
307 days_left,
308 current_option.underlying_price,
309 S::IS_LONG,
310 ) {
311 exit_reason = reason;
312
313 let move_from_open = if S::IS_LONG {
319 d_sub(
320 current_premium,
321 initial_premium,
322 "backtesting::strategy_simulation/pnl_move",
323 )?
324 } else {
325 d_sub(
326 initial_premium,
327 current_premium,
328 "backtesting::strategy_simulation/pnl_move",
329 )?
330 };
331 let pnl_percent = d_div(
332 move_from_open,
333 initial_premium,
334 "backtesting::strategy_simulation/pnl_percent",
335 )
336 .unwrap_or(Decimal::ZERO);
337 if pnl_percent >= TAKE_PROFIT_FRACTION {
338 hit_take_profit = true;
339 } else if pnl_percent <= STOP_LOSS_FRACTION {
340 hit_stop_loss = true;
341 }
342
343 let pnl = PnL {
348 realized: Some(move_from_open),
349 unrealized: None,
350 initial_costs: Positive::ZERO,
351 initial_income: Positive::ZERO,
352 date_time: chrono::Utc::now(),
353 };
354 final_pnl = Some(pnl);
355 break;
356 }
357 }
358
359 if final_pnl.is_none()
361 && let Some(last_step) = random_walk.last()
362 {
363 let final_price = last_step.y.positive()?;
364 let pnl = self.strategy.calculate_pnl_at_expiration(&final_price)?;
365
366 let mut exp_option = option.clone();
368 exp_option.underlying_price = final_price;
369 exp_option.expiration_date = ExpirationDate::Days(Positive::new(EXPIRATION_MARK_DAYS)?);
370 expiration_premium = Some(exp_option.calculate_price_black_scholes()?.abs());
371
372 expired = true;
373 exit_reason = ExitPolicy::Expiration;
374 final_pnl = Some(pnl);
375 holding_period = random_walk.len().checked_sub(1).ok_or_else(|| {
377 SimulationError::invalid_parameters(
378 "backtesting::strategy_simulation: a walk with a last step has no length",
379 )
380 })?;
381 }
382
383 let pnl = final_pnl.unwrap_or_default();
384 let avg_premium = div_unrounded(
385 premium_sum,
386 Decimal::from(premium_count),
387 "backtesting::strategy_simulation/avg_premium",
388 )?;
389
390 Ok(SimulationResult {
391 simulation_count: 1,
392 risk_metrics: None,
393 final_equity_percentiles: HashMap::new(),
394 max_premium,
395 min_premium,
396 avg_premium,
397 hit_take_profit,
398 hit_stop_loss,
399 expired,
400 expiration_premium,
401 pnl,
402 holding_period,
403 exit_reason,
404 })
405 }
406}
407
408#[must_use = "the simulation statistics are the only product of this call"]
424#[tracing::instrument(level = "debug", skip(strategy, sim), fields(walks = sim.len()))]
425pub fn simulate_single_leg<S, X, Y>(
426 strategy: &S,
427 sim: &Simulator<X, Y>,
428 exit: ExitPolicy,
429) -> Result<SimulationStatsResult, BacktestError>
430where
431 S: SingleLegSimulation + Sync,
432 X: Copy + Into<Positive> + AddAssign + Display + Send + Sync,
433 Y: Into<Positive> + Display + Clone + Send + Sync,
434{
435 let evaluator = SingleLegPathEvaluator::new(strategy)?;
436
437 let walks = sim.get_random_walks();
441 let walk_count = walks.len();
442 let outcomes: Vec<Result<SimulationResult, BacktestError>> = walks
443 .into_par_iter()
444 .enumerate()
445 .map(|(path, random_walk)| {
446 let outcome = evaluator.evaluate_path(random_walk, &exit);
447 tracing::debug!(path, total = walk_count, "simulated path evaluated");
448 outcome
449 })
450 .collect();
451 let simulation_results = outcomes.into_iter().collect::<Result<Vec<_>, _>>()?;
453 tracing::info!(total = walk_count, "simulations completed");
454
455 Ok(SimulationStatsResult::from_results(simulation_results)?)
456}
457
458impl<X, Y> Simulate<X, Y> for LongCall
459where
460 X: Copy + Into<Positive> + AddAssign + Display + Send + Sync,
461 Y: Into<Positive> + Display + Clone + Send + Sync,
462{
463 fn simulate(
464 &self,
465 sim: &Simulator<X, Y>,
466 exit: ExitPolicy,
467 ) -> Result<SimulationStatsResult, BacktestError> {
468 simulate_single_leg(self, sim, exit)
469 }
470}
471
472impl<X, Y> Simulate<X, Y> for LongPut
473where
474 X: Copy + Into<Positive> + AddAssign + Display + Send + Sync,
475 Y: Into<Positive> + Display + Clone + Send + Sync,
476{
477 fn simulate(
478 &self,
479 sim: &Simulator<X, Y>,
480 exit: ExitPolicy,
481 ) -> Result<SimulationStatsResult, BacktestError> {
482 simulate_single_leg(self, sim, exit)
483 }
484}
485
486impl<X, Y> Simulate<X, Y> for ShortCall
487where
488 X: Copy + Into<Positive> + AddAssign + Display + Send + Sync,
489 Y: Into<Positive> + Display + Clone + Send + Sync,
490{
491 fn simulate(
492 &self,
493 sim: &Simulator<X, Y>,
494 exit: ExitPolicy,
495 ) -> Result<SimulationStatsResult, BacktestError> {
496 simulate_single_leg(self, sim, exit)
497 }
498}
499
500impl<X, Y> Simulate<X, Y> for ShortPut
501where
502 X: Copy + Into<Positive> + AddAssign + Display + Send + Sync,
503 Y: Into<Positive> + Display + Clone + Send + Sync,
504{
505 fn simulate(
506 &self,
507 sim: &Simulator<X, Y>,
508 exit: ExitPolicy,
509 ) -> Result<SimulationStatsResult, BacktestError> {
510 simulate_single_leg(self, sim, exit)
511 }
512}
513
514#[cfg(test)]
515mod test_support {
516 use super::*;
520 use chrono::Utc;
521 use optionstratlib_core::model::position::Position;
522 use optionstratlib_core::model::types::{OptionStyle, OptionType, Side};
523 use optionstratlib_core::pos_or_panic;
524 use optionstratlib_core::utils::TimeFrame;
525 use optionstratlib_simulation::simulation::steps::Step;
526 use optionstratlib_simulation::simulation::{WalkParams, WalkType, WalkTypeAble};
527
528 #[derive(Clone)]
529 pub(super) struct TestWalker;
530 impl WalkTypeAble<Positive, Positive> for TestWalker {}
531
532 pub(super) fn test_leg(side: Side, style: OptionStyle) -> Position {
535 let option = Options::new(
536 OptionType::European,
537 side,
538 "TEST".to_string(),
539 Positive::HUNDRED,
540 ExpirationDate::Days(pos_or_panic!(30.0)),
541 pos_or_panic!(0.20),
542 Positive::ONE,
543 Positive::HUNDRED,
544 dec!(0.05),
545 style,
546 Positive::ZERO,
547 None,
548 );
549 Position::new(
550 option,
551 pos_or_panic!(5.0),
552 Utc::now(),
553 Positive::ZERO,
554 Positive::ZERO,
555 None,
556 None,
557 )
558 }
559
560 pub(super) fn create_test_long_call() -> LongCall {
561 LongCall::new(
562 "TEST".to_string(),
563 Positive::HUNDRED,
564 ExpirationDate::Days(pos_or_panic!(30.0)),
565 pos_or_panic!(0.20),
566 Positive::ONE,
567 Positive::HUNDRED,
568 dec!(0.05),
569 Positive::ZERO,
570 pos_or_panic!(5.0),
571 Positive::ZERO,
572 Positive::ZERO,
573 )
574 .unwrap()
575 }
576
577 pub(super) fn create_test_long_put() -> LongPut {
578 let mut strategy = LongPut::default();
579 strategy
580 .add_position(&test_leg(Side::Long, OptionStyle::Put))
581 .unwrap();
582 strategy
583 }
584
585 pub(super) fn create_test_short_call() -> ShortCall {
586 let mut strategy = ShortCall::default();
587 strategy
588 .add_position(&test_leg(Side::Short, OptionStyle::Call))
589 .unwrap();
590 strategy
591 }
592
593 pub(super) fn create_test_short_put() -> ShortPut {
594 let mut strategy = ShortPut::default();
595 strategy
596 .add_position(&test_leg(Side::Short, OptionStyle::Put))
597 .unwrap();
598 strategy
599 }
600
601 pub(super) fn create_walk_params(prices: Vec<Positive>) -> WalkParams<Positive, Positive> {
602 let init_step = Step::new(
603 Positive::ONE,
604 TimeFrame::Day,
605 ExpirationDate::Days(pos_or_panic!(30.0)),
606 Positive::HUNDRED,
607 );
608 WalkParams {
609 size: prices.len(),
610 init_step,
611 walker: Box::new(TestWalker),
612 walk_type: WalkType::Historical {
613 timeframe: TimeFrame::Day,
614 prices,
615 symbol: Some("TEST".to_string()),
616 },
617 seed: None,
618 }
619 }
620}
621
622#[cfg(test)]
623mod tests_single_leg_contract {
624 use super::test_support::*;
625 use super::*;
626 use optionstratlib_core::pos_or_panic;
627 use optionstratlib_simulation::simulation::generator_positive;
628 use rust_decimal::MathematicalOps;
629
630 #[test]
631 fn test_single_leg_sides_and_adjustments() {
632 let sides = [
633 <LongCall as SingleLegSimulation>::IS_LONG,
634 <LongPut as SingleLegSimulation>::IS_LONG,
635 <ShortCall as SingleLegSimulation>::IS_LONG,
636 <ShortPut as SingleLegSimulation>::IS_LONG,
637 ];
638 assert_eq!(sides, [true, true, false, false]);
639 assert_eq!(
640 create_test_long_call().mark_adjustment().unwrap(),
641 Positive::ZERO
642 );
643 let short_put = create_test_short_put();
644 assert_eq!(
645 short_put.mark_adjustment().unwrap(),
646 short_put.get_fees().unwrap()
647 );
648 }
649
650 #[test]
651 fn test_simulated_option_is_the_single_leg() {
652 let strategy = create_test_long_call();
653 let option = strategy.simulated_option().unwrap();
654 assert_eq!(option.strike_price, Positive::HUNDRED);
655 let evaluator = SingleLegPathEvaluator::new(&strategy).unwrap();
656 assert_eq!(
657 evaluator.initial_premium(),
658 option.calculate_price_black_scholes().unwrap().abs()
659 );
660 }
661
662 #[test]
666 fn test_evaluate_path_rejects_a_negative_step_index() {
667 let strategy = create_test_long_call();
668 let evaluator = SingleLegPathEvaluator::new(&strategy).unwrap();
669 let walk_params = create_walk_params(vec![Positive::HUNDRED, Positive::HUNDRED]);
670 let walk = RandomWalk::new("negative index".to_string(), &walk_params, |params| {
671 let first = params.init_step.clone();
672 let mut rewound = first.clone();
673 rewound.x = first.x.previous()?;
674 Ok::<_, SimulationError>(vec![first, rewound])
675 })
676 .unwrap();
677 assert_eq!(*walk.get_steps()[1].x.index(), -1);
678
679 let result = evaluator.evaluate_path(&walk, &ExitPolicy::Expiration);
680 assert!(result.is_err(), "negative index accepted: {result:?}");
681 }
682
683 #[test]
686 fn test_simulated_option_rejects_a_strategy_without_a_single_leg() {
687 use optionstratlib_core::model::position::Position;
688 use optionstratlib_pricing::error::PricingError;
689
690 struct Legs(Vec<Position>);
691 impl PnLCalculator for Legs {
692 fn calculate_pnl(
693 &self,
694 _underlying_price: &Positive,
695 _expiration_date: ExpirationDate,
696 _implied_volatility: &Positive,
697 ) -> Result<PnL, PricingError> {
698 Ok(PnL::default())
699 }
700 fn calculate_pnl_at_expiration(
701 &self,
702 _underlying_price: &Positive,
703 ) -> Result<PnL, PricingError> {
704 Ok(PnL::default())
705 }
706 }
707 impl Positionable for Legs {
708 fn get_positions(
709 &self,
710 ) -> Result<Vec<&Position>, optionstratlib_core::error::PositionError> {
711 Ok(self.0.iter().collect())
712 }
713 }
714 impl SingleLegSimulation for Legs {
715 const IS_LONG: bool = true;
716 }
717
718 assert!(matches!(
719 Legs(Vec::new()).simulated_option(),
720 Err(SimulationError::InvalidParameters { .. })
721 ));
722 let leg = create_test_long_call().get_positions().unwrap()[0].clone();
723 assert!(matches!(
726 Legs(vec![leg.clone(), leg]).simulated_option(),
727 Err(SimulationError::InvalidParameters { .. })
728 ));
729 assert!(matches!(
730 SingleLegPathEvaluator::new(&Legs(Vec::new())),
731 Err(BacktestError::Simulation(inner))
732 if matches!(*inner, SimulationError::InvalidParameters { .. })
733 ));
734 }
735
736 #[test]
739 fn test_simulate_aggregate_matches_from_results() {
740 let strategy = create_test_short_put();
741 let walk_params = create_walk_params(vec![
742 Positive::HUNDRED,
743 pos_or_panic!(105.0),
744 pos_or_panic!(110.0),
745 ]);
746 let simulator =
747 Simulator::new("Test".to_string(), 3, &walk_params, generator_positive).unwrap();
748 let stats = strategy
749 .simulate(&simulator, ExitPolicy::Expiration)
750 .unwrap();
751 let recomputed = SimulationStatsResult::from_results(stats.results.clone()).unwrap();
752 assert_eq!(stats.average_pnl, recomputed.average_pnl);
753 assert_eq!(stats.median_pnl, recomputed.median_pnl);
754 assert_eq!(stats.std_dev_pnl, recomputed.std_dev_pnl);
755 assert_eq!(stats.win_rate, recomputed.win_rate);
756 assert_eq!(
757 stats.average_holding_period,
758 recomputed.average_holding_period
759 );
760 assert_eq!(stats.std_dev_pnl, dec!(0.0).sqrt().unwrap());
761 }
762}
763
764#[cfg(test)]
765mod tests_simulate_long_call {
766 use super::test_support::*;
767 use super::*;
768 use optionstratlib_core::pos_or_panic;
769 use optionstratlib_simulation::simulation::generator_positive;
770
771 #[test]
772 fn test_simulate_profit_percent_exit() {
773 let strategy = create_test_long_call();
774 let prices = vec![
775 Positive::HUNDRED,
776 pos_or_panic!(105.0),
777 pos_or_panic!(110.0),
778 pos_or_panic!(115.0),
779 ];
780 let walk_params = create_walk_params(prices);
781 let Ok(simulator) = Simulator::new("Test".to_string(), 1, &walk_params, generator_positive)
782 else {
783 panic!("simulator setup failed");
784 };
785 let results = strategy.simulate(&simulator, ExitPolicy::ProfitPercent(dec!(0.5)));
786 assert!(results.is_ok());
787 let stats = results.unwrap();
788 assert_eq!(stats.total_simulations, 1);
789 }
790
791 #[test]
792 fn test_simulate_expiration_exit() {
793 let strategy = create_test_long_call();
794 let prices = vec![
795 Positive::HUNDRED,
796 pos_or_panic!(101.0),
797 pos_or_panic!(102.0),
798 ];
799 let walk_params = create_walk_params(prices);
800 let Ok(simulator) = Simulator::new("Test".to_string(), 1, &walk_params, generator_positive)
801 else {
802 panic!("simulator setup failed");
803 };
804 let results = strategy.simulate(&simulator, ExitPolicy::Expiration);
805 assert!(results.is_ok(), "Simulate failed: {:?}", results.err());
806 let stats = results.unwrap();
807 assert_eq!(stats.total_simulations, 1);
808 }
809
810 #[test]
811 fn test_simulate_stats_aggregation() {
812 let strategy = create_test_long_call();
813 let prices = vec![
814 Positive::HUNDRED,
815 pos_or_panic!(105.0),
816 pos_or_panic!(110.0),
817 ];
818 let walk_params = create_walk_params(prices);
819 let Ok(simulator) = Simulator::new("Test".to_string(), 3, &walk_params, generator_positive)
820 else {
821 panic!("simulator setup failed");
822 };
823 let results = strategy.simulate(&simulator, ExitPolicy::Expiration);
824 assert!(results.is_ok(), "Simulate failed: {:?}", results.err());
825 let stats = results.unwrap();
826 assert!(stats.total_simulations >= 1);
827 assert_eq!(stats.total_simulations, stats.results.len());
828 assert!(stats.win_rate >= dec!(0.0) && stats.win_rate <= dec!(100.0));
829 }
830}
831
832#[cfg(test)]
833mod tests_simulate_long_put {
834 use super::test_support::*;
835 use super::*;
836 use optionstratlib_core::pos_or_panic;
837 use optionstratlib_simulation::simulation::generator_positive;
838
839 #[test]
840 fn test_simulate_profit_percent_exit() {
841 let strategy = create_test_long_put();
842 let prices = vec![
843 Positive::HUNDRED,
844 pos_or_panic!(95.0),
845 pos_or_panic!(90.0),
846 pos_or_panic!(85.0),
847 ];
848 let walk_params = create_walk_params(prices);
849 let Ok(simulator) = Simulator::new("Test".to_string(), 1, &walk_params, generator_positive)
850 else {
851 panic!("simulator setup failed");
852 };
853 let results = strategy.simulate(&simulator, ExitPolicy::ProfitPercent(dec!(0.5)));
854 assert!(results.is_ok());
855 let stats = results.unwrap();
856 assert_eq!(stats.total_simulations, 1);
857 }
858
859 #[test]
860 fn test_simulate_expiration_exit() {
861 let strategy = create_test_long_put();
862 let prices = vec![Positive::HUNDRED, pos_or_panic!(99.0), pos_or_panic!(98.0)];
863 let walk_params = create_walk_params(prices);
864 let Ok(simulator) = Simulator::new("Test".to_string(), 1, &walk_params, generator_positive)
865 else {
866 panic!("simulator setup failed");
867 };
868 let results = strategy.simulate(&simulator, ExitPolicy::Expiration);
869 assert!(results.is_ok(), "Simulate failed: {:?}", results.err());
870 let stats = results.unwrap();
871 assert_eq!(stats.total_simulations, 1);
872 }
873
874 #[test]
875 fn test_simulate_stats_aggregation() {
876 let strategy = create_test_long_put();
877 let prices = vec![Positive::HUNDRED, pos_or_panic!(95.0), pos_or_panic!(90.0)];
878 let walk_params = create_walk_params(prices);
879 let Ok(simulator) = Simulator::new("Test".to_string(), 3, &walk_params, generator_positive)
880 else {
881 panic!("simulator setup failed");
882 };
883 let results = strategy.simulate(&simulator, ExitPolicy::Expiration);
884 assert!(results.is_ok(), "Simulate failed: {:?}", results.err());
885 let stats = results.unwrap();
886 assert!(stats.total_simulations >= 1);
887 assert_eq!(stats.total_simulations, stats.results.len());
888 assert!(stats.win_rate >= dec!(0.0) && stats.win_rate <= dec!(100.0));
889 }
890}
891
892#[cfg(test)]
893mod tests_simulate_short_call {
894 use super::test_support::*;
895 use super::*;
896 use optionstratlib_core::pos_or_panic;
897 use optionstratlib_simulation::simulation::generator_positive;
898
899 #[test]
900 fn test_simulate_profit_percent_exit() {
901 let strategy = create_test_short_call();
902 let prices = vec![
903 Positive::HUNDRED,
904 pos_or_panic!(95.0),
905 pos_or_panic!(90.0),
906 pos_or_panic!(85.0),
907 ];
908 let walk_params = create_walk_params(prices);
909 let Ok(simulator) = Simulator::new("Test".to_string(), 1, &walk_params, generator_positive)
910 else {
911 panic!("simulator setup failed");
912 };
913 let results = strategy.simulate(&simulator, ExitPolicy::ProfitPercent(dec!(0.5)));
914 assert!(results.is_ok());
915 let stats = results.unwrap();
916 assert_eq!(stats.total_simulations, 1);
917 }
918
919 #[test]
920 fn test_simulate_expiration_exit() {
921 let strategy = create_test_short_call();
922 let prices = vec![Positive::HUNDRED, pos_or_panic!(99.0), pos_or_panic!(98.0)];
923 let walk_params = create_walk_params(prices);
924 let Ok(simulator) = Simulator::new("Test".to_string(), 1, &walk_params, generator_positive)
925 else {
926 panic!("simulator setup failed");
927 };
928 let results = strategy.simulate(&simulator, ExitPolicy::Expiration);
929 assert!(results.is_ok(), "Simulate failed: {:?}", results.err());
930 let stats = results.unwrap();
931 assert_eq!(stats.total_simulations, 1);
932 }
933
934 #[test]
935 fn test_simulate_stats_aggregation() {
936 let strategy = create_test_short_call();
937 let prices = vec![Positive::HUNDRED, pos_or_panic!(95.0), pos_or_panic!(90.0)];
938 let walk_params = create_walk_params(prices);
939 let Ok(simulator) = Simulator::new("Test".to_string(), 3, &walk_params, generator_positive)
940 else {
941 panic!("simulator setup failed");
942 };
943 let results = strategy.simulate(&simulator, ExitPolicy::Expiration);
944 assert!(results.is_ok(), "Simulate failed: {:?}", results.err());
945 let stats = results.unwrap();
946 assert!(stats.total_simulations >= 1);
947 assert_eq!(stats.total_simulations, stats.results.len());
948 assert!(stats.win_rate >= dec!(0.0) && stats.win_rate <= dec!(100.0));
949 }
950}
951
952#[cfg(test)]
953mod tests_simulate_short_put {
954 use super::test_support::*;
955 use super::*;
956 use optionstratlib_core::pos_or_panic;
957 use optionstratlib_simulation::simulation::generator_positive;
958
959 #[test]
960 fn test_simulate_profit_percent_exit() {
961 let strategy = create_test_short_put();
962 let prices = vec![
963 Positive::HUNDRED,
964 pos_or_panic!(105.0),
965 pos_or_panic!(110.0),
966 pos_or_panic!(115.0),
967 ];
968
969 let walk_params = create_walk_params(prices);
970 let Ok(simulator) = Simulator::new(
971 "Test Simulator".to_string(),
972 1,
973 &walk_params,
974 generator_positive,
975 ) else {
976 panic!("simulator setup failed");
977 };
978
979 let exit_policy = ExitPolicy::ProfitPercent(dec!(0.5));
980 let results = strategy.simulate(&simulator, exit_policy);
981
982 assert!(results.is_ok());
983 let stats = results.unwrap();
984 assert_eq!(stats.total_simulations, 1);
985 assert!(!stats.results.is_empty());
986 }
987
988 #[test]
989 fn test_simulate_loss_percent_exit() {
990 let strategy = create_test_short_put();
991 let prices = vec![
992 Positive::HUNDRED,
993 pos_or_panic!(95.0),
994 pos_or_panic!(90.0),
995 pos_or_panic!(85.0),
996 ];
997
998 let walk_params = create_walk_params(prices);
999 let Ok(simulator) = Simulator::new(
1000 "Test Simulator".to_string(),
1001 1,
1002 &walk_params,
1003 generator_positive,
1004 ) else {
1005 panic!("simulator setup failed");
1006 };
1007
1008 let exit_policy = ExitPolicy::LossPercent(dec!(1.0));
1009 let results = strategy.simulate(&simulator, exit_policy);
1010
1011 assert!(results.is_ok());
1012 let stats = results.unwrap();
1013 assert_eq!(stats.total_simulations, 1);
1014 assert!(!stats.results.is_empty());
1015 }
1016
1017 #[test]
1018 fn test_simulate_expiration_exit() {
1019 let strategy = create_test_short_put();
1020 let prices = vec![
1021 Positive::HUNDRED,
1022 pos_or_panic!(101.0),
1023 pos_or_panic!(102.0),
1024 pos_or_panic!(103.0),
1025 ];
1026
1027 let walk_params = create_walk_params(prices);
1028 let Ok(simulator) = Simulator::new(
1029 "Test Simulator".to_string(),
1030 1,
1031 &walk_params,
1032 generator_positive,
1033 ) else {
1034 panic!("simulator setup failed");
1035 };
1036
1037 let exit_policy = ExitPolicy::Expiration;
1038 let results = strategy.simulate(&simulator, exit_policy);
1039
1040 assert!(results.is_ok(), "Simulate failed: {:?}", results.err());
1041 let stats = results.unwrap();
1042 assert_eq!(stats.total_simulations, 1);
1043 assert!(!stats.results.is_empty());
1044 }
1045
1046 #[test]
1047 fn test_simulate_or_exit_policy() {
1048 let strategy = create_test_short_put();
1049 let prices = vec![
1050 Positive::HUNDRED,
1051 pos_or_panic!(105.0),
1052 pos_or_panic!(110.0),
1053 pos_or_panic!(115.0),
1054 ];
1055
1056 let walk_params = create_walk_params(prices);
1057 let Ok(simulator) = Simulator::new(
1058 "Test Simulator".to_string(),
1059 1,
1060 &walk_params,
1061 generator_positive,
1062 ) else {
1063 panic!("simulator setup failed");
1064 };
1065
1066 let exit_policy = ExitPolicy::Or(vec![
1067 ExitPolicy::ProfitPercent(dec!(0.5)),
1068 ExitPolicy::UnderlyingAbove(pos_or_panic!(112.0)),
1069 ]);
1070 let results = strategy.simulate(&simulator, exit_policy);
1071
1072 assert!(results.is_ok());
1073 let stats = results.unwrap();
1074 assert_eq!(stats.total_simulations, 1);
1075 assert!(!stats.results.is_empty());
1076 }
1077
1078 #[test]
1079 fn test_simulate_stats_aggregation() {
1080 let strategy = create_test_short_put();
1081 let prices = vec![
1082 Positive::HUNDRED,
1083 pos_or_panic!(105.0),
1084 pos_or_panic!(110.0),
1085 ];
1086
1087 let walk_params = create_walk_params(prices);
1088 let Ok(simulator) = Simulator::new(
1089 "Test Simulator".to_string(),
1090 5,
1091 &walk_params,
1092 generator_positive,
1093 ) else {
1094 panic!("simulator setup failed");
1095 };
1096
1097 let exit_policy = ExitPolicy::Expiration;
1098 let results = strategy.simulate(&simulator, exit_policy);
1099
1100 assert!(results.is_ok(), "Simulate failed: {:?}", results.err());
1101 let stats = results.unwrap();
1102
1103 assert!(stats.total_simulations >= 1);
1104 assert_eq!(stats.total_simulations, stats.results.len());
1105 assert!(stats.win_rate >= dec!(0.0) && stats.win_rate <= dec!(100.0));
1106 assert!(stats.average_holding_period >= dec!(0.0));
1107 }
1108
1109 #[test]
1110 fn test_simulate_time_steps_exit() {
1111 let strategy = create_test_short_put();
1112 let prices = vec![
1113 Positive::HUNDRED,
1114 pos_or_panic!(101.0),
1115 pos_or_panic!(102.0),
1116 pos_or_panic!(103.0),
1117 pos_or_panic!(104.0),
1118 ];
1119
1120 let walk_params = create_walk_params(prices);
1121 let Ok(simulator) = Simulator::new(
1122 "Test Simulator".to_string(),
1123 1,
1124 &walk_params,
1125 generator_positive,
1126 ) else {
1127 panic!("simulator setup failed");
1128 };
1129
1130 let exit_policy = ExitPolicy::TimeSteps(2);
1131 let results = strategy.simulate(&simulator, exit_policy);
1132
1133 assert!(results.is_ok());
1134 let stats = results.unwrap();
1135 assert_eq!(stats.total_simulations, 1);
1136 assert!(!stats.results.is_empty());
1137 }
1138
1139 #[test]
1140 fn test_simulate_underlying_below_exit() {
1141 let strategy = create_test_short_put();
1142 let prices = vec![Positive::HUNDRED, pos_or_panic!(95.0), pos_or_panic!(90.0)];
1143
1144 let walk_params = create_walk_params(prices);
1145 let Ok(simulator) = Simulator::new(
1146 "Test Simulator".to_string(),
1147 1,
1148 &walk_params,
1149 generator_positive,
1150 ) else {
1151 panic!("simulator setup failed");
1152 };
1153
1154 let exit_policy = ExitPolicy::UnderlyingBelow(pos_or_panic!(92.0));
1155 let results = strategy.simulate(&simulator, exit_policy);
1156
1157 assert!(results.is_ok());
1158 let stats = results.unwrap();
1159 assert_eq!(stats.total_simulations, 1);
1160 }
1161
1162 #[test]
1163 fn test_simulate_and_exit_policy() {
1164 let strategy = create_test_short_put();
1165 let prices = vec![
1166 Positive::HUNDRED,
1167 pos_or_panic!(105.0),
1168 pos_or_panic!(110.0),
1169 pos_or_panic!(115.0),
1170 ];
1171
1172 let walk_params = create_walk_params(prices);
1173 let Ok(simulator) = Simulator::new(
1174 "Test Simulator".to_string(),
1175 1,
1176 &walk_params,
1177 generator_positive,
1178 ) else {
1179 panic!("simulator setup failed");
1180 };
1181
1182 let exit_policy = ExitPolicy::And(vec![
1183 ExitPolicy::TimeSteps(2),
1184 ExitPolicy::UnderlyingAbove(pos_or_panic!(105.0)),
1185 ]);
1186 let results = strategy.simulate(&simulator, exit_policy);
1187
1188 assert!(results.is_ok());
1189 let stats = results.unwrap();
1190 assert_eq!(stats.total_simulations, 1);
1191 }
1192
1193 #[test]
1194 fn test_simulate_multiple_simulations() {
1195 let strategy = create_test_short_put();
1196 let prices = vec![
1197 Positive::HUNDRED,
1198 pos_or_panic!(102.0),
1199 pos_or_panic!(104.0),
1200 ];
1201
1202 let walk_params = create_walk_params(prices);
1203 let Ok(simulator) = Simulator::new(
1204 "Test Simulator".to_string(),
1205 10,
1206 &walk_params,
1207 generator_positive,
1208 ) else {
1209 panic!("simulator setup failed");
1210 };
1211
1212 let exit_policy = ExitPolicy::Expiration;
1213 let results = strategy.simulate(&simulator, exit_policy);
1214
1215 assert!(results.is_ok());
1216 let stats = results.unwrap();
1217 assert_eq!(stats.total_simulations, 10);
1218 assert_eq!(stats.results.len(), 10);
1219 assert!(stats.profitable_count + stats.loss_count <= 10);
1220 }
1221
1222 #[test]
1223 fn test_simulate_stats_calculations() {
1224 let strategy = create_test_short_put();
1225 let prices = vec![
1226 Positive::HUNDRED,
1227 pos_or_panic!(105.0),
1228 pos_or_panic!(110.0),
1229 ];
1230
1231 let walk_params = create_walk_params(prices);
1232 let Ok(simulator) = Simulator::new(
1233 "Test Simulator".to_string(),
1234 3,
1235 &walk_params,
1236 generator_positive,
1237 ) else {
1238 panic!("simulator setup failed");
1239 };
1240
1241 let exit_policy = ExitPolicy::Expiration;
1242 let results = strategy.simulate(&simulator, exit_policy);
1243
1244 assert!(results.is_ok());
1245 let stats = results.unwrap();
1246
1247 assert!(stats.average_pnl.abs() >= dec!(0.0));
1249 assert!(stats.median_pnl.abs() >= dec!(0.0));
1250 assert!(stats.std_dev_pnl >= dec!(0.0));
1251 assert!(stats.best_pnl >= stats.worst_pnl);
1252 }
1253
1254 #[test]
1255 fn test_simulate_expiration_path() {
1256 let strategy = create_test_short_put();
1257 let prices = vec![
1259 Positive::HUNDRED,
1260 pos_or_panic!(100.5),
1261 pos_or_panic!(101.0),
1262 pos_or_panic!(100.8),
1263 ];
1264
1265 let walk_params = create_walk_params(prices);
1266 let Ok(simulator) = Simulator::new(
1267 "Test Simulator".to_string(),
1268 1,
1269 &walk_params,
1270 generator_positive,
1271 ) else {
1272 panic!("simulator setup failed");
1273 };
1274
1275 let exit_policy = ExitPolicy::Expiration;
1276 let results = strategy.simulate(&simulator, exit_policy);
1277
1278 assert!(results.is_ok());
1279 let stats = results.unwrap();
1280 assert_eq!(stats.total_simulations, 1);
1281 assert!(!stats.results.is_empty());
1282
1283 let result = &stats.results[0];
1285 assert!(result.expired);
1286 assert!(result.expiration_premium.is_some());
1287 }
1288
1289 #[test]
1290 fn test_simulate_profit_target_hit() {
1291 let strategy = create_test_short_put();
1292 let prices = vec![
1294 Positive::HUNDRED,
1295 pos_or_panic!(110.0),
1296 pos_or_panic!(120.0),
1297 ];
1298
1299 let walk_params = create_walk_params(prices);
1300 let Ok(simulator) = Simulator::new(
1301 "Test Simulator".to_string(),
1302 1,
1303 &walk_params,
1304 generator_positive,
1305 ) else {
1306 panic!("simulator setup failed");
1307 };
1308
1309 let exit_policy = ExitPolicy::ProfitPercent(dec!(0.5));
1310 let results = strategy.simulate(&simulator, exit_policy);
1311
1312 assert!(results.is_ok());
1313 let stats = results.unwrap();
1314 assert_eq!(stats.total_simulations, 1);
1315
1316 let result = &stats.results[0];
1317 assert!(result.hit_take_profit || result.expired);
1318 }
1319
1320 #[test]
1321 fn test_simulate_stop_loss_hit() {
1322 let strategy = create_test_short_put();
1323 let prices = vec![Positive::HUNDRED, pos_or_panic!(85.0), pos_or_panic!(70.0)];
1325
1326 let walk_params = create_walk_params(prices);
1327 let Ok(simulator) = Simulator::new(
1328 "Test Simulator".to_string(),
1329 1,
1330 &walk_params,
1331 generator_positive,
1332 ) else {
1333 panic!("simulator setup failed");
1334 };
1335
1336 let exit_policy = ExitPolicy::LossPercent(dec!(1.0));
1337 let results = strategy.simulate(&simulator, exit_policy);
1338
1339 assert!(results.is_ok());
1340 let stats = results.unwrap();
1341 assert_eq!(stats.total_simulations, 1);
1342
1343 let result = &stats.results[0];
1344 assert!(result.hit_stop_loss || result.expired);
1345 }
1346
1347 #[test]
1348 fn test_simulate_premium_tracking() {
1349 let strategy = create_test_short_put();
1350 let prices = vec![
1351 Positive::HUNDRED,
1352 pos_or_panic!(95.0),
1353 pos_or_panic!(105.0),
1354 Positive::HUNDRED,
1355 ];
1356
1357 let walk_params = create_walk_params(prices);
1358 let Ok(simulator) = Simulator::new(
1359 "Test Simulator".to_string(),
1360 1,
1361 &walk_params,
1362 generator_positive,
1363 ) else {
1364 panic!("simulator setup failed");
1365 };
1366
1367 let exit_policy = ExitPolicy::Expiration;
1368 let results = strategy.simulate(&simulator, exit_policy);
1369
1370 assert!(results.is_ok());
1371 let stats = results.unwrap();
1372
1373 let result = &stats.results[0];
1374 assert!(result.max_premium >= result.min_premium);
1376 assert!(result.avg_premium >= result.min_premium);
1377 assert!(result.avg_premium <= result.max_premium);
1378 }
1379
1380 #[test]
1381 fn test_simulate_holding_period() {
1382 let strategy = create_test_short_put();
1383 let prices = vec![
1384 Positive::HUNDRED,
1385 pos_or_panic!(101.0),
1386 pos_or_panic!(102.0),
1387 ];
1388 let price_count = prices.len();
1389
1390 let walk_params = create_walk_params(prices);
1391 let Ok(simulator) = Simulator::new(
1392 "Test Simulator".to_string(),
1393 1,
1394 &walk_params,
1395 generator_positive,
1396 ) else {
1397 panic!("simulator setup failed");
1398 };
1399
1400 let exit_policy = ExitPolicy::TimeSteps(1);
1401 let results = strategy.simulate(&simulator, exit_policy);
1402
1403 assert!(results.is_ok());
1404 let stats = results.unwrap();
1405
1406 let result = &stats.results[0];
1407 assert!(result.holding_period > 0);
1408 assert!(result.holding_period <= price_count);
1409 }
1410
1411 #[test]
1412 fn test_simulate_mixed_results() {
1413 let strategy = create_test_short_put();
1414 let prices = vec![Positive::HUNDRED, pos_or_panic!(102.0), pos_or_panic!(98.0)];
1415
1416 let walk_params = create_walk_params(prices);
1417 let Ok(simulator) = Simulator::new(
1418 "Test Simulator".to_string(),
1419 5,
1420 &walk_params,
1421 generator_positive,
1422 ) else {
1423 panic!("simulator setup failed");
1424 };
1425
1426 let exit_policy = ExitPolicy::Or(vec![
1427 ExitPolicy::ProfitPercent(dec!(0.5)),
1428 ExitPolicy::LossPercent(dec!(1.0)),
1429 ]);
1430 let results = strategy.simulate(&simulator, exit_policy);
1431
1432 assert!(results.is_ok());
1433 let stats = results.unwrap();
1434
1435 assert_eq!(stats.total_simulations, 5);
1437 assert_eq!(stats.results.len(), 5);
1438
1439 assert!(stats.win_rate >= dec!(0.0));
1441 assert!(stats.win_rate <= dec!(100.0));
1442 }
1443}