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> {
68 self.check_len(candles)?;
69
70 let n = candles.len();
71 let p = self.params.period;
72
73 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 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}