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, serde::Serialize, serde::Deserialize)]
51pub enum MarketRegime {
52 Trending,
54 MeanReverting,
56 HighVolatility,
58 LowVolatility,
60 Crisis,
62 Neutral,
64 Unknown,
66}
67
68impl MarketRegime {
69 pub fn is_risk_off(self) -> bool {
73 matches!(self, MarketRegime::Crisis | MarketRegime::Unknown)
74 }
75
76 pub fn short_code(self) -> &'static str {
78 match self {
79 MarketRegime::Trending => "TRD",
80 MarketRegime::MeanReverting => "MRV",
81 MarketRegime::HighVolatility => "HVL",
82 MarketRegime::LowVolatility => "LVL",
83 MarketRegime::Crisis => "CRS",
84 MarketRegime::Neutral => "NEU",
85 MarketRegime::Unknown => "UNK",
86 }
87 }
88}
89
90impl std::fmt::Display for MarketRegime {
91 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
92 match self {
93 MarketRegime::Trending => write!(f, "Trending"),
94 MarketRegime::MeanReverting => write!(f, "MeanReverting"),
95 MarketRegime::HighVolatility => write!(f, "HighVolatility"),
96 MarketRegime::LowVolatility => write!(f, "LowVolatility"),
97 MarketRegime::Crisis => write!(f, "Crisis"),
98 MarketRegime::Neutral => write!(f, "Neutral"),
99 MarketRegime::Unknown => write!(f, "Unknown"),
100 }
101 }
102}
103
104#[derive(Debug, Clone)]
111pub struct RegimeConfig {
112 pub hurst_trending: f64,
114 pub hurst_mean_reverting: f64,
116 pub vol_high_multiplier: f64,
118 pub vol_low_multiplier: f64,
120 pub adx_trend_threshold: f64,
122 pub bb_width_quiet: f64,
124 pub crisis_correlation_threshold: f64,
126 pub crisis_pair_fraction: f64,
129 pub garch_alpha: f64,
131 pub garch_beta: f64,
133 pub garch_omega: f64,
135 pub garch_vol_multiplier: f64,
137}
138
139impl Default for RegimeConfig {
140 fn default() -> Self {
141 Self {
142 hurst_trending: 0.6,
143 hurst_mean_reverting: 0.4,
144 vol_high_multiplier: 2.0,
145 vol_low_multiplier: 0.5,
146 adx_trend_threshold: 25.0,
147 bb_width_quiet: 0.02,
148 crisis_correlation_threshold: 0.3,
149 crisis_pair_fraction: 0.6,
150 garch_alpha: 0.1,
151 garch_beta: 0.85,
152 garch_omega: 1e-6,
153 garch_vol_multiplier: 1.5,
154 }
155 }
156}
157
158#[derive(Debug, Clone)]
180pub struct Garch11 {
181 alpha: f64,
182 beta: f64,
183 omega: f64,
184 variance: f64,
186 mean: f64,
188 count: usize,
190}
191
192impl Garch11 {
193 pub fn new(alpha: f64, beta: f64, omega: f64) -> Result<Self, FinError> {
202 if alpha <= 0.0 || beta <= 0.0 || omega <= 0.0 {
203 return Err(FinError::InvalidInput(
204 "GARCH parameters alpha, beta, and omega must all be positive".to_owned(),
205 ));
206 }
207 if alpha + beta >= 1.0 {
208 return Err(FinError::InvalidInput(format!(
209 "GARCH(1,1) requires alpha + beta < 1 for stationarity, got {:.4}",
210 alpha + beta
211 )));
212 }
213 let long_run_var = omega / (1.0 - alpha - beta);
215 Ok(Self { alpha, beta, omega, variance: long_run_var, mean: 0.0, count: 0 })
216 }
217
218 pub fn update(&mut self, log_return: f64) -> f64 {
221 self.count += 1;
222 let delta = log_return - self.mean;
224 self.mean += delta / self.count as f64;
225 let demeaned = log_return - self.mean;
226 self.variance = self.omega
228 + self.alpha * demeaned * demeaned
229 + self.beta * self.variance;
230 self.variance.sqrt()
231 }
232
233 pub fn variance(&self) -> f64 {
235 self.variance
236 }
237
238 pub fn sigma(&self) -> f64 {
240 self.variance.sqrt()
241 }
242
243 pub fn long_run_sigma(&self) -> f64 {
245 (self.omega / (1.0 - self.alpha - self.beta)).sqrt()
246 }
247
248 pub fn is_vol_elevated(&self, multiplier: f64) -> bool {
251 self.sigma() > self.long_run_sigma() * multiplier
252 }
253
254 pub fn count(&self) -> usize {
256 self.count
257 }
258
259 pub fn reset(&mut self) {
261 let long_run_var = self.omega / (1.0 - self.alpha - self.beta);
262 self.variance = long_run_var;
263 self.mean = 0.0;
264 self.count = 0;
265 }
266}
267
268#[derive(Debug, Clone)]
294pub struct CorrelationBreakdownDetector {
295 window: usize,
296 threshold: f64,
297 crisis_fraction: f64,
298 returns: Vec<std::collections::VecDeque<f64>>,
300 n_assets: usize,
301}
302
303impl CorrelationBreakdownDetector {
304 pub fn new(window: usize, threshold: f64, crisis_fraction: f64) -> Result<Self, FinError> {
313 if window < 3 {
314 return Err(FinError::InvalidInput(
315 "correlation window must be at least 3".to_owned(),
316 ));
317 }
318 if !(0.0..=1.0).contains(&threshold) {
319 return Err(FinError::InvalidInput(
320 "correlation threshold must be in [0, 1]".to_owned(),
321 ));
322 }
323 if !(0.0..=1.0).contains(&crisis_fraction) {
324 return Err(FinError::InvalidInput(
325 "crisis_fraction must be in [0, 1]".to_owned(),
326 ));
327 }
328 Ok(Self {
329 window,
330 threshold,
331 crisis_fraction,
332 returns: Vec::new(),
333 n_assets: 0,
334 })
335 }
336
337 pub fn update(&mut self, asset_idx: usize, log_return: f64) {
342 while self.returns.len() <= asset_idx {
344 self.returns.push(std::collections::VecDeque::with_capacity(self.window + 1));
345 self.n_assets = self.returns.len();
346 }
347 let buf = &mut self.returns[asset_idx];
348 buf.push_back(log_return);
349 if buf.len() > self.window {
350 buf.pop_front();
351 }
352 }
353
354 pub fn is_crisis(&self) -> bool {
356 if self.n_assets < 2 {
357 return false;
358 }
359 let mut total_pairs = 0usize;
360 let mut decorrelated_pairs = 0usize;
361
362 for i in 0..self.n_assets {
363 for j in (i + 1)..self.n_assets {
364 let ri = &self.returns[i];
365 let rj = &self.returns[j];
366 if ri.len() < 3 || rj.len() < 3 {
367 continue;
368 }
369 let len = ri.len().min(rj.len());
370 let r = pearson_r(
371 ri.iter().rev().take(len).copied().collect::<Vec<_>>().as_slice(),
372 rj.iter().rev().take(len).copied().collect::<Vec<_>>().as_slice(),
373 );
374 total_pairs += 1;
375 if r.abs() < self.threshold {
376 decorrelated_pairs += 1;
377 }
378 }
379 }
380
381 if total_pairs == 0 {
382 return false;
383 }
384 (decorrelated_pairs as f64 / total_pairs as f64) >= self.crisis_fraction
385 }
386
387 pub fn n_assets(&self) -> usize {
389 self.n_assets
390 }
391
392 pub fn reset(&mut self) {
394 for buf in &mut self.returns {
395 buf.clear();
396 }
397 }
398}
399
400fn pearson_r(x: &[f64], y: &[f64]) -> f64 {
402 let n = x.len().min(y.len());
403 if n < 2 {
404 return 0.0;
405 }
406 let n_f = n as f64;
407 let mean_x = x[..n].iter().sum::<f64>() / n_f;
408 let mean_y = y[..n].iter().sum::<f64>() / n_f;
409 let mut cov = 0.0;
410 let mut var_x = 0.0;
411 let mut var_y = 0.0;
412 for i in 0..n {
413 let dx = x[i] - mean_x;
414 let dy = y[i] - mean_y;
415 cov += dx * dy;
416 var_x += dx * dx;
417 var_y += dy * dy;
418 }
419 let denom = (var_x * var_y).sqrt();
420 if denom < 1e-12 {
421 return 0.0;
422 }
423 (cov / denom).clamp(-1.0, 1.0)
424}
425
426#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
433pub struct RegimeHistory {
434 pub regime: MarketRegime,
436 pub started_at_bar: usize,
438 pub confidence: f64,
441 pub ended_at_bar: Option<usize>,
443}
444
445impl RegimeHistory {
446 pub fn duration_bars(&self) -> Option<usize> {
450 self.ended_at_bar.map(|end| end - self.started_at_bar)
451 }
452
453 pub fn is_active(&self) -> bool {
455 self.ended_at_bar.is_none()
456 }
457}
458
459pub struct RegimeDetector {
484 adx: Adx,
485 hurst: HurstExponent,
486 hv: HistoricalVolatility,
487 bb_width: BollingerWidth,
488 garch: Garch11,
489 correlation: CorrelationBreakdownDetector,
490 config: RegimeConfig,
491 hv_mean: f64,
493 hv_count: usize,
494 prev_close: Option<f64>,
496 bar_count: usize,
498 history: Vec<RegimeHistory>,
500 current_regime: MarketRegime,
502}
503
504impl RegimeDetector {
505 pub fn new(period: usize, config: RegimeConfig) -> Result<Self, FinError> {
511 if period < 2 {
512 return Err(FinError::InvalidPeriod(period));
513 }
514 let garch = Garch11::new(config.garch_alpha, config.garch_beta, config.garch_omega)?;
515 let correlation = CorrelationBreakdownDetector::new(
516 period.max(5),
517 config.crisis_correlation_threshold,
518 config.crisis_pair_fraction,
519 )?;
520 Ok(Self {
521 adx: Adx::new("regime_adx", period)?,
522 hurst: HurstExponent::new("regime_hurst", period)?,
523 hv: HistoricalVolatility::new("regime_hv", period, 252)?,
524 bb_width: BollingerWidth::new("regime_bb_width", period, Decimal::from(2u32))?,
525 garch,
526 correlation,
527 config,
528 hv_mean: 0.0,
529 hv_count: 0,
530 prev_close: None,
531 bar_count: 0,
532 history: Vec::new(),
533 current_regime: MarketRegime::Unknown,
534 })
535 }
536
537 pub fn with_defaults(period: usize) -> Result<Self, FinError> {
542 Self::new(period, RegimeConfig::default())
543 }
544
545 pub fn update(
556 &mut self,
557 bar: &BarInput,
558 cross_returns: &[(usize, f64)],
559 ) -> Result<(MarketRegime, f64), FinError> {
560 self.bar_count += 1;
561
562 let close_f = bar.close.to_f64().unwrap_or(0.0);
564 if let Some(prev) = self.prev_close {
565 if prev > 0.0 {
566 let log_ret = (close_f / prev).ln();
567 self.garch.update(log_ret);
568 self.correlation.update(0, log_ret);
569 }
570 }
571 self.prev_close = Some(close_f);
572
573 for &(idx, ret) in cross_returns {
575 self.correlation.update(idx + 1, ret); }
577
578 let adx_val = self.adx.update(bar)?;
580 let hurst_val = self.hurst.update(bar)?;
581 let hv_val = self.hv.update(bar)?;
582 let bb_w_val = self.bb_width.update(bar)?;
583
584 let (adx_f, hurst_f, hv_f, bb_w_f) = match (adx_val, hurst_val, hv_val, bb_w_val) {
586 (
587 SignalValue::Scalar(a),
588 SignalValue::Scalar(h),
589 SignalValue::Scalar(v),
590 SignalValue::Scalar(b),
591 ) => (
592 a.to_f64().unwrap_or(0.0),
593 h.to_f64().unwrap_or(0.5),
594 v.to_f64().unwrap_or(0.0),
595 b.to_f64().unwrap_or(f64::MAX),
596 ),
597 _ => {
598 self.record_regime(MarketRegime::Unknown, 0.0);
599 return Ok((MarketRegime::Unknown, 0.0));
600 }
601 };
602
603 self.hv_count += 1;
605 self.hv_mean += (hv_f - self.hv_mean) / self.hv_count as f64;
606
607 if self.correlation.is_crisis() {
611 let conf = 0.9;
612 self.record_regime(MarketRegime::Crisis, conf);
613 return Ok((MarketRegime::Crisis, conf));
614 }
615
616 let vol_ratio = if self.hv_mean > 0.0 { hv_f / self.hv_mean } else { 1.0 };
619 let garch_elevated = self.garch.is_vol_elevated(self.config.garch_vol_multiplier);
620 if vol_ratio > self.config.vol_high_multiplier || (vol_ratio > 1.5 && garch_elevated) {
621 let conf = (vol_ratio - self.config.vol_high_multiplier).abs().min(1.0) * 0.8 + 0.2;
622 let conf = conf.min(1.0);
623 self.record_regime(MarketRegime::HighVolatility, conf);
624 return Ok((MarketRegime::HighVolatility, conf));
625 }
626
627 if hurst_f > self.config.hurst_trending {
629 let adx_factor = if adx_f > self.config.adx_trend_threshold { 1.0 } else { 0.7 };
630 let conf = ((hurst_f - self.config.hurst_trending)
631 / (1.0 - self.config.hurst_trending))
632 .min(1.0)
633 * adx_factor;
634 self.record_regime(MarketRegime::Trending, conf);
635 return Ok((MarketRegime::Trending, conf));
636 }
637
638 if hurst_f < self.config.hurst_mean_reverting {
640 let conf = ((self.config.hurst_mean_reverting - hurst_f)
641 / self.config.hurst_mean_reverting)
642 .min(1.0);
643 self.record_regime(MarketRegime::MeanReverting, conf);
644 return Ok((MarketRegime::MeanReverting, conf));
645 }
646
647 if vol_ratio < self.config.vol_low_multiplier || bb_w_f < self.config.bb_width_quiet {
649 let conf = (1.0 - vol_ratio / self.config.vol_low_multiplier).max(0.1).min(1.0);
650 self.record_regime(MarketRegime::LowVolatility, conf);
651 return Ok((MarketRegime::LowVolatility, conf));
652 }
653
654 self.record_regime(MarketRegime::Neutral, 0.5);
656 Ok((MarketRegime::Neutral, 0.5))
657 }
658
659 fn record_regime(&mut self, regime: MarketRegime, confidence: f64) {
661 if regime == self.current_regime {
662 return;
663 }
664 if let Some(last) = self.history.last_mut() {
666 if last.ended_at_bar.is_none() {
667 last.ended_at_bar = Some(self.bar_count);
668 }
669 }
670 self.current_regime = regime;
671 self.history.push(RegimeHistory {
672 regime,
673 started_at_bar: self.bar_count,
674 confidence,
675 ended_at_bar: None,
676 });
677 }
678
679 pub fn current_regime(&self) -> MarketRegime {
681 self.current_regime
682 }
683
684 pub fn history(&self) -> &[RegimeHistory] {
686 &self.history
687 }
688
689 pub fn is_ready(&self) -> bool {
691 self.adx.is_ready()
692 && self.hurst.is_ready()
693 && self.hv.is_ready()
694 && self.bb_width.is_ready()
695 }
696
697 pub fn config(&self) -> &RegimeConfig {
699 &self.config
700 }
701
702 pub fn garch(&self) -> &Garch11 {
704 &self.garch
705 }
706
707 pub fn correlation_detector(&self) -> &CorrelationBreakdownDetector {
709 &self.correlation
710 }
711
712 pub fn reset(&mut self) {
714 self.adx.reset();
715 self.hurst.reset();
716 self.hv.reset();
717 self.bb_width.reset();
718 self.garch.reset();
719 self.correlation.reset();
720 self.hv_mean = 0.0;
721 self.hv_count = 0;
722 self.prev_close = None;
723 self.bar_count = 0;
724 self.history.clear();
725 self.current_regime = MarketRegime::Unknown;
726 }
727
728 pub fn bar_count(&self) -> usize {
730 self.bar_count
731 }
732}
733
734pub struct MarketRegimeDetector {
754 adx: Adx,
755 hurst: HurstExponent,
756 hv: HistoricalVolatility,
757 bb_width: BollingerWidth,
758 config: RegimeConfig,
759}
760
761impl MarketRegimeDetector {
762 pub fn new(period: usize, config: RegimeConfig) -> Result<Self, FinError> {
767 if period < 2 {
768 return Err(FinError::InvalidPeriod(period));
769 }
770 Ok(Self {
771 adx: Adx::new("regime_adx", period)?,
772 hurst: HurstExponent::new("regime_hurst", period)?,
773 hv: HistoricalVolatility::new("regime_hv", period, 252)?,
774 bb_width: BollingerWidth::new("regime_bb_width", period, Decimal::from(2u32))?,
775 config,
776 })
777 }
778
779 pub fn with_defaults(period: usize) -> Result<Self, FinError> {
784 Self::new(period, RegimeConfig::default())
785 }
786
787 pub fn update(&mut self, bar: &BarInput) -> Result<MarketRegime, FinError> {
792 let adx_val = self.adx.update(bar)?;
793 let hurst_val = self.hurst.update(bar)?;
794 let hv_val = self.hv.update(bar)?;
795 let bb_w_val = self.bb_width.update(bar)?;
796
797 let (adx, hurst, hv, bb_w) = match (adx_val, hurst_val, hv_val, bb_w_val) {
798 (
799 SignalValue::Scalar(a),
800 SignalValue::Scalar(h),
801 SignalValue::Scalar(v),
802 SignalValue::Scalar(b),
803 ) => (a, h, v, b),
804 _ => return Ok(MarketRegime::Unknown),
805 };
806
807 let adx_f = adx.to_f64().unwrap_or(0.0);
808 let hurst_f = hurst.to_f64().unwrap_or(0.5);
809 let hv_f = hv.to_f64().unwrap_or(0.0);
810 let bb_w_f = bb_w.to_f64().unwrap_or(f64::MAX);
811
812 if hurst_f > self.config.hurst_trending && adx_f > self.config.adx_trend_threshold {
814 return Ok(MarketRegime::Trending);
815 }
816 if hv_f > self.config.vol_high_multiplier * 15.0 {
817 return Ok(MarketRegime::HighVolatility);
818 }
819 if hurst_f < self.config.hurst_mean_reverting {
820 return Ok(MarketRegime::MeanReverting);
821 }
822 if bb_w_f < self.config.bb_width_quiet {
823 return Ok(MarketRegime::LowVolatility);
824 }
825
826 Ok(MarketRegime::Neutral)
827 }
828
829 pub fn is_ready(&self) -> bool {
831 self.adx.is_ready()
832 && self.hurst.is_ready()
833 && self.hv.is_ready()
834 && self.bb_width.is_ready()
835 }
836
837 pub fn config(&self) -> &RegimeConfig {
839 &self.config
840 }
841
842 pub fn reset(&mut self) {
844 self.adx.reset();
845 self.hurst.reset();
846 self.hv.reset();
847 self.bb_width.reset();
848 }
849}
850
851pub struct RegimeConditionalSignal {
874 rsi_trending: crate::signals::indicators::Rsi,
876 rsi_mean_reverting: crate::signals::indicators::Rsi,
878 rsi_neutral: crate::signals::indicators::Rsi,
880}
881
882impl RegimeConditionalSignal {
883 pub fn new(
892 trending_period: usize,
893 mean_reverting_period: usize,
894 neutral_period: usize,
895 ) -> Result<Self, FinError> {
896 Ok(Self {
897 rsi_trending: crate::signals::indicators::Rsi::new(
898 "rsi_trending",
899 trending_period,
900 )?,
901 rsi_mean_reverting: crate::signals::indicators::Rsi::new(
902 "rsi_mean_reverting",
903 mean_reverting_period,
904 )?,
905 rsi_neutral: crate::signals::indicators::Rsi::new("rsi_neutral", neutral_period)?,
906 })
907 }
908
909 pub fn update(
917 &mut self,
918 bar: &BarInput,
919 regime: MarketRegime,
920 ) -> Option<Result<f64, FinError>> {
921 let v_trending = self.rsi_trending.update(bar);
923 let v_mr = self.rsi_mean_reverting.update(bar);
924 let v_neutral = self.rsi_neutral.update(bar);
925
926 if regime.is_risk_off() {
927 return None;
928 }
929
930 let chosen = match regime {
931 MarketRegime::Trending => v_trending,
932 MarketRegime::MeanReverting => v_mr,
933 _ => v_neutral,
934 };
935
936 match chosen {
937 Ok(SignalValue::Scalar(v)) => {
938 Some(Ok(v.to_f64().unwrap_or(50.0)))
939 }
940 Ok(_) => None,
941 Err(e) => Some(Err(e)),
942 }
943 }
944
945 pub fn is_ready(&self) -> bool {
947 self.rsi_trending.is_ready()
948 && self.rsi_mean_reverting.is_ready()
949 && self.rsi_neutral.is_ready()
950 }
951
952 pub fn reset(&mut self) {
954 self.rsi_trending.reset();
955 self.rsi_mean_reverting.reset();
956 self.rsi_neutral.reset();
957 }
958}
959
960#[cfg(test)]
963mod tests {
964 use super::*;
965 use rust_decimal_macros::dec;
966
967 fn bar(h: f64, l: f64, c: f64) -> BarInput {
968 BarInput::new(
969 Decimal::try_from(c).unwrap_or(dec!(100)),
970 Decimal::try_from(h).unwrap_or(dec!(102)),
971 Decimal::try_from(l).unwrap_or(dec!(98)),
972 Decimal::try_from(c).unwrap_or(dec!(100)),
973 dec!(1000),
974 )
975 }
976
977 #[test]
980 fn test_regime_display_all_variants() {
981 assert_eq!(MarketRegime::Trending.to_string(), "Trending");
982 assert_eq!(MarketRegime::MeanReverting.to_string(), "MeanReverting");
983 assert_eq!(MarketRegime::HighVolatility.to_string(), "HighVolatility");
984 assert_eq!(MarketRegime::LowVolatility.to_string(), "LowVolatility");
985 assert_eq!(MarketRegime::Crisis.to_string(), "Crisis");
986 assert_eq!(MarketRegime::Neutral.to_string(), "Neutral");
987 assert_eq!(MarketRegime::Unknown.to_string(), "Unknown");
988 }
989
990 #[test]
991 fn test_regime_short_codes() {
992 assert_eq!(MarketRegime::Trending.short_code(), "TRD");
993 assert_eq!(MarketRegime::Crisis.short_code(), "CRS");
994 assert_eq!(MarketRegime::Unknown.short_code(), "UNK");
995 }
996
997 #[test]
998 fn test_is_risk_off() {
999 assert!(MarketRegime::Crisis.is_risk_off());
1000 assert!(MarketRegime::Unknown.is_risk_off());
1001 assert!(!MarketRegime::Trending.is_risk_off());
1002 assert!(!MarketRegime::Neutral.is_risk_off());
1003 }
1004
1005 #[test]
1008 fn test_garch_invalid_params() {
1009 assert!(Garch11::new(0.0, 0.85, 1e-6).is_err());
1010 assert!(Garch11::new(0.1, 0.0, 1e-6).is_err());
1011 assert!(Garch11::new(0.1, 0.85, 0.0).is_err());
1012 assert!(Garch11::new(0.5, 0.6, 1e-6).is_err()); }
1014
1015 #[test]
1016 fn test_garch_produces_positive_sigma() {
1017 let mut g = Garch11::new(0.1, 0.85, 1e-6).unwrap();
1018 let returns = [-0.01, 0.02, -0.015, 0.005, 0.03, -0.02, 0.01];
1019 for ret in returns {
1020 let sigma = g.update(ret);
1021 assert!(sigma > 0.0, "sigma must be positive, got {sigma}");
1022 }
1023 }
1024
1025 #[test]
1026 fn test_garch_reset() {
1027 let mut g = Garch11::new(0.1, 0.85, 1e-6).unwrap();
1028 for ret in [-0.05, 0.05, -0.05] {
1029 g.update(ret);
1030 }
1031 let sigma_before = g.sigma();
1032 g.reset();
1033 let lr = g.long_run_sigma();
1035 assert!((g.sigma() - lr).abs() < 1e-10);
1036 assert_ne!(sigma_before, g.sigma());
1037 assert_eq!(g.count(), 0);
1038 }
1039
1040 #[test]
1041 fn test_garch_vol_elevated() {
1042 let mut g = Garch11::new(0.1, 0.85, 1e-4).unwrap();
1043 for _ in 0..10 {
1045 g.update(0.1); }
1047 assert!(g.is_vol_elevated(1.0) || g.sigma() > 0.0); }
1050
1051 #[test]
1054 fn test_correlation_invalid_params() {
1055 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()); }
1059
1060 #[test]
1061 fn test_no_crisis_single_asset() {
1062 let mut d = CorrelationBreakdownDetector::new(10, 0.3, 0.6).unwrap();
1063 for i in 0..15 {
1064 d.update(0, if i % 2 == 0 { 0.01 } else { -0.01 });
1065 }
1066 assert!(!d.is_crisis()); }
1068
1069 #[test]
1070 fn test_correlation_reset() {
1071 let mut d = CorrelationBreakdownDetector::new(10, 0.3, 0.6).unwrap();
1072 for i in 0..15 {
1073 d.update(0, if i % 2 == 0 { 0.01 } else { -0.01 });
1074 d.update(1, if i % 3 == 0 { 0.01 } else { -0.01 });
1075 }
1076 d.reset();
1077 assert!(!d.is_crisis());
1078 }
1079
1080 #[test]
1083 fn test_pearson_r_perfect_correlation() {
1084 let x = [1.0, 2.0, 3.0, 4.0, 5.0];
1085 let r = pearson_r(&x, &x);
1086 assert!((r - 1.0).abs() < 1e-10);
1087 }
1088
1089 #[test]
1090 fn test_pearson_r_perfect_anti_correlation() {
1091 let x = [1.0, 2.0, 3.0, 4.0, 5.0];
1092 let y: Vec<f64> = x.iter().map(|v| -v).collect();
1093 let r = pearson_r(&x, &y);
1094 assert!((r + 1.0).abs() < 1e-10);
1095 }
1096
1097 #[test]
1098 fn test_pearson_r_constant_series_returns_zero() {
1099 let x = [1.0, 1.0, 1.0, 1.0];
1100 let y = [2.0, 2.0, 2.0, 2.0];
1101 let r = pearson_r(&x, &y);
1102 assert_eq!(r, 0.0);
1103 }
1104
1105 #[test]
1108 fn test_regime_history_duration() {
1109 let h = RegimeHistory {
1110 regime: MarketRegime::Trending,
1111 started_at_bar: 10,
1112 confidence: 0.8,
1113 ended_at_bar: Some(25),
1114 };
1115 assert_eq!(h.duration_bars(), Some(15));
1116 assert!(!h.is_active());
1117 }
1118
1119 #[test]
1120 fn test_regime_history_active() {
1121 let h = RegimeHistory {
1122 regime: MarketRegime::Neutral,
1123 started_at_bar: 5,
1124 confidence: 0.5,
1125 ended_at_bar: None,
1126 };
1127 assert!(h.is_active());
1128 assert_eq!(h.duration_bars(), None);
1129 }
1130
1131 #[test]
1134 fn test_detector_period_validation() {
1135 assert!(RegimeDetector::new(0, RegimeConfig::default()).is_err());
1136 assert!(RegimeDetector::new(1, RegimeConfig::default()).is_err());
1137 assert!(RegimeDetector::new(2, RegimeConfig::default()).is_ok());
1138 }
1139
1140 #[test]
1141 fn test_detector_unknown_before_warmup() {
1142 let mut d = RegimeDetector::new(5, RegimeConfig::default()).unwrap();
1143 let (regime, _) = d.update(&bar(102.0, 98.0, 100.0), &[]).unwrap();
1144 assert_eq!(regime, MarketRegime::Unknown);
1145 assert!(!d.is_ready());
1146 }
1147
1148 #[test]
1149 fn test_detector_bar_count() {
1150 let mut d = RegimeDetector::new(5, RegimeConfig::default()).unwrap();
1151 for i in 0..5 {
1152 d.update(&bar(100.0 + i as f64, 99.0, 100.0 + i as f64), &[]).unwrap();
1153 }
1154 assert_eq!(d.bar_count(), 5);
1155 }
1156
1157 #[test]
1158 fn test_detector_reset_clears_state() {
1159 let mut d = RegimeDetector::with_defaults(5).unwrap();
1160 for i in 0..30 {
1161 let c = 100.0 + i as f64;
1162 d.update(&bar(c + 1.0, c - 1.0, c), &[]).unwrap();
1163 }
1164 d.reset();
1165 assert!(!d.is_ready());
1166 assert_eq!(d.bar_count(), 0);
1167 assert!(d.history().is_empty());
1168 }
1169
1170 #[test]
1171 fn test_detector_history_populated_after_transition() {
1172 let mut d = RegimeDetector::new(3, RegimeConfig::default()).unwrap();
1173 for i in 0..40 {
1174 let c = 100.0 + i as f64 * 0.1;
1175 d.update(&bar(c + 0.2, c - 0.2, c), &[]).unwrap();
1176 }
1177 let _ = d.history(); }
1181
1182 #[test]
1183 fn test_detector_garch_accessor() {
1184 let d = RegimeDetector::with_defaults(5).unwrap();
1185 assert!(d.garch().sigma() > 0.0);
1186 }
1187
1188 #[test]
1189 fn test_detector_no_panic_many_bars() {
1190 let mut d = RegimeDetector::new(10, RegimeConfig::default()).unwrap();
1191 for i in 0..200 {
1192 let c = 100.0 + (i as f64 * 0.5).sin() * 5.0;
1193 d.update(&bar(c + 1.0, c - 1.0, c), &[]).unwrap();
1194 }
1195 }
1196
1197 #[test]
1200 fn test_legacy_detector_period_zero_fails() {
1201 assert!(MarketRegimeDetector::new(0, RegimeConfig::default()).is_err());
1202 assert!(MarketRegimeDetector::new(1, RegimeConfig::default()).is_err());
1203 }
1204
1205 #[test]
1206 fn test_legacy_unknown_before_warmup() {
1207 let mut d = MarketRegimeDetector::new(5, RegimeConfig::default()).unwrap();
1208 let regime = d.update(&bar(102.0, 98.0, 100.0)).unwrap();
1209 assert_eq!(regime, MarketRegime::Unknown);
1210 assert!(!d.is_ready());
1211 }
1212
1213 #[test]
1214 fn test_legacy_reset_clears_warmup() {
1215 let mut d = MarketRegimeDetector::with_defaults(5).unwrap();
1216 for i in 0..30 {
1217 let h = 100.0 + i as f64;
1218 d.update(&bar(h + 1.0, h - 1.0, h)).unwrap();
1219 }
1220 d.reset();
1221 assert!(!d.is_ready());
1222 }
1223
1224 #[test]
1227 fn test_conditional_signal_invalid_period() {
1228 assert!(RegimeConditionalSignal::new(0, 21, 14).is_err());
1229 assert!(RegimeConditionalSignal::new(14, 0, 14).is_err());
1230 assert!(RegimeConditionalSignal::new(14, 21, 0).is_err());
1231 }
1232
1233 #[test]
1234 fn test_conditional_signal_suppressed_in_crisis() {
1235 let mut sig = RegimeConditionalSignal::new(5, 10, 7).unwrap();
1236 let b = bar(102.0, 98.0, 100.0);
1237 let result = sig.update(&b, MarketRegime::Crisis);
1238 assert!(result.is_none());
1239 }
1240
1241 #[test]
1242 fn test_conditional_signal_suppressed_when_unknown() {
1243 let mut sig = RegimeConditionalSignal::new(5, 10, 7).unwrap();
1244 let b = bar(102.0, 98.0, 100.0);
1245 let result = sig.update(&b, MarketRegime::Unknown);
1246 assert!(result.is_none());
1247 }
1248
1249 #[test]
1250 fn test_conditional_signal_produces_value_after_warmup() {
1251 let period = 5usize;
1252 let mut sig = RegimeConditionalSignal::new(period, period + 2, period).unwrap();
1253 let mut last_val = None;
1254 for i in 0..((period + 2) * 3) {
1255 let c = 100.0 + i as f64 * 0.1;
1256 last_val = sig.update(&bar(c + 0.5, c - 0.5, c), MarketRegime::Trending);
1257 }
1258 if let Some(Ok(rsi_val)) = last_val {
1260 assert!(rsi_val >= 0.0 && rsi_val <= 100.0);
1261 }
1262 }
1264
1265 #[test]
1266 fn test_conditional_signal_reset() {
1267 let mut sig = RegimeConditionalSignal::new(5, 10, 7).unwrap();
1268 let b = bar(102.0, 98.0, 100.0);
1269 for _ in 0..30 {
1270 let _ = sig.update(&b, MarketRegime::Neutral);
1271 }
1272 sig.reset();
1273 assert!(!sig.is_ready());
1274 }
1275}