sandbox_quant/runtime/
regime.rs1use crate::event::{MarketRegime, MarketRegimeSignal};
2use crate::indicator::ema::Ema;
3use std::collections::VecDeque;
4
5#[derive(Debug, Clone)]
6pub struct RegimeDetectorConfig {
7 pub fast_period: usize,
8 pub slow_period: usize,
9 pub vol_window: usize,
10 pub range_vol_threshold: f64,
11}
12
13impl Default for RegimeDetectorConfig {
14 fn default() -> Self {
15 Self {
16 fast_period: 10,
17 slow_period: 30,
18 vol_window: 20,
19 range_vol_threshold: 0.0045,
20 }
21 }
22}
23
24#[derive(Debug)]
25pub struct RegimeDetector {
26 fast_ema: Ema,
27 slow_ema: Ema,
28 closes: VecDeque<f64>,
29 returns: VecDeque<f64>,
30 prev_fast: Option<f64>,
31 config: RegimeDetectorConfig,
32}
33
34impl Default for RegimeDetector {
35 fn default() -> Self {
36 Self::new(RegimeDetectorConfig::default())
37 }
38}
39
40impl RegimeDetector {
41 pub fn new(config: RegimeDetectorConfig) -> Self {
42 let fast = Ema::new(config.fast_period.max(2));
43 let slow = Ema::new(config.slow_period.max(config.fast_period.max(2)).max(2));
44 Self {
45 fast_ema: fast,
46 slow_ema: slow,
47 closes: VecDeque::new(),
48 returns: VecDeque::new(),
49 prev_fast: None,
50 config,
51 }
52 }
53
54 pub fn update(&mut self, price: f64, now_ms: u64) -> MarketRegimeSignal {
55 if !price.is_finite() || price <= f64::EPSILON {
56 return MarketRegimeSignal {
57 regime: MarketRegime::Unknown,
58 confidence: 0.0,
59 ema_fast: 0.0,
60 ema_slow: 0.0,
61 vol_ratio: 0.0,
62 slope: 0.0,
63 updated_at_ms: now_ms,
64 };
65 }
66
67 let fast = self.fast_ema.push(price).unwrap_or(price);
68 let slow = self.slow_ema.push(price).unwrap_or(price);
69 self.closes.push_back(price);
70 if self.closes.len() > self.config.slow_period {
71 let _ = self.closes.pop_front();
72 }
73
74 if self.closes.len() >= 2 {
75 if let Some(prev) = self.closes.get(self.closes.len() - 2).copied() {
76 let ret = (price / prev - 1.0).abs();
77 self.returns.push_back(ret);
78 while self.returns.len() > self.config.vol_window {
79 let _ = self.returns.pop_front();
80 }
81 }
82 }
83 let slope = self
84 .prev_fast
85 .and_then(|prev| ((fast - prev) / prev.max(f64::EPSILON)).into())
86 .unwrap_or(0.0);
87 self.prev_fast = Some(fast);
88
89 if !self.fast_ema.is_ready() || !self.slow_ema.is_ready() {
90 return MarketRegimeSignal {
91 regime: MarketRegime::Unknown,
92 confidence: 0.0,
93 ema_fast: fast,
94 ema_slow: slow,
95 vol_ratio: 0.0,
96 slope,
97 updated_at_ms: now_ms,
98 };
99 }
100
101 let vol_ratio = if self.returns.is_empty() {
102 0.0
103 } else {
104 let mean_abs = self.returns.iter().copied().sum::<f64>() / self.returns.len() as f64;
105 let centered = self
106 .returns
107 .iter()
108 .map(|v| {
109 let d = *v - mean_abs;
110 d * d
111 })
112 .sum::<f64>()
113 / self.returns.len() as f64;
114 let stdev = centered.sqrt();
115 if mean_abs > 0.0 {
116 stdev / mean_abs
117 } else {
118 0.0
119 }
120 };
121
122 let regime = if vol_ratio <= self.config.range_vol_threshold {
123 MarketRegime::Range
124 } else if fast > slow && slope > 0.0 {
125 MarketRegime::TrendUp
126 } else if fast < slow && slope < 0.0 {
127 MarketRegime::TrendDown
128 } else {
129 MarketRegime::Range
130 };
131
132 let trend_gap = (fast - slow).abs() / slow.abs().max(f64::EPSILON);
133 let confidence = if matches!(regime, MarketRegime::Range) {
134 (1.0 - (vol_ratio / self.config.range_vol_threshold.max(f64::EPSILON)))
135 .max(0.0)
136 .min(1.0)
137 } else {
138 (trend_gap * 50.0).max(slope.abs() * 1500.0).min(1.0)
139 };
140
141 MarketRegimeSignal {
142 regime,
143 confidence,
144 ema_fast: fast,
145 ema_slow: slow,
146 vol_ratio,
147 slope,
148 updated_at_ms: now_ms,
149 }
150 }
151}