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