Skip to main content

indicators/
confluence.rs

1//! Confluence Engine.
2//!
3//! Scores bullish/bearish confluence from EMA stack, MACD, RSI, ADX, and volume.
4
5use crate::types::Candle;
6use std::collections::VecDeque;
7
8pub struct ConfluenceEngine {
9    fast_len: usize,
10    slow_len: usize,
11    trend_len: usize,
12    rsi_len: usize,
13    adx_len: usize,
14
15    closes: VecDeque<f64>,
16    volumes: VecDeque<f64>,
17    highs: VecDeque<f64>,
18    lows: VecDeque<f64>,
19
20    // EMAs
21    ema_f: Option<f64>,
22    ema_s: Option<f64>,
23    ema_t: Option<f64>,
24    // MACD
25    macd_ema12: Option<f64>,
26    macd_ema26: Option<f64>,
27    macd_sig: Option<f64>,
28    // RSI (RMA)
29    rsi_prev_c: Option<f64>,
30    rsi_gain: Option<f64>,
31    rsi_loss: Option<f64>,
32    // ADX (RMA)
33    adx_prev_h: Option<f64>,
34    adx_prev_l: Option<f64>,
35    adx_prev_c: Option<f64>,
36    adx_val: Option<f64>,
37    di_plus: Option<f64>,
38    di_minus: Option<f64>,
39    atr_adx: Option<f64>,
40
41    pub bull_score: f64,
42    pub bear_score: f64,
43    pub ema_fast: Option<f64>,
44    pub ema_slow: Option<f64>,
45}
46
47impl ConfluenceEngine {
48    pub fn new(fast: usize, slow: usize, trend: usize, rsi_len: usize, adx_len: usize) -> Self {
49        let maxlen = (slow * 3).max(trend + 10).max(300);
50        Self {
51            fast_len: fast,
52            slow_len: slow,
53            trend_len: trend,
54            rsi_len,
55            adx_len,
56            closes: VecDeque::with_capacity(maxlen),
57            volumes: VecDeque::with_capacity(maxlen),
58            highs: VecDeque::with_capacity(maxlen),
59            lows: VecDeque::with_capacity(maxlen),
60            ema_f: None,
61            ema_s: None,
62            ema_t: None,
63            macd_ema12: None,
64            macd_ema26: None,
65            macd_sig: None,
66            rsi_prev_c: None,
67            rsi_gain: None,
68            rsi_loss: None,
69            adx_prev_h: None,
70            adx_prev_l: None,
71            adx_prev_c: None,
72            adx_val: None,
73            di_plus: None,
74            di_minus: None,
75            atr_adx: None,
76            bull_score: 0.0,
77            bear_score: 0.0,
78            ema_fast: None,
79            ema_slow: None,
80        }
81    }
82
83    #[inline]
84    fn ema_step(prev: Option<f64>, val: f64, len: usize) -> f64 {
85        let k = 2.0 / (len as f64 + 1.0);
86        prev.map_or(val, |p| val * k + p * (1.0 - k))
87    }
88
89    #[inline]
90    fn rma_step(prev: Option<f64>, val: f64, len: usize) -> f64 {
91        let k = 1.0 / len as f64;
92        prev.map_or(val, |p| val * k + p * (1.0 - k))
93    }
94
95    fn update_rsi(&mut self, close: f64) -> f64 {
96        let Some(prev) = self.rsi_prev_c else {
97            self.rsi_prev_c = Some(close);
98            return 50.0;
99        };
100        let delta = close - prev;
101        self.rsi_prev_c = Some(close);
102        self.rsi_gain = Some(Self::rma_step(self.rsi_gain, delta.max(0.0), self.rsi_len));
103        self.rsi_loss = Some(Self::rma_step(
104            self.rsi_loss,
105            (-delta).max(0.0),
106            self.rsi_len,
107        ));
108        let gain = self.rsi_gain.unwrap_or(0.0);
109        let loss = self.rsi_loss.unwrap_or(1e-9).max(1e-9);
110        100.0 - 100.0 / (1.0 + gain / loss)
111    }
112
113    fn update_adx(&mut self, high: f64, low: f64, close: f64) {
114        let (Some(ph), Some(pl), Some(pc)) = (self.adx_prev_h, self.adx_prev_l, self.adx_prev_c)
115        else {
116            self.adx_prev_h = Some(high);
117            self.adx_prev_l = Some(low);
118            self.adx_prev_c = Some(close);
119            return;
120        };
121
122        let tr = (high - low).max((high - pc).abs()).max((low - pc).abs());
123        let up = high - ph;
124        let down = pl - low;
125        let dm_p = if up > down && up > 0.0 { up } else { 0.0 };
126        let dm_m = if down > up && down > 0.0 { down } else { 0.0 };
127
128        self.atr_adx = Some(Self::rma_step(self.atr_adx, tr, self.adx_len));
129        let atr = self.atr_adx.unwrap_or(1e-9).max(1e-9);
130
131        self.di_plus = Some(Self::rma_step(
132            self.di_plus,
133            dm_p / atr * 100.0,
134            self.adx_len,
135        ));
136        self.di_minus = Some(Self::rma_step(
137            self.di_minus,
138            dm_m / atr * 100.0,
139            self.adx_len,
140        ));
141
142        let dip = self.di_plus.unwrap_or(0.0);
143        let dim = self.di_minus.unwrap_or(0.0);
144        let di_sum = (dip + dim).max(1e-9);
145        let dx = (dip - dim).abs() / di_sum * 100.0;
146        self.adx_val = Some(Self::rma_step(self.adx_val, dx, self.adx_len));
147
148        self.adx_prev_h = Some(high);
149        self.adx_prev_l = Some(low);
150        self.adx_prev_c = Some(close);
151    }
152
153    pub fn update(&mut self, candle: &Candle) {
154        let (cl, vol, h, lo) = (candle.close, candle.volume, candle.high, candle.low);
155
156        let cap = self.closes.capacity();
157        if self.closes.len() == cap {
158            self.closes.pop_front();
159        }
160        if self.volumes.len() == cap {
161            self.volumes.pop_front();
162        }
163        if self.highs.len() == cap {
164            self.highs.pop_front();
165        }
166        if self.lows.len() == cap {
167            self.lows.pop_front();
168        }
169        self.closes.push_back(cl);
170        self.volumes.push_back(vol);
171        self.highs.push_back(h);
172        self.lows.push_back(lo);
173
174        self.ema_f = Some(Self::ema_step(self.ema_f, cl, self.fast_len));
175        self.ema_s = Some(Self::ema_step(self.ema_s, cl, self.slow_len));
176        self.ema_t = Some(Self::ema_step(self.ema_t, cl, self.trend_len));
177        self.ema_fast = self.ema_f;
178        self.ema_slow = self.ema_s;
179
180        self.macd_ema12 = Some(Self::ema_step(self.macd_ema12, cl, 12));
181        self.macd_ema26 = Some(Self::ema_step(self.macd_ema26, cl, 26));
182        let macd_line = self.macd_ema12.unwrap_or(cl) - self.macd_ema26.unwrap_or(cl);
183        self.macd_sig = Some(Self::ema_step(self.macd_sig, macd_line, 9));
184        let macd_hist = macd_line - self.macd_sig.unwrap_or(0.0);
185
186        let rsi_val = self.update_rsi(cl);
187        self.update_adx(h, lo, cl);
188
189        let adx = self.adx_val.unwrap_or(0.0);
190        let dip = self.di_plus.unwrap_or(0.0);
191        let dim = self.di_minus.unwrap_or(0.0);
192
193        // Volume filter
194        let vols: Vec<f64> = self.volumes.iter().copied().collect();
195        let vol_sma = if vols.len() >= 20 {
196            vols[vols.len() - 20..].iter().sum::<f64>() / 20.0
197        } else {
198            vol
199        };
200        let vol_ok = vol > vol_sma * 1.2;
201
202        let ef = self.ema_f.unwrap_or(cl);
203        let es = self.ema_s.unwrap_or(cl);
204        let et = self.ema_t.unwrap_or(cl);
205        let sig = self.macd_sig.unwrap_or(0.0);
206
207        let mut b = 0.0_f64;
208        b += if ef > es { 1.0 } else { 0.0 };
209        b += if cl > et { 1.0 } else { 0.0 };
210        b += if (50.0..75.0).contains(&rsi_val) {
211            1.0
212        } else {
213            0.0
214        };
215        b += if macd_hist > 0.0 { 1.0 } else { 0.0 };
216        b += if macd_line > sig { 1.0 } else { 0.0 };
217        b += if vol_ok { 1.0 } else { 0.0 };
218        b += if adx > 20.0 && dip > dim { 1.0 } else { 0.0 };
219        b += if cl > ef { 0.5 } else { 0.0 };
220        self.bull_score = b;
221
222        let mut s = 0.0_f64;
223        s += if ef < es { 1.0 } else { 0.0 };
224        s += if cl < et { 1.0 } else { 0.0 };
225        s += if (25.0..50.0).contains(&rsi_val) {
226            1.0
227        } else {
228            0.0
229        };
230        s += if macd_hist < 0.0 { 1.0 } else { 0.0 };
231        s += if macd_line < sig { 1.0 } else { 0.0 };
232        s += if vol_ok { 1.0 } else { 0.0 };
233        s += if adx > 20.0 && dim > dip { 1.0 } else { 0.0 };
234        s += if cl < ef { 0.5 } else { 0.0 };
235        self.bear_score = s;
236    }
237
238    pub fn grade(score: f64) -> &'static str {
239        if score >= 8.0 {
240            "A+"
241        } else if score >= 6.5 {
242            "A"
243        } else if score >= 5.0 {
244            "B"
245        } else {
246            "C"
247        }
248    }
249}