kestrel_chartkit/indicator/
adx.rs1use std::collections::HashMap;
2
3use crate::model::Bar;
4
5use super::smoothing::{crossed_over, crossed_under, Ema, Rma};
6use super::{Indicator, IndicatorAlert, IndicatorOutput};
7
8#[derive(Debug, Clone)]
28pub struct Adx {
29 level_weak: f64,
30
31 prev_bar: Option<(f64, f64, f64)>,
32 tr_rma: Rma,
33 dm_plus_rma: Rma,
34 dm_minus_rma: Rma,
35 dx_rma: Rma,
36 adx_signal: Ema,
37
38 prev_di_plus: Option<f64>,
39 prev_di_minus: Option<f64>,
40 prev_adx_raw: Option<f64>,
41 bars_seen: usize,
42 warmup_period: usize,
43
44 alerts: AdxAlerts,
45}
46
47#[derive(Debug, Clone, Copy, PartialEq, Default)]
48pub struct AdxAlerts {
49 pub bull_di_cross: bool,
50 pub bear_di_cross: bool,
51 pub adx_activated: bool,
52 pub adx_deactivated: bool,
53 pub regime_strength: f64,
54}
55
56impl Adx {
57 pub fn new(di_len: usize, adx_smooth: usize, sig_len: usize, level_weak: f64) -> Self {
58 Self {
59 level_weak,
60 prev_bar: None,
61 tr_rma: Rma::new(di_len),
62 dm_plus_rma: Rma::new(di_len),
63 dm_minus_rma: Rma::new(di_len),
64 dx_rma: Rma::new(adx_smooth),
65 adx_signal: Ema::new(sig_len),
66 prev_di_plus: None,
67 prev_di_minus: None,
68 prev_adx_raw: None,
69 bars_seen: 0,
70 warmup_period: di_len + adx_smooth,
71 alerts: AdxAlerts::default(),
72 }
73 }
74
75 pub fn with_defaults() -> Self {
76 Self::new(14, 14, 3, 20.0)
80 }
81
82 pub fn with_period(period: usize) -> Self {
83 Self::new(period, period, 14, 20.0)
84 }
85}
86
87impl Indicator for Adx {
88 fn name(&self) -> &str {
89 "adx"
90 }
91
92 fn warmup_period(&self) -> usize {
93 self.warmup_period
94 }
95
96 fn on_bar(&mut self, bar: &Bar) -> Option<IndicatorOutput> {
97 self.alerts = AdxAlerts::default();
98 self.bars_seen += 1;
99
100 let (prev_high, prev_low, prev_close) = match self.prev_bar {
101 None => {
102 self.prev_bar = Some((bar.high, bar.low, bar.close));
103 return None;
104 }
105 Some(p) => p,
106 };
107 self.prev_bar = Some((bar.high, bar.low, bar.close));
108
109 let up_move = bar.high - prev_high;
110 let down_move = prev_low - bar.low;
111 let dm_plus = if up_move > down_move && up_move > 0.0 {
112 up_move
113 } else {
114 0.0
115 };
116 let dm_minus = if down_move > up_move && down_move > 0.0 {
117 down_move
118 } else {
119 0.0
120 };
121 let tr = (bar.high - bar.low)
122 .max((bar.high - prev_close).abs())
123 .max((bar.low - prev_close).abs());
124
125 let (tr_avg, dm_plus_avg, dm_minus_avg) = match (
126 self.tr_rma.update(tr),
127 self.dm_plus_rma.update(dm_plus),
128 self.dm_minus_rma.update(dm_minus),
129 ) {
130 (Some(t), Some(p), Some(m)) => (t, p, m),
131 _ => return None,
132 };
133
134 let di_plus = if tr_avg != 0.0 {
135 100.0 * dm_plus_avg / tr_avg
136 } else {
137 0.0
138 };
139 let di_minus = if tr_avg != 0.0 {
140 100.0 * dm_minus_avg / tr_avg
141 } else {
142 0.0
143 };
144 let di_sum = di_plus + di_minus;
145 let dx = if di_sum != 0.0 {
146 100.0 * (di_plus - di_minus).abs() / di_sum
147 } else {
148 0.0
149 };
150
151 let adx_raw = self.dx_rma.update(dx)?;
152 let adx_line = self.adx_signal.update(adx_raw)?;
153
154 if let (Some(prev_plus), Some(prev_minus), Some(prev_adx)) =
155 (self.prev_di_plus, self.prev_di_minus, self.prev_adx_raw)
156 {
157 self.alerts.bull_di_cross = crossed_over(prev_plus, prev_minus, di_plus, di_minus);
158 self.alerts.bear_di_cross = crossed_under(prev_plus, prev_minus, di_plus, di_minus);
159 self.alerts.adx_activated =
160 crossed_over(prev_adx, self.level_weak, adx_raw, self.level_weak);
161 self.alerts.adx_deactivated =
162 crossed_under(prev_adx, self.level_weak, adx_raw, self.level_weak);
163 if self.alerts.adx_activated {
164 self.alerts.regime_strength =
165 ((adx_raw - self.level_weak) / self.level_weak).clamp(0.0, 1.0);
166 } else if self.alerts.adx_deactivated {
167 self.alerts.regime_strength =
168 ((self.level_weak - adx_raw) / self.level_weak).clamp(0.0, 1.0);
169 }
170 }
171 self.prev_di_plus = Some(di_plus);
172 self.prev_di_minus = Some(di_minus);
173 self.prev_adx_raw = Some(adx_raw);
174
175 let mut extra = HashMap::new();
176 extra.insert("signal".to_string(), adx_line);
177 extra.insert("di_plus".to_string(), di_plus);
178 extra.insert("di_minus".to_string(), di_minus);
179
180 Some(IndicatorOutput::with_extra(adx_raw, extra))
181 }
182
183 fn reset(&mut self) {
184 self.prev_bar = None;
185 self.tr_rma.reset();
186 self.dm_plus_rma.reset();
187 self.dm_minus_rma.reset();
188 self.dx_rma.reset();
189 self.adx_signal.reset();
190 self.prev_di_plus = None;
191 self.prev_di_minus = None;
192 self.prev_adx_raw = None;
193 self.bars_seen = 0;
194 self.alerts = AdxAlerts::default();
195 }
196
197 fn alerts(&self) -> Vec<IndicatorAlert> {
198 let a = self.alerts;
199 let mut out = Vec::new();
200 if a.bull_di_cross {
201 out.push(IndicatorAlert {
202 kind: "bull_di_cross".to_string(),
203 note: "ADX · DI BULL CROSS".to_string(),
204 strength: 1.0,
205 });
206 }
207 if a.bear_di_cross {
208 out.push(IndicatorAlert {
209 kind: "bear_di_cross".to_string(),
210 note: "ADX · DI BEAR CROSS".to_string(),
211 strength: 1.0,
212 });
213 }
214 if a.adx_activated {
215 out.push(IndicatorAlert {
216 kind: "adx_activated".to_string(),
217 note: "ADX · ADX ACTIVATED".to_string(),
218 strength: a.regime_strength,
219 });
220 }
221 if a.adx_deactivated {
222 out.push(IndicatorAlert {
223 kind: "adx_deactivated".to_string(),
224 note: "ADX · ADX DEACTIVATED".to_string(),
225 strength: a.regime_strength,
226 });
227 }
228 out
229 }
230}