1pub mod hmm;
37
38use crate::error::FinError;
39use crate::signals::indicators::{Adx, BollingerWidth, HistoricalVolatility, HurstExponent};
40use crate::signals::{BarInput, Signal, SignalValue};
41use rust_decimal::prelude::ToPrimitive;
42use rust_decimal::Decimal;
43
44#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
51#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
52pub enum MarketRegime {
53 Trending,
55 MeanReverting,
57 HighVolatility,
59 LowVolatility,
61 Crisis,
63 Neutral,
65 Unknown,
67}
68
69impl MarketRegime {
70 pub fn is_risk_off(self) -> bool {
74 matches!(self, MarketRegime::Crisis | MarketRegime::Unknown)
75 }
76
77 pub fn short_code(self) -> &'static str {
79 match self {
80 MarketRegime::Trending => "TRD",
81 MarketRegime::MeanReverting => "MRV",
82 MarketRegime::HighVolatility => "HVL",
83 MarketRegime::LowVolatility => "LVL",
84 MarketRegime::Crisis => "CRS",
85 MarketRegime::Neutral => "NEU",
86 MarketRegime::Unknown => "UNK",
87 }
88 }
89}
90
91impl std::fmt::Display for MarketRegime {
92 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
93 match self {
94 MarketRegime::Trending => write!(f, "Trending"),
95 MarketRegime::MeanReverting => write!(f, "MeanReverting"),
96 MarketRegime::HighVolatility => write!(f, "HighVolatility"),
97 MarketRegime::LowVolatility => write!(f, "LowVolatility"),
98 MarketRegime::Crisis => write!(f, "Crisis"),
99 MarketRegime::Neutral => write!(f, "Neutral"),
100 MarketRegime::Unknown => write!(f, "Unknown"),
101 }
102 }
103}
104
105#[derive(Debug, Clone)]
112pub struct RegimeConfig {
113 pub hurst_trending: f64,
115 pub hurst_mean_reverting: f64,
117 pub vol_high_multiplier: f64,
119 pub vol_low_multiplier: f64,
121 pub adx_trend_threshold: f64,
123 pub bb_width_quiet: f64,
125 pub crisis_correlation_threshold: f64,
127 pub crisis_pair_fraction: f64,
130 pub garch_alpha: f64,
132 pub garch_beta: f64,
134 pub garch_omega: f64,
136 pub garch_vol_multiplier: f64,
138}
139
140impl Default for RegimeConfig {
141 fn default() -> Self {
142 Self {
143 hurst_trending: 0.6,
144 hurst_mean_reverting: 0.4,
145 vol_high_multiplier: 2.0,
146 vol_low_multiplier: 0.5,
147 adx_trend_threshold: 25.0,
148 bb_width_quiet: 0.02,
149 crisis_correlation_threshold: 0.3,
150 crisis_pair_fraction: 0.6,
151 garch_alpha: 0.1,
152 garch_beta: 0.85,
153 garch_omega: 1e-6,
154 garch_vol_multiplier: 1.5,
155 }
156 }
157}
158
159#[derive(Debug, Clone)]
181pub struct Garch11 {
182 alpha: f64,
183 beta: f64,
184 omega: f64,
185 variance: f64,
187 mean: f64,
189 count: usize,
191}
192
193impl Garch11 {
194 pub fn new(alpha: f64, beta: f64, omega: f64) -> Result<Self, FinError> {
203 if alpha <= 0.0 || beta <= 0.0 || omega <= 0.0 {
204 return Err(FinError::InvalidInput(
205 "GARCH parameters alpha, beta, and omega must all be positive".to_owned(),
206 ));
207 }
208 if alpha + beta >= 1.0 {
209 return Err(FinError::InvalidInput(format!(
210 "GARCH(1,1) requires alpha + beta < 1 for stationarity, got {:.4}",
211 alpha + beta
212 )));
213 }
214 let long_run_var = omega / (1.0 - alpha - beta);
216 Ok(Self { alpha, beta, omega, variance: long_run_var, mean: 0.0, count: 0 })
217 }
218
219 pub fn update(&mut self, log_return: f64) -> f64 {
222 self.count += 1;
223 let delta = log_return - self.mean;
225 self.mean += delta / self.count as f64;
226 let demeaned = log_return - self.mean;
227 self.variance = self.omega
229 + self.alpha * demeaned * demeaned
230 + self.beta * self.variance;
231 self.variance.sqrt()
232 }
233
234 pub fn variance(&self) -> f64 {
236 self.variance
237 }
238
239 pub fn sigma(&self) -> f64 {
241 self.variance.sqrt()
242 }
243
244 pub fn long_run_sigma(&self) -> f64 {
246 (self.omega / (1.0 - self.alpha - self.beta)).sqrt()
247 }
248
249 pub fn is_vol_elevated(&self, multiplier: f64) -> bool {
252 self.sigma() > self.long_run_sigma() * multiplier
253 }
254
255 pub fn count(&self) -> usize {
257 self.count
258 }
259
260 pub fn reset(&mut self) {
262 let long_run_var = self.omega / (1.0 - self.alpha - self.beta);
263 self.variance = long_run_var;
264 self.mean = 0.0;
265 self.count = 0;
266 }
267}
268
269#[derive(Debug, Clone)]
295pub struct CorrelationBreakdownDetector {
296 window: usize,
297 threshold: f64,
298 crisis_fraction: f64,
299 returns: Vec<std::collections::VecDeque<f64>>,
301 n_assets: usize,
302}
303
304impl CorrelationBreakdownDetector {
305 pub fn new(window: usize, threshold: f64, crisis_fraction: f64) -> Result<Self, FinError> {
314 if window < 3 {
315 return Err(FinError::InvalidInput(
316 "correlation window must be at least 3".to_owned(),
317 ));
318 }
319 if !(0.0..=1.0).contains(&threshold) {
320 return Err(FinError::InvalidInput(
321 "correlation threshold must be in [0, 1]".to_owned(),
322 ));
323 }
324 if !(0.0..=1.0).contains(&crisis_fraction) {
325 return Err(FinError::InvalidInput(
326 "crisis_fraction must be in [0, 1]".to_owned(),
327 ));
328 }
329 Ok(Self {
330 window,
331 threshold,
332 crisis_fraction,
333 returns: Vec::new(),
334 n_assets: 0,
335 })
336 }
337
338 pub fn update(&mut self, asset_idx: usize, log_return: f64) {
343 while self.returns.len() <= asset_idx {
345 self.returns.push(std::collections::VecDeque::with_capacity(self.window + 1));
346 self.n_assets = self.returns.len();
347 }
348 let buf = &mut self.returns[asset_idx];
349 buf.push_back(log_return);
350 if buf.len() > self.window {
351 buf.pop_front();
352 }
353 }
354
355 pub fn is_crisis(&self) -> bool {
357 if self.n_assets < 2 {
358 return false;
359 }
360 let mut total_pairs = 0usize;
361 let mut decorrelated_pairs = 0usize;
362
363 for i in 0..self.n_assets {
364 for j in (i + 1)..self.n_assets {
365 let ri = &self.returns[i];
366 let rj = &self.returns[j];
367 if ri.len() < 3 || rj.len() < 3 {
368 continue;
369 }
370 let len = ri.len().min(rj.len());
371 let r = pearson_r(
372 ri.iter().rev().take(len).copied().collect::<Vec<_>>().as_slice(),
373 rj.iter().rev().take(len).copied().collect::<Vec<_>>().as_slice(),
374 );
375 total_pairs += 1;
376 if r.abs() < self.threshold {
377 decorrelated_pairs += 1;
378 }
379 }
380 }
381
382 if total_pairs == 0 {
383 return false;
384 }
385 (decorrelated_pairs as f64 / total_pairs as f64) >= self.crisis_fraction
386 }
387
388 pub fn n_assets(&self) -> usize {
390 self.n_assets
391 }
392
393 pub fn reset(&mut self) {
395 for buf in &mut self.returns {
396 buf.clear();
397 }
398 }
399}
400
401fn pearson_r(x: &[f64], y: &[f64]) -> f64 {
403 let n = x.len().min(y.len());
404 if n < 2 {
405 return 0.0;
406 }
407 let n_f = n as f64;
408 let mean_x = x[..n].iter().sum::<f64>() / n_f;
409 let mean_y = y[..n].iter().sum::<f64>() / n_f;
410 let mut cov = 0.0;
411 let mut var_x = 0.0;
412 let mut var_y = 0.0;
413 for i in 0..n {
414 let dx = x[i] - mean_x;
415 let dy = y[i] - mean_y;
416 cov += dx * dy;
417 var_x += dx * dx;
418 var_y += dy * dy;
419 }
420 let denom = (var_x * var_y).sqrt();
421 if denom < 1e-12 {
422 return 0.0;
423 }
424 (cov / denom).clamp(-1.0, 1.0)
425}
426
427#[derive(Debug, Clone)]
434#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
435pub struct RegimeHistory {
436 pub regime: MarketRegime,
438 pub started_at_bar: usize,
440 pub confidence: f64,
443 pub ended_at_bar: Option<usize>,
445}
446
447impl RegimeHistory {
448 pub fn duration_bars(&self) -> Option<usize> {
452 self.ended_at_bar.map(|end| end - self.started_at_bar)
453 }
454
455 pub fn is_active(&self) -> bool {
457 self.ended_at_bar.is_none()
458 }
459}
460
461pub struct RegimeDetector {
486 adx: Adx,
487 hurst: HurstExponent,
488 hv: HistoricalVolatility,
489 bb_width: BollingerWidth,
490 garch: Garch11,
491 correlation: CorrelationBreakdownDetector,
492 config: RegimeConfig,
493 hv_mean: f64,
495 hv_count: usize,
496 prev_close: Option<f64>,
498 bar_count: usize,
500 history: Vec<RegimeHistory>,
502 current_regime: MarketRegime,
504}
505
506impl RegimeDetector {
507 pub fn new(period: usize, config: RegimeConfig) -> Result<Self, FinError> {
513 if period < 2 {
514 return Err(FinError::InvalidPeriod(period));
515 }
516 let garch = Garch11::new(config.garch_alpha, config.garch_beta, config.garch_omega)?;
517 let correlation = CorrelationBreakdownDetector::new(
518 period.max(5),
519 config.crisis_correlation_threshold,
520 config.crisis_pair_fraction,
521 )?;
522 Ok(Self {
523 adx: Adx::new("regime_adx", period)?,
524 hurst: HurstExponent::new("regime_hurst", period)?,
525 hv: HistoricalVolatility::new("regime_hv", period, 252)?,
526 bb_width: BollingerWidth::new("regime_bb_width", period, Decimal::from(2u32))?,
527 garch,
528 correlation,
529 config,
530 hv_mean: 0.0,
531 hv_count: 0,
532 prev_close: None,
533 bar_count: 0,
534 history: Vec::new(),
535 current_regime: MarketRegime::Unknown,
536 })
537 }
538
539 pub fn with_defaults(period: usize) -> Result<Self, FinError> {
544 Self::new(period, RegimeConfig::default())
545 }
546
547 pub fn update(
558 &mut self,
559 bar: &BarInput,
560 cross_returns: &[(usize, f64)],
561 ) -> Result<(MarketRegime, f64), FinError> {
562 self.bar_count += 1;
563
564 let close_f = bar.close.to_f64().unwrap_or(0.0);
566 if let Some(prev) = self.prev_close {
567 if prev > 0.0 {
568 let log_ret = (close_f / prev).ln();
569 self.garch.update(log_ret);
570 self.correlation.update(0, log_ret);
571 }
572 }
573 self.prev_close = Some(close_f);
574
575 for &(idx, ret) in cross_returns {
577 self.correlation.update(idx + 1, ret); }
579
580 let adx_val = self.adx.update(bar)?;
582 let hurst_val = self.hurst.update(bar)?;
583 let hv_val = self.hv.update(bar)?;
584 let bb_w_val = self.bb_width.update(bar)?;
585
586 let (adx_f, hurst_f, hv_f, bb_w_f) = match (adx_val, hurst_val, hv_val, bb_w_val) {
588 (
589 SignalValue::Scalar(a),
590 SignalValue::Scalar(h),
591 SignalValue::Scalar(v),
592 SignalValue::Scalar(b),
593 ) => (
594 a.to_f64().unwrap_or(0.0),
595 h.to_f64().unwrap_or(0.5),
596 v.to_f64().unwrap_or(0.0),
597 b.to_f64().unwrap_or(f64::MAX),
598 ),
599 _ => {
600 self.record_regime(MarketRegime::Unknown, 0.0);
601 return Ok((MarketRegime::Unknown, 0.0));
602 }
603 };
604
605 self.hv_count += 1;
607 self.hv_mean += (hv_f - self.hv_mean) / self.hv_count as f64;
608
609 if self.correlation.is_crisis() {
613 let conf = 0.9;
614 self.record_regime(MarketRegime::Crisis, conf);
615 return Ok((MarketRegime::Crisis, conf));
616 }
617
618 let vol_ratio = if self.hv_mean > 0.0 { hv_f / self.hv_mean } else { 1.0 };
621 let garch_elevated = self.garch.is_vol_elevated(self.config.garch_vol_multiplier);
622 if vol_ratio > self.config.vol_high_multiplier || (vol_ratio > 1.5 && garch_elevated) {
623 let conf = (vol_ratio - self.config.vol_high_multiplier).abs().min(1.0) * 0.8 + 0.2;
624 let conf = conf.min(1.0);
625 self.record_regime(MarketRegime::HighVolatility, conf);
626 return Ok((MarketRegime::HighVolatility, conf));
627 }
628
629 if hurst_f > self.config.hurst_trending {
631 let adx_factor = if adx_f > self.config.adx_trend_threshold { 1.0 } else { 0.7 };
632 let conf = ((hurst_f - self.config.hurst_trending)
633 / (1.0 - self.config.hurst_trending))
634 .min(1.0)
635 * adx_factor;
636 self.record_regime(MarketRegime::Trending, conf);
637 return Ok((MarketRegime::Trending, conf));
638 }
639
640 if hurst_f < self.config.hurst_mean_reverting {
642 let conf = ((self.config.hurst_mean_reverting - hurst_f)
643 / self.config.hurst_mean_reverting)
644 .min(1.0);
645 self.record_regime(MarketRegime::MeanReverting, conf);
646 return Ok((MarketRegime::MeanReverting, conf));
647 }
648
649 if vol_ratio < self.config.vol_low_multiplier || bb_w_f < self.config.bb_width_quiet {
651 let conf = (1.0 - vol_ratio / self.config.vol_low_multiplier).max(0.1).min(1.0);
652 self.record_regime(MarketRegime::LowVolatility, conf);
653 return Ok((MarketRegime::LowVolatility, conf));
654 }
655
656 self.record_regime(MarketRegime::Neutral, 0.5);
658 Ok((MarketRegime::Neutral, 0.5))
659 }
660
661 fn record_regime(&mut self, regime: MarketRegime, confidence: f64) {
663 if regime == self.current_regime {
664 return;
665 }
666 if let Some(last) = self.history.last_mut() {
668 if last.ended_at_bar.is_none() {
669 last.ended_at_bar = Some(self.bar_count);
670 }
671 }
672 self.current_regime = regime;
673 self.history.push(RegimeHistory {
674 regime,
675 started_at_bar: self.bar_count,
676 confidence,
677 ended_at_bar: None,
678 });
679 }
680
681 pub fn current_regime(&self) -> MarketRegime {
683 self.current_regime
684 }
685
686 pub fn history(&self) -> &[RegimeHistory] {
688 &self.history
689 }
690
691 pub fn is_ready(&self) -> bool {
693 self.adx.is_ready()
694 && self.hurst.is_ready()
695 && self.hv.is_ready()
696 && self.bb_width.is_ready()
697 }
698
699 pub fn config(&self) -> &RegimeConfig {
701 &self.config
702 }
703
704 pub fn garch(&self) -> &Garch11 {
706 &self.garch
707 }
708
709 pub fn correlation_detector(&self) -> &CorrelationBreakdownDetector {
711 &self.correlation
712 }
713
714 pub fn reset(&mut self) {
716 self.adx.reset();
717 self.hurst.reset();
718 self.hv.reset();
719 self.bb_width.reset();
720 self.garch.reset();
721 self.correlation.reset();
722 self.hv_mean = 0.0;
723 self.hv_count = 0;
724 self.prev_close = None;
725 self.bar_count = 0;
726 self.history.clear();
727 self.current_regime = MarketRegime::Unknown;
728 }
729
730 pub fn bar_count(&self) -> usize {
732 self.bar_count
733 }
734}
735
736pub struct MarketRegimeDetector {
756 adx: Adx,
757 hurst: HurstExponent,
758 hv: HistoricalVolatility,
759 bb_width: BollingerWidth,
760 config: RegimeConfig,
761}
762
763impl MarketRegimeDetector {
764 pub fn new(period: usize, config: RegimeConfig) -> Result<Self, FinError> {
769 if period < 2 {
770 return Err(FinError::InvalidPeriod(period));
771 }
772 Ok(Self {
773 adx: Adx::new("regime_adx", period)?,
774 hurst: HurstExponent::new("regime_hurst", period)?,
775 hv: HistoricalVolatility::new("regime_hv", period, 252)?,
776 bb_width: BollingerWidth::new("regime_bb_width", period, Decimal::from(2u32))?,
777 config,
778 })
779 }
780
781 pub fn with_defaults(period: usize) -> Result<Self, FinError> {
786 Self::new(period, RegimeConfig::default())
787 }
788
789 pub fn update(&mut self, bar: &BarInput) -> Result<MarketRegime, FinError> {
794 let adx_val = self.adx.update(bar)?;
795 let hurst_val = self.hurst.update(bar)?;
796 let hv_val = self.hv.update(bar)?;
797 let bb_w_val = self.bb_width.update(bar)?;
798
799 let (adx, hurst, hv, bb_w) = match (adx_val, hurst_val, hv_val, bb_w_val) {
800 (
801 SignalValue::Scalar(a),
802 SignalValue::Scalar(h),
803 SignalValue::Scalar(v),
804 SignalValue::Scalar(b),
805 ) => (a, h, v, b),
806 _ => return Ok(MarketRegime::Unknown),
807 };
808
809 let adx_f = adx.to_f64().unwrap_or(0.0);
810 let hurst_f = hurst.to_f64().unwrap_or(0.5);
811 let hv_f = hv.to_f64().unwrap_or(0.0);
812 let bb_w_f = bb_w.to_f64().unwrap_or(f64::MAX);
813
814 if hurst_f > self.config.hurst_trending && adx_f > self.config.adx_trend_threshold {
816 return Ok(MarketRegime::Trending);
817 }
818 if hv_f > self.config.vol_high_multiplier * 15.0 {
819 return Ok(MarketRegime::HighVolatility);
820 }
821 if hurst_f < self.config.hurst_mean_reverting {
822 return Ok(MarketRegime::MeanReverting);
823 }
824 if bb_w_f < self.config.bb_width_quiet {
825 return Ok(MarketRegime::LowVolatility);
826 }
827
828 Ok(MarketRegime::Neutral)
829 }
830
831 pub fn is_ready(&self) -> bool {
833 self.adx.is_ready()
834 && self.hurst.is_ready()
835 && self.hv.is_ready()
836 && self.bb_width.is_ready()
837 }
838
839 pub fn config(&self) -> &RegimeConfig {
841 &self.config
842 }
843
844 pub fn reset(&mut self) {
846 self.adx.reset();
847 self.hurst.reset();
848 self.hv.reset();
849 self.bb_width.reset();
850 }
851}
852
853pub struct RegimeConditionalSignal {
876 rsi_trending: crate::signals::indicators::Rsi,
878 rsi_mean_reverting: crate::signals::indicators::Rsi,
880 rsi_neutral: crate::signals::indicators::Rsi,
882}
883
884impl RegimeConditionalSignal {
885 pub fn new(
894 trending_period: usize,
895 mean_reverting_period: usize,
896 neutral_period: usize,
897 ) -> Result<Self, FinError> {
898 Ok(Self {
899 rsi_trending: crate::signals::indicators::Rsi::new(
900 "rsi_trending",
901 trending_period,
902 )?,
903 rsi_mean_reverting: crate::signals::indicators::Rsi::new(
904 "rsi_mean_reverting",
905 mean_reverting_period,
906 )?,
907 rsi_neutral: crate::signals::indicators::Rsi::new("rsi_neutral", neutral_period)?,
908 })
909 }
910
911 pub fn update(
919 &mut self,
920 bar: &BarInput,
921 regime: MarketRegime,
922 ) -> Option<Result<f64, FinError>> {
923 let v_trending = self.rsi_trending.update(bar);
925 let v_mr = self.rsi_mean_reverting.update(bar);
926 let v_neutral = self.rsi_neutral.update(bar);
927
928 if regime.is_risk_off() {
929 return None;
930 }
931
932 let chosen = match regime {
933 MarketRegime::Trending => v_trending,
934 MarketRegime::MeanReverting => v_mr,
935 _ => v_neutral,
936 };
937
938 match chosen {
939 Ok(SignalValue::Scalar(v)) => {
940 Some(Ok(v.to_f64().unwrap_or(50.0)))
941 }
942 Ok(_) => None,
943 Err(e) => Some(Err(e)),
944 }
945 }
946
947 pub fn is_ready(&self) -> bool {
949 self.rsi_trending.is_ready()
950 && self.rsi_mean_reverting.is_ready()
951 && self.rsi_neutral.is_ready()
952 }
953
954 pub fn reset(&mut self) {
956 self.rsi_trending.reset();
957 self.rsi_mean_reverting.reset();
958 self.rsi_neutral.reset();
959 }
960}
961
962#[cfg(test)]
965mod tests {
966 use super::*;
967 use rust_decimal_macros::dec;
968
969 fn bar(h: f64, l: f64, c: f64) -> BarInput {
970 BarInput::new(
971 Decimal::try_from(c).unwrap_or(dec!(100)),
972 Decimal::try_from(h).unwrap_or(dec!(102)),
973 Decimal::try_from(l).unwrap_or(dec!(98)),
974 Decimal::try_from(c).unwrap_or(dec!(100)),
975 dec!(1000),
976 )
977 }
978
979 #[test]
982 fn test_regime_display_all_variants() {
983 assert_eq!(MarketRegime::Trending.to_string(), "Trending");
984 assert_eq!(MarketRegime::MeanReverting.to_string(), "MeanReverting");
985 assert_eq!(MarketRegime::HighVolatility.to_string(), "HighVolatility");
986 assert_eq!(MarketRegime::LowVolatility.to_string(), "LowVolatility");
987 assert_eq!(MarketRegime::Crisis.to_string(), "Crisis");
988 assert_eq!(MarketRegime::Neutral.to_string(), "Neutral");
989 assert_eq!(MarketRegime::Unknown.to_string(), "Unknown");
990 }
991
992 #[test]
993 fn test_regime_short_codes() {
994 assert_eq!(MarketRegime::Trending.short_code(), "TRD");
995 assert_eq!(MarketRegime::Crisis.short_code(), "CRS");
996 assert_eq!(MarketRegime::Unknown.short_code(), "UNK");
997 }
998
999 #[test]
1000 fn test_is_risk_off() {
1001 assert!(MarketRegime::Crisis.is_risk_off());
1002 assert!(MarketRegime::Unknown.is_risk_off());
1003 assert!(!MarketRegime::Trending.is_risk_off());
1004 assert!(!MarketRegime::Neutral.is_risk_off());
1005 }
1006
1007 #[test]
1010 fn test_garch_invalid_params() {
1011 assert!(Garch11::new(0.0, 0.85, 1e-6).is_err());
1012 assert!(Garch11::new(0.1, 0.0, 1e-6).is_err());
1013 assert!(Garch11::new(0.1, 0.85, 0.0).is_err());
1014 assert!(Garch11::new(0.5, 0.6, 1e-6).is_err()); }
1016
1017 #[test]
1018 fn test_garch_produces_positive_sigma() {
1019 let mut g = Garch11::new(0.1, 0.85, 1e-6).unwrap();
1020 let returns = [-0.01, 0.02, -0.015, 0.005, 0.03, -0.02, 0.01];
1021 for ret in returns {
1022 let sigma = g.update(ret);
1023 assert!(sigma > 0.0, "sigma must be positive, got {sigma}");
1024 }
1025 }
1026
1027 #[test]
1028 fn test_garch_reset() {
1029 let mut g = Garch11::new(0.1, 0.85, 1e-6).unwrap();
1030 for ret in [-0.05, 0.05, -0.05] {
1031 g.update(ret);
1032 }
1033 let sigma_before = g.sigma();
1034 g.reset();
1035 let lr = g.long_run_sigma();
1037 assert!((g.sigma() - lr).abs() < 1e-10);
1038 assert_ne!(sigma_before, g.sigma());
1039 assert_eq!(g.count(), 0);
1040 }
1041
1042 #[test]
1043 fn test_garch_vol_elevated() {
1044 let mut g = Garch11::new(0.1, 0.85, 1e-4).unwrap();
1045 for _ in 0..10 {
1047 g.update(0.1); }
1049 assert!(g.is_vol_elevated(1.0) || g.sigma() > 0.0); }
1052
1053 #[test]
1056 fn test_correlation_invalid_params() {
1057 assert!(CorrelationBreakdownDetector::new(1, 0.3, 0.6).is_err()); assert!(CorrelationBreakdownDetector::new(20, 1.5, 0.6).is_err()); assert!(CorrelationBreakdownDetector::new(20, 0.3, 1.5).is_err()); }
1061
1062 #[test]
1063 fn test_no_crisis_single_asset() {
1064 let mut d = CorrelationBreakdownDetector::new(10, 0.3, 0.6).unwrap();
1065 for i in 0..15 {
1066 d.update(0, if i % 2 == 0 { 0.01 } else { -0.01 });
1067 }
1068 assert!(!d.is_crisis()); }
1070
1071 #[test]
1072 fn test_correlation_reset() {
1073 let mut d = CorrelationBreakdownDetector::new(10, 0.3, 0.6).unwrap();
1074 for i in 0..15 {
1075 d.update(0, if i % 2 == 0 { 0.01 } else { -0.01 });
1076 d.update(1, if i % 3 == 0 { 0.01 } else { -0.01 });
1077 }
1078 d.reset();
1079 assert!(!d.is_crisis());
1080 }
1081
1082 #[test]
1085 fn test_pearson_r_perfect_correlation() {
1086 let x = [1.0, 2.0, 3.0, 4.0, 5.0];
1087 let r = pearson_r(&x, &x);
1088 assert!((r - 1.0).abs() < 1e-10);
1089 }
1090
1091 #[test]
1092 fn test_pearson_r_perfect_anti_correlation() {
1093 let x = [1.0, 2.0, 3.0, 4.0, 5.0];
1094 let y: Vec<f64> = x.iter().map(|v| -v).collect();
1095 let r = pearson_r(&x, &y);
1096 assert!((r + 1.0).abs() < 1e-10);
1097 }
1098
1099 #[test]
1100 fn test_pearson_r_constant_series_returns_zero() {
1101 let x = [1.0, 1.0, 1.0, 1.0];
1102 let y = [2.0, 2.0, 2.0, 2.0];
1103 let r = pearson_r(&x, &y);
1104 assert_eq!(r, 0.0);
1105 }
1106
1107 #[test]
1110 fn test_regime_history_duration() {
1111 let h = RegimeHistory {
1112 regime: MarketRegime::Trending,
1113 started_at_bar: 10,
1114 confidence: 0.8,
1115 ended_at_bar: Some(25),
1116 };
1117 assert_eq!(h.duration_bars(), Some(15));
1118 assert!(!h.is_active());
1119 }
1120
1121 #[test]
1122 fn test_regime_history_active() {
1123 let h = RegimeHistory {
1124 regime: MarketRegime::Neutral,
1125 started_at_bar: 5,
1126 confidence: 0.5,
1127 ended_at_bar: None,
1128 };
1129 assert!(h.is_active());
1130 assert_eq!(h.duration_bars(), None);
1131 }
1132
1133 #[test]
1136 fn test_detector_period_validation() {
1137 assert!(RegimeDetector::new(0, RegimeConfig::default()).is_err());
1138 assert!(RegimeDetector::new(1, RegimeConfig::default()).is_err());
1139 assert!(RegimeDetector::new(2, RegimeConfig::default()).is_ok());
1140 }
1141
1142 #[test]
1143 fn test_detector_unknown_before_warmup() {
1144 let mut d = RegimeDetector::new(5, RegimeConfig::default()).unwrap();
1145 let (regime, _) = d.update(&bar(102.0, 98.0, 100.0), &[]).unwrap();
1146 assert_eq!(regime, MarketRegime::Unknown);
1147 assert!(!d.is_ready());
1148 }
1149
1150 #[test]
1151 fn test_detector_bar_count() {
1152 let mut d = RegimeDetector::new(5, RegimeConfig::default()).unwrap();
1153 for i in 0..5 {
1154 d.update(&bar(100.0 + i as f64, 99.0, 100.0 + i as f64), &[]).unwrap();
1155 }
1156 assert_eq!(d.bar_count(), 5);
1157 }
1158
1159 #[test]
1160 fn test_detector_reset_clears_state() {
1161 let mut d = RegimeDetector::with_defaults(5).unwrap();
1162 for i in 0..30 {
1163 let c = 100.0 + i as f64;
1164 d.update(&bar(c + 1.0, c - 1.0, c), &[]).unwrap();
1165 }
1166 d.reset();
1167 assert!(!d.is_ready());
1168 assert_eq!(d.bar_count(), 0);
1169 assert!(d.history().is_empty());
1170 }
1171
1172 #[test]
1173 fn test_detector_history_populated_after_transition() {
1174 let mut d = RegimeDetector::new(3, RegimeConfig::default()).unwrap();
1175 for i in 0..40 {
1176 let c = 100.0 + i as f64 * 0.1;
1177 d.update(&bar(c + 0.2, c - 0.2, c), &[]).unwrap();
1178 }
1179 let _ = d.history(); }
1183
1184 #[test]
1185 fn test_detector_garch_accessor() {
1186 let d = RegimeDetector::with_defaults(5).unwrap();
1187 assert!(d.garch().sigma() > 0.0);
1188 }
1189
1190 #[test]
1191 fn test_detector_no_panic_many_bars() {
1192 let mut d = RegimeDetector::new(10, RegimeConfig::default()).unwrap();
1193 for i in 0..200 {
1194 let c = 100.0 + (i as f64 * 0.5).sin() * 5.0;
1195 d.update(&bar(c + 1.0, c - 1.0, c), &[]).unwrap();
1196 }
1197 }
1198
1199 #[test]
1202 fn test_legacy_detector_period_zero_fails() {
1203 assert!(MarketRegimeDetector::new(0, RegimeConfig::default()).is_err());
1204 assert!(MarketRegimeDetector::new(1, RegimeConfig::default()).is_err());
1205 }
1206
1207 #[test]
1208 fn test_legacy_unknown_before_warmup() {
1209 let mut d = MarketRegimeDetector::new(5, RegimeConfig::default()).unwrap();
1210 let regime = d.update(&bar(102.0, 98.0, 100.0)).unwrap();
1211 assert_eq!(regime, MarketRegime::Unknown);
1212 assert!(!d.is_ready());
1213 }
1214
1215 #[test]
1216 fn test_legacy_reset_clears_warmup() {
1217 let mut d = MarketRegimeDetector::with_defaults(5).unwrap();
1218 for i in 0..30 {
1219 let h = 100.0 + i as f64;
1220 d.update(&bar(h + 1.0, h - 1.0, h)).unwrap();
1221 }
1222 d.reset();
1223 assert!(!d.is_ready());
1224 }
1225
1226 #[test]
1229 fn test_conditional_signal_invalid_period() {
1230 assert!(RegimeConditionalSignal::new(0, 21, 14).is_err());
1231 assert!(RegimeConditionalSignal::new(14, 0, 14).is_err());
1232 assert!(RegimeConditionalSignal::new(14, 21, 0).is_err());
1233 }
1234
1235 #[test]
1236 fn test_conditional_signal_suppressed_in_crisis() {
1237 let mut sig = RegimeConditionalSignal::new(5, 10, 7).unwrap();
1238 let b = bar(102.0, 98.0, 100.0);
1239 let result = sig.update(&b, MarketRegime::Crisis);
1240 assert!(result.is_none());
1241 }
1242
1243 #[test]
1244 fn test_conditional_signal_suppressed_when_unknown() {
1245 let mut sig = RegimeConditionalSignal::new(5, 10, 7).unwrap();
1246 let b = bar(102.0, 98.0, 100.0);
1247 let result = sig.update(&b, MarketRegime::Unknown);
1248 assert!(result.is_none());
1249 }
1250
1251 #[test]
1252 fn test_conditional_signal_produces_value_after_warmup() {
1253 let period = 5usize;
1254 let mut sig = RegimeConditionalSignal::new(period, period + 2, period).unwrap();
1255 let mut last_val = None;
1256 for i in 0..((period + 2) * 3) {
1257 let c = 100.0 + i as f64 * 0.1;
1258 last_val = sig.update(&bar(c + 0.5, c - 0.5, c), MarketRegime::Trending);
1259 }
1260 if let Some(Ok(rsi_val)) = last_val {
1262 assert!((0.0..=100.0).contains(&rsi_val));
1263 }
1264 }
1266
1267 #[test]
1268 fn test_conditional_signal_reset() {
1269 let mut sig = RegimeConditionalSignal::new(5, 10, 7).unwrap();
1270 let b = bar(102.0, 98.0, 100.0);
1271 for _ in 0..30 {
1272 let _ = sig.update(&b, MarketRegime::Neutral);
1273 }
1274 sig.reset();
1275 assert!(!sig.is_ready());
1276 }
1277}