indicators/volume/
chaikin_money_flow.rs1use 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 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 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 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 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}