1use std::collections::HashMap;
22
23use serde::{Deserialize, Serialize};
24
25use crate::error::IndicatorError;
26use crate::indicator::{Indicator, IndicatorOutput};
27use crate::regime::detector::RegimeDetector;
28use crate::regime::ensemble::{EnsembleConfig, EnsembleRegimeDetector, EnsembleResult};
29use crate::regime::hmm::{HMMConfig, HMMRegimeDetector};
30use crate::registry::param_usize;
31use crate::types::{Candle, MarketRegime, RegimeConfidence, RegimeConfig, TrendDirection};
32
33#[derive(Debug, Clone)]
40pub struct RouterIndicator {
41 pub config: EnhancedRouterConfig,
42}
43
44impl RouterIndicator {
45 pub fn new(config: EnhancedRouterConfig) -> Self {
46 Self { config }
47 }
48 pub fn with_defaults() -> Self {
49 Self::new(EnhancedRouterConfig::default())
50 }
51}
52
53fn routed_regime_id(r: MarketRegime) -> f64 {
54 match r {
55 MarketRegime::MeanReverting => 1.0,
56 MarketRegime::Volatile => 2.0,
57 MarketRegime::Trending(TrendDirection::Bullish) => 3.0,
58 MarketRegime::Trending(TrendDirection::Bearish) => 4.0,
59 MarketRegime::Uncertain => 0.0,
60 }
61}
62
63impl Indicator for RouterIndicator {
64 fn name(&self) -> &'static str {
65 "Router"
66 }
67 fn required_len(&self) -> usize {
68 self.config.indicator_config.adx_period * 2
69 + self.config.indicator_config.regime_stability_bars
70 }
71 fn required_columns(&self) -> &[&'static str] {
72 &["high", "low", "close"]
73 }
74
75 fn calculate(&self, candles: &[Candle]) -> Result<IndicatorOutput, IndicatorError> {
76 self.check_len(candles)?;
77 let mut router = EnhancedRouter::new(self.config.clone());
78 let sym = "_batch";
79 let n = candles.len();
80 let mut conf_out = vec![f64::NAN; n];
81 let mut factor_out = vec![f64::NAN; n];
82 let mut regime_out = vec![f64::NAN; n];
83 for (i, c) in candles.iter().enumerate() {
84 if let Some(sig) = router.update(sym, c.high, c.low, c.close) {
85 conf_out[i] = sig.confidence;
86 factor_out[i] = sig.position_factor;
87 regime_out[i] = routed_regime_id(sig.regime);
88 }
89 }
90 Ok(IndicatorOutput::from_pairs([
91 ("router_conf", conf_out),
92 ("router_factor", factor_out),
93 ("router_regime", regime_out),
94 ]))
95 }
96}
97
98pub fn factory<S: ::std::hash::BuildHasher>(params: &HashMap<String, String, S>) -> Result<Box<dyn Indicator>, IndicatorError> {
101 let adx_period = param_usize(params, "adx_period", 14)?;
102 let bb_period = param_usize(params, "bb_period", 20)?;
103 let indicator_config = RegimeConfig {
104 adx_period,
105 bb_period,
106 ..RegimeConfig::default()
107 };
108 let config = EnhancedRouterConfig {
109 indicator_config,
110 ..EnhancedRouterConfig::default()
111 };
112 Ok(Box::new(RouterIndicator::new(config)))
113}
114
115#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
117pub enum DetectionMethod {
118 Indicators,
120 #[allow(clippy::upper_case_acronyms)]
122 HMM,
123 #[default]
125 Ensemble,
126}
127
128impl std::fmt::Display for DetectionMethod {
129 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
130 match self {
131 DetectionMethod::Indicators => write!(f, "Indicators"),
132 DetectionMethod::HMM => write!(f, "HMM"),
133 DetectionMethod::Ensemble => write!(f, "Ensemble"),
134 }
135 }
136}
137
138#[derive(Debug, Clone, Serialize, Deserialize)]
140pub struct EnhancedRouterConfig {
141 pub detection_method: DetectionMethod,
143
144 pub indicator_config: RegimeConfig,
146
147 pub hmm_config: Option<HMMConfig>,
149
150 pub ensemble_config: Option<EnsembleConfig>,
152
153 pub volatile_position_factor: f64,
155
156 pub min_confidence: f64,
158
159 pub log_changes: bool,
161}
162
163impl Default for EnhancedRouterConfig {
164 fn default() -> Self {
165 Self {
166 detection_method: DetectionMethod::Ensemble,
167 indicator_config: RegimeConfig::crypto_optimized(),
168 hmm_config: Some(HMMConfig::crypto_optimized()),
169 ensemble_config: Some(EnsembleConfig::default()),
170 volatile_position_factor: 0.5,
171 min_confidence: 0.5,
172 log_changes: true,
173 }
174 }
175}
176
177#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
179pub enum ActiveStrategy {
180 TrendFollowing,
182 MeanReversion,
184 NoTrade,
186}
187
188impl std::fmt::Display for ActiveStrategy {
189 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
190 match self {
191 ActiveStrategy::TrendFollowing => write!(f, "Trend Following"),
192 ActiveStrategy::MeanReversion => write!(f, "Mean Reversion"),
193 ActiveStrategy::NoTrade => write!(f, "No Trade"),
194 }
195 }
196}
197
198#[derive(Debug, Clone)]
200pub struct RoutedSignal {
201 pub strategy: ActiveStrategy,
203 pub regime: MarketRegime,
205 pub confidence: f64,
207 pub position_factor: f64,
209 pub reason: String,
211
212 pub detection_method: DetectionMethod,
214
215 pub methods_agree: Option<bool>,
217
218 pub state_probabilities: Option<Vec<f64>>,
220
221 pub expected_duration: Option<f64>,
223
224 pub trend_direction: Option<TrendDirection>,
226}
227
228impl std::fmt::Display for RoutedSignal {
229 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
230 write!(
231 f,
232 "Strategy: {} | Regime: {} | Conf: {:.0}% | Size: {:.0}%",
233 self.strategy,
234 self.regime,
235 self.confidence * 100.0,
236 self.position_factor * 100.0
237 )?;
238
239 if let Some(agree) = self.methods_agree {
240 write!(f, " | Agree: {}", if agree { "✓" } else { "✗" })?;
241 }
242
243 if let Some(dur) = self.expected_duration {
244 write!(f, " | ExpDur: {dur:.0} bars")?;
245 }
246
247 Ok(())
248 }
249}
250
251#[allow(clippy::upper_case_acronyms)]
253enum Detector {
254 Indicator(Box<RegimeDetector>),
255 HMM(Box<HMMRegimeDetector>),
256 Ensemble(Box<EnsembleRegimeDetector>),
257}
258
259impl std::fmt::Debug for Detector {
260 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
261 match self {
262 Detector::Indicator(_) => write!(f, "Detector::Indicator(...)"),
263 Detector::HMM(_) => write!(f, "Detector::HMM(...)"),
264 Detector::Ensemble(_) => write!(f, "Detector::Ensemble(...)"),
265 }
266 }
267}
268
269#[derive(Debug)]
271struct AssetState {
272 detector: Detector,
273 current_strategy: ActiveStrategy,
274 last_regime: MarketRegime,
275 regime_change_count: u32,
276 last_confidence: Option<RegimeConfidence>,
279}
280
281pub struct EnhancedRouter {
305 config: EnhancedRouterConfig,
306 assets: HashMap<String, AssetState>,
307}
308
309impl std::fmt::Debug for EnhancedRouter {
310 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
311 f.debug_struct("EnhancedRouter")
312 .field("config", &self.config)
313 .field("assets", &self.assets.keys().collect::<Vec<_>>())
314 .finish()
315 }
316}
317
318impl EnhancedRouter {
319 pub fn new(config: EnhancedRouterConfig) -> Self {
321 Self {
322 config,
323 assets: HashMap::new(),
324 }
325 }
326
327 pub fn with_indicators() -> Self {
329 Self::new(EnhancedRouterConfig {
330 detection_method: DetectionMethod::Indicators,
331 ..Default::default()
332 })
333 }
334
335 pub fn with_hmm() -> Self {
337 Self::new(EnhancedRouterConfig {
338 detection_method: DetectionMethod::HMM,
339 hmm_config: Some(HMMConfig::crypto_optimized()),
340 ..Default::default()
341 })
342 }
343
344 pub fn with_ensemble() -> Self {
346 Self::new(EnhancedRouterConfig {
347 detection_method: DetectionMethod::Ensemble,
348 ensemble_config: Some(EnsembleConfig::default()),
349 ..Default::default()
350 })
351 }
352
353 pub fn register_asset(&mut self, symbol: &str) {
358 if self.assets.contains_key(symbol) {
359 return;
360 }
361
362 let detector = match self.config.detection_method {
363 DetectionMethod::Indicators => Detector::Indicator(Box::new(RegimeDetector::new(
364 self.config.indicator_config.clone(),
365 ))),
366 DetectionMethod::HMM => {
367 let hmm_config = self.config.hmm_config.clone().unwrap_or_default();
368 Detector::HMM(Box::new(HMMRegimeDetector::new(hmm_config)))
369 }
370 DetectionMethod::Ensemble => {
371 let ens_config = self.config.ensemble_config.clone().unwrap_or_default();
372 Detector::Ensemble(Box::new(EnsembleRegimeDetector::new(
373 ens_config,
374 self.config.indicator_config.clone(),
375 )))
376 }
377 };
378
379 self.assets.insert(
380 symbol.to_string(),
381 AssetState {
382 detector,
383 current_strategy: ActiveStrategy::NoTrade,
384 last_regime: MarketRegime::Uncertain,
385 regime_change_count: 0,
386 last_confidence: None,
387 },
388 );
389 }
390
391 pub fn unregister_asset(&mut self, symbol: &str) -> bool {
393 self.assets.remove(symbol).is_some()
394 }
395
396 pub fn update(
401 &mut self,
402 symbol: &str,
403 high: f64,
404 low: f64,
405 close: f64,
406 ) -> Option<RoutedSignal> {
407 if !self.assets.contains_key(symbol) {
408 self.register_asset(symbol);
409 }
410
411 let state = self.assets.get_mut(symbol)?;
412
413 let (regime_result, methods_agree, state_probs, expected_duration) =
417 match &mut state.detector {
418 Detector::Indicator(det) => {
419 let result = det.update(high, low, close);
420 (result, None, None, None)
421 }
422 Detector::HMM(det) => {
423 let result = det.update_ohlc(high, low, close);
424 let probs = det.state_probabilities().to_vec();
425 let duration = det.expected_regime_duration(det.current_state_index());
426 (result, None, Some(probs), Some(duration))
427 }
428 Detector::Ensemble(det) => {
429 let ens_result: EnsembleResult = det.update(high, low, close);
430 let probs = det.hmm_state_probabilities().to_vec();
431 let duration = det.expected_regime_duration();
432 (
433 ens_result.to_regime_confidence(),
434 Some(ens_result.methods_agree),
435 Some(probs),
436 Some(duration),
437 )
438 }
439 };
440
441 state.last_confidence = Some(regime_result);
443
444 if regime_result.regime != state.last_regime {
446 state.regime_change_count += 1;
447 if self.config.log_changes {
448 println!(
449 "[{}] Regime change #{} ({:?}): {} → {} (conf: {:.2})",
450 symbol,
451 state.regime_change_count,
452 self.config.detection_method,
453 state.last_regime,
454 regime_result.regime,
455 regime_result.confidence
456 );
457 }
458 state.last_regime = regime_result.regime;
459 }
460
461 let min_confidence = self.config.min_confidence;
463 let volatile_factor = self.config.volatile_position_factor;
464 let (strategy, position_factor, reason) =
465 Self::compute_strategy(®ime_result, min_confidence, volatile_factor);
466 state.current_strategy = strategy;
467
468 let trend_direction = match regime_result.regime {
470 MarketRegime::Trending(dir) => Some(dir),
471 _ => None,
472 };
473
474 Some(RoutedSignal {
475 strategy,
476 regime: regime_result.regime,
477 confidence: regime_result.confidence,
478 position_factor,
479 reason,
480 detection_method: self.config.detection_method,
481 methods_agree,
482 state_probabilities: state_probs,
483 expected_duration,
484 trend_direction,
485 })
486 }
487
488 fn compute_strategy(
492 regime: &RegimeConfidence,
493 min_confidence: f64,
494 volatile_factor: f64,
495 ) -> (ActiveStrategy, f64, String) {
496 if regime.confidence < min_confidence {
497 return (
498 ActiveStrategy::NoTrade,
499 0.0,
500 format!(
501 "Confidence too low ({:.0}% < {:.0}%)",
502 regime.confidence * 100.0,
503 min_confidence * 100.0
504 ),
505 );
506 }
507
508 match regime.regime {
509 MarketRegime::Trending(dir) => (
510 ActiveStrategy::TrendFollowing,
511 1.0,
512 format!(
513 "{} trend detected (ADX: {:.1}, conf: {:.0}%)",
514 dir,
515 regime.adx_value,
516 regime.confidence * 100.0
517 ),
518 ),
519 MarketRegime::MeanReverting => (
520 ActiveStrategy::MeanReversion,
521 1.0,
522 format!(
523 "Mean-reverting regime (BB%: {:.0}, conf: {:.0}%)",
524 regime.bb_width_percentile,
525 regime.confidence * 100.0
526 ),
527 ),
528 MarketRegime::Volatile => (
529 ActiveStrategy::MeanReversion,
530 volatile_factor,
531 format!(
532 "Volatile regime — reduced size to {:.0}% (conf: {:.0}%)",
533 volatile_factor * 100.0,
534 regime.confidence * 100.0
535 ),
536 ),
537 MarketRegime::Uncertain => (
538 ActiveStrategy::NoTrade,
539 0.0,
540 "Uncertain regime — staying out".to_string(),
541 ),
542 }
543 }
544
545 pub fn get_regime(&self, symbol: &str) -> Option<MarketRegime> {
551 self.assets.get(symbol).map(|s| s.last_regime)
552 }
553
554 pub fn last_regime_confidence(&self, symbol: &str) -> Option<&RegimeConfidence> {
560 self.assets
561 .get(symbol)
562 .and_then(|s| s.last_confidence.as_ref())
563 }
564
565 pub fn atr_value(&self, symbol: &str) -> Option<f64> {
575 self.assets.get(symbol).and_then(|s| match &s.detector {
576 Detector::Indicator(det) => det.atr_value(),
577 Detector::HMM(_) => None,
578 Detector::Ensemble(det) => det.indicator_detector().atr_value(),
579 })
580 }
581
582 pub fn adx_value(&self, symbol: &str) -> Option<f64> {
592 self.assets.get(symbol).and_then(|s| match &s.detector {
593 Detector::Indicator(det) => det.adx_value(),
594 Detector::HMM(_) => None,
595 Detector::Ensemble(det) => det.indicator_detector().adx_value(),
596 })
597 }
598
599 pub fn get_strategy(&self, symbol: &str) -> Option<ActiveStrategy> {
601 self.assets.get(symbol).map(|s| s.current_strategy)
602 }
603
604 pub fn is_ready(&self, symbol: &str) -> bool {
606 self.assets.get(symbol).is_some_and(|s| match &s.detector {
607 Detector::Indicator(d) => d.is_ready(),
608 Detector::HMM(d) => d.is_ready(),
609 Detector::Ensemble(d) => d.is_ready(),
610 })
611 }
612
613 pub fn detection_method(&self) -> DetectionMethod {
615 self.config.detection_method
616 }
617
618 pub fn regime_changes(&self, symbol: &str) -> u32 {
620 self.assets.get(symbol).map_or(0, |s| s.regime_change_count)
621 }
622
623 pub fn registered_assets(&self) -> Vec<&str> {
625 self.assets.keys().map(String::as_str).collect()
626 }
627
628 pub fn config(&self) -> &EnhancedRouterConfig {
630 &self.config
631 }
632
633 pub fn summary(&self) -> Vec<AssetSummary> {
635 self.assets
636 .iter()
637 .map(|(symbol, state)| AssetSummary {
638 symbol: symbol.clone(),
639 regime: state.last_regime,
640 strategy: state.current_strategy,
641 regime_changes: state.regime_change_count,
642 is_ready: match &state.detector {
643 Detector::Indicator(d) => d.is_ready(),
644 Detector::HMM(d) => d.is_ready(),
645 Detector::Ensemble(d) => d.is_ready(),
646 },
647 })
648 .collect()
649 }
650}
651
652#[derive(Debug, Clone, Serialize, Deserialize)]
654pub struct AssetSummary {
655 pub symbol: String,
656 pub regime: MarketRegime,
657 pub strategy: ActiveStrategy,
658 pub regime_changes: u32,
659 pub is_ready: bool,
660}
661
662impl std::fmt::Display for AssetSummary {
663 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
664 write!(
665 f,
666 "{}: {} → {} (changes: {}, ready: {})",
667 self.symbol, self.regime, self.strategy, self.regime_changes, self.is_ready
668 )
669 }
670}
671
672#[cfg(test)]
677mod tests {
678 use super::*;
679
680 #[test]
681 fn test_router_creation_ensemble() {
682 let router = EnhancedRouter::with_ensemble();
683 assert_eq!(router.detection_method(), DetectionMethod::Ensemble);
684 }
685
686 #[test]
687 fn test_router_creation_indicators() {
688 let router = EnhancedRouter::with_indicators();
689 assert_eq!(router.detection_method(), DetectionMethod::Indicators);
690 }
691
692 #[test]
693 fn test_router_creation_hmm() {
694 let router = EnhancedRouter::with_hmm();
695 assert_eq!(router.detection_method(), DetectionMethod::HMM);
696 }
697
698 #[test]
699 fn test_method_switching() {
700 let indicator_router = EnhancedRouter::with_indicators();
701 let hmm_router = EnhancedRouter::with_hmm();
702 let ensemble_router = EnhancedRouter::with_ensemble();
703
704 assert_eq!(
705 indicator_router.detection_method(),
706 DetectionMethod::Indicators
707 );
708 assert_eq!(hmm_router.detection_method(), DetectionMethod::HMM);
709 assert_eq!(
710 ensemble_router.detection_method(),
711 DetectionMethod::Ensemble
712 );
713 }
714
715 #[test]
716 fn test_asset_registration() {
717 let mut router = EnhancedRouter::with_ensemble();
718 router.register_asset("BTC/USD");
719 router.register_asset("ETH/USD");
720
721 assert!(router.get_regime("BTC/USD").is_some());
722 assert!(router.get_regime("ETH/USD").is_some());
723 assert!(router.get_regime("SOL/USD").is_none());
724 }
725
726 #[test]
727 fn test_asset_unregistration() {
728 let mut router = EnhancedRouter::with_ensemble();
729 router.register_asset("BTC/USD");
730 assert!(router.get_regime("BTC/USD").is_some());
731
732 assert!(router.unregister_asset("BTC/USD"));
733 assert!(router.get_regime("BTC/USD").is_none());
734
735 assert!(!router.unregister_asset("BTC/USD"));
737 }
738
739 #[test]
740 fn test_auto_registration() {
741 let mut router = EnhancedRouter::with_indicators();
742
743 assert!(router.get_regime("BTC/USD").is_none());
745 let signal = router.update("BTC/USD", 101.0, 99.0, 100.0);
746 assert!(signal.is_some());
747 assert!(router.get_regime("BTC/USD").is_some());
748 }
749
750 #[test]
751 fn test_duplicate_registration_noop() {
752 let mut router = EnhancedRouter::with_ensemble();
753 router.register_asset("BTC/USD");
754
755 for i in 0..50 {
757 let price = 100.0 + i as f64;
758 router.update("BTC/USD", price + 1.0, price - 1.0, price);
759 }
760
761 let changes_before = router.regime_changes("BTC/USD");
762
763 router.register_asset("BTC/USD");
765
766 let changes_after = router.regime_changes("BTC/USD");
767 assert_eq!(changes_before, changes_after);
768 }
769
770 #[test]
771 fn test_registered_assets() {
772 let mut router = EnhancedRouter::with_ensemble();
773 router.register_asset("BTC/USD");
774 router.register_asset("ETH/USD");
775 router.register_asset("SOL/USD");
776
777 let assets = router.registered_assets();
778 assert_eq!(assets.len(), 3);
779 assert!(assets.contains(&"BTC/USD"));
780 assert!(assets.contains(&"ETH/USD"));
781 assert!(assets.contains(&"SOL/USD"));
782 }
783
784 #[test]
785 fn test_initial_regime_is_uncertain() {
786 let mut router = EnhancedRouter::with_ensemble();
787 router.register_asset("BTC/USD");
788
789 assert_eq!(router.get_regime("BTC/USD"), Some(MarketRegime::Uncertain));
790 assert_eq!(
791 router.get_strategy("BTC/USD"),
792 Some(ActiveStrategy::NoTrade)
793 );
794 }
795
796 #[test]
797 fn test_not_ready_before_warmup() {
798 let mut router = EnhancedRouter::with_indicators();
799 router.register_asset("BTC/USD");
800
801 assert!(!router.is_ready("BTC/USD"));
802
803 for i in 0..10 {
805 let price = 100.0 + i as f64;
806 router.update("BTC/USD", price + 1.0, price - 1.0, price);
807 }
808
809 assert!(!router.is_ready("BTC/USD"));
810 }
811
812 #[test]
813 fn test_is_ready_unknown_asset() {
814 let router = EnhancedRouter::with_ensemble();
815 assert!(!router.is_ready("UNKNOWN"));
816 }
817
818 #[test]
819 fn test_regime_changes_counted() {
820 let mut router = EnhancedRouter::new(EnhancedRouterConfig {
821 detection_method: DetectionMethod::Indicators,
822 log_changes: false, ..Default::default()
824 });
825
826 router.register_asset("BTC/USD");
827 assert_eq!(router.regime_changes("BTC/USD"), 0);
828
829 for i in 0..300 {
831 let price = 100.0 + i as f64 * 0.5;
832 router.update("BTC/USD", price + 1.0, price - 1.0, price);
833 }
834
835 let changes = router.regime_changes("BTC/USD");
838 let _ = changes; }
840
841 #[test]
842 fn test_routed_signal_fields() {
843 let mut router = EnhancedRouter::new(EnhancedRouterConfig {
844 detection_method: DetectionMethod::Indicators,
845 log_changes: false,
846 ..Default::default()
847 });
848
849 let signal = router.update("BTC/USD", 101.0, 99.0, 100.0);
850 assert!(signal.is_some());
851
852 let signal = signal.unwrap();
853 assert_eq!(signal.detection_method, DetectionMethod::Indicators);
854 assert!((0.0..=1.0).contains(&signal.confidence));
855 assert!((0.0..=1.0).contains(&signal.position_factor));
856 assert!(!signal.reason.is_empty());
857 assert!(signal.methods_agree.is_none());
859 assert!(signal.state_probabilities.is_none());
860 assert!(signal.expected_duration.is_none());
861 }
862
863 #[test]
864 fn test_routed_signal_display() {
865 let signal = RoutedSignal {
866 strategy: ActiveStrategy::TrendFollowing,
867 regime: MarketRegime::Trending(TrendDirection::Bullish),
868 confidence: 0.85,
869 position_factor: 1.0,
870 reason: "Bullish trend".to_string(),
871 detection_method: DetectionMethod::Ensemble,
872 methods_agree: Some(true),
873 state_probabilities: Some(vec![0.6, 0.2, 0.2]),
874 expected_duration: Some(15.0),
875 trend_direction: Some(TrendDirection::Bullish),
876 };
877
878 let display = format!("{signal}");
879 assert!(display.contains("Trend Following"));
880 assert!(display.contains("85%"));
881 assert!(display.contains("100%"));
882 assert!(display.contains("✓"));
883 assert!(display.contains("15 bars"));
884 }
885
886 #[test]
887 fn test_compute_strategy_low_confidence() {
888 let regime = RegimeConfidence::new(MarketRegime::Trending(TrendDirection::Bullish), 0.3);
889 let (strategy, factor, reason) = EnhancedRouter::compute_strategy(®ime, 0.5, 0.5);
890
891 assert_eq!(strategy, ActiveStrategy::NoTrade);
892 assert_eq!(factor, 0.0);
893 assert!(reason.contains("Confidence too low"));
894 }
895
896 #[test]
897 fn test_compute_strategy_trending() {
898 let regime = RegimeConfidence::with_metrics(
899 MarketRegime::Trending(TrendDirection::Bullish),
900 0.8,
901 30.0,
902 50.0,
903 0.7,
904 );
905 let (strategy, factor, reason) = EnhancedRouter::compute_strategy(®ime, 0.5, 0.5);
906
907 assert_eq!(strategy, ActiveStrategy::TrendFollowing);
908 assert_eq!(factor, 1.0);
909 assert!(reason.contains("Bullish"));
910 }
911
912 #[test]
913 fn test_compute_strategy_mean_reverting() {
914 let regime =
915 RegimeConfidence::with_metrics(MarketRegime::MeanReverting, 0.7, 15.0, 30.0, 0.2);
916 let (strategy, factor, reason) = EnhancedRouter::compute_strategy(®ime, 0.5, 0.5);
917
918 assert_eq!(strategy, ActiveStrategy::MeanReversion);
919 assert_eq!(factor, 1.0);
920 assert!(reason.contains("Mean-reverting"));
921 }
922
923 #[test]
924 fn test_compute_strategy_volatile() {
925 let regime = RegimeConfidence::with_metrics(MarketRegime::Volatile, 0.75, 22.0, 85.0, 0.3);
926 let (strategy, factor, reason) = EnhancedRouter::compute_strategy(®ime, 0.5, 0.4);
927
928 assert_eq!(strategy, ActiveStrategy::MeanReversion);
929 assert_eq!(factor, 0.4);
930 assert!(reason.contains("Volatile"));
931 assert!(reason.contains("40%"));
932 }
933
934 #[test]
935 fn test_compute_strategy_uncertain() {
936 let regime = RegimeConfidence::new(MarketRegime::Uncertain, 0.6);
937 let (strategy, factor, _) = EnhancedRouter::compute_strategy(®ime, 0.5, 0.5);
938
939 assert_eq!(strategy, ActiveStrategy::NoTrade);
940 assert_eq!(factor, 0.0);
941 }
942
943 #[test]
944 fn test_active_strategy_display() {
945 assert_eq!(
946 format!("{}", ActiveStrategy::TrendFollowing),
947 "Trend Following"
948 );
949 assert_eq!(
950 format!("{}", ActiveStrategy::MeanReversion),
951 "Mean Reversion"
952 );
953 assert_eq!(format!("{}", ActiveStrategy::NoTrade), "No Trade");
954 }
955
956 #[test]
957 fn test_detection_method_display() {
958 assert_eq!(format!("{}", DetectionMethod::Indicators), "Indicators");
959 assert_eq!(format!("{}", DetectionMethod::HMM), "HMM");
960 assert_eq!(format!("{}", DetectionMethod::Ensemble), "Ensemble");
961 }
962
963 #[test]
964 fn test_summary() {
965 let mut router = EnhancedRouter::new(EnhancedRouterConfig {
966 detection_method: DetectionMethod::Indicators,
967 log_changes: false,
968 ..Default::default()
969 });
970
971 router.register_asset("BTC/USD");
972 router.register_asset("ETH/USD");
973
974 let summary = router.summary();
975 assert_eq!(summary.len(), 2);
976
977 for s in &summary {
978 assert!(s.symbol == "BTC/USD" || s.symbol == "ETH/USD");
979 assert_eq!(s.regime, MarketRegime::Uncertain);
980 assert_eq!(s.strategy, ActiveStrategy::NoTrade);
981 assert_eq!(s.regime_changes, 0);
982 }
983 }
984
985 #[test]
986 fn test_asset_summary_display() {
987 let summary = AssetSummary {
988 symbol: "BTC/USD".to_string(),
989 regime: MarketRegime::Trending(TrendDirection::Bullish),
990 strategy: ActiveStrategy::TrendFollowing,
991 regime_changes: 3,
992 is_ready: true,
993 };
994
995 let display = format!("{summary}");
996 assert!(display.contains("BTC/USD"));
997 assert!(display.contains("Trending"));
998 assert!(display.contains("Trend Following"));
999 }
1000
1001 #[test]
1002 fn test_hmm_signal_has_state_probs() {
1003 let mut router = EnhancedRouter::new(EnhancedRouterConfig {
1004 detection_method: DetectionMethod::HMM,
1005 log_changes: false,
1006 ..Default::default()
1007 });
1008
1009 let signal = router.update("BTC/USD", 101.0, 99.0, 100.0);
1010 let signal = signal.unwrap();
1011
1012 assert!(signal.state_probabilities.is_some());
1013 let probs = signal.state_probabilities.unwrap();
1014 assert_eq!(probs.len(), 3);
1015
1016 let sum: f64 = probs.iter().sum();
1017 assert!(
1018 (sum - 1.0).abs() < 1e-6,
1019 "State probabilities should sum to 1.0"
1020 );
1021 }
1022
1023 #[test]
1024 fn test_ensemble_signal_has_agreement() {
1025 let mut router = EnhancedRouter::new(EnhancedRouterConfig {
1026 detection_method: DetectionMethod::Ensemble,
1027 log_changes: false,
1028 ..Default::default()
1029 });
1030
1031 let signal = router.update("BTC/USD", 101.0, 99.0, 100.0);
1032 let signal = signal.unwrap();
1033
1034 assert!(signal.methods_agree.is_some());
1035 assert!(signal.state_probabilities.is_some());
1036 assert!(signal.expected_duration.is_some());
1037 }
1038}