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