kestrel_chartkit/indicator/
volume_indicators.rs1use std::collections::{HashMap, VecDeque};
2
3use crate::indicator::{Indicator, IndicatorAlert, IndicatorOutput};
4use crate::model::Bar;
5
6pub 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#[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
136pub 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
192pub 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#[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}