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