1use crate::detector::RegimeDetector;
22use crate::ensemble::{EnsembleConfig, EnsembleRegimeDetector, EnsembleResult};
23use crate::hmm::{HMMConfig, HMMRegimeDetector};
24use crate::types::{MarketRegime, RegimeConfidence, RegimeConfig, TrendDirection};
25use serde::{Deserialize, Serialize};
26use std::collections::HashMap;
27
28#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
30pub enum DetectionMethod {
31 Indicators,
33 #[allow(clippy::upper_case_acronyms)]
35 HMM,
36 #[default]
38 Ensemble,
39}
40
41impl std::fmt::Display for DetectionMethod {
42 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
43 match self {
44 DetectionMethod::Indicators => write!(f, "Indicators"),
45 DetectionMethod::HMM => write!(f, "HMM"),
46 DetectionMethod::Ensemble => write!(f, "Ensemble"),
47 }
48 }
49}
50
51#[derive(Debug, Clone, Serialize, Deserialize)]
53pub struct EnhancedRouterConfig {
54 pub detection_method: DetectionMethod,
56
57 pub indicator_config: RegimeConfig,
59
60 pub hmm_config: Option<HMMConfig>,
62
63 pub ensemble_config: Option<EnsembleConfig>,
65
66 pub volatile_position_factor: f64,
68
69 pub min_confidence: f64,
71
72 pub log_changes: bool,
74}
75
76impl Default for EnhancedRouterConfig {
77 fn default() -> Self {
78 Self {
79 detection_method: DetectionMethod::Ensemble,
80 indicator_config: RegimeConfig::crypto_optimized(),
81 hmm_config: Some(HMMConfig::crypto_optimized()),
82 ensemble_config: Some(EnsembleConfig::default()),
83 volatile_position_factor: 0.5,
84 min_confidence: 0.5,
85 log_changes: true,
86 }
87 }
88}
89
90#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
92pub enum ActiveStrategy {
93 TrendFollowing,
95 MeanReversion,
97 NoTrade,
99}
100
101impl std::fmt::Display for ActiveStrategy {
102 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
103 match self {
104 ActiveStrategy::TrendFollowing => write!(f, "Trend Following"),
105 ActiveStrategy::MeanReversion => write!(f, "Mean Reversion"),
106 ActiveStrategy::NoTrade => write!(f, "No Trade"),
107 }
108 }
109}
110
111#[derive(Debug, Clone)]
113pub struct RoutedSignal {
114 pub strategy: ActiveStrategy,
116 pub regime: MarketRegime,
118 pub confidence: f64,
120 pub position_factor: f64,
122 pub reason: String,
124
125 pub detection_method: DetectionMethod,
127
128 pub methods_agree: Option<bool>,
130
131 pub state_probabilities: Option<Vec<f64>>,
133
134 pub expected_duration: Option<f64>,
136
137 pub trend_direction: Option<TrendDirection>,
139}
140
141impl std::fmt::Display for RoutedSignal {
142 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
143 write!(
144 f,
145 "Strategy: {} | Regime: {} | Conf: {:.0}% | Size: {:.0}%",
146 self.strategy,
147 self.regime,
148 self.confidence * 100.0,
149 self.position_factor * 100.0
150 )?;
151
152 if let Some(agree) = self.methods_agree {
153 write!(f, " | Agree: {}", if agree { "✓" } else { "✗" })?;
154 }
155
156 if let Some(dur) = self.expected_duration {
157 write!(f, " | ExpDur: {dur:.0} bars")?;
158 }
159
160 Ok(())
161 }
162}
163
164#[allow(clippy::upper_case_acronyms)]
166enum Detector {
167 Indicator(Box<RegimeDetector>),
168 HMM(Box<HMMRegimeDetector>),
169 Ensemble(Box<EnsembleRegimeDetector>),
170}
171
172impl std::fmt::Debug for Detector {
173 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
174 match self {
175 Detector::Indicator(_) => write!(f, "Detector::Indicator(...)"),
176 Detector::HMM(_) => write!(f, "Detector::HMM(...)"),
177 Detector::Ensemble(_) => write!(f, "Detector::Ensemble(...)"),
178 }
179 }
180}
181
182#[derive(Debug)]
184struct AssetState {
185 detector: Detector,
186 current_strategy: ActiveStrategy,
187 last_regime: MarketRegime,
188 regime_change_count: u32,
189 last_confidence: Option<RegimeConfidence>,
192}
193
194pub struct EnhancedRouter {
218 config: EnhancedRouterConfig,
219 assets: HashMap<String, AssetState>,
220}
221
222impl std::fmt::Debug for EnhancedRouter {
223 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
224 f.debug_struct("EnhancedRouter")
225 .field("config", &self.config)
226 .field("assets", &self.assets.keys().collect::<Vec<_>>())
227 .finish()
228 }
229}
230
231impl EnhancedRouter {
232 pub fn new(config: EnhancedRouterConfig) -> Self {
234 Self {
235 config,
236 assets: HashMap::new(),
237 }
238 }
239
240 pub fn with_indicators() -> Self {
242 Self::new(EnhancedRouterConfig {
243 detection_method: DetectionMethod::Indicators,
244 ..Default::default()
245 })
246 }
247
248 pub fn with_hmm() -> Self {
250 Self::new(EnhancedRouterConfig {
251 detection_method: DetectionMethod::HMM,
252 hmm_config: Some(HMMConfig::crypto_optimized()),
253 ..Default::default()
254 })
255 }
256
257 pub fn with_ensemble() -> Self {
259 Self::new(EnhancedRouterConfig {
260 detection_method: DetectionMethod::Ensemble,
261 ensemble_config: Some(EnsembleConfig::default()),
262 ..Default::default()
263 })
264 }
265
266 pub fn register_asset(&mut self, symbol: &str) {
271 if self.assets.contains_key(symbol) {
272 return;
273 }
274
275 let detector = match self.config.detection_method {
276 DetectionMethod::Indicators => Detector::Indicator(Box::new(RegimeDetector::new(
277 self.config.indicator_config.clone(),
278 ))),
279 DetectionMethod::HMM => {
280 let hmm_config = self.config.hmm_config.clone().unwrap_or_default();
281 Detector::HMM(Box::new(HMMRegimeDetector::new(hmm_config)))
282 }
283 DetectionMethod::Ensemble => {
284 let ens_config = self.config.ensemble_config.clone().unwrap_or_default();
285 Detector::Ensemble(Box::new(EnsembleRegimeDetector::new(
286 ens_config,
287 self.config.indicator_config.clone(),
288 )))
289 }
290 };
291
292 self.assets.insert(
293 symbol.to_string(),
294 AssetState {
295 detector,
296 current_strategy: ActiveStrategy::NoTrade,
297 last_regime: MarketRegime::Uncertain,
298 regime_change_count: 0,
299 last_confidence: None,
300 },
301 );
302 }
303
304 pub fn unregister_asset(&mut self, symbol: &str) -> bool {
306 self.assets.remove(symbol).is_some()
307 }
308
309 pub fn update(
314 &mut self,
315 symbol: &str,
316 high: f64,
317 low: f64,
318 close: f64,
319 ) -> Option<RoutedSignal> {
320 if !self.assets.contains_key(symbol) {
321 self.register_asset(symbol);
322 }
323
324 let state = self.assets.get_mut(symbol)?;
325
326 let (regime_result, methods_agree, state_probs, expected_duration) =
330 match &mut state.detector {
331 Detector::Indicator(det) => {
332 let result = det.update(high, low, close);
333 (result, None, None, None)
334 }
335 Detector::HMM(det) => {
336 let result = det.update_ohlc(high, low, close);
337 let probs = det.state_probabilities().to_vec();
338 let duration = det.expected_regime_duration(det.current_state_index());
339 (result, None, Some(probs), Some(duration))
340 }
341 Detector::Ensemble(det) => {
342 let ens_result: EnsembleResult = det.update(high, low, close);
343 let probs = det.hmm_state_probabilities().to_vec();
344 let duration = det.expected_regime_duration();
345 (
346 ens_result.to_regime_confidence(),
347 Some(ens_result.methods_agree),
348 Some(probs),
349 Some(duration),
350 )
351 }
352 };
353
354 state.last_confidence = Some(regime_result);
356
357 if regime_result.regime != state.last_regime {
359 state.regime_change_count += 1;
360 if self.config.log_changes {
361 println!(
362 "[{}] Regime change #{} ({:?}): {} → {} (conf: {:.2})",
363 symbol,
364 state.regime_change_count,
365 self.config.detection_method,
366 state.last_regime,
367 regime_result.regime,
368 regime_result.confidence
369 );
370 }
371 state.last_regime = regime_result.regime;
372 }
373
374 let min_confidence = self.config.min_confidence;
376 let volatile_factor = self.config.volatile_position_factor;
377 let (strategy, position_factor, reason) =
378 Self::compute_strategy(®ime_result, min_confidence, volatile_factor);
379 state.current_strategy = strategy;
380
381 let trend_direction = match regime_result.regime {
383 MarketRegime::Trending(dir) => Some(dir),
384 _ => None,
385 };
386
387 Some(RoutedSignal {
388 strategy,
389 regime: regime_result.regime,
390 confidence: regime_result.confidence,
391 position_factor,
392 reason,
393 detection_method: self.config.detection_method,
394 methods_agree,
395 state_probabilities: state_probs,
396 expected_duration,
397 trend_direction,
398 })
399 }
400
401 fn compute_strategy(
405 regime: &RegimeConfidence,
406 min_confidence: f64,
407 volatile_factor: f64,
408 ) -> (ActiveStrategy, f64, String) {
409 if regime.confidence < min_confidence {
410 return (
411 ActiveStrategy::NoTrade,
412 0.0,
413 format!(
414 "Confidence too low ({:.0}% < {:.0}%)",
415 regime.confidence * 100.0,
416 min_confidence * 100.0
417 ),
418 );
419 }
420
421 match regime.regime {
422 MarketRegime::Trending(dir) => (
423 ActiveStrategy::TrendFollowing,
424 1.0,
425 format!(
426 "{} trend detected (ADX: {:.1}, conf: {:.0}%)",
427 dir,
428 regime.adx_value,
429 regime.confidence * 100.0
430 ),
431 ),
432 MarketRegime::MeanReverting => (
433 ActiveStrategy::MeanReversion,
434 1.0,
435 format!(
436 "Mean-reverting regime (BB%: {:.0}, conf: {:.0}%)",
437 regime.bb_width_percentile,
438 regime.confidence * 100.0
439 ),
440 ),
441 MarketRegime::Volatile => (
442 ActiveStrategy::MeanReversion,
443 volatile_factor,
444 format!(
445 "Volatile regime — reduced size to {:.0}% (conf: {:.0}%)",
446 volatile_factor * 100.0,
447 regime.confidence * 100.0
448 ),
449 ),
450 MarketRegime::Uncertain => (
451 ActiveStrategy::NoTrade,
452 0.0,
453 "Uncertain regime — staying out".to_string(),
454 ),
455 }
456 }
457
458 pub fn get_regime(&self, symbol: &str) -> Option<MarketRegime> {
464 self.assets.get(symbol).map(|s| s.last_regime)
465 }
466
467 pub fn last_regime_confidence(&self, symbol: &str) -> Option<&RegimeConfidence> {
473 self.assets
474 .get(symbol)
475 .and_then(|s| s.last_confidence.as_ref())
476 }
477
478 pub fn atr_value(&self, symbol: &str) -> Option<f64> {
488 self.assets.get(symbol).and_then(|s| match &s.detector {
489 Detector::Indicator(det) => det.atr_value(),
490 Detector::HMM(_) => None,
491 Detector::Ensemble(det) => det.indicator_detector().atr_value(),
492 })
493 }
494
495 pub fn adx_value(&self, symbol: &str) -> Option<f64> {
505 self.assets.get(symbol).and_then(|s| match &s.detector {
506 Detector::Indicator(det) => det.adx_value(),
507 Detector::HMM(_) => None,
508 Detector::Ensemble(det) => det.indicator_detector().adx_value(),
509 })
510 }
511
512 pub fn get_strategy(&self, symbol: &str) -> Option<ActiveStrategy> {
514 self.assets.get(symbol).map(|s| s.current_strategy)
515 }
516
517 pub fn is_ready(&self, symbol: &str) -> bool {
519 self.assets.get(symbol).is_some_and(|s| match &s.detector {
520 Detector::Indicator(d) => d.is_ready(),
521 Detector::HMM(d) => d.is_ready(),
522 Detector::Ensemble(d) => d.is_ready(),
523 })
524 }
525
526 pub fn detection_method(&self) -> DetectionMethod {
528 self.config.detection_method
529 }
530
531 pub fn regime_changes(&self, symbol: &str) -> u32 {
533 self.assets.get(symbol).map_or(0, |s| s.regime_change_count)
534 }
535
536 pub fn registered_assets(&self) -> Vec<&str> {
538 self.assets.keys().map(String::as_str).collect()
539 }
540
541 pub fn config(&self) -> &EnhancedRouterConfig {
543 &self.config
544 }
545
546 pub fn summary(&self) -> Vec<AssetSummary> {
548 self.assets
549 .iter()
550 .map(|(symbol, state)| AssetSummary {
551 symbol: symbol.clone(),
552 regime: state.last_regime,
553 strategy: state.current_strategy,
554 regime_changes: state.regime_change_count,
555 is_ready: match &state.detector {
556 Detector::Indicator(d) => d.is_ready(),
557 Detector::HMM(d) => d.is_ready(),
558 Detector::Ensemble(d) => d.is_ready(),
559 },
560 })
561 .collect()
562 }
563}
564
565#[derive(Debug, Clone, Serialize, Deserialize)]
567pub struct AssetSummary {
568 pub symbol: String,
569 pub regime: MarketRegime,
570 pub strategy: ActiveStrategy,
571 pub regime_changes: u32,
572 pub is_ready: bool,
573}
574
575impl std::fmt::Display for AssetSummary {
576 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
577 write!(
578 f,
579 "{}: {} → {} (changes: {}, ready: {})",
580 self.symbol, self.regime, self.strategy, self.regime_changes, self.is_ready
581 )
582 }
583}
584
585#[cfg(test)]
590mod tests {
591 use super::*;
592
593 #[test]
594 fn test_router_creation_ensemble() {
595 let router = EnhancedRouter::with_ensemble();
596 assert_eq!(router.detection_method(), DetectionMethod::Ensemble);
597 }
598
599 #[test]
600 fn test_router_creation_indicators() {
601 let router = EnhancedRouter::with_indicators();
602 assert_eq!(router.detection_method(), DetectionMethod::Indicators);
603 }
604
605 #[test]
606 fn test_router_creation_hmm() {
607 let router = EnhancedRouter::with_hmm();
608 assert_eq!(router.detection_method(), DetectionMethod::HMM);
609 }
610
611 #[test]
612 fn test_method_switching() {
613 let indicator_router = EnhancedRouter::with_indicators();
614 let hmm_router = EnhancedRouter::with_hmm();
615 let ensemble_router = EnhancedRouter::with_ensemble();
616
617 assert_eq!(
618 indicator_router.detection_method(),
619 DetectionMethod::Indicators
620 );
621 assert_eq!(hmm_router.detection_method(), DetectionMethod::HMM);
622 assert_eq!(
623 ensemble_router.detection_method(),
624 DetectionMethod::Ensemble
625 );
626 }
627
628 #[test]
629 fn test_asset_registration() {
630 let mut router = EnhancedRouter::with_ensemble();
631 router.register_asset("BTC/USD");
632 router.register_asset("ETH/USD");
633
634 assert!(router.get_regime("BTC/USD").is_some());
635 assert!(router.get_regime("ETH/USD").is_some());
636 assert!(router.get_regime("SOL/USD").is_none());
637 }
638
639 #[test]
640 fn test_asset_unregistration() {
641 let mut router = EnhancedRouter::with_ensemble();
642 router.register_asset("BTC/USD");
643 assert!(router.get_regime("BTC/USD").is_some());
644
645 assert!(router.unregister_asset("BTC/USD"));
646 assert!(router.get_regime("BTC/USD").is_none());
647
648 assert!(!router.unregister_asset("BTC/USD"));
650 }
651
652 #[test]
653 fn test_auto_registration() {
654 let mut router = EnhancedRouter::with_indicators();
655
656 assert!(router.get_regime("BTC/USD").is_none());
658 let signal = router.update("BTC/USD", 101.0, 99.0, 100.0);
659 assert!(signal.is_some());
660 assert!(router.get_regime("BTC/USD").is_some());
661 }
662
663 #[test]
664 fn test_duplicate_registration_noop() {
665 let mut router = EnhancedRouter::with_ensemble();
666 router.register_asset("BTC/USD");
667
668 for i in 0..50 {
670 let price = 100.0 + i as f64;
671 router.update("BTC/USD", price + 1.0, price - 1.0, price);
672 }
673
674 let changes_before = router.regime_changes("BTC/USD");
675
676 router.register_asset("BTC/USD");
678
679 let changes_after = router.regime_changes("BTC/USD");
680 assert_eq!(changes_before, changes_after);
681 }
682
683 #[test]
684 fn test_registered_assets() {
685 let mut router = EnhancedRouter::with_ensemble();
686 router.register_asset("BTC/USD");
687 router.register_asset("ETH/USD");
688 router.register_asset("SOL/USD");
689
690 let assets = router.registered_assets();
691 assert_eq!(assets.len(), 3);
692 assert!(assets.contains(&"BTC/USD"));
693 assert!(assets.contains(&"ETH/USD"));
694 assert!(assets.contains(&"SOL/USD"));
695 }
696
697 #[test]
698 fn test_initial_regime_is_uncertain() {
699 let mut router = EnhancedRouter::with_ensemble();
700 router.register_asset("BTC/USD");
701
702 assert_eq!(router.get_regime("BTC/USD"), Some(MarketRegime::Uncertain));
703 assert_eq!(
704 router.get_strategy("BTC/USD"),
705 Some(ActiveStrategy::NoTrade)
706 );
707 }
708
709 #[test]
710 fn test_not_ready_before_warmup() {
711 let mut router = EnhancedRouter::with_indicators();
712 router.register_asset("BTC/USD");
713
714 assert!(!router.is_ready("BTC/USD"));
715
716 for i in 0..10 {
718 let price = 100.0 + i as f64;
719 router.update("BTC/USD", price + 1.0, price - 1.0, price);
720 }
721
722 assert!(!router.is_ready("BTC/USD"));
723 }
724
725 #[test]
726 fn test_is_ready_unknown_asset() {
727 let router = EnhancedRouter::with_ensemble();
728 assert!(!router.is_ready("UNKNOWN"));
729 }
730
731 #[test]
732 fn test_regime_changes_counted() {
733 let mut router = EnhancedRouter::new(EnhancedRouterConfig {
734 detection_method: DetectionMethod::Indicators,
735 log_changes: false, ..Default::default()
737 });
738
739 router.register_asset("BTC/USD");
740 assert_eq!(router.regime_changes("BTC/USD"), 0);
741
742 for i in 0..300 {
744 let price = 100.0 + i as f64 * 0.5;
745 router.update("BTC/USD", price + 1.0, price - 1.0, price);
746 }
747
748 let changes = router.regime_changes("BTC/USD");
751 let _ = changes; }
753
754 #[test]
755 fn test_routed_signal_fields() {
756 let mut router = EnhancedRouter::new(EnhancedRouterConfig {
757 detection_method: DetectionMethod::Indicators,
758 log_changes: false,
759 ..Default::default()
760 });
761
762 let signal = router.update("BTC/USD", 101.0, 99.0, 100.0);
763 assert!(signal.is_some());
764
765 let signal = signal.unwrap();
766 assert_eq!(signal.detection_method, DetectionMethod::Indicators);
767 assert!((0.0..=1.0).contains(&signal.confidence));
768 assert!((0.0..=1.0).contains(&signal.position_factor));
769 assert!(!signal.reason.is_empty());
770 assert!(signal.methods_agree.is_none());
772 assert!(signal.state_probabilities.is_none());
773 assert!(signal.expected_duration.is_none());
774 }
775
776 #[test]
777 fn test_routed_signal_display() {
778 let signal = RoutedSignal {
779 strategy: ActiveStrategy::TrendFollowing,
780 regime: MarketRegime::Trending(TrendDirection::Bullish),
781 confidence: 0.85,
782 position_factor: 1.0,
783 reason: "Bullish trend".to_string(),
784 detection_method: DetectionMethod::Ensemble,
785 methods_agree: Some(true),
786 state_probabilities: Some(vec![0.6, 0.2, 0.2]),
787 expected_duration: Some(15.0),
788 trend_direction: Some(TrendDirection::Bullish),
789 };
790
791 let display = format!("{signal}");
792 assert!(display.contains("Trend Following"));
793 assert!(display.contains("85%"));
794 assert!(display.contains("100%"));
795 assert!(display.contains("✓"));
796 assert!(display.contains("15 bars"));
797 }
798
799 #[test]
800 fn test_compute_strategy_low_confidence() {
801 let regime = RegimeConfidence::new(MarketRegime::Trending(TrendDirection::Bullish), 0.3);
802 let (strategy, factor, reason) = EnhancedRouter::compute_strategy(®ime, 0.5, 0.5);
803
804 assert_eq!(strategy, ActiveStrategy::NoTrade);
805 assert_eq!(factor, 0.0);
806 assert!(reason.contains("Confidence too low"));
807 }
808
809 #[test]
810 fn test_compute_strategy_trending() {
811 let regime = RegimeConfidence::with_metrics(
812 MarketRegime::Trending(TrendDirection::Bullish),
813 0.8,
814 30.0,
815 50.0,
816 0.7,
817 );
818 let (strategy, factor, reason) = EnhancedRouter::compute_strategy(®ime, 0.5, 0.5);
819
820 assert_eq!(strategy, ActiveStrategy::TrendFollowing);
821 assert_eq!(factor, 1.0);
822 assert!(reason.contains("Bullish"));
823 }
824
825 #[test]
826 fn test_compute_strategy_mean_reverting() {
827 let regime =
828 RegimeConfidence::with_metrics(MarketRegime::MeanReverting, 0.7, 15.0, 30.0, 0.2);
829 let (strategy, factor, reason) = EnhancedRouter::compute_strategy(®ime, 0.5, 0.5);
830
831 assert_eq!(strategy, ActiveStrategy::MeanReversion);
832 assert_eq!(factor, 1.0);
833 assert!(reason.contains("Mean-reverting"));
834 }
835
836 #[test]
837 fn test_compute_strategy_volatile() {
838 let regime = RegimeConfidence::with_metrics(MarketRegime::Volatile, 0.75, 22.0, 85.0, 0.3);
839 let (strategy, factor, reason) = EnhancedRouter::compute_strategy(®ime, 0.5, 0.4);
840
841 assert_eq!(strategy, ActiveStrategy::MeanReversion);
842 assert_eq!(factor, 0.4);
843 assert!(reason.contains("Volatile"));
844 assert!(reason.contains("40%"));
845 }
846
847 #[test]
848 fn test_compute_strategy_uncertain() {
849 let regime = RegimeConfidence::new(MarketRegime::Uncertain, 0.6);
850 let (strategy, factor, _) = EnhancedRouter::compute_strategy(®ime, 0.5, 0.5);
851
852 assert_eq!(strategy, ActiveStrategy::NoTrade);
853 assert_eq!(factor, 0.0);
854 }
855
856 #[test]
857 fn test_active_strategy_display() {
858 assert_eq!(
859 format!("{}", ActiveStrategy::TrendFollowing),
860 "Trend Following"
861 );
862 assert_eq!(
863 format!("{}", ActiveStrategy::MeanReversion),
864 "Mean Reversion"
865 );
866 assert_eq!(format!("{}", ActiveStrategy::NoTrade), "No Trade");
867 }
868
869 #[test]
870 fn test_detection_method_display() {
871 assert_eq!(format!("{}", DetectionMethod::Indicators), "Indicators");
872 assert_eq!(format!("{}", DetectionMethod::HMM), "HMM");
873 assert_eq!(format!("{}", DetectionMethod::Ensemble), "Ensemble");
874 }
875
876 #[test]
877 fn test_summary() {
878 let mut router = EnhancedRouter::new(EnhancedRouterConfig {
879 detection_method: DetectionMethod::Indicators,
880 log_changes: false,
881 ..Default::default()
882 });
883
884 router.register_asset("BTC/USD");
885 router.register_asset("ETH/USD");
886
887 let summary = router.summary();
888 assert_eq!(summary.len(), 2);
889
890 for s in &summary {
891 assert!(s.symbol == "BTC/USD" || s.symbol == "ETH/USD");
892 assert_eq!(s.regime, MarketRegime::Uncertain);
893 assert_eq!(s.strategy, ActiveStrategy::NoTrade);
894 assert_eq!(s.regime_changes, 0);
895 }
896 }
897
898 #[test]
899 fn test_asset_summary_display() {
900 let summary = AssetSummary {
901 symbol: "BTC/USD".to_string(),
902 regime: MarketRegime::Trending(TrendDirection::Bullish),
903 strategy: ActiveStrategy::TrendFollowing,
904 regime_changes: 3,
905 is_ready: true,
906 };
907
908 let display = format!("{summary}");
909 assert!(display.contains("BTC/USD"));
910 assert!(display.contains("Trending"));
911 assert!(display.contains("Trend Following"));
912 }
913
914 #[test]
915 fn test_hmm_signal_has_state_probs() {
916 let mut router = EnhancedRouter::new(EnhancedRouterConfig {
917 detection_method: DetectionMethod::HMM,
918 log_changes: false,
919 ..Default::default()
920 });
921
922 let signal = router.update("BTC/USD", 101.0, 99.0, 100.0);
923 let signal = signal.unwrap();
924
925 assert!(signal.state_probabilities.is_some());
926 let probs = signal.state_probabilities.unwrap();
927 assert_eq!(probs.len(), 3);
928
929 let sum: f64 = probs.iter().sum();
930 assert!(
931 (sum - 1.0).abs() < 1e-6,
932 "State probabilities should sum to 1.0"
933 );
934 }
935
936 #[test]
937 fn test_ensemble_signal_has_agreement() {
938 let mut router = EnhancedRouter::new(EnhancedRouterConfig {
939 detection_method: DetectionMethod::Ensemble,
940 log_changes: false,
941 ..Default::default()
942 });
943
944 let signal = router.update("BTC/USD", 101.0, 99.0, 100.0);
945 let signal = signal.unwrap();
946
947 assert!(signal.methods_agree.is_some());
948 assert!(signal.state_probabilities.is_some());
949 assert!(signal.expected_duration.is_some());
950 }
951}