kestrel_chartkit/indicator/
volatility_indicators.rs1use std::collections::{HashMap, VecDeque};
2
3use crate::indicator::{Indicator, IndicatorAlert, IndicatorOutput};
4use crate::model::Bar;
5
6pub struct TrueRangeEngine {
8 prev_close: Option<f64>,
9 alerts: Vec<IndicatorAlert>,
10}
11
12impl TrueRangeEngine {
13 pub fn new() -> Self {
14 Self {
15 prev_close: None,
16 alerts: Vec::new(),
17 }
18 }
19}
20
21impl Default for TrueRangeEngine {
22 fn default() -> Self {
23 Self::new()
24 }
25}
26
27impl Indicator for TrueRangeEngine {
28 fn name(&self) -> &str {
29 "true_range"
30 }
31
32 fn warmup_period(&self) -> usize {
33 1
34 }
35
36 fn reset(&mut self) {
37 self.prev_close = None;
38 self.alerts.clear();
39 }
40
41 fn on_bar(&mut self, bar: &Bar) -> Option<IndicatorOutput> {
42 let tr = if let Some(prev) = self.prev_close {
43 (bar.high - bar.low)
44 .max((bar.high - prev).abs())
45 .max((bar.low - prev).abs())
46 } else {
47 bar.high - bar.low
48 };
49 self.prev_close = Some(bar.close);
50
51 Some(IndicatorOutput::new(tr))
52 }
53
54 fn alerts(&self) -> Vec<IndicatorAlert> {
55 self.alerts.clone()
56 }
57}
58
59#[derive(Debug, Clone)]
61pub struct KeltnerChannelEngine {
62 ema_period: usize,
63 atr_period: usize,
64 multiplier: f64,
65 closes: VecDeque<f64>,
66 trs: VecDeque<f64>,
67 prev_close: Option<f64>,
68 current_ema: Option<f64>,
69 alerts: Vec<IndicatorAlert>,
70}
71
72impl KeltnerChannelEngine {
73 pub fn new(ema_period: usize, atr_period: usize, multiplier: f64) -> Self {
74 Self {
75 ema_period,
76 atr_period,
77 multiplier,
78 closes: VecDeque::new(),
79 trs: VecDeque::new(),
80 prev_close: None,
81 current_ema: None,
82 alerts: Vec::new(),
83 }
84 }
85}
86
87impl Indicator for KeltnerChannelEngine {
88 fn name(&self) -> &str {
89 "keltner"
90 }
91
92 fn warmup_period(&self) -> usize {
93 self.ema_period.max(self.atr_period)
94 }
95
96 fn reset(&mut self) {
97 self.closes.clear();
98 self.trs.clear();
99 self.prev_close = None;
100 self.current_ema = None;
101 self.alerts.clear();
102 }
103
104 fn on_bar(&mut self, bar: &Bar) -> Option<IndicatorOutput> {
105 let tr = if let Some(prev) = self.prev_close {
106 (bar.high - bar.low)
107 .max((bar.high - prev).abs())
108 .max((bar.low - prev).abs())
109 } else {
110 bar.high - bar.low
111 };
112 self.prev_close = Some(bar.close);
113
114 self.closes.push_back(bar.close);
115 self.trs.push_back(tr);
116
117 let k = 2.0 / (self.ema_period as f64 + 1.0);
118 self.current_ema = match self.current_ema {
119 Some(prev_ema) => Some(bar.close * k + prev_ema * (1.0 - k)),
120 None => Some(bar.close),
121 };
122
123 if self.closes.len() > self.ema_period {
124 self.closes.pop_front();
125 }
126 if self.trs.len() > self.atr_period {
127 self.trs.pop_front();
128 }
129
130 self.alerts.clear();
131 if self.trs.len() < self.atr_period {
132 return None;
133 }
134
135 let basis = self.current_ema.unwrap_or(bar.close);
136 let atr: f64 = self.trs.iter().sum::<f64>() / self.atr_period as f64;
137 let upper = basis + self.multiplier * atr;
138 let lower = basis - self.multiplier * atr;
139
140 let mut extra = HashMap::new();
141 extra.insert("upper".to_string(), upper);
142 extra.insert("lower".to_string(), lower);
143 extra.insert("atr".to_string(), atr);
144
145 if bar.close > upper {
146 self.alerts.push(IndicatorAlert::new(
147 "keltner_upper_breakout",
148 format!("Price Above Upper Keltner Channel (${:.2})", upper),
149 0.80,
150 ));
151 } else if bar.close < lower {
152 self.alerts.push(IndicatorAlert::new(
153 "keltner_lower_breakout",
154 format!("Price Below Lower Keltner Channel (${:.2})", lower),
155 0.80,
156 ));
157 }
158
159 Some(IndicatorOutput::with_extra(basis, extra))
160 }
161
162 fn alerts(&self) -> Vec<IndicatorAlert> {
163 self.alerts.clone()
164 }
165}
166
167pub struct DonchianChannelEngine {
169 period: usize,
170 bars: VecDeque<Bar>,
171 alerts: Vec<IndicatorAlert>,
172}
173
174impl DonchianChannelEngine {
175 pub fn new(period: usize) -> Self {
176 Self {
177 period,
178 bars: VecDeque::new(),
179 alerts: Vec::new(),
180 }
181 }
182}
183
184impl Indicator for DonchianChannelEngine {
185 fn name(&self) -> &str {
186 "donchian"
187 }
188
189 fn warmup_period(&self) -> usize {
190 self.period
191 }
192
193 fn reset(&mut self) {
194 self.bars.clear();
195 self.alerts.clear();
196 }
197
198 fn on_bar(&mut self, bar: &Bar) -> Option<IndicatorOutput> {
199 self.bars.push_back(bar.clone());
200 if self.bars.len() > self.period {
201 self.bars.pop_front();
202 }
203
204 self.alerts.clear();
205 if self.bars.len() < self.period {
206 return None;
207 }
208
209 let upper = self.bars.iter().map(|b| b.high).fold(f64::MIN, f64::max);
210 let lower = self.bars.iter().map(|b| b.low).fold(f64::MAX, f64::min);
211 let basis = (upper + lower) / 2.0;
212
213 let mut extra = HashMap::new();
214 extra.insert("upper".to_string(), upper);
215 extra.insert("lower".to_string(), lower);
216 extra.insert("width".to_string(), upper - lower);
217
218 if (bar.high - upper).abs() < 1e-8 {
219 self.alerts.push(IndicatorAlert::new(
220 "donchian_new_high",
221 format!("{}-Period Donchian High: ${:.2}", self.period, upper),
222 0.85,
223 ));
224 } else if (bar.low - lower).abs() < 1e-8 {
225 self.alerts.push(IndicatorAlert::new(
226 "donchian_new_low",
227 format!("{}-Period Donchian Low: ${:.2}", self.period, lower),
228 0.85,
229 ));
230 }
231
232 Some(IndicatorOutput::with_extra(basis, extra))
233 }
234
235 fn alerts(&self) -> Vec<IndicatorAlert> {
236 self.alerts.clone()
237 }
238}
239
240pub struct HistoricalVolatilityEngine {
242 period: usize,
243 closes: VecDeque<f64>,
244 alerts: Vec<IndicatorAlert>,
245}
246
247impl HistoricalVolatilityEngine {
248 pub fn new(period: usize) -> Self {
249 Self {
250 period,
251 closes: VecDeque::new(),
252 alerts: Vec::new(),
253 }
254 }
255}
256
257impl Indicator for HistoricalVolatilityEngine {
258 fn name(&self) -> &str {
259 "historical_volatility"
260 }
261
262 fn warmup_period(&self) -> usize {
263 self.period + 1
264 }
265
266 fn reset(&mut self) {
267 self.closes.clear();
268 self.alerts.clear();
269 }
270
271 fn on_bar(&mut self, bar: &Bar) -> Option<IndicatorOutput> {
272 self.closes.push_back(bar.close);
273 if self.closes.len() > self.period + 1 {
274 self.closes.pop_front();
275 }
276
277 self.alerts.clear();
278 if self.closes.len() < self.period + 1 {
279 return None;
280 }
281
282 let mut log_returns = Vec::with_capacity(self.period);
283 for pair in self.closes.iter().collect::<Vec<_>>().windows(2) {
284 let prev = *pair[0];
285 let curr = *pair[1];
286 if prev > 0.0 && curr > 0.0 {
287 log_returns.push((curr / prev).ln());
288 } else {
289 log_returns.push(0.0);
290 }
291 }
292
293 let mean = log_returns.iter().sum::<f64>() / log_returns.len() as f64;
294 let variance = log_returns.iter().map(|r| (r - mean).powi(2)).sum::<f64>()
295 / (log_returns.len() as f64 - 1.0).max(1.0);
296 let daily_std_dev = variance.sqrt();
297 let annualized_hv = daily_std_dev * (252.0f64).sqrt() * 100.0; Some(IndicatorOutput::new(annualized_hv))
300 }
301
302 fn alerts(&self) -> Vec<IndicatorAlert> {
303 self.alerts.clone()
304 }
305}
306
307pub struct GarmanKlassVolatilityEngine {
309 period: usize,
310 bars: VecDeque<Bar>,
311 alerts: Vec<IndicatorAlert>,
312}
313
314impl GarmanKlassVolatilityEngine {
315 pub fn new(period: usize) -> Self {
316 Self {
317 period,
318 bars: VecDeque::new(),
319 alerts: Vec::new(),
320 }
321 }
322}
323
324impl Indicator for GarmanKlassVolatilityEngine {
325 fn name(&self) -> &str {
326 "garman_klass"
327 }
328
329 fn warmup_period(&self) -> usize {
330 self.period
331 }
332
333 fn reset(&mut self) {
334 self.bars.clear();
335 self.alerts.clear();
336 }
337
338 fn on_bar(&mut self, bar: &Bar) -> Option<IndicatorOutput> {
339 self.bars.push_back(bar.clone());
340 if self.bars.len() > self.period {
341 self.bars.pop_front();
342 }
343
344 self.alerts.clear();
345 if self.bars.len() < self.period {
346 return None;
347 }
348
349 let mut sum_var = 0.0f64;
350 for b in &self.bars {
351 if b.open > 0.0 && b.close > 0.0 && b.high > 0.0 && b.low > 0.0 {
352 let log_hl = (b.high / b.low).ln();
353 let log_co = (b.close / b.open).ln();
354 let bar_var = 0.5 * log_hl.powi(2) - (2.0 * (2.0f64).ln() - 1.0) * log_co.powi(2);
355 sum_var += bar_var.max(0.0);
356 }
357 }
358
359 let avg_var = sum_var / self.period as f64;
360 let annualized_gk = avg_var.sqrt() * (252.0f64).sqrt() * 100.0;
361
362 Some(IndicatorOutput::new(annualized_gk))
363 }
364
365 fn alerts(&self) -> Vec<IndicatorAlert> {
366 self.alerts.clone()
367 }
368}