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)]
9pub struct CvdEngine {
10 cum_cvd: f64,
11}
12
13impl CvdEngine {
14 pub fn new() -> Self {
15 Self { cum_cvd: 0.0 }
16 }
17}
18
19impl Default for CvdEngine {
20 fn default() -> Self {
21 Self::new()
22 }
23}
24
25impl Indicator for CvdEngine {
26 fn name(&self) -> &str {
27 "cvd"
28 }
29
30 fn warmup_period(&self) -> usize {
31 1
32 }
33
34 fn reset(&mut self) {
35 self.cum_cvd = 0.0;
36 }
37
38 fn on_bar(&mut self, bar: &Bar) -> Option<IndicatorOutput> {
39 let range = (bar.high - bar.low).max(1e-8);
40 let buy_pct = (bar.close - bar.low) / range;
42 let buy_vol = bar.volume * buy_pct;
43 let sell_vol = bar.volume * (1.0 - buy_pct);
44
45 let delta = buy_vol - sell_vol;
46 self.cum_cvd += delta;
47
48 let mut extra = HashMap::new();
49 extra.insert("delta".to_string(), delta);
50 extra.insert("buy_volume".to_string(), buy_vol);
51 extra.insert("sell_volume".to_string(), sell_vol);
52
53 Some(IndicatorOutput::with_extra(self.cum_cvd, extra))
54 }
55
56 fn alerts(&self) -> Vec<IndicatorAlert> {
57 Vec::new()
58 }
59}
60
61#[derive(Debug, Clone)]
63pub struct KlingerVolumeForceEngine {
64 fast_len: usize,
65 slow_len: usize,
66 signal_len: usize,
67 fast_ema: Ema,
68 slow_ema: Ema,
69 signal_ema: Ema,
70 prev_hlc_sum: Option<f64>,
71 prev_trend: f64,
72 prev_dm: f64,
73 cumulative_measurement: f64,
74 count: usize,
75}
76
77impl KlingerVolumeForceEngine {
78 pub fn new(fast_len: usize, slow_len: usize, signal_len: usize) -> Self {
79 Self {
80 fast_len,
81 slow_len,
82 signal_len,
83 fast_ema: Ema::new(fast_len),
84 slow_ema: Ema::new(slow_len),
85 signal_ema: Ema::new(signal_len),
86 prev_hlc_sum: None,
87 prev_trend: 1.0,
88 prev_dm: 0.0,
89 cumulative_measurement: 0.0,
90 count: 0,
91 }
92 }
93
94 pub fn with_defaults() -> Self {
95 Self::new(34, 55, 13)
96 }
97
98 pub fn fast_len(&self) -> usize {
99 self.fast_len
100 }
101}
102
103impl Indicator for KlingerVolumeForceEngine {
104 fn name(&self) -> &str {
105 "klinger"
106 }
107
108 fn warmup_period(&self) -> usize {
109 self.slow_len + self.signal_len
110 }
111
112 fn reset(&mut self) {
113 self.fast_ema.reset();
114 self.slow_ema.reset();
115 self.signal_ema.reset();
116 self.prev_hlc_sum = None;
117 self.prev_trend = 1.0;
118 self.prev_dm = 0.0;
119 self.cumulative_measurement = 0.0;
120 self.count = 0;
121 }
122
123 fn on_bar(&mut self, bar: &Bar) -> Option<IndicatorOutput> {
124 self.count += 1;
125 let hlc_sum = bar.high + bar.low + bar.close;
126 let trend = match self.prev_hlc_sum {
127 Some(previous) => {
128 if hlc_sum > previous {
129 1.0
130 } else if hlc_sum < previous {
131 -1.0
132 } else {
133 self.prev_trend
134 }
135 }
136 None => 1.0,
137 };
138 let dm = bar.high - bar.low;
139 self.cumulative_measurement = if self.prev_hlc_sum.is_none() {
140 dm
141 } else if trend == self.prev_trend {
142 self.cumulative_measurement + dm
143 } else {
144 self.prev_dm + dm
145 };
146 let ratio = if self.cumulative_measurement.abs() > f64::EPSILON {
147 dm / self.cumulative_measurement
148 } else {
149 0.0
150 };
151 let vf = bar.volume * (2.0 * ratio - 1.0).abs() * trend * 100.0;
152 self.prev_hlc_sum = Some(hlc_sum);
153 self.prev_trend = trend;
154 self.prev_dm = dm;
155
156 let fast_v = self.fast_ema.update(vf);
157 let slow_v = self.slow_ema.update(vf);
158
159 if self.count < self.slow_len {
160 return None;
161 }
162
163 let kvo = fast_v - slow_v;
164 let sig = self.signal_ema.update(kvo);
165
166 let mut extra = HashMap::new();
167 extra.insert("volume_force".to_string(), vf);
168 extra.insert("kvo".to_string(), kvo);
169 extra.insert("signal".to_string(), sig);
170 extra.insert("hist".to_string(), kvo - sig);
171
172 Some(IndicatorOutput::with_extra(kvo, extra))
173 }
174
175 fn alerts(&self) -> Vec<IndicatorAlert> {
176 Vec::new()
177 }
178}
179
180#[cfg(test)]
181mod tests {
182 use super::*;
183
184 #[test]
185 fn test_cvd_accumulation() {
186 let mut cvd = CvdEngine::new();
187 let bar1 = Bar::new(1, 100.0, 105.0, 95.0, 105.0, 1000.0); let out1 = cvd.on_bar(&bar1).unwrap();
189 assert_eq!(out1.value, 1000.0);
190 }
191
192 #[test]
193 fn klinger_matches_original_volume_force_formula() {
194 let mut klinger = KlingerVolumeForceEngine::new(1, 2, 2);
195 let first = Bar::new(1, 1.0, 3.0, 1.0, 2.0, 100.0);
196 assert!(klinger.on_bar(&first).is_none());
197
198 let second = Bar::new(2, 2.0, 3.0, 1.0, 2.0, 100.0);
199 let output = klinger.on_bar(&second).unwrap();
200 assert_eq!(output.extra["volume_force"], 0.0);
201 assert!((output.value + 3_333.333_333_333_333).abs() < 1e-9);
202 }
203}