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