Skip to main content

kestrel_chartkit/indicator/
vix_fix.rs

1use std::collections::HashMap;
2
3use crate::indicator::smoothing::{ExtremeWindow, Sma};
4use crate::indicator::{Indicator, IndicatorAlert, IndicatorOutput};
5use crate::model::Bar;
6
7/// Williams VIX Fix Advanced indicator.
8/// Measures market synthetic fear/volatility spikes to identify market bottoms.
9pub struct WilliamsVixFix {
10    #[allow(dead_code)]
11    pd: usize,
12    bband_len: usize,
13    mult: f64,
14
15    close_window: ExtremeWindow,
16    wvf_window: Vec<f64>,
17    sma: Sma,
18    alerts: Vec<IndicatorAlert>,
19}
20
21impl WilliamsVixFix {
22    pub fn new(pd: usize, bband_len: usize, mult: f64) -> Self {
23        Self {
24            pd,
25            bband_len,
26            mult,
27            close_window: ExtremeWindow::new(pd),
28            wvf_window: Vec::with_capacity(bband_len),
29            sma: Sma::new(bband_len),
30            alerts: Vec::new(),
31        }
32    }
33}
34
35impl Indicator for WilliamsVixFix {
36    fn name(&self) -> &str {
37        "vix_fix"
38    }
39
40    fn reset(&mut self) {
41        self.close_window.reset();
42        self.wvf_window.clear();
43        self.sma.reset();
44        self.alerts.clear();
45    }
46
47    fn on_bar(&mut self, bar: &Bar) -> Option<IndicatorOutput> {
48        let highest_close = self
49            .close_window
50            .push(bar.close)
51            .map(|(_, high)| high)
52            .unwrap_or(bar.close);
53        self.alerts.clear();
54
55        let wvf = if highest_close > 0.0 {
56            ((highest_close - bar.low) / highest_close) * 100.0
57        } else {
58            0.0
59        };
60
61        let sma_val = self.sma.update(wvf)?;
62
63        self.wvf_window.push(wvf);
64        if self.wvf_window.len() > self.bband_len {
65            self.wvf_window.remove(0);
66        }
67
68        if self.wvf_window.len() < self.bband_len {
69            return None;
70        }
71
72        let mean = sma_val;
73        let variance = self
74            .wvf_window
75            .iter()
76            .map(|v| (v - mean).powi(2))
77            .sum::<f64>()
78            / self.bband_len as f64;
79        let std_dev = variance.sqrt();
80
81        let upper_band = mean + self.mult * std_dev;
82        let is_spike = wvf >= upper_band;
83
84        if is_spike {
85            self.alerts.push(IndicatorAlert::new(
86                "panic_bottom",
87                format!(
88                    "Williams VIX Fix Spike ({:.2} >= Upper Band {:.2}) - Market Bottom Zone",
89                    wvf, upper_band
90                ),
91                1.0,
92            ));
93        }
94
95        Some(IndicatorOutput::new(wvf).with_secondary(upper_band))
96    }
97
98    fn alerts(&self) -> Vec<IndicatorAlert> {
99        self.alerts.clone()
100    }
101}
102
103pub fn build_vix_fix(params: &HashMap<String, f64>) -> WilliamsVixFix {
104    let pd = params.get("pd").copied().unwrap_or(22.0) as usize;
105    let bband_len = params.get("bband_len").copied().unwrap_or(20.0) as usize;
106    let mult = params.get("mult").copied().unwrap_or(2.0);
107    WilliamsVixFix::new(pd, bband_len, mult)
108}