Skip to main content

indicators/
indicator_config.rs

1//! `IndicatorConfig` — the indicator-math subset of `BotSettings`.
2//!
3//! Pure configuration: no I/O, no runtime, no exchange types.
4//! This struct is what `compute_signal` and all indicator constructors need.
5//!
6
7use serde::{Deserialize, Serialize};
8
9/// Engine-internal recompute cadences and iteration caps.
10///
11/// These were hard-coded in the signal engine before 0.2.0. They are not part
12/// of the Python `SETTINGS` dict; `#[serde(default)]` on the parent field
13/// means existing tuned JSON files load unchanged.
14#[derive(Debug, Clone, Serialize, Deserialize)]
15#[serde(default)]
16pub struct SignalEngineConfig {
17    /// Recompute the KMeans ATR centroids every N bars (L3 SuperTrend).
18    pub kmeans_recompute_bars: usize,
19    /// Maximum Lloyd iterations per KMeans centroid fit.
20    pub kmeans_max_iters: usize,
21    /// Recompute the Hurst exponent every N bars (L10).
22    pub hurst_recompute_bars: usize,
23}
24
25impl Default for SignalEngineConfig {
26    fn default() -> Self {
27        Self {
28            kmeans_recompute_bars: 10,
29            kmeans_max_iters: 100,
30            hurst_recompute_bars: 10,
31        }
32    }
33}
34
35/// All tunable parameters that live inside indicators and `compute_signal`.
36///
37/// Every field except [`engine`](Self::engine) maps 1-to-1 to a key in the
38/// Python `SETTINGS` dict so Optuna-tuned JSON files load with zero field
39/// renaming (`engine` is additive and defaults when absent).
40#[derive(Debug, Clone, Serialize, Deserialize)]
41pub struct IndicatorConfig {
42    // ── Engine Buffer ────────────────────────────────────────────────────────
43    /// Candle buffer capacity
44    pub history_candles: usize,
45
46    // ── Layer 1-2: VWAP & EMA ────────────────────────────────────────────────
47    pub ema_len: usize,
48
49    // ── Layer 3: ML SuperTrend ───────────────────────────────────────────────
50    pub atr_len: usize,
51    pub st_factor: f64,
52    pub training_period: usize,
53    pub highvol_pct: f64,
54    pub midvol_pct: f64,
55    pub lowvol_pct: f64,
56
57    // ── Layer 4: Trend Speed ─────────────────────────────────────────────────
58    pub ts_max_length: usize,
59    pub ts_accel_mult: f64,
60    pub ts_rma_len: usize,
61    pub ts_hma_len: usize,
62    pub ts_collen: usize,
63    pub ts_lookback: usize,
64    pub ts_speed_exit_threshold: Option<f64>,
65
66    // ── Layer 5: Liquidity Profile ───────────────────────────────────────────
67    pub liq_period: usize,
68    pub liq_bins: usize,
69
70    // ── Layer 6: Confluence ──────────────────────────────────────────────────
71    pub conf_ema_fast: usize,
72    pub conf_ema_slow: usize,
73    pub conf_ema_trend: usize,
74    pub conf_rsi_len: usize,
75    pub conf_adx_len: usize,
76    pub conf_min_score: f64,
77
78    // ── Layer 7: Market Structure + Fibonacci ────────────────────────────────
79    pub struct_swing_len: usize,
80    pub struct_atr_mult: f64,
81    pub fib_zone_enabled: bool,
82
83    // ── Signal mode ──────────────────────────────────────────────────────────
84    /// `"majority"` | `"strict"` | `"any"`
85    pub signal_mode: String,
86    pub signal_confirm_bars: usize,
87
88    // ── Layer 8: CVD ─────────────────────────────────────────────────────────
89    pub cvd_slope_bars: usize,
90    pub cvd_div_lookback: usize,
91
92    // ── Layer 9: AO + Percentile gates ──────────────────────────────────────
93    pub wave_pct_l: f64,
94    pub wave_pct_s: f64,
95    pub mom_pct_min: f64,
96    pub vol_pct_window: usize,
97
98    // ── Layer 10: Hurst ──────────────────────────────────────────────────────
99    pub hurst_threshold: f64,
100    pub hurst_lookback: usize,
101
102    // ── Layer 11: Price Acceleration / Stop ──────────────────────────────────
103    pub stop_atr_mult: f64,
104
105    // ── Entry gates ──────────────────────────────────────────────────────────
106    pub min_vol_pct: f64,
107    pub min_hold_candles: usize,
108
109    // ── Engine internals (not in the Python SETTINGS dict) ───────────────────
110    #[serde(default)]
111    pub engine: SignalEngineConfig,
112}
113
114impl Default for IndicatorConfig {
115    fn default() -> Self {
116        Self {
117            history_candles: 200,
118            ema_len: 9,
119            atr_len: 10,
120            st_factor: 3.0,
121            training_period: 100,
122            highvol_pct: 0.75,
123            midvol_pct: 0.50,
124            lowvol_pct: 0.25,
125            ts_max_length: 50,
126            ts_accel_mult: 5.0,
127            ts_rma_len: 10,
128            ts_hma_len: 5,
129            ts_collen: 100,
130            ts_lookback: 50,
131            ts_speed_exit_threshold: None,
132            liq_period: 100,
133            liq_bins: 31,
134            conf_ema_fast: 9,
135            conf_ema_slow: 21,
136            conf_ema_trend: 55,
137            conf_rsi_len: 13,
138            conf_adx_len: 14,
139            conf_min_score: 5.0,
140            struct_swing_len: 10,
141            struct_atr_mult: 0.5,
142            fib_zone_enabled: true,
143            signal_mode: "majority".into(),
144            signal_confirm_bars: 2,
145            cvd_slope_bars: 10,
146            cvd_div_lookback: 30,
147            wave_pct_l: 0.25,
148            wave_pct_s: 0.75,
149            mom_pct_min: 0.30,
150            vol_pct_window: 200,
151            hurst_threshold: 0.52,
152            hurst_lookback: 20,
153            stop_atr_mult: 1.5,
154            min_vol_pct: 0.20,
155            min_hold_candles: 2,
156            engine: SignalEngineConfig::default(),
157        }
158    }
159}
160
161#[cfg(test)]
162mod tests {
163    use super::*;
164
165    #[test]
166    fn default_signal_mode_is_majority() {
167        assert_eq!(IndicatorConfig::default().signal_mode, "majority");
168    }
169
170    #[test]
171    fn json_without_engine_block_still_loads() {
172        // Tuned JSON files predate `engine`; it must default when absent.
173        let mut json = serde_json::to_value(IndicatorConfig::default()).unwrap();
174        json.as_object_mut().unwrap().remove("engine");
175        let cfg: IndicatorConfig = serde_json::from_value(json).unwrap();
176        assert_eq!(cfg.engine.kmeans_recompute_bars, 10);
177        assert_eq!(cfg.engine.kmeans_max_iters, 100);
178        assert_eq!(cfg.engine.hurst_recompute_bars, 10);
179    }
180
181    #[test]
182    fn serde_round_trip() {
183        let cfg = IndicatorConfig::default();
184        let json = serde_json::to_string(&cfg).unwrap();
185        let back: IndicatorConfig = serde_json::from_str(&json).unwrap();
186        assert_eq!(back.ema_len, cfg.ema_len);
187        assert_eq!(back.signal_mode, cfg.signal_mode);
188    }
189}