Skip to main content

indicators/signal/
confluence.rs

1//! Confluence Engine.
2//!
3//! Scores bullish/bearish confluence from EMA stack, MACD, RSI, ADX, and volume.
4
5use std::collections::{HashMap, VecDeque};
6
7use crate::error::IndicatorError;
8use crate::indicator::{Indicator, IndicatorOutput};
9use crate::registry::param_usize;
10use crate::types::Candle;
11
12// ── Params ────────────────────────────────────────────────────────────────────
13
14#[derive(Debug, Clone)]
15pub struct ConfluenceParams {
16    pub fast_len: usize,
17    pub slow_len: usize,
18    pub trend_len: usize,
19    pub rsi_len: usize,
20    pub adx_len: usize,
21}
22
23impl Default for ConfluenceParams {
24    fn default() -> Self {
25        Self {
26            fast_len: 8,
27            slow_len: 21,
28            trend_len: 50,
29            rsi_len: 14,
30            adx_len: 14,
31        }
32    }
33}
34
35// ── Indicator wrapper ─────────────────────────────────────────────────────────
36
37/// Batch `Indicator` adapter for [`ConfluenceEngine`].
38///
39/// Replays the candle slice through the streaming engine and collects per-bar
40/// `bull_score` and `bear_score`.
41#[derive(Debug, Clone)]
42pub struct ConfluenceIndicator {
43    pub params: ConfluenceParams,
44}
45
46impl ConfluenceIndicator {
47    pub fn new(params: ConfluenceParams) -> Self {
48        Self { params }
49    }
50}
51
52impl Indicator for ConfluenceIndicator {
53    fn name(&self) -> &'static str {
54        "Confluence"
55    }
56    fn required_len(&self) -> usize {
57        self.params.trend_len + 1
58    }
59    fn required_columns(&self) -> &[&'static str] {
60        &["high", "low", "close", "volume"]
61    }
62
63    fn calculate(&self, candles: &[Candle]) -> Result<IndicatorOutput, IndicatorError> {
64        self.check_len(candles)?;
65        let p = &self.params;
66        let mut eng =
67            ConfluenceEngine::new(p.fast_len, p.slow_len, p.trend_len, p.rsi_len, p.adx_len);
68        let n = candles.len();
69        let mut bull = vec![f64::NAN; n];
70        let mut bear = vec![f64::NAN; n];
71        for (i, c) in candles.iter().enumerate() {
72            eng.update(c);
73            bull[i] = eng.bull_score;
74            bear[i] = eng.bear_score;
75        }
76        Ok(IndicatorOutput::from_pairs([
77            ("confluence_bull", bull),
78            ("confluence_bear", bear),
79        ]))
80    }
81}
82
83// ── Registry factory ──────────────────────────────────────────────────────────
84
85pub fn factory<S: ::std::hash::BuildHasher>(params: &HashMap<String, String, S>) -> Result<Box<dyn Indicator>, IndicatorError> {
86    let fast_len = param_usize(params, "fast_len", 8)?;
87    let slow_len = param_usize(params, "slow_len", 21)?;
88    let trend_len = param_usize(params, "trend_len", 50)?;
89    let rsi_len = param_usize(params, "rsi_len", 14)?;
90    let adx_len = param_usize(params, "adx_len", 14)?;
91    Ok(Box::new(ConfluenceIndicator::new(ConfluenceParams {
92        fast_len,
93        slow_len,
94        trend_len,
95        rsi_len,
96        adx_len,
97    })))
98}
99
100#[derive(Debug)]
101pub struct ConfluenceEngine {
102    fast_len: usize,
103    slow_len: usize,
104    trend_len: usize,
105    rsi_len: usize,
106    adx_len: usize,
107
108    closes: VecDeque<f64>,
109    volumes: VecDeque<f64>,
110    highs: VecDeque<f64>,
111    lows: VecDeque<f64>,
112
113    // EMAs
114    ema_f: Option<f64>,
115    ema_s: Option<f64>,
116    ema_t: Option<f64>,
117    // MACD
118    macd_ema12: Option<f64>,
119    macd_ema26: Option<f64>,
120    macd_sig: Option<f64>,
121    // RSI (RMA)
122    rsi_prev_c: Option<f64>,
123    rsi_gain: Option<f64>,
124    rsi_loss: Option<f64>,
125    // ADX (RMA)
126    adx_prev_h: Option<f64>,
127    adx_prev_l: Option<f64>,
128    adx_prev_c: Option<f64>,
129    adx_val: Option<f64>,
130    di_plus: Option<f64>,
131    di_minus: Option<f64>,
132    atr_adx: Option<f64>,
133
134    pub bull_score: f64,
135    pub bear_score: f64,
136    pub ema_fast: Option<f64>,
137    pub ema_slow: Option<f64>,
138}
139
140impl ConfluenceEngine {
141    pub fn new(fast: usize, slow: usize, trend: usize, rsi_len: usize, adx_len: usize) -> Self {
142        let maxlen = (slow * 3).max(trend + 10).max(300);
143        Self {
144            fast_len: fast,
145            slow_len: slow,
146            trend_len: trend,
147            rsi_len,
148            adx_len,
149            closes: VecDeque::with_capacity(maxlen),
150            volumes: VecDeque::with_capacity(maxlen),
151            highs: VecDeque::with_capacity(maxlen),
152            lows: VecDeque::with_capacity(maxlen),
153            ema_f: None,
154            ema_s: None,
155            ema_t: None,
156            macd_ema12: None,
157            macd_ema26: None,
158            macd_sig: None,
159            rsi_prev_c: None,
160            rsi_gain: None,
161            rsi_loss: None,
162            adx_prev_h: None,
163            adx_prev_l: None,
164            adx_prev_c: None,
165            adx_val: None,
166            di_plus: None,
167            di_minus: None,
168            atr_adx: None,
169            bull_score: 0.0,
170            bear_score: 0.0,
171            ema_fast: None,
172            ema_slow: None,
173        }
174    }
175
176    #[inline]
177    fn ema_step(prev: Option<f64>, val: f64, len: usize) -> f64 {
178        let k = 2.0 / (len as f64 + 1.0);
179        prev.map_or(val, |p| val * k + p * (1.0 - k))
180    }
181
182    #[inline]
183    fn rma_step(prev: Option<f64>, val: f64, len: usize) -> f64 {
184        let k = 1.0 / len as f64;
185        prev.map_or(val, |p| val * k + p * (1.0 - k))
186    }
187
188    fn update_rsi(&mut self, close: f64) -> f64 {
189        let Some(prev) = self.rsi_prev_c else {
190            self.rsi_prev_c = Some(close);
191            return 50.0;
192        };
193        let delta = close - prev;
194        self.rsi_prev_c = Some(close);
195        self.rsi_gain = Some(Self::rma_step(self.rsi_gain, delta.max(0.0), self.rsi_len));
196        self.rsi_loss = Some(Self::rma_step(
197            self.rsi_loss,
198            (-delta).max(0.0),
199            self.rsi_len,
200        ));
201        let gain = self.rsi_gain.unwrap_or(0.0);
202        let loss = self.rsi_loss.unwrap_or(1e-9).max(1e-9);
203        100.0 - 100.0 / (1.0 + gain / loss)
204    }
205
206    fn update_adx(&mut self, high: f64, low: f64, close: f64) {
207        let (Some(ph), Some(pl), Some(pc)) = (self.adx_prev_h, self.adx_prev_l, self.adx_prev_c)
208        else {
209            self.adx_prev_h = Some(high);
210            self.adx_prev_l = Some(low);
211            self.adx_prev_c = Some(close);
212            return;
213        };
214
215        let tr = (high - low).max((high - pc).abs()).max((low - pc).abs());
216        let up = high - ph;
217        let down = pl - low;
218        let dm_p = if up > down && up > 0.0 { up } else { 0.0 };
219        let dm_m = if down > up && down > 0.0 { down } else { 0.0 };
220
221        self.atr_adx = Some(Self::rma_step(self.atr_adx, tr, self.adx_len));
222        let atr = self.atr_adx.unwrap_or(1e-9).max(1e-9);
223
224        self.di_plus = Some(Self::rma_step(
225            self.di_plus,
226            dm_p / atr * 100.0,
227            self.adx_len,
228        ));
229        self.di_minus = Some(Self::rma_step(
230            self.di_minus,
231            dm_m / atr * 100.0,
232            self.adx_len,
233        ));
234
235        let dip = self.di_plus.unwrap_or(0.0);
236        let dim = self.di_minus.unwrap_or(0.0);
237        let di_sum = (dip + dim).max(1e-9);
238        let dx = (dip - dim).abs() / di_sum * 100.0;
239        self.adx_val = Some(Self::rma_step(self.adx_val, dx, self.adx_len));
240
241        self.adx_prev_h = Some(high);
242        self.adx_prev_l = Some(low);
243        self.adx_prev_c = Some(close);
244    }
245
246    pub fn update(&mut self, candle: &Candle) {
247        let (cl, vol, h, lo) = (candle.close, candle.volume, candle.high, candle.low);
248
249        let cap = self.closes.capacity();
250        if self.closes.len() == cap {
251            self.closes.pop_front();
252        }
253        if self.volumes.len() == cap {
254            self.volumes.pop_front();
255        }
256        if self.highs.len() == cap {
257            self.highs.pop_front();
258        }
259        if self.lows.len() == cap {
260            self.lows.pop_front();
261        }
262        self.closes.push_back(cl);
263        self.volumes.push_back(vol);
264        self.highs.push_back(h);
265        self.lows.push_back(lo);
266
267        self.ema_f = Some(Self::ema_step(self.ema_f, cl, self.fast_len));
268        self.ema_s = Some(Self::ema_step(self.ema_s, cl, self.slow_len));
269        self.ema_t = Some(Self::ema_step(self.ema_t, cl, self.trend_len));
270        self.ema_fast = self.ema_f;
271        self.ema_slow = self.ema_s;
272
273        self.macd_ema12 = Some(Self::ema_step(self.macd_ema12, cl, 12));
274        self.macd_ema26 = Some(Self::ema_step(self.macd_ema26, cl, 26));
275        let macd_line = self.macd_ema12.unwrap_or(cl) - self.macd_ema26.unwrap_or(cl);
276        self.macd_sig = Some(Self::ema_step(self.macd_sig, macd_line, 9));
277        let macd_hist = macd_line - self.macd_sig.unwrap_or(0.0);
278
279        let rsi_val = self.update_rsi(cl);
280        self.update_adx(h, lo, cl);
281
282        let adx = self.adx_val.unwrap_or(0.0);
283        let dip = self.di_plus.unwrap_or(0.0);
284        let dim = self.di_minus.unwrap_or(0.0);
285
286        // Volume filter
287        let vols: Vec<f64> = self.volumes.iter().copied().collect();
288        let vol_sma = if vols.len() >= 20 {
289            vols[vols.len() - 20..].iter().sum::<f64>() / 20.0
290        } else {
291            vol
292        };
293        let vol_ok = vol > vol_sma * 1.2;
294
295        let ef = self.ema_f.unwrap_or(cl);
296        let es = self.ema_s.unwrap_or(cl);
297        let et = self.ema_t.unwrap_or(cl);
298        let sig = self.macd_sig.unwrap_or(0.0);
299
300        let mut b = 0.0_f64;
301        b += if ef > es { 1.0 } else { 0.0 };
302        b += if cl > et { 1.0 } else { 0.0 };
303        b += if (50.0..75.0).contains(&rsi_val) {
304            1.0
305        } else {
306            0.0
307        };
308        b += if macd_hist > 0.0 { 1.0 } else { 0.0 };
309        b += if macd_line > sig { 1.0 } else { 0.0 };
310        b += if vol_ok { 1.0 } else { 0.0 };
311        b += if adx > 20.0 && dip > dim { 1.0 } else { 0.0 };
312        b += if cl > ef { 0.5 } else { 0.0 };
313        self.bull_score = b;
314
315        let mut s = 0.0_f64;
316        s += if ef < es { 1.0 } else { 0.0 };
317        s += if cl < et { 1.0 } else { 0.0 };
318        s += if (25.0..50.0).contains(&rsi_val) {
319            1.0
320        } else {
321            0.0
322        };
323        s += if macd_hist < 0.0 { 1.0 } else { 0.0 };
324        s += if macd_line < sig { 1.0 } else { 0.0 };
325        s += if vol_ok { 1.0 } else { 0.0 };
326        s += if adx > 20.0 && dim > dip { 1.0 } else { 0.0 };
327        s += if cl < ef { 0.5 } else { 0.0 };
328        self.bear_score = s;
329    }
330
331    pub fn grade(score: f64) -> &'static str {
332        if score >= 8.0 {
333            "A+"
334        } else if score >= 6.5 {
335            "A"
336        } else if score >= 5.0 {
337            "B"
338        } else {
339            "C"
340        }
341    }
342}