kestrel_chartkit/indicator/
atr.rs1use std::collections::HashMap;
2
3#[cfg(feature = "serde")]
4use serde::{Deserialize, Serialize};
5
6use crate::model::Bar;
7
8use super::smoothing::{crossed_over, crossed_under, Ema, Rma, Sma, Wma};
9use super::{Indicator, IndicatorAlert, IndicatorOutput};
10
11#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
16#[cfg_attr(
17 feature = "serde",
18 derive(Serialize, Deserialize),
19 serde(rename_all = "snake_case")
20)]
21pub enum TrueRangeSmoothing {
22 #[default]
25 Rma,
26 Sma,
28 Ema,
31 Wma,
33}
34
35#[derive(Debug, Clone)]
38enum TrSmoother {
39 Rma(Rma),
40 Sma(Sma),
41 Ema { ema: Ema, len: usize, seen: usize },
42 Wma(Wma),
43}
44
45impl TrSmoother {
46 fn new(method: TrueRangeSmoothing, len: usize) -> Self {
47 match method {
48 TrueRangeSmoothing::Rma => Self::Rma(Rma::new(len)),
49 TrueRangeSmoothing::Sma => Self::Sma(Sma::new(len)),
50 TrueRangeSmoothing::Ema => Self::Ema {
51 ema: Ema::new(len),
52 len,
53 seen: 0,
54 },
55 TrueRangeSmoothing::Wma => Self::Wma(Wma::new(len)),
56 }
57 }
58
59 fn update(&mut self, tr: f64) -> Option<f64> {
60 match self {
61 Self::Rma(rma) => rma.update(tr),
62 Self::Sma(sma) => sma.update(tr),
63 Self::Ema { ema, len, seen } => {
64 let value = ema.update(tr)?;
65 *seen += 1;
66 (*seen >= *len).then_some(value)
67 }
68 Self::Wma(wma) => wma.update(tr),
69 }
70 }
71
72 fn reset(&mut self) {
73 match self {
74 Self::Rma(rma) => rma.reset(),
75 Self::Sma(sma) => sma.reset(),
76 Self::Ema { ema, seen, .. } => {
77 ema.reset();
78 *seen = 0;
79 }
80 Self::Wma(wma) => wma.reset(),
81 }
82 }
83}
84
85#[derive(Debug, Clone)]
107pub struct Atr {
108 atr_len: usize,
109 smoothing: TrueRangeSmoothing,
110 prev_close: Option<f64>,
111 tr_average: TrSmoother,
112 signal_rma: Rma,
113
114 prev_atr_disp: Option<f64>,
115 prev_signal: Option<f64>,
116 bars_seen: usize,
117 warmup_period: usize,
118
119 alerts: AtrAlerts,
120}
121
122#[derive(Debug, Clone, Copy, PartialEq, Default)]
123pub struct AtrAlerts {
124 pub expansion: bool,
125 pub contraction: bool,
126 pub regime_strength: f64,
127}
128
129impl Atr {
130 pub fn new(atr_len: usize, sig_len: usize) -> Self {
131 Self {
132 atr_len,
133 smoothing: TrueRangeSmoothing::Rma,
134 prev_close: None,
135 tr_average: TrSmoother::new(TrueRangeSmoothing::Rma, atr_len),
136 signal_rma: Rma::new(sig_len),
137 prev_atr_disp: None,
138 prev_signal: None,
139 bars_seen: 0,
140 warmup_period: atr_len + sig_len - 1,
141 alerts: AtrAlerts::default(),
142 }
143 }
144
145 pub fn with_defaults() -> Self {
146 Self::new(14, 20)
149 }
150
151 pub fn with_period(atr_len: usize) -> Self {
152 Self::new(atr_len, 14)
153 }
154
155 pub fn with_smoothing(mut self, method: TrueRangeSmoothing) -> Self {
163 self.smoothing = method;
164 self.tr_average = TrSmoother::new(method, self.atr_len);
165 self
166 }
167
168 pub fn smoothing(&self) -> TrueRangeSmoothing {
169 self.smoothing
170 }
171}
172
173impl Indicator for Atr {
174 fn name(&self) -> &str {
175 "atr"
176 }
177
178 fn warmup_period(&self) -> usize {
179 self.warmup_period
180 }
181
182 fn on_bar(&mut self, bar: &Bar) -> Option<IndicatorOutput> {
183 self.alerts = AtrAlerts::default();
184 self.bars_seen += 1;
185
186 let tr = match self.prev_close {
187 None => bar.high - bar.low,
188 Some(prev_close) => (bar.high - bar.low)
189 .max((bar.high - prev_close).abs())
190 .max((bar.low - prev_close).abs()),
191 };
192 self.prev_close = Some(bar.close);
193
194 let atr_raw = self.tr_average.update(tr)?;
195 let atr_disp = if bar.close > 0.0 {
196 100.0 * atr_raw / bar.close
197 } else {
198 0.0
199 };
200 let atr_signal = self.signal_rma.update(atr_disp)?;
201
202 if let (Some(prev_disp), Some(prev_sig)) = (self.prev_atr_disp, self.prev_signal) {
203 self.alerts.expansion = crossed_over(prev_disp, prev_sig, atr_disp, atr_signal);
204 self.alerts.contraction = crossed_under(prev_disp, prev_sig, atr_disp, atr_signal);
205 self.alerts.regime_strength = if atr_signal != 0.0 {
206 ((atr_disp - atr_signal) / atr_signal).abs().clamp(0.0, 1.0)
207 } else {
208 0.0
209 };
210 }
211 self.prev_atr_disp = Some(atr_disp);
212 self.prev_signal = Some(atr_signal);
213
214 let mut extra = HashMap::new();
215 extra.insert("signal".to_string(), atr_signal);
216 extra.insert("raw".to_string(), atr_raw);
217
218 Some(IndicatorOutput::with_extra(atr_disp, extra))
219 }
220
221 fn reset(&mut self) {
222 self.prev_close = None;
223 self.tr_average.reset();
224 self.signal_rma.reset();
225 self.prev_atr_disp = None;
226 self.prev_signal = None;
227 self.bars_seen = 0;
228 self.alerts = AtrAlerts::default();
229 }
230
231 fn alerts(&self) -> Vec<IndicatorAlert> {
232 let a = self.alerts;
233 let mut out = Vec::new();
234 if a.expansion {
235 out.push(IndicatorAlert {
236 kind: "expansion".to_string(),
237 note: "ATR · VOLA EXPANSION".to_string(),
238 strength: a.regime_strength,
239 });
240 }
241 if a.contraction {
242 out.push(IndicatorAlert {
243 kind: "contraction".to_string(),
244 note: "ATR · VOLA CONTRACTION".to_string(),
245 strength: a.regime_strength,
246 });
247 }
248 out
249 }
250}