1use crate::error::SimulationError;
18use crate::simulation::steps::{Step, Xstep, Ystep};
19use crate::simulation::{WalkParams, WalkType};
20use optionstratlib_core::model::Positive;
21use optionstratlib_core::model::decimal::d_sqrt;
22use optionstratlib_core::utils::TimeFrame;
23use optionstratlib_core::utils::numeric::calculate_log_returns;
24use optionstratlib_core::utils::time::ReferenceCarrier;
25use optionstratlib_pricing::volatility::{adjust_volatility, constant_volatility};
26use rust_decimal::Decimal;
27use std::convert::TryInto;
28use std::fmt::Display;
29use tracing::debug;
30
31fn walk_volatility<Y>(
44 walk_params: &WalkParams<Positive, Y>,
45) -> Result<Option<Positive>, SimulationError>
46where
47 Y: TryInto<Positive> + Display + Clone,
48{
49 match &walk_params.walk_type {
50 WalkType::Historical {
51 timeframe, prices, ..
52 } => {
53 let log_returns: Vec<Decimal> = calculate_log_returns(prices)?;
54 let constant_volatility = constant_volatility(&log_returns)?;
55 let implied_volatility =
56 adjust_volatility(constant_volatility, *timeframe, TimeFrame::Year)?;
57 Ok(Some(implied_volatility))
58 }
59 walk_type => Ok(walk_type.volatility()),
60 }
61}
62
63pub fn expanding_window_vols(
121 prices: &[Positive],
122 timeframe: TimeFrame,
123) -> Result<Option<Vec<Positive>>, SimulationError> {
124 let n = prices.len();
125 if n < 3 {
126 return Ok(None);
128 }
129 let log_returns: Vec<Decimal> = calculate_log_returns(prices)?;
130
131 let overflow =
132 |what: &str| SimulationError::walk_error(&format!("expanding-window {what} overflowed"));
133
134 let mut sum = Decimal::ZERO;
135 let mut sq_sum = Decimal::ZERO;
136 let mut raw: Vec<Option<Positive>> = Vec::with_capacity(n);
137 raw.push(None); for (i, r) in log_returns.iter().copied().enumerate() {
140 sum = sum
141 .checked_add(r)
142 .ok_or_else(|| overflow("running sum of log returns"))?;
143 let r_sq = r
144 .checked_mul(r)
145 .ok_or_else(|| overflow("squared log return"))?;
146 sq_sum = sq_sum
147 .checked_add(r_sq)
148 .ok_or_else(|| overflow("running sum of squared log returns"))?;
149
150 let count = i.checked_add(1).ok_or_else(|| overflow("return count"))?;
153 if count < 2 {
154 raw.push(None);
155 continue;
156 }
157 let count_dec = Decimal::from(count as u64);
158 let denom = count_dec
159 .checked_sub(Decimal::ONE)
160 .ok_or_else(|| overflow("sample size minus one"))?;
161 let sum_sq = sum
165 .checked_mul(sum)
166 .ok_or_else(|| overflow("squared running sum"))?;
167 let mean_sq_total = sum_sq
168 .checked_div(count_dec)
169 .ok_or_else(|| overflow("mean projection"))?;
170 let variance = sq_sum
171 .checked_sub(mean_sq_total)
172 .ok_or_else(|| overflow("variance numerator"))?
173 .checked_div(denom)
174 .ok_or_else(|| overflow("variance"))?
175 .max(Decimal::ZERO);
176 let std_dev = d_sqrt(variance, "simulation::walk_driver::volatility_sqrt")
177 .map_err(|_| overflow("volatility sqrt"))?;
178 let std_dev = Positive::new_decimal(std_dev).unwrap_or(Positive::ZERO);
179 let annualized = adjust_volatility(std_dev, timeframe, TimeFrame::Year)?;
180 raw.push(Some(annualized));
181 }
182
183 let first_computable = raw.iter().flatten().next().copied();
184 match first_computable {
185 None => Ok(None),
186 Some(fill) => Ok(Some(
187 raw.into_iter().map(|vol| vol.unwrap_or(fill)).collect(),
188 )),
189 }
190}
191
192pub fn walk_steps<Y, E, F>(
245 walk_params: &WalkParams<Positive, Y>,
246 mut next_y: F,
247) -> Result<Vec<Step<Positive, Y>>, E>
248where
249 Y: TryInto<Positive> + Display + Clone,
250 E: From<SimulationError>,
251 F: FnMut(&Positive, Option<Positive>, &Xstep<Positive>) -> Result<Option<Y>, E>,
252{
253 debug!("{}", walk_params);
254 let path = walk_params
255 .walker
256 .generate_with_vol(walk_params)
257 .map_err(E::from)?;
258 let y_steps = path.prices;
259 if y_steps.len() <= 1 {
260 return Ok(vec![walk_params.init_step.clone()]);
264 }
265
266 let step_vols: Option<Vec<Positive>> = match path.vols {
271 Some(vols) => Some(vols),
272 None => match &walk_params.walk_type {
273 WalkType::Historical { timeframe, .. } => {
274 expanding_window_vols(&y_steps, *timeframe).map_err(E::from)?
275 }
276 _ => None,
277 },
278 };
279 let constant_vol = if step_vols.is_some() {
280 None
281 } else {
282 walk_volatility(walk_params).map_err(E::from)?
283 };
284
285 let mut steps: Vec<Step<Positive, Y>> = vec![walk_params.init_step.clone()];
286 let mut previous_x_step = walk_params.init_step.x;
287 let mut y_index = *walk_params.ystep_ref().index();
288
289 for (i, y_step) in y_steps.iter().enumerate().skip(1) {
291 previous_x_step = match previous_x_step.next() {
292 Ok(x_step) => x_step,
293 Err(SimulationError::ExpirationReached) => break,
295 Err(e) => return Err(E::from(e)),
297 };
298 let volatility = step_vols
299 .as_ref()
300 .and_then(|vols| vols.get(i).copied())
301 .or(constant_vol);
302 let Some(y_value) = next_y(y_step, volatility, &previous_x_step)? else {
303 break;
304 };
305 y_index = y_index.checked_add(1).ok_or_else(|| {
308 E::from(SimulationError::step_error(
309 "step index overflowed advancing the walk",
310 ))
311 })?;
312 steps.push(Step {
313 x: previous_x_step,
314 y: Ystep::new(y_index, y_value),
315 });
316 }
317
318 if steps.len() > walk_params.size {
319 debug!(
320 "walk produced {} steps, truncating to configured size {}",
321 steps.len(),
322 walk_params.size
323 );
324 steps.truncate(walk_params.size);
325 }
326 Ok(steps)
327}
328
329pub fn walk_steps_par<Y, E, F>(
370 walk_params: &WalkParams<Positive, Y>,
371 next_y: F,
372) -> Result<Vec<Step<Positive, Y>>, E>
373where
374 Y: TryInto<Positive> + Display + Clone + Send,
375 E: From<SimulationError> + Send,
376 F: Fn(&Positive, Option<Positive>, &Xstep<Positive>) -> Result<Option<Y>, E> + Sync,
377{
378 use rayon::prelude::*;
379
380 debug!("{}", walk_params);
381 let path = walk_params
382 .walker
383 .generate_with_vol(walk_params)
384 .map_err(E::from)?;
385 let y_steps = path.prices;
386 if y_steps.len() <= 1 {
387 return Ok(vec![walk_params.init_step.clone()]);
391 }
392
393 let step_vols: Option<Vec<Positive>> = match path.vols {
395 Some(vols) => Some(vols),
396 None => match &walk_params.walk_type {
397 WalkType::Historical { timeframe, .. } => {
398 expanding_window_vols(&y_steps, *timeframe).map_err(E::from)?
399 }
400 _ => None,
401 },
402 };
403 let constant_vol = if step_vols.is_some() {
404 None
405 } else {
406 walk_volatility(walk_params).map_err(E::from)?
407 };
408
409 let mut x_steps: Vec<Xstep<Positive>> = Vec::with_capacity(y_steps.iter().skip(1).len());
412 let mut current_x = walk_params.init_step.x;
413 for _ in 1..y_steps.len() {
414 current_x = match current_x.next() {
415 Ok(x_step) => x_step,
416 Err(SimulationError::ExpirationReached) => break,
418 Err(e) => return Err(E::from(e)),
420 };
421 x_steps.push(current_x);
422 }
423
424 let carrier = ReferenceCarrier::capture();
430 let built: Vec<Result<Option<Y>, E>> = x_steps
431 .par_iter()
432 .enumerate()
433 .map(|(offset, x_step)| {
434 let price_index = offset
435 .checked_add(1)
436 .ok_or_else(|| E::from(SimulationError::walk_error("x-step offset overflowed")))?;
437 let y_step = y_steps.get(price_index).copied().ok_or_else(|| {
438 E::from(SimulationError::walk_error(
439 "walker path shorter than x-steps",
440 ))
441 })?;
442 let volatility = step_vols
443 .as_ref()
444 .and_then(|vols| vols.get(price_index).copied())
445 .or(constant_vol);
446 carrier.run(|| next_y(&y_step, volatility, x_step))
447 })
448 .collect();
449
450 let mut steps: Vec<Step<Positive, Y>> = vec![walk_params.init_step.clone()];
454 let mut y_index = *walk_params.ystep_ref().index();
455 for (x_step, y_result) in x_steps.into_iter().zip(built) {
456 let y_value = match y_result {
457 Ok(Some(y_value)) => y_value,
458 Ok(None) => break,
461 Err(e) => return Err(e),
462 };
463 y_index = y_index.checked_add(1).ok_or_else(|| {
466 E::from(SimulationError::step_error(
467 "step index overflowed advancing the walk",
468 ))
469 })?;
470 steps.push(Step {
471 x: x_step,
472 y: Ystep::new(y_index, y_value),
473 });
474 }
475
476 if steps.len() > walk_params.size {
477 debug!(
478 "walk produced {} steps, truncating to configured size {}",
479 steps.len(),
480 walk_params.size
481 );
482 steps.truncate(walk_params.size);
483 }
484 Ok(steps)
485}
486
487pub fn generator_positive(
517 walk_params: &WalkParams<Positive, Positive>,
518) -> Result<Vec<Step<Positive, Positive>>, SimulationError> {
519 walk_steps(walk_params, |new_price, _volatility, _x_step| {
522 Ok(Some(*new_price))
523 })
524}
525
526#[cfg(test)]
527mod tests {
528 use super::*;
529 use crate::simulation::WalkTypeAble;
530 use crate::simulation::randomwalk::RandomWalk;
531 use optionstratlib_core::model::ExpirationDate;
532 use optionstratlib_core::pos_or_panic;
533 use optionstratlib_core::utils::time::convert_time_frame;
534 use optionstratlib_core::utils::{Len, TimeFrame};
535 use rust_decimal_macros::dec;
536
537 const SEED: u64 = 685;
539
540 #[derive(Clone)]
541 struct Walker {}
542 impl Walker {
543 fn new() -> Self {
544 Walker {}
545 }
546 }
547 impl WalkTypeAble<Positive, Positive> for Walker {}
548
549 #[test]
550 fn test_generator_positive() -> Result<(), Box<dyn std::error::Error>> {
551 let n_steps = 100;
552 let initial_price = Positive::HUNDRED;
553 let std_dev = pos_or_panic!(20.0);
554 let walker = Box::new(Walker::new());
555 let days = pos_or_panic!(30.0);
556
557 let walk_params = WalkParams {
558 size: n_steps,
559 init_step: Step {
560 x: Xstep::new(Positive::ONE, TimeFrame::Minute, ExpirationDate::Days(days)),
561 y: Ystep::new(0, initial_price),
562 },
563 walk_type: WalkType::GeometricBrownian {
564 dt: convert_time_frame(Positive::ONE / days, &TimeFrame::Minute, &TimeFrame::Day)?,
565 drift: dec!(0.0),
566 volatility: std_dev,
567 },
568 walker,
569 seed: Some(SEED),
570 };
571 let random_walk =
572 RandomWalk::new("Random Walk".to_string(), &walk_params, generator_positive)
573 .expect("random walk construction");
574 assert_eq!(random_walk.len(), n_steps);
575 Ok(())
576 }
577
578 #[test]
579 fn test_generator_positive_early_return() {
580 let initial_price = Positive::HUNDRED;
581 let walker = Box::new(Walker::new());
582
583 let walk_params = WalkParams {
584 size: 1, init_step: Step {
586 x: Xstep::new(
587 Positive::ONE,
588 TimeFrame::Minute,
589 ExpirationDate::Days(pos_or_panic!(30.0)),
590 ),
591 y: Ystep::new(0, initial_price),
592 },
593 walk_type: WalkType::GeometricBrownian {
594 dt: pos_or_panic!(0.01),
595 drift: dec!(0.0),
596 volatility: pos_or_panic!(0.2),
597 },
598 walker,
599 seed: Some(SEED),
600 };
601
602 let steps = match generator_positive(&walk_params) {
603 Ok(steps) => steps,
604 Err(e) => panic!("generator_positive failed: {e}"),
605 };
606
607 assert_eq!(steps.len(), 1);
609 }
610
611 #[test]
614 fn test_walk_steps_empty_walker_output() {
615 #[derive(Clone)]
616 struct EmptyWalker {}
617 impl WalkTypeAble<Positive, Positive> for EmptyWalker {
618 fn brownian(
619 &self,
620 _params: &WalkParams<Positive, Positive>,
621 ) -> Result<Vec<Positive>, SimulationError> {
622 Ok(Vec::new())
623 }
624 }
625
626 let walk_params = WalkParams {
627 size: 5,
628 init_step: Step {
629 x: Xstep::new(
630 Positive::ONE,
631 TimeFrame::Day,
632 ExpirationDate::Days(pos_or_panic!(30.0)),
633 ),
634 y: Ystep::new(0, Positive::HUNDRED),
635 },
636 walk_type: WalkType::Brownian {
637 dt: pos_or_panic!(0.01),
638 drift: dec!(0.0),
639 volatility: pos_or_panic!(0.2),
640 },
641 walker: Box::new(EmptyWalker {}),
642 seed: None,
643 };
644
645 let steps = match generator_positive(&walk_params) {
646 Ok(steps) => steps,
647 Err(e) => panic!("empty walker output must not error: {e}"),
648 };
649 assert_eq!(steps.len(), 1, "empty walker output yields init-only walk");
650 }
651
652 #[test]
655 fn test_heston_with_vol_exposes_varying_vols() {
656 use rust_decimal_macros::dec;
657 let walker = Walker::new();
658 let params: WalkParams<Positive, Positive> = WalkParams {
659 size: 50,
660 init_step: Step {
661 x: Xstep::new(
662 Positive::ONE,
663 TimeFrame::Day,
664 ExpirationDate::Days(pos_or_panic!(365.0)),
665 ),
666 y: Ystep::new(0, Positive::HUNDRED),
667 },
668 walk_type: WalkType::Heston {
669 dt: pos_or_panic!(0.004),
670 drift: dec!(0.0),
671 volatility: pos_or_panic!(0.2),
672 kappa: Positive::TWO,
673 theta: pos_or_panic!(0.04),
674 xi: Positive::TWO, rho: dec!(-0.5),
676 },
677 walker: Box::new(Walker::new()),
678 seed: Some(SEED),
679 };
680 let path = match walker.heston_with_vol(¶ms) {
681 Ok(path) => path,
682 Err(e) => panic!("heston_with_vol failed: {e}"),
683 };
684 let vols = match path.vols {
685 Some(vols) => vols,
686 None => panic!("heston must expose a vol path"),
687 };
688 assert_eq!(vols.len(), path.prices.len());
689 assert_eq!(vols.first(), Some(&pos_or_panic!(0.2)));
690 let varying = vols.windows(2).any(|w| w[0] != w[1]);
691 assert!(varying, "high vol-of-vol Heston vol path cannot be flat");
692 }
693
694 #[test]
696 fn test_generate_with_vol_none_for_gbm() {
697 use rust_decimal_macros::dec;
698 let walker = Walker::new();
699 let params: WalkParams<Positive, Positive> = WalkParams {
700 size: 10,
701 init_step: Step {
702 x: Xstep::new(
703 Positive::ONE,
704 TimeFrame::Day,
705 ExpirationDate::Days(pos_or_panic!(30.0)),
706 ),
707 y: Ystep::new(0, Positive::HUNDRED),
708 },
709 walk_type: WalkType::GeometricBrownian {
710 dt: pos_or_panic!(0.01),
711 drift: dec!(0.0),
712 volatility: pos_or_panic!(0.2),
713 },
714 walker: Box::new(Walker::new()),
715 seed: Some(SEED),
716 };
717 let path = match walker.generate_with_vol(¶ms) {
718 Ok(path) => path,
719 Err(e) => panic!("generate_with_vol failed: {e}"),
720 };
721 assert!(path.vols.is_none());
722 assert_eq!(path.prices.len(), 10);
723 }
724
725 #[test]
729 fn test_expanding_window_vols_no_look_ahead() {
730 let mut prices = vec![Positive::HUNDRED; 8];
731 prices.push(pos_or_panic!(200.0));
732 prices.push(pos_or_panic!(150.0));
733
734 let vols = match expanding_window_vols(&prices, TimeFrame::Day) {
735 Ok(Some(vols)) => vols,
736 Ok(None) => panic!("estimator returned no vols"),
737 Err(e) => panic!("estimator failed: {e}"),
738 };
739 assert_eq!(vols.len(), prices.len());
740 assert_eq!(vols.get(5), Some(&Positive::ZERO));
742 match vols.last() {
744 Some(last) => assert!(*last > Positive::ZERO),
745 None => panic!("empty vols"),
746 }
747 }
748
749 #[test]
754 fn test_expanding_window_vols_prefix_is_stable() {
755 let prices = vec![
756 Positive::HUNDRED,
757 pos_or_panic!(101.5),
758 pos_or_panic!(99.25),
759 pos_or_panic!(104.0),
760 pos_or_panic!(97.5),
761 pos_or_panic!(110.0),
762 pos_or_panic!(108.75),
763 ];
764
765 let full = match expanding_window_vols(&prices, TimeFrame::Day) {
766 Ok(Some(vols)) => vols,
767 Ok(None) => panic!("estimator returned no vols"),
768 Err(e) => panic!("estimator failed: {e}"),
769 };
770
771 for len in 3..=prices.len() {
773 let prefix = match prices.get(..len) {
774 Some(prefix) => prefix,
775 None => panic!("prefix of len {len} missing"),
776 };
777 let vols = match expanding_window_vols(prefix, TimeFrame::Day) {
778 Ok(Some(vols)) => vols,
779 Ok(None) => panic!("prefix of len {len} returned no vols"),
780 Err(e) => panic!("estimator failed on prefix {len}: {e}"),
781 };
782 assert_eq!(vols.len(), len, "one estimate per price");
783 match full.get(..len) {
784 Some(head) => assert_eq!(vols, head, "prefix of len {len} drifted"),
785 None => panic!("full series shorter than {len}"),
786 }
787 }
788 }
789
790 #[test]
793 fn test_expanding_window_vols_needs_three_prices() {
794 for len in 0..3 {
795 let prices = vec![Positive::HUNDRED; len];
796 match expanding_window_vols(&prices, TimeFrame::Day) {
797 Ok(None) => {}
798 Ok(Some(vols)) => panic!("len {len} unexpectedly produced {vols:?}"),
799 Err(e) => panic!("estimator failed on len {len}: {e}"),
800 }
801 }
802 }
803
804 #[test]
809 fn test_expanding_window_vols_last_matches_constant_volatility() {
810 let prices = vec![
811 Positive::HUNDRED,
812 pos_or_panic!(102.0),
813 pos_or_panic!(98.5),
814 pos_or_panic!(103.25),
815 pos_or_panic!(101.0),
816 ];
817
818 let vols = match expanding_window_vols(&prices, TimeFrame::Day) {
819 Ok(Some(vols)) => vols,
820 Ok(None) => panic!("estimator returned no vols"),
821 Err(e) => panic!("estimator failed: {e}"),
822 };
823 let last = match vols.last() {
824 Some(last) => *last,
825 None => panic!("empty vols"),
826 };
827
828 let log_returns = match calculate_log_returns(&prices) {
829 Ok(returns) => returns,
830 Err(e) => panic!("log returns failed: {e}"),
831 };
832 let expected = match constant_volatility(&log_returns)
833 .and_then(|vol| adjust_volatility(vol, TimeFrame::Day, TimeFrame::Year))
834 {
835 Ok(expected) => expected,
836 Err(e) => panic!("reference volatility failed: {e}"),
837 };
838
839 let diff = (last.to_dec() - expected.to_dec()).abs();
840 assert!(
841 diff < dec!(0.000_000_1),
842 "expanding window {last} != constant volatility {expected}"
843 );
844 }
845
846 #[test]
848 fn test_walk_steps_next_y_none_stops_walk() {
849 let walker = Box::new(Walker::new());
850 let walk_params = WalkParams {
851 size: 10,
852 init_step: Step {
853 x: Xstep::new(
854 Positive::ONE,
855 TimeFrame::Day,
856 ExpirationDate::Days(pos_or_panic!(30.0)),
857 ),
858 y: Ystep::new(0, Positive::HUNDRED),
859 },
860 walk_type: WalkType::GeometricBrownian {
861 dt: pos_or_panic!(0.01),
862 drift: dec!(0.0),
863 volatility: pos_or_panic!(0.2),
864 },
865 walker,
866 seed: Some(SEED),
867 };
868
869 let mut calls = 0;
870 let steps = match walk_steps(
871 &walk_params,
872 |price, _vol, _x| -> Result<Option<Positive>, SimulationError> {
873 calls += 1;
874 if calls > 3 {
875 Ok(None)
876 } else {
877 Ok(Some(*price))
878 }
879 },
880 ) {
881 Ok(steps) => steps,
882 Err(e) => panic!("walk_steps failed: {e}"),
883 };
884 assert_eq!(steps.len(), 4);
886 }
887
888 #[test]
891 fn test_generator_positive_multi_step_behavior() {
892 use crate::simulation::walk_test_support::RampWalker;
893 use rust_decimal_macros::dec;
894
895 let size = 5;
896 let walk_params = WalkParams {
897 size,
898 init_step: Step {
899 x: Xstep::new(
900 Positive::ONE,
901 TimeFrame::Day,
902 ExpirationDate::Days(pos_or_panic!(30.0)),
903 ),
904 y: Ystep::new(0, Positive::HUNDRED),
905 },
906 walk_type: WalkType::GeometricBrownian {
907 dt: pos_or_panic!(0.01),
908 drift: dec!(0.0),
909 volatility: pos_or_panic!(0.2),
910 },
911 walker: Box::new(RampWalker {
912 delta: Positive::TWO,
913 }),
914 seed: None,
915 };
916
917 let steps = match generator_positive(&walk_params) {
918 Ok(steps) => steps,
919 Err(e) => panic!("generator_positive failed: {e}"),
920 };
921 assert_eq!(steps.len(), size);
922 for (i, step) in steps.iter().enumerate() {
923 let expected_price = Positive::HUNDRED + Positive::TWO * i as f64;
925 assert_eq!(*step.y.value(), expected_price, "price at step {i}");
926 assert_eq!(*step.y.index(), i as i32, "y index at step {i}");
928 let days = match step.x.days_left() {
930 Ok(days) => days,
931 Err(e) => panic!("days_left failed at step {i}: {e}"),
932 };
933 assert_eq!(days, pos_or_panic!(30.0 - i as f64), "days at step {i}");
934 }
935 }
936
937 #[test]
940 fn test_generator_positive_truncates_at_expiration() {
941 use crate::simulation::walk_test_support::RampWalker;
942 use rust_decimal_macros::dec;
943
944 let walk_params = WalkParams {
945 size: 10,
946 init_step: Step {
947 x: Xstep::new(
948 Positive::ONE,
949 TimeFrame::Day,
950 ExpirationDate::Days(pos_or_panic!(3.0)),
951 ),
952 y: Ystep::new(0, Positive::HUNDRED),
953 },
954 walk_type: WalkType::GeometricBrownian {
955 dt: pos_or_panic!(0.01),
956 drift: dec!(0.0),
957 volatility: pos_or_panic!(0.2),
958 },
959 walker: Box::new(RampWalker {
960 delta: Positive::ONE,
961 }),
962 seed: None,
963 };
964
965 let steps = match generator_positive(&walk_params) {
966 Ok(steps) => steps,
967 Err(e) => panic!("generator_positive failed: {e}"),
968 };
969 assert_eq!(steps.len(), 4);
971 match steps.last() {
972 Some(last) => match last.x.days_left() {
973 Ok(days) => assert_eq!(days, Positive::ZERO),
974 Err(e) => panic!("days_left failed: {e}"),
975 },
976 None => panic!("empty steps"),
977 }
978 }
979
980 #[test]
982 fn test_generator_positive_size_zero() {
983 use crate::simulation::walk_test_support::RampWalker;
984 use rust_decimal_macros::dec;
985
986 let walk_params = WalkParams {
987 size: 0,
988 init_step: Step {
989 x: Xstep::new(
990 Positive::ONE,
991 TimeFrame::Day,
992 ExpirationDate::Days(pos_or_panic!(30.0)),
993 ),
994 y: Ystep::new(0, Positive::HUNDRED),
995 },
996 walk_type: WalkType::GeometricBrownian {
997 dt: pos_or_panic!(0.01),
998 drift: dec!(0.0),
999 volatility: pos_or_panic!(0.2),
1000 },
1001 walker: Box::new(RampWalker {
1002 delta: Positive::ONE,
1003 }),
1004 seed: None,
1005 };
1006
1007 let steps = match generator_positive(&walk_params) {
1008 Ok(steps) => steps,
1009 Err(e) => panic!("size-0 walk must not error: {e}"),
1010 };
1011 assert_eq!(steps.len(), 1);
1012 }
1013
1014 #[test]
1017 fn test_walk_steps_par_matches_serial() {
1018 use crate::simulation::walk_test_support::RampWalker;
1019 use rust_decimal_macros::dec;
1020
1021 let walk_params = WalkParams {
1022 size: 8,
1023 init_step: Step {
1024 x: Xstep::new(
1025 Positive::ONE,
1026 TimeFrame::Day,
1027 ExpirationDate::Days(pos_or_panic!(30.0)),
1028 ),
1029 y: Ystep::new(0, Positive::HUNDRED),
1030 },
1031 walk_type: WalkType::GeometricBrownian {
1032 dt: pos_or_panic!(0.01),
1033 drift: dec!(0.0),
1034 volatility: pos_or_panic!(0.2),
1035 },
1036 walker: Box::new(RampWalker {
1037 delta: Positive::TWO,
1038 }),
1039 seed: None,
1040 };
1041
1042 let double =
1043 |price: &Positive,
1044 _vol: Option<Positive>,
1045 _x: &Xstep<Positive>|
1046 -> Result<Option<Positive>, SimulationError> { Ok(Some(*price * 2.0)) };
1047
1048 let serial = match walk_steps(&walk_params, double) {
1049 Ok(steps) => steps,
1050 Err(e) => panic!("serial driver failed: {e}"),
1051 };
1052 let parallel = match walk_steps_par(&walk_params, double) {
1053 Ok(steps) => steps,
1054 Err(e) => panic!("parallel driver failed: {e}"),
1055 };
1056
1057 assert_eq!(serial.len(), parallel.len());
1058 for (i, (s, p)) in serial.iter().zip(parallel.iter()).enumerate() {
1059 assert_eq!(s.y.value(), p.y.value(), "y value differs at step {i}");
1060 assert_eq!(s.y.index(), p.y.index(), "y index differs at step {i}");
1061 let (sd, pd) = match (s.x.days_left(), p.x.days_left()) {
1062 (Ok(sd), Ok(pd)) => (sd, pd),
1063 _ => panic!("days_left failed at step {i}"),
1064 };
1065 assert_eq!(sd, pd, "x days differ at step {i}");
1066 }
1067 }
1068
1069 #[test]
1073 fn test_walk_steps_par_error_after_stop_is_discarded() {
1074 use crate::simulation::walk_test_support::RampWalker;
1075 use rust_decimal_macros::dec;
1076
1077 let make_params = || WalkParams {
1078 size: 8,
1079 init_step: Step {
1080 x: Xstep::new(
1081 Positive::ONE,
1082 TimeFrame::Day,
1083 ExpirationDate::Days(pos_or_panic!(30.0)),
1084 ),
1085 y: Ystep::new(0, Positive::HUNDRED),
1086 },
1087 walk_type: WalkType::GeometricBrownian {
1088 dt: pos_or_panic!(0.01),
1089 drift: dec!(0.0),
1090 volatility: pos_or_panic!(0.2),
1091 },
1092 walker: Box::new(RampWalker {
1093 delta: Positive::ONE, }),
1095 seed: None,
1096 };
1097
1098 let steps = match walk_steps_par(
1101 &make_params(),
1102 |price: &Positive, _vol, _x| -> Result<Option<Positive>, SimulationError> {
1103 if *price == pos_or_panic!(103.0) {
1104 Err(SimulationError::invalid_parameters("speculative error"))
1105 } else if *price == pos_or_panic!(102.0) {
1106 Ok(None)
1107 } else {
1108 Ok(Some(*price))
1109 }
1110 },
1111 ) {
1112 Ok(steps) => steps,
1113 Err(e) => panic!("error past the stop signal must be discarded: {e}"),
1114 };
1115 assert_eq!(steps.len(), 2);
1117
1118 let result = walk_steps_par(
1122 &make_params(),
1123 |price: &Positive, _vol, _x| -> Result<Option<Positive>, SimulationError> {
1124 if *price == pos_or_panic!(101.0) {
1125 Err(SimulationError::invalid_parameters("early error"))
1126 } else if *price == pos_or_panic!(102.0) {
1127 Ok(None)
1128 } else {
1129 Ok(Some(*price))
1130 }
1131 },
1132 );
1133 assert!(result.is_err(), "error before the stop must propagate");
1134 }
1135}