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) -> &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.iter().map(|c| {
75            let range = c.high - c.low;
76            let mfm = if range == 0.0 {
77                0.0
78            } else {
79                ((c.close - c.low) - (c.high - c.close)) / range
80            };
81            mfm * c.volume
82        }).collect();
83        let vol: Vec<f64> = candles.iter().map(|c| c.volume).collect();
84
85        let mut values = vec![f64::NAN; n];
86        // TODO: port Python rolling sum.
87        for i in (p - 1)..n {
88            let sum_mfv: f64 = mfv[(i + 1 - p)..=i].iter().sum();
89            let sum_vol: f64 = vol[(i + 1 - p)..=i].iter().sum();
90            values[i] = if sum_vol == 0.0 {
91                f64::NAN
92            } else {
93                sum_mfv / sum_vol
94            };
95        }
96
97        Ok(IndicatorOutput::from_pairs([(self.output_key(), values)]))
98    }
99}
100
101pub fn factory(params: &HashMap<String, String>) -> Result<Box<dyn Indicator>, IndicatorError> {
102    Ok(Box::new(ChaikinMoneyFlow::new(CmfParams {
103        period: param_usize(params, "period", 20)?,
104    })))
105}
106
107#[cfg(test)]
108mod tests {
109    use super::*;
110
111    fn candles(n: usize) -> Vec<Candle> {
112        (0..n).map(|i| Candle {
113            time: i as i64, open: 10.0, high: 12.0, low: 8.0, close: 11.0, volume: 100.0,
114        }).collect()
115    }
116
117    #[test]
118    fn cmf_output_column() {
119        let out = ChaikinMoneyFlow::with_period(20).calculate(&candles(25)).unwrap();
120        assert!(out.get("CMF_20").is_some());
121    }
122
123    #[test]
124    fn cmf_range_neg1_to_pos1() {
125        let out = ChaikinMoneyFlow::with_period(5).calculate(&candles(10)).unwrap();
126        for &v in out.get("CMF_5").unwrap() {
127            if !v.is_nan() {
128                assert!(v >= -1.0 && v <= 1.0, "out of range: {v}");
129            }
130        }
131    }
132
133    #[test]
134    fn factory_creates_cmf() {
135        assert_eq!(factory(&HashMap::new()).unwrap().name(), "ChaikinMoneyFlow");
136    }
137}