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