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