Skip to main content

indicators/volume/
chaikin_money_flow.rs

1//! Chaikin Money Flow (CMF).
2//!
3//! Python source: `indicators/other/chaikin_money_flow.py :: class ChaikinMoneyFlow`
4//!
5//! # Python algorithm (to port)
6//! ```python
7//! high_low_range       = (df["High"] - df["Low"]).replace(0, np.nan)
8//! money_flow_mult      = ((df["Close"] - df["Low"]) - (df["High"] - df["Close"])) / high_low_range
9//! money_flow_volume    = money_flow_mult * df["Volume"]
10//! sum_mfv              = money_flow_volume.rolling(window=self.period).sum()
11//! sum_vol              = df["Volume"].rolling(window=self.period).sum().replace(0, np.nan)
12//! cmf                  = sum_mfv / sum_vol
13//! ```
14//!
15//! Values above +0.20 → strong buying; below -0.20 → strong selling.
16//! Oscillates between -1 and +1.
17//!
18//! Output column: `"CMF_{period}"`.
19
20use std::collections::HashMap;
21
22use crate::error::IndicatorError;
23use crate::indicator::{Indicator, IndicatorOutput};
24use crate::registry::param_usize;
25use crate::types::Candle;
26
27#[derive(Debug, Clone)]
28pub struct CmfParams {
29    /// Rolling period.  Python default: 20.
30    pub period: usize,
31}
32impl Default for CmfParams {
33    fn default() -> Self {
34        Self { period: 20 }
35    }
36}
37
38#[derive(Debug, Clone)]
39pub struct ChaikinMoneyFlow {
40    pub params: CmfParams,
41}
42
43impl ChaikinMoneyFlow {
44    pub fn new(params: CmfParams) -> Self {
45        Self { params }
46    }
47    pub fn with_period(period: usize) -> Self {
48        Self::new(CmfParams { period })
49    }
50    fn output_key(&self) -> String {
51        format!("CMF_{}", self.params.period)
52    }
53}
54
55impl Indicator for ChaikinMoneyFlow {
56    fn name(&self) -> &'static str {
57        "ChaikinMoneyFlow"
58    }
59    fn required_len(&self) -> usize {
60        self.params.period
61    }
62    fn required_columns(&self) -> &[&'static str] {
63        &["high", "low", "close", "volume"]
64    }
65
66    /// TODO: port Python MFM * Volume rolling-sum CMF.
67    fn calculate(&self, candles: &[Candle]) -> Result<IndicatorOutput, IndicatorError> {
68        self.check_len(candles)?;
69
70        let n = candles.len();
71        let p = self.params.period;
72
73        // Money flow multiplier and volume per bar.
74        let mfv: Vec<f64> = candles
75            .iter()
76            .map(|c| {
77                let range = c.high - c.low;
78                let mfm = if range == 0.0 {
79                    0.0
80                } else {
81                    ((c.close - c.low) - (c.high - c.close)) / range
82                };
83                mfm * c.volume
84            })
85            .collect();
86        let vol: Vec<f64> = candles.iter().map(|c| c.volume).collect();
87
88        let mut values = vec![f64::NAN; n];
89        // TODO: port Python rolling sum.
90        for i in (p - 1)..n {
91            let sum_mfv: f64 = mfv[(i + 1 - p)..=i].iter().sum();
92            let sum_vol: f64 = vol[(i + 1 - p)..=i].iter().sum();
93            values[i] = if sum_vol == 0.0 {
94                f64::NAN
95            } else {
96                sum_mfv / sum_vol
97            };
98        }
99
100        Ok(IndicatorOutput::from_pairs([(self.output_key(), values)]))
101    }
102}
103
104pub fn factory<S: ::std::hash::BuildHasher>(params: &HashMap<String, String, S>) -> Result<Box<dyn Indicator>, IndicatorError> {
105    Ok(Box::new(ChaikinMoneyFlow::new(CmfParams {
106        period: param_usize(params, "period", 20)?,
107    })))
108}
109
110#[cfg(test)]
111mod tests {
112    use super::*;
113
114    fn candles(n: usize) -> Vec<Candle> {
115        (0..n)
116            .map(|i| Candle {
117                time: i64::try_from(i).expect("time index fits i64"),
118                open: 10.0,
119                high: 12.0,
120                low: 8.0,
121                close: 11.0,
122                volume: 100.0,
123            })
124            .collect()
125    }
126
127    #[test]
128    fn cmf_output_column() {
129        let out = ChaikinMoneyFlow::with_period(20)
130            .calculate(&candles(25))
131            .unwrap();
132        assert!(out.get("CMF_20").is_some());
133    }
134
135    #[test]
136    fn cmf_range_neg1_to_pos1() {
137        let out = ChaikinMoneyFlow::with_period(5)
138            .calculate(&candles(10))
139            .unwrap();
140        for &v in out.get("CMF_5").unwrap() {
141            if !v.is_nan() {
142                assert!((-1.0..=1.0).contains(&v), "out of range: {v}");
143            }
144        }
145    }
146
147    #[test]
148    fn factory_creates_cmf() {
149        assert_eq!(factory(&HashMap::new()).unwrap().name(), "ChaikinMoneyFlow");
150    }
151}