Skip to main content

indicators/
types.rs

1//! Core domain types: `Candle`, market regime classification, regime config.
2
3use serde::{Deserialize, Serialize};
4use std::fmt;
5
6use crate::functions::IndicatorError;
7
8// ── MACD
9pub type TripleVec = (Vec<f64>, Vec<f64>, Vec<f64>);
10pub type MacdResult = Result<TripleVec, IndicatorError>;
11
12// ── Candle ────────────────────────────────────────────────────────────────────
13
14/// One OHLCV bar.
15#[derive(Debug, Clone, Serialize, Deserialize)]
16pub struct Candle {
17    /// Open time in milliseconds (Unix epoch).
18    pub time: i64,
19    pub open: f64,
20    pub high: f64,
21    pub low: f64,
22    pub close: f64,
23    pub volume: f64,
24}
25
26impl Candle {
27    /// Parse from a raw 6-element array `[timestamp_ms, open, high, low, close, volume]`
28    /// where every element is a JSON string.
29    pub fn from_raw(row: &[serde_json::Value]) -> Option<Self> {
30        Some(Self {
31            time: row.first()?.as_str()?.parse().ok()?,
32            open: row.get(1)?.as_str()?.parse().ok()?,
33            high: row.get(2)?.as_str()?.parse().ok()?,
34            low: row.get(3)?.as_str()?.parse().ok()?,
35            close: row.get(4)?.as_str()?.parse().ok()?,
36            volume: row.get(5)?.as_str()?.parse().ok()?,
37        })
38    }
39
40    /// Typical price `(H+L+C)/3`.
41    pub fn typical_price(&self) -> f64 {
42        (self.high + self.low + self.close) / 3.0
43    }
44
45    /// Mid-price `(H+L)/2`.
46    pub fn hl2(&self) -> f64 {
47        (self.high + self.low) / 2.0
48    }
49
50    /// True range against an optional previous close.
51    pub fn true_range(&self, prev_close: Option<f64>) -> f64 {
52        let hl = self.high - self.low;
53        match prev_close {
54            Some(pc) => hl.max((self.high - pc).abs()).max((self.low - pc).abs()),
55            None => hl,
56        }
57    }
58}
59
60// ── MarketRegime ──────────────────────────────────────────────────────────────
61
62/// Regime classification used by the statistical regime detectors.
63#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
64pub enum MarketRegime {
65    /// Strong directional move — use trend-following.
66    Trending(TrendDirection),
67    /// Price oscillating around a mean — use mean reversion.
68    MeanReverting,
69    /// High volatility, no clear direction — reduce exposure.
70    Volatile,
71    /// Insufficient data or unclear signals.
72    #[default]
73    Uncertain,
74}
75
76impl MarketRegime {
77    pub fn is_tradeable(&self) -> bool {
78        matches!(
79            self,
80            MarketRegime::Trending(_) | MarketRegime::MeanReverting
81        )
82    }
83
84    pub fn size_multiplier(&self) -> f64 {
85        match self {
86            MarketRegime::Trending(_) => 1.0,
87            MarketRegime::MeanReverting => 0.8,
88            MarketRegime::Volatile => 0.3,
89            MarketRegime::Uncertain => 0.0,
90        }
91    }
92
93    pub fn recommended_strategy(&self) -> RecommendedStrategy {
94        RecommendedStrategy::from(self)
95    }
96}
97
98impl fmt::Display for MarketRegime {
99    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
100        match self {
101            MarketRegime::Trending(TrendDirection::Bullish) => write!(f, "Trending (Bullish)"),
102            MarketRegime::Trending(TrendDirection::Bearish) => write!(f, "Trending (Bearish)"),
103            MarketRegime::MeanReverting => write!(f, "Mean-Reverting"),
104            MarketRegime::Volatile => write!(f, "Volatile/Choppy"),
105            MarketRegime::Uncertain => write!(f, "Uncertain"),
106        }
107    }
108}
109
110// ── TrendDirection ────────────────────────────────────────────────────────────
111
112#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
113pub enum TrendDirection {
114    Bullish,
115    Bearish,
116}
117
118impl fmt::Display for TrendDirection {
119    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
120        match self {
121            TrendDirection::Bullish => write!(f, "Bullish"),
122            TrendDirection::Bearish => write!(f, "Bearish"),
123        }
124    }
125}
126
127// ── RegimeConfidence ──────────────────────────────────────────────────────────
128
129#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
130pub struct RegimeConfidence {
131    pub regime: MarketRegime,
132    pub confidence: f64,
133    pub adx_value: f64,
134    pub bb_width_percentile: f64,
135    pub trend_strength: f64,
136}
137
138impl RegimeConfidence {
139    pub fn new(regime: MarketRegime, confidence: f64) -> Self {
140        Self {
141            regime,
142            confidence: confidence.clamp(0.0, 1.0),
143            adx_value: 0.0,
144            bb_width_percentile: 0.0,
145            trend_strength: 0.0,
146        }
147    }
148
149    pub fn with_metrics(
150        regime: MarketRegime,
151        confidence: f64,
152        adx: f64,
153        bb_width: f64,
154        trend_strength: f64,
155    ) -> Self {
156        Self {
157            regime,
158            confidence: confidence.clamp(0.0, 1.0),
159            adx_value: adx,
160            bb_width_percentile: bb_width,
161            trend_strength,
162        }
163    }
164
165    pub fn is_actionable(&self) -> bool {
166        self.confidence >= 0.6
167    }
168    pub fn is_strong(&self) -> bool {
169        self.confidence >= 0.75
170    }
171}
172
173impl Default for RegimeConfidence {
174    fn default() -> Self {
175        Self::new(MarketRegime::Uncertain, 0.0)
176    }
177}
178
179impl fmt::Display for RegimeConfidence {
180    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
181        write!(
182            f,
183            "{} (conf: {:.0}%, ADX: {:.1}, BB%: {:.0}, trend: {:.2})",
184            self.regime,
185            self.confidence * 100.0,
186            self.adx_value,
187            self.bb_width_percentile,
188            self.trend_strength,
189        )
190    }
191}
192
193// ── RecommendedStrategy ───────────────────────────────────────────────────────
194
195#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
196pub enum RecommendedStrategy {
197    TrendFollowing,
198    MeanReversion,
199    ReducedExposure,
200    StayCash,
201}
202
203impl From<&MarketRegime> for RecommendedStrategy {
204    fn from(regime: &MarketRegime) -> Self {
205        match regime {
206            MarketRegime::Trending(_) => RecommendedStrategy::TrendFollowing,
207            MarketRegime::MeanReverting => RecommendedStrategy::MeanReversion,
208            MarketRegime::Volatile => RecommendedStrategy::ReducedExposure,
209            MarketRegime::Uncertain => RecommendedStrategy::StayCash,
210        }
211    }
212}
213
214impl fmt::Display for RecommendedStrategy {
215    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
216        match self {
217            RecommendedStrategy::TrendFollowing => write!(f, "Trend Following"),
218            RecommendedStrategy::MeanReversion => write!(f, "Mean Reversion"),
219            RecommendedStrategy::ReducedExposure => write!(f, "Reduced Exposure"),
220            RecommendedStrategy::StayCash => write!(f, "Stay Cash"),
221        }
222    }
223}
224
225// ── RegimeConfig ──────────────────────────────────────────────────────────────
226
227#[derive(Debug, Clone, Serialize, Deserialize)]
228pub struct RegimeConfig {
229    pub adx_period: usize,
230    pub adx_trending_threshold: f64,
231    pub adx_ranging_threshold: f64,
232    pub bb_period: usize,
233    pub bb_std_dev: f64,
234    pub bb_width_volatility_threshold: f64,
235    pub ema_short_period: usize,
236    pub ema_long_period: usize,
237    pub atr_period: usize,
238    pub atr_expansion_threshold: f64,
239    pub regime_stability_bars: usize,
240    pub min_regime_duration: usize,
241}
242
243impl Default for RegimeConfig {
244    fn default() -> Self {
245        Self {
246            adx_period: 14,
247            adx_trending_threshold: 25.0,
248            adx_ranging_threshold: 20.0,
249            bb_period: 20,
250            bb_std_dev: 2.0,
251            bb_width_volatility_threshold: 75.0,
252            ema_short_period: 50,
253            ema_long_period: 200,
254            atr_period: 14,
255            atr_expansion_threshold: 1.5,
256            regime_stability_bars: 3,
257            min_regime_duration: 5,
258        }
259    }
260}
261
262impl RegimeConfig {
263    pub fn crypto_optimized() -> Self {
264        Self {
265            adx_trending_threshold: 20.0,
266            adx_ranging_threshold: 15.0,
267            bb_width_volatility_threshold: 70.0,
268            ema_short_period: 21,
269            ema_long_period: 50,
270            atr_expansion_threshold: 1.3,
271            regime_stability_bars: 2,
272            min_regime_duration: 3,
273            ..Default::default()
274        }
275    }
276
277    pub fn conservative() -> Self {
278        Self {
279            adx_trending_threshold: 30.0,
280            adx_ranging_threshold: 18.0,
281            bb_width_volatility_threshold: 80.0,
282            atr_expansion_threshold: 2.0,
283            regime_stability_bars: 5,
284            min_regime_duration: 10,
285            ..Default::default()
286        }
287    }
288}