1use std::collections::VecDeque;
4
5use crate::error::{Error, Result};
6use crate::ohlcv::Candle;
7use crate::traits::Indicator;
8
9#[derive(Debug, Clone)]
44pub struct ChaikinMoneyFlow {
45 period: usize,
46 mfv_window: VecDeque<f64>,
47 vol_window: VecDeque<f64>,
48 mfv_sum: f64,
49 vol_sum: f64,
50}
51
52impl ChaikinMoneyFlow {
53 pub fn new(period: usize) -> Result<Self> {
58 if period == 0 {
59 return Err(Error::PeriodZero);
60 }
61 Ok(Self {
62 period,
63 mfv_window: VecDeque::with_capacity(period),
64 vol_window: VecDeque::with_capacity(period),
65 mfv_sum: 0.0,
66 vol_sum: 0.0,
67 })
68 }
69
70 pub const fn period(&self) -> usize {
72 self.period
73 }
74}
75
76impl Indicator for ChaikinMoneyFlow {
77 type Input = Candle;
78 type Output = f64;
79
80 fn update(&mut self, candle: Candle) -> Option<f64> {
81 let range = candle.high - candle.low;
82 let mfv = if range == 0.0 {
83 0.0
85 } else {
86 let mfm = ((candle.close - candle.low) - (candle.high - candle.close)) / range;
87 mfm * candle.volume
88 };
89
90 if self.mfv_window.len() == self.period {
91 self.mfv_sum -= self.mfv_window.pop_front().expect("non-empty");
92 self.vol_sum -= self.vol_window.pop_front().expect("non-empty");
93 }
94 self.mfv_window.push_back(mfv);
95 self.vol_window.push_back(candle.volume);
96 self.mfv_sum += mfv;
97 self.vol_sum += candle.volume;
98
99 if self.mfv_window.len() < self.period {
100 return None;
101 }
102 if self.vol_sum == 0.0 {
103 return Some(0.0);
105 }
106 Some(self.mfv_sum / self.vol_sum)
107 }
108
109 fn reset(&mut self) {
110 self.mfv_window.clear();
111 self.vol_window.clear();
112 self.mfv_sum = 0.0;
113 self.vol_sum = 0.0;
114 }
115
116 fn warmup_period(&self) -> usize {
117 self.period
118 }
119
120 fn is_ready(&self) -> bool {
121 self.mfv_window.len() == self.period
122 }
123
124 fn name(&self) -> &'static str {
125 "CMF"
126 }
127}
128
129#[cfg(test)]
130mod tests {
131 use super::*;
132 use crate::traits::BatchExt;
133 use approx::assert_relative_eq;
134
135 fn candle(open: f64, high: f64, low: f64, close: f64, volume: f64, ts: i64) -> Candle {
136 Candle::new(open, high, low, close, volume, ts).unwrap()
137 }
138
139 #[test]
140 fn reference_values() {
141 let mut cmf = ChaikinMoneyFlow::new(2).unwrap();
145 let out = cmf.batch(&[
146 candle(8.0, 10.0, 8.0, 10.0, 100.0, 0),
147 candle(10.0, 12.0, 8.0, 10.0, 100.0, 1),
148 ]);
149 assert!(out[0].is_none());
150 assert_relative_eq!(out[1].unwrap(), 0.5, epsilon = 1e-12);
151 }
152
153 #[test]
154 fn stays_within_unit_range() {
155 let candles: Vec<Candle> = (0..120)
156 .map(|i| {
157 let mid = 100.0 + (i as f64 * 0.25).sin() * 10.0;
158 candle(
159 mid,
160 mid + 3.0,
161 mid - 3.0,
162 mid + (i as f64 * 0.5).cos() * 2.0,
163 10.0 + (i % 7) as f64,
164 i,
165 )
166 })
167 .collect();
168 let mut cmf = ChaikinMoneyFlow::new(20).unwrap();
169 for v in cmf.batch(&candles).into_iter().flatten() {
170 assert!((-1.0..=1.0).contains(&v), "CMF {v} outside [-1, 1]");
171 }
172 }
173
174 #[test]
175 fn closes_at_high_yield_cmf_one() {
176 let candles: Vec<Candle> = (0..30)
178 .map(|i| candle(9.0, 10.0, 8.0, 10.0, 50.0, i))
179 .collect();
180 let mut cmf = ChaikinMoneyFlow::new(14).unwrap();
181 for v in cmf.batch(&candles).into_iter().flatten() {
182 assert_relative_eq!(v, 1.0, epsilon = 1e-12);
183 }
184 }
185
186 #[test]
187 fn zero_volume_window_yields_zero() {
188 let candles: Vec<Candle> = (0..20)
190 .map(|i| candle(9.0, 10.0, 8.0, 10.0, 0.0, i))
191 .collect();
192 let mut cmf = ChaikinMoneyFlow::new(10).unwrap();
193 for v in cmf.batch(&candles).into_iter().flatten() {
194 assert_relative_eq!(v, 0.0, epsilon = 1e-12);
195 }
196 }
197
198 #[test]
199 fn first_value_on_period_th_candle() {
200 let candles: Vec<Candle> = (0..10)
201 .map(|i| candle(9.0, 10.0, 8.0, 9.5, 50.0, i))
202 .collect();
203 let mut cmf = ChaikinMoneyFlow::new(5).unwrap();
204 let out = cmf.batch(&candles);
205 for (i, v) in out.iter().enumerate().take(4) {
206 assert!(v.is_none(), "index {i} must be None during warmup");
207 }
208 assert!(out[4].is_some(), "first CMF lands at index period - 1");
209 assert_eq!(cmf.warmup_period(), 5);
210 }
211
212 #[test]
213 fn rejects_zero_period() {
214 assert!(matches!(ChaikinMoneyFlow::new(0), Err(Error::PeriodZero)));
215 }
216
217 #[test]
220 fn accessors_and_metadata() {
221 let cmf = ChaikinMoneyFlow::new(20).unwrap();
222 assert_eq!(cmf.period(), 20);
223 assert_eq!(cmf.name(), "CMF");
224 }
225
226 #[test]
230 fn zero_range_candle_contributes_zero_mfv() {
231 let mut cmf = ChaikinMoneyFlow::new(3).unwrap();
232 let candles: Vec<Candle> = (0..5)
233 .map(|i| Candle::new(10.0, 10.0, 10.0, 10.0, 5.0, i).unwrap())
234 .collect();
235 let last = cmf
236 .batch(&candles)
237 .into_iter()
238 .flatten()
239 .last()
240 .expect("emits");
241 assert_eq!(last, 0.0);
243 }
244
245 #[test]
246 fn reset_clears_state() {
247 let candles: Vec<Candle> = (0..20)
248 .map(|i| candle(9.0, 11.0, 8.0, 10.0, 50.0, i))
249 .collect();
250 let mut cmf = ChaikinMoneyFlow::new(10).unwrap();
251 cmf.batch(&candles);
252 assert!(cmf.is_ready());
253 cmf.reset();
254 assert!(!cmf.is_ready());
255 assert_eq!(cmf.update(candles[0]), None);
256 }
257
258 #[test]
259 fn batch_equals_streaming() {
260 let candles: Vec<Candle> = (0..80)
261 .map(|i| {
262 let mid = 100.0 + (i as f64 * 0.3).sin() * 8.0;
263 candle(
264 mid,
265 mid + 2.0,
266 mid - 2.0,
267 mid + 0.5,
268 10.0 + (i % 5) as f64,
269 i,
270 )
271 })
272 .collect();
273 let mut a = ChaikinMoneyFlow::new(20).unwrap();
274 let mut b = ChaikinMoneyFlow::new(20).unwrap();
275 assert_eq!(
276 a.batch(&candles),
277 candles.iter().map(|x| b.update(*x)).collect::<Vec<_>>()
278 );
279 }
280}