1use crate::simulation::WalkParams;
2use crate::simulation::randomwalk::RandomWalk;
3use crate::simulation::steps::Step;
4use optionstratlib_core::model::Options;
5use optionstratlib_core::model::Positive;
6use optionstratlib_core::utils::time::ReferenceCarrier;
7use optionstratlib_core::utils::{Len, deterministic_rng};
8use optionstratlib_pricing::error::PricingError;
9use optionstratlib_pricing::pricing::Profit;
10use optionstratlib_pricing::pricing::monte_carlo::price_option_monte_carlo;
11use optionstratlib_pricing::pricing::unified::MonteCarloPricer;
12use rand::RngExt;
13use rayon::prelude::*;
14use rust_decimal::Decimal;
15use std::fmt::Display;
16use std::ops::{AddAssign, Index, IndexMut};
17use tracing::warn;
18
19pub const PARALLEL_MIN_WALKS: usize = 3;
25
26const PARALLEL_ROUND_WALKS: usize = 1024;
31
32#[derive(Debug, Clone)]
58pub struct Simulator<X, Y>
59where
60 X: Copy + TryInto<Positive> + AddAssign + Display,
61 Y: TryInto<Positive> + Display + Clone,
62{
63 title: String,
64 random_walks: Vec<RandomWalk<X, Y>>,
65}
66
67impl<X, Y> Simulator<X, Y>
68where
69 X: Copy + TryInto<Positive> + AddAssign + Display,
70 Y: TryInto<Positive> + Display + Clone,
71{
72 pub fn new<F, E>(
114 title: String,
115 size: usize,
116 params: &WalkParams<X, Y>,
117 generator: F,
118 ) -> Result<Self, E>
119 where
120 F: Fn(&WalkParams<X, Y>) -> Result<Vec<Step<X, Y>>, E> + Clone + Send + Sync,
121 E: Send,
122 X: Copy + TryInto<Positive> + AddAssign + Display + Send + Sync,
123 Y: TryInto<Positive> + Display + Clone + Send + Sync,
124 {
125 let mut random_walks = Vec::new();
131 if let Err(e) = random_walks.try_reserve_exact(size) {
132 warn!(
133 size,
134 error = %e,
135 "Simulator::new: cannot reserve the walks up front; growing on demand"
136 );
137 }
138 let mut seeds = params.seed.map(deterministic_rng);
144 if size < PARALLEL_MIN_WALKS {
145 let mut walk_params = params.clone();
146 for i in 0..size {
147 if let Some(seeds) = seeds.as_mut() {
148 walk_params.seed = Some(seeds.random::<u64>());
149 }
150 let walk_title = format!("{title}_{i}");
151 random_walks.push(RandomWalk::new(
152 walk_title,
153 &walk_params,
154 generator.clone(),
155 )?);
156 }
157 } else {
158 let mut indices = 0..size;
164 loop {
165 let jobs: Vec<(usize, Option<u64>)> = indices
166 .by_ref()
167 .take(PARALLEL_ROUND_WALKS)
168 .map(|i| (i, seeds.as_mut().map(|seeds| seeds.random::<u64>())))
169 .collect();
170 if jobs.is_empty() {
171 break;
172 }
173 let carrier = ReferenceCarrier::capture();
175 let built: Vec<Result<RandomWalk<X, Y>, E>> = jobs
176 .into_par_iter()
177 .map_init(
178 || params.clone(),
179 |walk_params, (i, seed)| {
180 if seed.is_some() {
181 walk_params.seed = seed;
182 }
183 carrier.run(|| {
184 RandomWalk::new(
185 format!("{title}_{i}"),
186 walk_params,
187 generator.clone(),
188 )
189 })
190 },
191 )
192 .collect();
193 for walk in built {
194 random_walks.push(walk?);
195 }
196 }
197 }
198 Ok(Self {
199 title,
200 random_walks,
201 })
202 }
203
204 #[must_use]
210 pub fn get_title(&self) -> &str {
211 &self.title
212 }
213
214 pub fn set_title(&mut self, title: String) {
220 self.title = title;
221 }
222
223 #[must_use]
240 pub fn get_random_walks(&self) -> Vec<&RandomWalk<X, Y>> {
241 self.random_walks.iter().collect::<Vec<&RandomWalk<X, Y>>>()
242 }
243
244 #[must_use]
260 pub fn get_random_walk(&self, index: usize) -> Option<&RandomWalk<X, Y>> {
261 self.random_walks.get(index)
262 }
263
264 pub fn get_random_walk_mut(&mut self, index: usize) -> Option<&mut RandomWalk<X, Y>> {
278 self.random_walks.get_mut(index)
279 }
280
281 #[must_use]
288 pub fn first(&self) -> Option<&RandomWalk<X, Y>> {
289 self.random_walks.first()
290 }
291
292 #[must_use]
301 pub fn last(&self) -> Option<&RandomWalk<X, Y>> {
302 self.random_walks.last()
303 }
304
305 #[must_use]
320 pub fn get_steps(&self) -> Vec<Vec<&Step<X, Y>>> {
321 self.into_iter().map(|step| step.get_steps()).collect()
322 }
323
324 #[must_use]
334 pub fn get_last_steps(&self) -> Vec<&Step<X, Y>> {
335 self.into_iter().filter_map(|step| step.last()).collect()
336 }
337
338 #[must_use]
345 pub fn get_last_values(&self) -> Vec<&Step<X, Y>> {
346 self.into_iter().filter_map(|step| step.last()).collect()
347 }
348
349 #[must_use]
362 pub fn get_last_positive_values(&self) -> Vec<Positive> {
363 let last_values = self.get_last_values();
364 last_values
365 .iter()
366 .filter_map(|step| step.get_positive_value().ok())
367 .collect::<Vec<Positive>>()
368 }
369
370 pub fn get_mc_option_price(&self, option: &Options) -> Result<Positive, PricingError> {
397 let last_values = self.get_last_positive_values();
398 price_option_monte_carlo(option, &last_values)
399 }
400}
401
402impl MonteCarloPricer for Simulator<Positive, Positive> {
403 #[inline]
408 fn price_monte_carlo(&self, option: &Options) -> Result<Positive, PricingError> {
409 self.get_mc_option_price(option)
410 .map_err(|e| PricingError::simulation_error(&e.to_string()))
411 }
412}
413
414impl<X, Y> Len for Simulator<X, Y>
415where
416 X: Copy + TryInto<Positive> + AddAssign + Display,
417 Y: TryInto<Positive> + Display + Clone,
418{
419 fn len(&self) -> usize {
427 self.random_walks.len()
428 }
429
430 fn is_empty(&self) -> bool {
437 self.random_walks.is_empty()
438 }
439}
440
441impl<X, Y> Index<usize> for Simulator<X, Y>
442where
443 X: Copy + TryInto<Positive> + AddAssign + Display,
444 Y: TryInto<Positive> + Display + Clone,
445{
446 type Output = RandomWalk<X, Y>;
456
457 #[allow(clippy::indexing_slicing)]
473 fn index(&self, index: usize) -> &Self::Output {
474 &self.random_walks[index]
478 }
479}
480
481impl<X, Y> IndexMut<usize> for Simulator<X, Y>
482where
483 X: Copy + TryInto<Positive> + AddAssign + Display,
484 Y: TryInto<Positive> + Display + Clone,
485{
486 #[allow(clippy::indexing_slicing)]
494 fn index_mut(&mut self, index: usize) -> &mut Self::Output {
495 &mut self.random_walks[index]
499 }
500}
501
502impl<X, Y> Display for Simulator<X, Y>
503where
504 X: Copy + TryInto<Positive> + AddAssign + Display,
505 Y: TryInto<Positive> + Display + Clone,
506{
507 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
508 writeln!(f, "{}", self.title)?;
509 for random_walk in &self.random_walks {
510 writeln!(f, "\t{random_walk}")?;
511 }
512 Ok(())
513 }
514}
515
516impl<X, Y> Profit for Simulator<X, Y>
517where
518 X: AddAssign + Copy + Display + TryInto<Positive>,
519 Y: Clone + Display + TryInto<Positive>,
520{
521 fn calculate_profit_at(&self, _price: &Positive) -> Result<Decimal, PricingError> {
522 Err(PricingError::other(
523 "Profit calculation not implemented for Simulator",
524 ))
525 }
526}
527
528impl<'a, X, Y> IntoIterator for &'a Simulator<X, Y>
529where
530 X: Copy + TryInto<Positive> + AddAssign + Display,
531 Y: TryInto<Positive> + Display + Clone,
532{
533 type Item = &'a RandomWalk<X, Y>;
534 type IntoIter = std::slice::Iter<'a, RandomWalk<X, Y>>;
535
536 fn into_iter(self) -> Self::IntoIter {
537 self.random_walks.iter()
538 }
539}
540
541#[cfg(test)]
542#[allow(irrefutable_let_patterns)]
543mod tests {
544 use super::*;
545 use crate::error::SimulationError;
546 use crate::simulation::generator_positive;
547 use crate::simulation::{
548 WalkParams, WalkType, WalkTypeAble,
549 steps::{Step, Xstep, Ystep},
550 };
551 use optionstratlib_core::model::ExpirationDate;
552 use optionstratlib_core::pos_or_panic;
553 use optionstratlib_core::utils::{TimeFrame, time::convert_time_frame};
554 use optionstratlib_pricing::pricing::OptionPricing;
555 use rust_decimal_macros::dec;
556 use std::convert::Infallible;
557 use tracing::{debug, info};
558
559 const SEED: u64 = 685;
561
562 #[derive(Clone)]
564 struct TestWalker;
565
566 impl TestWalker {
567 fn new() -> Self {
568 TestWalker {}
569 }
570 }
571 impl WalkTypeAble<Positive, Positive> for TestWalker {}
572
573 fn test_generator(
574 params: &WalkParams<Positive, Positive>,
575 ) -> Result<Vec<Step<Positive, Positive>>, Infallible> {
576 Ok(vec![params.init_step.clone()])
577 }
578
579 #[test]
583 fn test_monte_carlo_pricer_matches_the_simulator_on_the_same_paths() {
584 use optionstratlib_core::model::types::{OptionStyle, OptionType, Side};
585 use optionstratlib_pricing::pricing::{GenericPricingEngine, price_option_with};
586
587 let prices: Vec<Positive> = (0..12)
588 .map(|i| pos_or_panic!(100.0 + f64::from(i) * 1.5))
589 .collect();
590 let params = WalkParams {
591 size: prices.len(),
592 init_step: Step::new(
593 Positive::ONE,
594 TimeFrame::Day,
595 ExpirationDate::Days(pos_or_panic!(30.0)),
596 Positive::HUNDRED,
597 ),
598 walker: Box::new(TestWalker),
599 walk_type: WalkType::Historical {
600 timeframe: TimeFrame::Day,
601 prices,
602 symbol: None,
603 },
604 seed: None,
605 };
606 let simulator =
607 Simulator::new("MC parity".to_string(), 8, ¶ms, generator_positive).unwrap();
608 let option = Options {
609 option_type: OptionType::European,
610 side: Side::Long,
611 underlying_symbol: "TEST".to_string(),
612 strike_price: Positive::HUNDRED,
613 expiration_date: ExpirationDate::Days(pos_or_panic!(30.0)),
614 implied_volatility: pos_or_panic!(0.2),
615 quantity: Positive::ONE,
616 contract_size: Positive::ONE,
617 underlying_price: Positive::HUNDRED,
618 risk_free_rate: dec!(0.05),
619 option_style: OptionStyle::Call,
620 dividend_yield: Positive::ZERO,
621 exotic_params: None,
622 };
623
624 let expected = simulator.get_mc_option_price(&option).unwrap();
625 assert!(expected > Positive::ZERO);
626 assert_eq!(expected, simulator.price_monte_carlo(&option).unwrap());
627
628 let generic = GenericPricingEngine::MonteCarlo {
629 simulator: simulator.clone(),
630 };
631 assert_eq!(price_option_with(&option, &generic).unwrap(), expected);
632
633 let borrowed = GenericPricingEngine::MonteCarlo {
634 simulator: &simulator,
635 };
636 assert_eq!(price_option_with(&option, &borrowed).unwrap(), expected);
637 }
638
639 #[test]
643 fn test_monte_carlo_price_from_simulator_matches_black_scholes()
644 -> Result<(), Box<dyn std::error::Error>> {
645 use optionstratlib_core::assert_pos_relative_eq;
646 use optionstratlib_core::model::utils::create_sample_option;
647 use optionstratlib_core::model::{OptionStyle, Side};
648 use optionstratlib_pricing::pricing::monte_carlo::price_option_monte_carlo;
649
650 let walker = Box::new(TestWalker);
651 let initial_price = pos_or_panic!(1000.0);
652 let days = pos_or_panic!(365.0);
653 let volatility = pos_or_panic!(0.2);
654 let mut option = create_sample_option(
655 OptionStyle::Call,
656 Side::Long,
657 initial_price,
658 Positive::ONE,
659 initial_price,
660 volatility,
661 );
662 option.risk_free_rate = dec!(0.05);
663 option.dividend_yield = pos_or_panic!(0.02);
664 option.expiration_date = ExpirationDate::Days(days);
665
666 let init_step = Step {
667 x: Xstep::new(Positive::ONE, TimeFrame::Day, ExpirationDate::Days(days)),
668 y: Ystep::new(0, initial_price),
669 };
670
671 let dt = convert_time_frame(Positive::ONE, &TimeFrame::Day, &TimeFrame::Year)?;
672 let walk_params = WalkParams {
673 size: 365,
674 init_step,
675 walk_type: WalkType::Custom {
676 dt,
677 drift: dec!(0.02),
678 volatility,
679 vov: pos_or_panic!(0.01),
680 vol_speed: Default::default(),
681 vol_mean: pos_or_panic!(0.2),
682 },
683 walker,
684 seed: Some(SEED),
685 };
686
687 let Ok(simulator) = Simulator::new(
688 "Test Simulator".to_string(),
689 100,
690 &walk_params,
691 generator_positive,
692 ) else {
693 panic!("simulator setup failed");
694 };
695
696 let get_last_positive_values = simulator.get_last_positive_values();
697
698 let result = price_option_monte_carlo(&option, &get_last_positive_values);
699 assert!(result.is_ok());
700
701 let bs = option.calculate_price_black_scholes().unwrap();
702 assert_pos_relative_eq!(
703 result.unwrap(),
704 Positive::new_decimal(bs).unwrap(),
705 pos_or_panic!(10.0)
706 );
707 Ok(())
708 }
709
710 #[test]
712 fn test_simulator_creation() -> Result<(), Box<dyn std::error::Error>> {
713 let walker = Box::new(TestWalker);
714 let initial_price = Positive::HUNDRED;
715 let init_step = Step {
716 x: Xstep::new(
717 Positive::ONE,
718 TimeFrame::Minute,
719 ExpirationDate::Days(pos_or_panic!(30.0)),
720 ),
721 y: Ystep::new(0, initial_price),
722 };
723
724 let walk_params = WalkParams {
725 size: 5,
726 init_step,
727 walk_type: WalkType::GeometricBrownian {
728 dt: convert_time_frame(
729 Positive::ONE / pos_or_panic!(30.0),
730 &TimeFrame::Minute,
731 &TimeFrame::Day,
732 )?,
733 drift: dec!(0.0),
734 volatility: pos_or_panic!(0.2),
735 },
736 walker,
737 seed: Some(SEED),
738 };
739
740 let Ok(simulator) = Simulator::new(
741 "Test Simulator".to_string(),
742 5,
743 &walk_params,
744 test_generator,
745 ) else {
746 unreachable!()
747 };
748
749 assert_eq!(simulator.get_title(), "Test Simulator");
750 assert_eq!(simulator.len(), 5);
751 assert!(!simulator.is_empty());
752 Ok(())
753 }
754
755 #[test]
757 fn test_simulator_title_methods() -> Result<(), Box<dyn std::error::Error>> {
758 let walker = Box::new(TestWalker);
759 let initial_price = Positive::HUNDRED;
760 let init_step = Step {
761 x: Xstep::new(
762 Positive::ONE,
763 TimeFrame::Minute,
764 ExpirationDate::Days(pos_or_panic!(30.0)),
765 ),
766 y: Ystep::new(0, initial_price),
767 };
768
769 let walk_params = WalkParams {
770 size: 3,
771 init_step,
772 walk_type: WalkType::GeometricBrownian {
773 dt: convert_time_frame(
774 Positive::ONE / pos_or_panic!(30.0),
775 &TimeFrame::Minute,
776 &TimeFrame::Day,
777 )?,
778 drift: dec!(0.0),
779 volatility: pos_or_panic!(0.2),
780 },
781 walker,
782 seed: Some(SEED),
783 };
784
785 let Ok(mut simulator) = Simulator::new(
786 "Original Title".to_string(),
787 3,
788 &walk_params,
789 test_generator,
790 ) else {
791 unreachable!()
792 };
793
794 assert_eq!(simulator.get_title(), "Original Title");
795
796 simulator.set_title("New Title".to_string());
797 assert_eq!(simulator.get_title(), "New Title");
798 Ok(())
799 }
800
801 #[test]
803 fn test_simulator_step_access() -> Result<(), Box<dyn std::error::Error>> {
804 let walker = Box::new(TestWalker);
805 let initial_price = Positive::HUNDRED;
806 let init_step = Step {
807 x: Xstep::new(
808 Positive::ONE,
809 TimeFrame::Minute,
810 ExpirationDate::Days(pos_or_panic!(30.0)),
811 ),
812 y: Ystep::new(0, initial_price),
813 };
814
815 let walk_params = WalkParams {
816 size: 3,
817 init_step,
818 walk_type: WalkType::GeometricBrownian {
819 dt: convert_time_frame(
820 Positive::ONE / pos_or_panic!(30.0),
821 &TimeFrame::Minute,
822 &TimeFrame::Day,
823 )?,
824 drift: dec!(0.0),
825 volatility: pos_or_panic!(0.2),
826 },
827 walker,
828 seed: Some(SEED),
829 };
830
831 let Ok(simulator) = Simulator::new(
832 "Test Simulator".to_string(),
833 3,
834 &walk_params,
835 test_generator,
836 ) else {
837 unreachable!()
838 };
839
840 let steps = simulator.get_random_walks();
842 assert_eq!(steps.len(), 3);
843
844 let step = simulator.get_random_walk(1).unwrap();
846 assert_eq!(step.get_title(), "Test Simulator_1");
847 assert!(simulator.get_random_walk(99).is_none());
851
852 assert!(simulator.first().is_some());
854 assert!(simulator.last().is_some());
855 assert_eq!(
856 simulator.first().expect("should be Ok").get_title(),
857 "Test Simulator_0"
858 );
859 assert_eq!(
860 simulator.last().expect("should be Ok").get_title(),
861 "Test Simulator_2"
862 );
863 Ok(())
864 }
865
866 #[test]
868 fn test_simulator_indexing() -> Result<(), Box<dyn std::error::Error>> {
869 let walker = Box::new(TestWalker);
870 let initial_price = Positive::HUNDRED;
871 let init_step = Step {
872 x: Xstep::new(
873 Positive::ONE,
874 TimeFrame::Minute,
875 ExpirationDate::Days(pos_or_panic!(30.0)),
876 ),
877 y: Ystep::new(0, initial_price),
878 };
879
880 let walk_params = WalkParams {
881 size: 3,
882 init_step,
883 walk_type: WalkType::GeometricBrownian {
884 dt: convert_time_frame(
885 Positive::ONE / pos_or_panic!(30.0),
886 &TimeFrame::Minute,
887 &TimeFrame::Day,
888 )?,
889 drift: dec!(0.0),
890 volatility: pos_or_panic!(0.2),
891 },
892 walker,
893 seed: Some(SEED),
894 };
895
896 let Ok(mut simulator) = Simulator::new(
897 "Test Simulator".to_string(),
898 3,
899 &walk_params,
900 test_generator,
901 ) else {
902 unreachable!()
903 };
904
905 assert_eq!(simulator[0].get_title(), "Test Simulator_0");
907 assert_eq!(simulator[1].get_title(), "Test Simulator_1");
908 assert_eq!(simulator[2].get_title(), "Test Simulator_2");
909
910 simulator[1].set_title("Modified Title".to_string());
912 assert_eq!(simulator[1].get_title(), "Modified Title");
913 Ok(())
914 }
915
916 #[test]
918 fn test_simulator_display() -> Result<(), Box<dyn std::error::Error>> {
919 let walker = Box::new(TestWalker);
920 let initial_price = Positive::HUNDRED;
921 let init_step = Step {
922 x: Xstep::new(
923 Positive::ONE,
924 TimeFrame::Minute,
925 ExpirationDate::Days(pos_or_panic!(30.0)),
926 ),
927 y: Ystep::new(0, initial_price),
928 };
929
930 let walk_params = WalkParams {
931 size: 2,
932 init_step,
933 walk_type: WalkType::GeometricBrownian {
934 dt: convert_time_frame(
935 Positive::ONE / pos_or_panic!(30.0),
936 &TimeFrame::Minute,
937 &TimeFrame::Day,
938 )?,
939 drift: dec!(0.0),
940 volatility: pos_or_panic!(0.2),
941 },
942 walker,
943 seed: Some(SEED),
944 };
945
946 let Ok(simulator) =
947 Simulator::new("Display Test".to_string(), 2, &walk_params, test_generator)
948 else {
949 unreachable!()
950 };
951
952 let display_output = format!("{simulator}");
953 assert!(display_output.starts_with("Display Test"));
954 assert!(display_output.contains("Display Test_0"));
955 assert!(display_output.contains("Display Test_1"));
956 Ok(())
957 }
958
959 #[test]
961 fn test_simulator_empty() {
962 let simulator: Simulator<Positive, Positive> = Simulator {
963 title: "Empty Simulator".to_string(),
964 random_walks: Vec::new(),
965 };
966
967 assert_eq!(simulator.get_title(), "Empty Simulator");
968 assert_eq!(simulator.len(), 0);
969 assert!(simulator.is_empty());
970 assert!(simulator.first().is_none());
971 assert!(simulator.last().is_none());
972 }
973
974 #[test]
975 fn test_simulator_new_propagates_generator_error_short_circuits()
976 -> Result<(), Box<dyn std::error::Error>> {
977 use std::sync::atomic::{AtomicU32, Ordering};
981 let walker = Box::new(TestWalker);
982 let initial_price = Positive::HUNDRED;
983 let init_step = Step {
984 x: Xstep::new(
985 Positive::ONE,
986 TimeFrame::Minute,
987 ExpirationDate::Days(pos_or_panic!(30.0)),
988 ),
989 y: Ystep::new(0, initial_price),
990 };
991
992 let walk_params = WalkParams {
993 size: 1,
994 init_step,
995 walk_type: WalkType::GeometricBrownian {
996 dt: convert_time_frame(
997 Positive::ONE / pos_or_panic!(30.0),
998 &TimeFrame::Minute,
999 &TimeFrame::Day,
1000 )?,
1001 drift: dec!(0.0),
1002 volatility: pos_or_panic!(0.2),
1003 },
1004 walker,
1005 seed: Some(SEED),
1006 };
1007
1008 let calls = AtomicU32::new(0);
1013 let size = PARALLEL_MIN_WALKS - 1;
1014 let result: Result<Simulator<Positive, Positive>, &'static str> =
1015 Simulator::new("Err Sim".to_string(), size, &walk_params, |p| {
1016 let n = calls.fetch_add(1, Ordering::SeqCst);
1017 if n == 0 {
1018 Err("boom")
1019 } else {
1020 Ok(vec![p.init_step.clone()])
1021 }
1022 });
1023
1024 match result {
1025 Err(msg) => assert_eq!(msg, "boom"),
1026 Ok(_) => panic!("expected generator error to propagate"),
1027 }
1028 assert_eq!(calls.load(Ordering::SeqCst), 1);
1030 Ok(())
1031 }
1032
1033 #[test]
1035 #[should_panic(expected = "index out of bounds")]
1036 fn test_simulator_index_out_of_bounds() {
1037 let walker = Box::new(TestWalker);
1038 let initial_price = Positive::HUNDRED;
1039 let init_step = Step {
1040 x: Xstep::new(
1041 Positive::ONE,
1042 TimeFrame::Minute,
1043 ExpirationDate::Days(pos_or_panic!(30.0)),
1044 ),
1045 y: Ystep::new(0, initial_price),
1046 };
1047
1048 let walk_params = WalkParams {
1049 size: 3,
1050 init_step,
1051 walk_type: WalkType::GeometricBrownian {
1052 dt: match convert_time_frame(
1053 Positive::ONE / pos_or_panic!(30.0),
1054 &TimeFrame::Minute,
1055 &TimeFrame::Day,
1056 ) {
1057 Ok(value) => value,
1058 Err(error) => panic!("convert_time_frame failed: {error}"),
1059 },
1060 drift: dec!(0.0),
1061 volatility: pos_or_panic!(0.2),
1062 },
1063 walker,
1064 seed: Some(SEED),
1065 };
1066
1067 let Ok(simulator) =
1068 Simulator::new("Panic Test".to_string(), 3, &walk_params, test_generator)
1069 else {
1070 unreachable!()
1071 };
1072
1073 let _ = simulator[3];
1075 }
1076
1077 #[test]
1078 fn test_full_simulation() -> Result<(), SimulationError> {
1079 let simulator_size: usize = 5;
1080 let n_steps = 10;
1081 let initial_price = Positive::HUNDRED;
1082 let std_dev = pos_or_panic!(20.0);
1083 let walker = Box::new(TestWalker::new());
1084 let days = Positive::TWO;
1085
1086 let walk_params = WalkParams {
1087 size: n_steps,
1088 init_step: Step {
1089 x: Xstep::new(Positive::ONE, TimeFrame::Hour, ExpirationDate::Days(days)),
1090 y: Ystep::new(0, initial_price),
1091 },
1092 walk_type: WalkType::GeometricBrownian {
1093 dt: convert_time_frame(Positive::ONE / days, &TimeFrame::Hour, &TimeFrame::Day)?,
1094 drift: dec!(0.0),
1095 volatility: std_dev,
1096 },
1097 walker,
1098 seed: Some(SEED),
1099 };
1100
1101 assert_eq!(walk_params.size, n_steps);
1102 assert_eq!(walk_params.init_step.get_value(), &Positive::HUNDRED);
1103 assert_eq!(walk_params.y(), &Positive::HUNDRED);
1104
1105 let simulator = Simulator::new(
1106 "Simulator".to_string(),
1107 simulator_size,
1108 &walk_params,
1109 generator_positive,
1110 )?;
1111 debug!("Simulator: {}", simulator);
1112 assert_eq!(simulator.get_title(), "Simulator");
1113 assert_eq!(simulator.len(), simulator_size);
1114
1115 let random_walk = simulator[0].clone();
1116 assert_eq!(random_walk.get_title(), "Simulator_0");
1117 assert_eq!(random_walk.len(), n_steps);
1118
1119 let step = random_walk[0].clone();
1120 assert_eq!(*step.get_index(), Positive::ONE);
1121 let step_string = format!("{step}");
1122 assert_eq!(step.to_string(), step_string);
1123
1124 let y_step = step.get_y_step();
1125 assert_eq!(*y_step.index(), 0);
1126 assert_eq!(*y_step.value(), Positive::HUNDRED);
1127
1128 let x_step = step.get_x_step();
1129 assert_eq!(*x_step.index(), 0);
1130 assert_eq!(*x_step.step_size_in_time(), Positive::ONE);
1131 assert_eq!(x_step.time_unit(), &TimeFrame::Hour);
1132 assert_eq!(x_step.days_left()?, Positive::TWO);
1133
1134 let next_step = step.next(pos_or_panic!(200.0)).expect("should be Ok");
1135 assert_eq!(next_step.get_value(), &pos_or_panic!(200.0));
1136 let next_step_string = format!("{next_step}");
1137 assert_eq!(next_step.to_string(), next_step_string);
1138
1139 let previous_step = step.previous(pos_or_panic!(50.0))?;
1140 assert_eq!(previous_step.get_value(), &pos_or_panic!(50.0));
1141 let previous_step_string = format!("{previous_step}");
1142 assert_eq!(previous_step.to_string(), previous_step_string);
1143
1144 let x_step = step.get_x_step();
1145 let next_x_step = x_step.next().expect("should be Ok");
1146 assert_eq!(*next_x_step.index(), 1);
1147 assert_eq!(*next_x_step.step_size_in_time(), Positive::ONE);
1148 let next_x_step_string = format!("{next_x_step}");
1149 assert_eq!(next_x_step.to_string(), next_x_step_string);
1150
1151 let y_step = step.get_y_step();
1152 assert_eq!(*y_step.index(), 0);
1153 assert_eq!(*y_step.value(), Positive::HUNDRED);
1154 assert_eq!(y_step.positive().unwrap(), Positive::HUNDRED);
1155
1156 let last_steps: Vec<&Step<Positive, Positive>> = simulator
1157 .into_iter()
1158 .map(|step| step.last().expect("should be Ok"))
1159 .collect();
1160 info!("Last Steps: {:?}", last_steps);
1161 assert_eq!(last_steps.len(), simulator_size);
1162
1163 let last_values: Vec<&Positive> = simulator
1164 .into_iter()
1165 .map(|step| step.last().expect("should be Ok").get_value())
1166 .collect();
1167 info!("Last Values: {:?}", last_values);
1168 assert_eq!(last_values.len(), simulator_size);
1169
1170 Ok(())
1171 }
1172}
1173
1174#[cfg(test)]
1178mod tests_new_panic_freedom {
1179 use super::*;
1180 use crate::simulation::{WalkType, WalkTypeAble};
1181 use optionstratlib_core::model::ExpirationDate;
1182 use optionstratlib_core::utils::TimeFrame;
1183 use rust_decimal::Decimal;
1184
1185 #[derive(Debug, Clone)]
1186 struct BareWalker;
1187
1188 impl WalkTypeAble<Positive, Positive> for BareWalker {}
1189
1190 #[test]
1191 fn test_new_unreservable_size_reaches_the_generator() {
1192 let params = WalkParams {
1193 size: 3,
1194 init_step: Step::new(
1195 Positive::ONE,
1196 TimeFrame::Day,
1197 ExpirationDate::Days(Positive::HUNDRED),
1198 Positive::HUNDRED,
1199 ),
1200 walk_type: WalkType::Brownian {
1201 dt: Positive::ONE,
1202 drift: Decimal::ZERO,
1203 volatility: Positive::ONE,
1204 },
1205 walker: Box::new(BareWalker),
1206 seed: Some(788),
1207 };
1208 let result: Result<Simulator<Positive, Positive>, &'static str> =
1209 Simulator::new("Huge".to_string(), usize::MAX, ¶ms, |_| Err("boom"));
1210 assert!(matches!(result, Err("boom")));
1211 }
1212}