Skip to main content

sandbox_quant/runtime/
regime.rs

1use 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}