Skip to main content

indicators/
signal.rs

1//! Signal aggregator — combines all 11 layers into a single trading signal.
2//!
3//! Port of the Python `compute_signal` function and `SignalStreak` class.
4
5use crate::confluence::ConfluenceEngine;
6use crate::cvd::CVDTracker;
7use crate::engine::Indicators;
8use crate::liquidity::LiquidityProfile;
9use crate::settings::BotSettings;
10use crate::structure::MarketStructure;
11use crate::vol_regime::VolatilityPercentile;
12
13// ── Signal components (debug / logging) ──────────────────────────────────────
14
15/// All per-layer signal votes and supporting values.
16#[derive(Debug, Clone)]
17pub struct SignalComponents {
18    // Votes: +1 = bull, -1 = bear, 0 = neutral
19    pub v_vwap: i8,
20    pub v_ema: i8,
21    pub v_st: i8,
22    pub v_ts: i8,
23    pub v_liq: i8,
24    pub v_conf_bull: i8,
25    pub v_conf_bear: i8,
26    pub v_struct: i8,
27    pub v_cvd: i8,
28    pub v_ao: i8,
29    pub v_hurst: i8,
30    pub v_accel_bull: i8,
31    pub v_accel_bear: i8,
32
33    // Supporting values
34    pub hurst: f64,
35    pub price_accel: f64,
36    pub bull_score: f64,
37    pub bear_score: f64,
38    pub conf_min_adj: f64,
39    pub liq_imbalance: f64,
40    pub liq_buy_pct: f64,
41    pub poc: Option<f64>,
42    pub struct_bias: i8,
43    pub fib618: Option<f64>,
44    pub fib_zone: &'static str,
45    pub fib_ok: bool,
46    pub bos: bool,
47    pub choch: bool,
48    pub ts_norm: f64,
49    pub dominance: f64,
50    pub cvd_slope: Option<f64>,
51    pub cvd_div: i8,
52    pub ao: f64,
53    pub ao_rising: bool,
54    pub wr_pct: f64,
55    pub mom_pct: f64,
56    pub wave_ok_long: bool,
57    pub wave_ok_short: bool,
58    pub mom_ok_long: bool,
59    pub mom_ok_short: bool,
60    pub vol_pct: Option<f64>,
61    pub vol_regime: Option<&'static str>,
62}
63
64// ── compute_signal ────────────────────────────────────────────────────────────
65
66/// Aggregate all layers into a single trading signal.
67///
68/// Returns `(signal, components)` where `signal` is:
69/// - `1`  → long
70/// - `-1` → short
71/// - `0`  → neutral / no trade
72pub fn compute_signal(
73    close: f64,
74    ind: &Indicators,
75    liq: &LiquidityProfile,
76    conf: &ConfluenceEngine,
77    ms: &MarketStructure,
78    s: &BotSettings,
79    cvd: Option<&CVDTracker>,
80    vol: Option<&VolatilityPercentile>,
81) -> (i32, SignalComponents) {
82    // Not ready yet
83    if ind.vwap.is_none() || ind.ema.is_none() || ind.st.is_none() {
84        let comps = empty_components(ind, liq, conf, ms, cvd, vol);
85        return (0, comps);
86    }
87
88    let vwap = ind.vwap.unwrap();
89    let ema = ind.ema.unwrap();
90
91    // ── Layer votes ───────────────────────────────────────────────────────────
92    let v1 = if close > vwap { 1_i8 } else { -1 }; // L1 VWAP
93    let v2 = if close > ema { 1 } else { -1 }; // L2 EMA
94    let v3 = if ind.st_dir_pub == -1 { -1 } else { 1 }; // L3 SuperTrend (-1=bullish)
95    let v4 = if ind.ts_bullish { 1 } else { -1 }; // L4 TrendSpeed
96    let v5 = if liq.bullish() { 1 } else { -1 }; // L5 Liquidity
97
98    let conf_adj = vol.map_or(1.0, |v| v.conf_adj);
99    let adj_min = s.conf_min_score * conf_adj;
100    let v6_bull = if conf.bull_score >= adj_min { 1_i8 } else { -1 }; // L6 bull
101    let v6_bear = if conf.bear_score >= adj_min { 1_i8 } else { -1 }; // L6 bear
102
103    let v7 = ms.bias; // L7 Market Structure
104
105    let v8: i8 = cvd.map_or(0, |c| {
106        if c.divergence != 0 {
107            c.divergence
108        } else if c.bullish {
109            1
110        } else {
111            -1
112        }
113    }); // L8 CVD
114
115    let v9: i8 = if ind.highs.len() >= 34 {
116        if ind.ao_rising { 1 } else { -1 }
117    } else {
118        0
119    }; // L9 AO
120
121    let v10: i8 = if (ind.hurst - 0.5).abs() < 0.005 {
122        0
123    } else if ind.hurst >= s.hurst_threshold {
124        1
125    } else {
126        -1
127    }; // L10 Hurst
128
129    let (v11_bull, v11_bear): (i8, i8) = if ind.price_accel.abs() < 0.005 {
130        (0, 0)
131    } else {
132        (
133            if ind.price_accel > 0.0 { 1 } else { -1 },
134            if ind.price_accel < 0.0 { 1 } else { -1 },
135        )
136    }; // L11 PriceAccel
137
138    // Fibonacci zone gates
139    let fib_ok_long = !s.fib_zone_enabled || ms.in_discount || ms.fib500.is_none();
140    let fib_ok_short = !s.fib_zone_enabled || ms.in_premium || ms.fib500.is_none();
141
142    // ── Signal logic ──────────────────────────────────────────────────────────
143    let (bull, bear) = match s.signal_mode.as_str() {
144        "strict" => {
145            let bull = v1 == 1
146                && v2 == 1
147                && v3 == -1
148                && v4 == 1
149                && v5 == 1
150                && v6_bull == 1
151                && v7 == 1
152                && fib_ok_long
153                && (v8 == 1 || v8 == 0);
154            let bear = v1 == -1
155                && v2 == -1
156                && v3 == 1
157                && v4 == -1
158                && v5 == -1
159                && v6_bear == 1
160                && v7 == -1
161                && fib_ok_short
162                && (v8 == -1 || v8 == 0);
163            (bull, bear)
164        }
165        "majority" => {
166            let core_bull = v1 == 1 && v2 == 1 && v3 == -1 && v4 == 1;
167            let core_bear = v1 == -1 && v2 == -1 && v3 == 1 && v4 == -1;
168
169            let ext_bull_count = [
170                v5 == 1,
171                v6_bull == 1,
172                v7 == 1,
173                fib_ok_long,
174                v8 == 1,
175                v9 == 1,
176                ind.wave_ok_long,
177                ind.mom_ok_long,
178                v10 == 1,
179                v11_bull == 1,
180            ]
181            .iter()
182            .filter(|&&b| b)
183            .count();
184
185            let ext_bear_count = [
186                v5 == -1,
187                v6_bear == 1,
188                v7 == -1,
189                fib_ok_short,
190                v8 == -1,
191                v9 == -1,
192                ind.wave_ok_short,
193                ind.mom_ok_short,
194                v10 == 1,
195                v11_bear == 1,
196            ]
197            .iter()
198            .filter(|&&b| b)
199            .count();
200
201            (
202                core_bull && ext_bull_count >= 2,
203                core_bear && ext_bear_count >= 2,
204            )
205        }
206        _ => {
207            // "any" / default — core layers only
208            let bull = v1 == 1 && v2 == 1 && v3 == -1 && v4 == 1;
209            let bear = v1 == -1 && v2 == -1 && v3 == 1 && v4 == -1;
210            (bull, bear)
211        }
212    };
213
214    let fib_zone = if ms.in_discount {
215        "discount"
216    } else if ms.in_premium {
217        "premium"
218    } else {
219        "mid"
220    };
221
222    let comps = SignalComponents {
223        v_vwap: v1,
224        v_ema: v2,
225        v_st: v3,
226        v_ts: v4,
227        v_liq: v5,
228        v_conf_bull: v6_bull,
229        v_conf_bear: v6_bear,
230        v_struct: v7,
231        v_cvd: v8,
232        v_ao: v9,
233        v_hurst: v10,
234        v_accel_bull: v11_bull,
235        v_accel_bear: v11_bear,
236        hurst: ind.hurst,
237        price_accel: ind.price_accel,
238        bull_score: conf.bull_score,
239        bear_score: conf.bear_score,
240        conf_min_adj: adj_min,
241        liq_imbalance: liq.imbalance,
242        liq_buy_pct: liq.buy_pct * 100.0,
243        poc: liq.poc_price,
244        struct_bias: ms.bias,
245        fib618: ms.fib618,
246        fib_zone,
247        fib_ok: if bull { fib_ok_long } else { fib_ok_short },
248        bos: ms.bos,
249        choch: ms.choch,
250        ts_norm: ind.ts_norm,
251        dominance: ind.dominance,
252        cvd_slope: cvd.map(|c| c.cvd_slope),
253        cvd_div: cvd.map_or(0, |c| c.divergence),
254        ao: ind.ao,
255        ao_rising: ind.ao_rising,
256        wr_pct: ind.wr_pct,
257        mom_pct: ind.mom_pct,
258        wave_ok_long: ind.wave_ok_long,
259        wave_ok_short: ind.wave_ok_short,
260        mom_ok_long: ind.mom_ok_long,
261        mom_ok_short: ind.mom_ok_short,
262        vol_pct: vol.map(|v| v.vol_pct),
263        vol_regime: vol.map(|v| v.vol_regime),
264    };
265
266    if bull {
267        return (1, comps);
268    }
269    if bear {
270        return (-1, comps);
271    }
272    (0, comps)
273}
274
275fn empty_components(
276    ind: &Indicators,
277    liq: &LiquidityProfile,
278    conf: &ConfluenceEngine,
279    ms: &MarketStructure,
280    cvd: Option<&CVDTracker>,
281    vol: Option<&VolatilityPercentile>,
282) -> SignalComponents {
283    SignalComponents {
284        v_vwap: 0,
285        v_ema: 0,
286        v_st: 0,
287        v_ts: 0,
288        v_liq: 0,
289        v_conf_bull: 0,
290        v_conf_bear: 0,
291        v_struct: 0,
292        v_cvd: 0,
293        v_ao: 0,
294        v_hurst: 0,
295        v_accel_bull: 0,
296        v_accel_bear: 0,
297        hurst: ind.hurst,
298        price_accel: ind.price_accel,
299        bull_score: conf.bull_score,
300        bear_score: conf.bear_score,
301        conf_min_adj: 0.0,
302        liq_imbalance: liq.imbalance,
303        liq_buy_pct: liq.buy_pct * 100.0,
304        poc: liq.poc_price,
305        struct_bias: ms.bias,
306        fib618: ms.fib618,
307        fib_zone: "mid",
308        fib_ok: false,
309        bos: false,
310        choch: false,
311        ts_norm: 0.5,
312        dominance: 0.0,
313        cvd_slope: cvd.map(|c| c.cvd_slope),
314        cvd_div: 0,
315        ao: ind.ao,
316        ao_rising: false,
317        wr_pct: 0.5,
318        mom_pct: 0.5,
319        wave_ok_long: false,
320        wave_ok_short: false,
321        mom_ok_long: false,
322        mom_ok_short: false,
323        vol_pct: vol.map(|v| v.vol_pct),
324        vol_regime: vol.map(|v| v.vol_regime),
325    }
326}
327
328// ── SignalStreak ──────────────────────────────────────────────────────────────
329
330/// Confirmation filter — signal must agree for `required` consecutive bars.
331pub struct SignalStreak {
332    required: usize,
333    direction: i32,
334    count: usize,
335}
336
337impl SignalStreak {
338    pub fn new(required: usize) -> Self {
339        Self {
340            required,
341            direction: 0,
342            count: 0,
343        }
344    }
345
346    /// Feed a raw signal (`+1`, `-1`, or `0`).
347    /// Returns `true` when the streak reaches `required` and `signal != 0`.
348    pub fn update(&mut self, signal: i32) -> bool {
349        if signal != 0 && signal == self.direction {
350            self.count += 1;
351        } else {
352            self.direction = signal;
353            self.count = usize::from(signal != 0);
354        }
355        self.count >= self.required && signal != 0
356    }
357
358    pub fn reset(&mut self) {
359        self.direction = 0;
360        self.count = 0;
361    }
362
363    pub fn current_direction(&self) -> i32 {
364        self.direction
365    }
366    pub fn current_count(&self) -> usize {
367        self.count
368    }
369}