Skip to main content

kestrel_chartkit/indicator/
volume_indicators.rs

1use std::collections::{HashMap, VecDeque};
2
3use crate::indicator::{Indicator, IndicatorAlert, IndicatorOutput};
4use crate::model::Bar;
5
6/// Volume & Average Volume Indicator.
7pub struct VolumeEngine {
8    ma_period: usize,
9    volumes: VecDeque<f64>,
10    alerts: Vec<IndicatorAlert>,
11}
12
13impl VolumeEngine {
14    pub fn new(ma_period: usize) -> Self {
15        Self {
16            ma_period,
17            volumes: VecDeque::new(),
18            alerts: Vec::new(),
19        }
20    }
21}
22
23impl Indicator for VolumeEngine {
24    fn name(&self) -> &str {
25        "volume"
26    }
27
28    fn warmup_period(&self) -> usize {
29        self.ma_period
30    }
31
32    fn reset(&mut self) {
33        self.volumes.clear();
34        self.alerts.clear();
35    }
36
37    fn on_bar(&mut self, bar: &Bar) -> Option<IndicatorOutput> {
38        let vol = bar.volume;
39        self.volumes.push_back(vol);
40        if self.volumes.len() > self.ma_period {
41            self.volumes.pop_front();
42        }
43
44        self.alerts.clear();
45        if self.volumes.len() < self.ma_period {
46            return Some(IndicatorOutput::new(vol));
47        }
48
49        let avg_vol: f64 = self.volumes.iter().sum::<f64>() / self.ma_period as f64;
50        let mut extra = HashMap::new();
51        extra.insert("avg_volume".to_string(), avg_vol);
52        extra.insert(
53            "volume_ratio".to_string(),
54            if avg_vol > 0.0 { vol / avg_vol } else { 1.0 },
55        );
56
57        if avg_vol > 0.0 && vol > 2.0 * avg_vol {
58            self.alerts.push(IndicatorAlert::new(
59                "high_volume_spike",
60                format!("High Volume Spike: {:.0} (>2.0x avg {:.0})", vol, avg_vol),
61                0.80,
62            ));
63        }
64
65        Some(IndicatorOutput::with_extra(vol, extra))
66    }
67
68    fn alerts(&self) -> Vec<IndicatorAlert> {
69        self.alerts.clone()
70    }
71}
72
73/// Relative Volume (RVOL) Indicator.
74#[derive(Debug, Clone)]
75pub struct RvolEngine {
76    period: usize,
77    volumes: VecDeque<f64>,
78    alerts: Vec<IndicatorAlert>,
79}
80
81impl RvolEngine {
82    pub fn new(period: usize) -> Self {
83        Self {
84            period,
85            volumes: VecDeque::new(),
86            alerts: Vec::new(),
87        }
88    }
89}
90
91impl Indicator for RvolEngine {
92    fn name(&self) -> &str {
93        "rvol"
94    }
95
96    fn warmup_period(&self) -> usize {
97        self.period
98    }
99
100    fn reset(&mut self) {
101        self.volumes.clear();
102        self.alerts.clear();
103    }
104
105    fn on_bar(&mut self, bar: &Bar) -> Option<IndicatorOutput> {
106        let vol = bar.volume;
107        self.volumes.push_back(vol);
108        if self.volumes.len() > self.period {
109            self.volumes.pop_front();
110        }
111
112        self.alerts.clear();
113        if self.volumes.len() < self.period {
114            return None;
115        }
116
117        let avg_vol: f64 = self.volumes.iter().sum::<f64>() / self.period as f64;
118        let rvol = if avg_vol > 0.0 { vol / avg_vol } else { 1.0 };
119
120        if rvol >= 2.5 {
121            self.alerts.push(IndicatorAlert::new(
122                "extreme_rvol",
123                format!("Extreme Relative Volume: {:.2}x", rvol),
124                0.90,
125            ));
126        }
127
128        Some(IndicatorOutput::new(rvol))
129    }
130
131    fn alerts(&self) -> Vec<IndicatorAlert> {
132        self.alerts.clone()
133    }
134}
135
136/// On-Balance Volume (OBV) Indicator.
137pub struct ObvEngine {
138    prev_close: Option<f64>,
139    cum_obv: f64,
140    alerts: Vec<IndicatorAlert>,
141}
142
143impl ObvEngine {
144    pub fn new() -> Self {
145        Self {
146            prev_close: None,
147            cum_obv: 0.0,
148            alerts: Vec::new(),
149        }
150    }
151}
152
153impl Default for ObvEngine {
154    fn default() -> Self {
155        Self::new()
156    }
157}
158
159impl Indicator for ObvEngine {
160    fn name(&self) -> &str {
161        "obv"
162    }
163
164    fn warmup_period(&self) -> usize {
165        1
166    }
167
168    fn reset(&mut self) {
169        self.prev_close = None;
170        self.cum_obv = 0.0;
171        self.alerts.clear();
172    }
173
174    fn on_bar(&mut self, bar: &Bar) -> Option<IndicatorOutput> {
175        if let Some(prev) = self.prev_close {
176            if bar.close > prev {
177                self.cum_obv += bar.volume;
178            } else if bar.close < prev {
179                self.cum_obv -= bar.volume;
180            }
181        }
182        self.prev_close = Some(bar.close);
183
184        Some(IndicatorOutput::new(self.cum_obv))
185    }
186
187    fn alerts(&self) -> Vec<IndicatorAlert> {
188        self.alerts.clone()
189    }
190}
191
192/// Chaikin Money Flow (CMF) Indicator.
193pub struct CmfEngine {
194    period: usize,
195    mf_volumes: VecDeque<f64>,
196    volumes: VecDeque<f64>,
197    alerts: Vec<IndicatorAlert>,
198}
199
200impl CmfEngine {
201    pub fn new(period: usize) -> Self {
202        Self {
203            period,
204            mf_volumes: VecDeque::new(),
205            volumes: VecDeque::new(),
206            alerts: Vec::new(),
207        }
208    }
209}
210
211impl Indicator for CmfEngine {
212    fn name(&self) -> &str {
213        "cmf"
214    }
215
216    fn warmup_period(&self) -> usize {
217        self.period
218    }
219
220    fn reset(&mut self) {
221        self.mf_volumes.clear();
222        self.volumes.clear();
223        self.alerts.clear();
224    }
225
226    fn on_bar(&mut self, bar: &Bar) -> Option<IndicatorOutput> {
227        let high_low = bar.high - bar.low;
228        let mfm = if high_low > 1e-8 {
229            ((bar.close - bar.low) - (bar.high - bar.close)) / high_low
230        } else {
231            0.0
232        };
233        let mfv = mfm * bar.volume;
234
235        self.mf_volumes.push_back(mfv);
236        self.volumes.push_back(bar.volume);
237
238        if self.mf_volumes.len() > self.period {
239            self.mf_volumes.pop_front();
240            self.volumes.pop_front();
241        }
242
243        self.alerts.clear();
244        if self.mf_volumes.len() < self.period {
245            return None;
246        }
247
248        let sum_mfv: f64 = self.mf_volumes.iter().sum();
249        let sum_vol: f64 = self.volumes.iter().sum();
250
251        let cmf = if sum_vol > 0.0 {
252            sum_mfv / sum_vol
253        } else {
254            0.0
255        };
256
257        if cmf > 0.20 {
258            self.alerts.push(IndicatorAlert::new(
259                "cmf_bullish",
260                format!("Strong Buying Pressure (CMF: {:.2})", cmf),
261                0.80,
262            ));
263        } else if cmf < -0.20 {
264            self.alerts.push(IndicatorAlert::new(
265                "cmf_bearish",
266                format!("Strong Selling Pressure (CMF: {:.2})", cmf),
267                0.80,
268            ));
269        }
270
271        Some(IndicatorOutput::new(cmf.clamp(-1.0, 1.0)))
272    }
273
274    fn alerts(&self) -> Vec<IndicatorAlert> {
275        self.alerts.clone()
276    }
277}
278
279/// Accumulation / Distribution Line (A/D) Indicator.
280#[derive(Debug, Clone)]
281pub struct AccDistEngine {
282    cum_ad: f64,
283    alerts: Vec<IndicatorAlert>,
284}
285
286impl AccDistEngine {
287    pub fn new() -> Self {
288        Self {
289            cum_ad: 0.0,
290            alerts: Vec::new(),
291        }
292    }
293}
294
295impl Default for AccDistEngine {
296    fn default() -> Self {
297        Self::new()
298    }
299}
300
301impl Indicator for AccDistEngine {
302    fn name(&self) -> &str {
303        "acc_dist"
304    }
305
306    fn warmup_period(&self) -> usize {
307        1
308    }
309
310    fn reset(&mut self) {
311        self.cum_ad = 0.0;
312        self.alerts.clear();
313    }
314
315    fn on_bar(&mut self, bar: &Bar) -> Option<IndicatorOutput> {
316        let high_low = bar.high - bar.low;
317        let mfm = if high_low > 1e-8 {
318            ((bar.close - bar.low) - (bar.high - bar.close)) / high_low
319        } else {
320            0.0
321        };
322        let mfv = mfm * bar.volume;
323        self.cum_ad += mfv;
324
325        Some(IndicatorOutput::new(self.cum_ad))
326    }
327
328    fn alerts(&self) -> Vec<IndicatorAlert> {
329        self.alerts.clone()
330    }
331}