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