kestrel_chartkit/indicator/
volume_flow.rs1use super::smoothing::Ema;
2use super::{Indicator, IndicatorAlert, IndicatorOutput};
3use crate::model::Bar;
4use std::collections::HashMap;
5
6#[derive(Debug, Clone)]
21pub struct CvdEngine {
22 cum_cvd: f64,
23}
24
25impl CvdEngine {
26 pub fn new() -> Self {
27 Self { cum_cvd: 0.0 }
28 }
29}
30
31impl Default for CvdEngine {
32 fn default() -> Self {
33 Self::new()
34 }
35}
36
37impl Indicator for CvdEngine {
38 fn name(&self) -> &str {
39 "cvd"
40 }
41
42 fn warmup_period(&self) -> usize {
43 1
44 }
45
46 fn reset(&mut self) {
47 self.cum_cvd = 0.0;
48 }
49
50 fn on_bar(&mut self, bar: &Bar) -> Option<IndicatorOutput> {
51 let range = (bar.high - bar.low).max(1e-8);
52 let buy_pct = (bar.close - bar.low) / range;
54 let buy_vol = bar.volume * buy_pct;
55 let sell_vol = bar.volume * (1.0 - buy_pct);
56
57 let delta = buy_vol - sell_vol;
58 self.cum_cvd += delta;
59
60 let mut extra = HashMap::new();
61 extra.insert("delta".to_string(), delta);
62 extra.insert("buy_volume".to_string(), buy_vol);
63 extra.insert("sell_volume".to_string(), sell_vol);
64
65 Some(IndicatorOutput::with_extra(self.cum_cvd, extra))
66 }
67
68 fn alerts(&self) -> Vec<IndicatorAlert> {
69 Vec::new()
70 }
71}
72
73#[derive(Debug, Clone)]
93pub struct KlingerVolumeForceEngine {
94 fast_len: usize,
95 slow_len: usize,
96 signal_len: usize,
97 fast_ema: Ema,
98 slow_ema: Ema,
99 signal_ema: Ema,
100 prev_hlc_sum: Option<f64>,
101 prev_trend: f64,
102 prev_dm: f64,
103 cumulative_measurement: f64,
104 count: usize,
105}
106
107impl KlingerVolumeForceEngine {
108 pub fn new(fast_len: usize, slow_len: usize, signal_len: usize) -> Self {
109 Self {
110 fast_len,
111 slow_len,
112 signal_len,
113 fast_ema: Ema::new(fast_len),
114 slow_ema: Ema::new(slow_len),
115 signal_ema: Ema::new(signal_len),
116 prev_hlc_sum: None,
117 prev_trend: 1.0,
118 prev_dm: 0.0,
119 cumulative_measurement: 0.0,
120 count: 0,
121 }
122 }
123
124 pub fn with_defaults() -> Self {
125 Self::new(34, 55, 13)
126 }
127
128 pub fn fast_len(&self) -> usize {
129 self.fast_len
130 }
131}
132
133impl Indicator for KlingerVolumeForceEngine {
134 fn name(&self) -> &str {
135 "klinger"
136 }
137
138 fn warmup_period(&self) -> usize {
139 self.slow_len + self.signal_len
140 }
141
142 fn reset(&mut self) {
143 self.fast_ema.reset();
144 self.slow_ema.reset();
145 self.signal_ema.reset();
146 self.prev_hlc_sum = None;
147 self.prev_trend = 1.0;
148 self.prev_dm = 0.0;
149 self.cumulative_measurement = 0.0;
150 self.count = 0;
151 }
152
153 fn on_bar(&mut self, bar: &Bar) -> Option<IndicatorOutput> {
154 self.count += 1;
155 let hlc_sum = bar.high + bar.low + bar.close;
156 let trend = match self.prev_hlc_sum {
157 Some(previous) => {
158 if hlc_sum > previous {
159 1.0
160 } else if hlc_sum < previous {
161 -1.0
162 } else {
163 self.prev_trend
164 }
165 }
166 None => 1.0,
167 };
168 let dm = bar.high - bar.low;
169 self.cumulative_measurement = if self.prev_hlc_sum.is_none() {
170 dm
171 } else if trend == self.prev_trend {
172 self.cumulative_measurement + dm
173 } else {
174 self.prev_dm + dm
175 };
176 let ratio = if self.cumulative_measurement.abs() > f64::EPSILON {
177 dm / self.cumulative_measurement
178 } else {
179 0.0
180 };
181 let vf = bar.volume * (2.0 * ratio - 1.0).abs() * trend * 100.0;
182 self.prev_hlc_sum = Some(hlc_sum);
183 self.prev_trend = trend;
184 self.prev_dm = dm;
185
186 let fast_v = self.fast_ema.update(vf)?;
187 let slow_v = self.slow_ema.update(vf)?;
188
189 if self.count < self.slow_len {
190 return None;
191 }
192
193 let kvo = fast_v - slow_v;
194 let sig = self.signal_ema.update(kvo)?;
195
196 let mut extra = HashMap::new();
197 extra.insert("volume_force".to_string(), vf);
198 extra.insert("kvo".to_string(), kvo);
199 extra.insert("signal".to_string(), sig);
200 extra.insert("hist".to_string(), kvo - sig);
201
202 Some(IndicatorOutput::with_extra(kvo, extra))
203 }
204
205 fn alerts(&self) -> Vec<IndicatorAlert> {
206 Vec::new()
207 }
208}
209
210#[cfg(test)]
211mod tests {
212 use super::*;
213
214 #[test]
215 fn test_cvd_accumulation() {
216 let mut cvd = CvdEngine::new();
217 let bar1 = Bar::new(1, 100.0, 105.0, 95.0, 105.0, 1000.0); let out1 = cvd.on_bar(&bar1).unwrap();
219 assert_eq!(out1.value, 1000.0);
220 }
221
222 #[test]
223 fn klinger_matches_original_volume_force_formula() {
224 let mut klinger = KlingerVolumeForceEngine::new(1, 2, 2);
225 let first = Bar::new(1, 1.0, 3.0, 1.0, 2.0, 100.0);
226 assert!(klinger.on_bar(&first).is_none());
227
228 let second = Bar::new(2, 2.0, 3.0, 1.0, 2.0, 100.0);
229 let output = klinger.on_bar(&second).unwrap();
230 assert_eq!(output.extra["volume_force"], 0.0);
231 assert!((output.value + 3_333.333_333_333_333).abs() < 1e-9);
232 }
233}