Skip to main content

egui_charts/studies/builtin/
mfi.rs

1//! Money Flow Index (MFI) Indicator
2//!
3//! MFI is a volume-weighted RSI, measuring buying and selling pressure.
4//! It oscillates between 0 and 100, similar to RSI.
5//!
6//! # Interpretation
7//! - MFI > 80: Overbought (potential reversal down)
8//! - MFI < 20: Oversold (potential reversal up)
9//! - Divergence between MFI and price can signal trend changes
10//!
11//! # Calculation
12//! Raw Money Flow = |Typical Price| * Volume, assigned to positive or negative flow by
13//! the change in typical price; an unchanged typical price contributes to neither.
14//! MFI = 100 * Positive Money Flow / (Positive Money Flow + Negative Money Flow).
15//! A window with no flow gives 50, only positive flow gives 100 and only negative flow gives 0.
16//!
17//! # Example
18//! ```
19//! # let bars: Vec<egui_charts::model::Bar> = (0..60).map(|i| { let p = 100.0 + (i as f64 * 0.3).sin() * 5.0; egui_charts::model::Bar::new(chrono::Utc::now(), p, p + 1.0, p - 1.0, p + 0.5, 1_000.0) }).collect();
20//! use egui_charts::studies::{MoneyFlowIndex, Indicator};
21//!
22//! let mut mfi = MoneyFlowIndex::new(14);
23//! mfi.calculate(&bars);
24//! ```
25
26use super::over_finite_bars;
27use crate::model::Bar;
28use crate::studies::palette;
29use crate::studies::stepper::{update_window, usable_behind};
30use crate::studies::{Indicator, IndicatorValue, IndicatorValues};
31use egui::Color32;
32
33/// Money Flow Index indicator
34#[derive(Clone)]
35pub struct MoneyFlowIndex {
36    period: usize,
37    values: IndicatorValues,
38    color: Color32,
39    visible: bool,
40}
41
42impl MoneyFlowIndex {
43    /// Create a new MFI indicator
44    ///
45    /// # Arguments
46    /// * `period` - Lookback period (typically 14)
47    pub fn new(period: usize) -> Self {
48        Self {
49            period: period.max(1),
50            values: IndicatorValues::new(),
51            color: palette::PURPLE,
52            visible: true,
53        }
54    }
55
56    /// Set the indicator color
57    pub fn with_color(mut self, color: Color32) -> Self {
58        self.color = color;
59        self
60    }
61}
62
63/// Construct with the conventional default parameters.
64impl Default for MoneyFlowIndex {
65    fn default() -> Self {
66        Self::new(14)
67    }
68}
69
70impl MoneyFlowIndex {
71    /// The values over `data`, every bar of which has finite inputs.
72    fn compute(&self, data: &[Bar]) -> IndicatorValues {
73        let mut values = IndicatorValues::new();
74        if data.len() < self.period.saturating_add(1) {
75            for _ in 0..data.len() {
76                values.push(IndicatorValue::None);
77            }
78            return values;
79        }
80
81        // Calculate typical prices and raw money flow
82        let typical_prices: Vec<f64> = data.iter().map(Bar::typical_price).collect();
83        let raw_money_flows: Vec<f64> = data
84            .iter()
85            .zip(typical_prices.iter())
86            .map(|(bar, tp)| tp.abs() * bar.volume)
87            .collect();
88
89        // Determine positive/negative money flow
90        let mut positive_flows = vec![0.0];
91        let mut negative_flows = vec![0.0];
92
93        for i in 1..data.len() {
94            if typical_prices[i] > typical_prices[i - 1] {
95                positive_flows.push(raw_money_flows[i]);
96                negative_flows.push(0.0);
97            } else if typical_prices[i] < typical_prices[i - 1] {
98                positive_flows.push(0.0);
99                negative_flows.push(raw_money_flows[i]);
100            } else {
101                positive_flows.push(0.0);
102                negative_flows.push(0.0);
103            }
104        }
105
106        // Calculate MFI for each bar
107        for i in 0..data.len() {
108            if i < self.period {
109                values.push(IndicatorValue::None);
110            } else {
111                let start = i + 1 - self.period;
112                let pos_sum: f64 = positive_flows[start..=i].iter().sum();
113                let neg_sum: f64 = negative_flows[start..=i].iter().sum();
114
115                let mfi = super::rsi::from_averages(pos_sum, neg_sum);
116
117                values.push(IndicatorValue::Single(mfi.clamp(0.0, 100.0)));
118            }
119        }
120
121        values
122    }
123}
124
125impl Indicator for MoneyFlowIndex {
126    crate::studies::inputs::study_inputs!(self;
127        period: length("Period", 1, usize::MAX)
128    ; {});
129
130    fn name(&self) -> &str {
131        "MFI"
132    }
133
134    fn desc(&self) -> &str {
135        "Money Flow Index - Volume-weighted RSI (0-100)"
136    }
137
138    fn calculate(&mut self, data: &[Bar]) {
139        self.update(data, 0);
140    }
141
142    fn update(&mut self, data: &[Bar], from: usize) {
143        let mut values = std::mem::take(&mut self.values);
144        update_window(
145            &mut values,
146            data,
147            from,
148            usable_behind(self.period, |b| {
149                b.high.is_finite()
150                    && b.low.is_finite()
151                    && b.close.is_finite()
152                    && b.volume.is_finite()
153            }),
154            |data| {
155                over_finite_bars(
156                    data,
157                    |b| {
158                        b.high.is_finite()
159                            && b.low.is_finite()
160                            && b.close.is_finite()
161                            && b.volume.is_finite()
162                    },
163                    |data| self.compute(data),
164                )
165            },
166        );
167        self.values = values;
168    }
169
170    fn is_incremental(&self) -> bool {
171        true
172    }
173
174    fn values(&self) -> &IndicatorValues {
175        &self.values
176    }
177
178    fn colors(&self) -> Vec<Color32> {
179        vec![self.color]
180    }
181
182    fn set_colors(&mut self, colors: Vec<Color32>) {
183        if !colors.is_empty() {
184            self.color = colors[0];
185        }
186    }
187
188    fn is_overlay(&self) -> bool {
189        false // MFI is drawn in separate pane
190    }
191
192    fn is_visible(&self) -> bool {
193        self.visible
194    }
195
196    fn set_visible(&mut self, visible: bool) {
197        self.visible = visible;
198    }
199
200    fn clone_box(&self) -> Box<dyn Indicator> {
201        Box::new(self.clone())
202    }
203
204    fn line_names(&self) -> Vec<String> {
205        vec![format!("MFI({})", self.period)]
206    }
207}
208
209#[cfg(test)]
210mod tests {
211    use super::*;
212
213    #[test]
214    fn endpoints_and_scale_invariance() {
215        // Period 2 sums the flows of the last two changes. With unit volume,
216        // [1, 2, 1, 3] gives 100 * 2/3 then 100 * 3/4.
217        for (closes, volume, expected) in [
218            ([1.0, 2.0, 3.0, 4.0], 0.0, [50.0, 50.0]),
219            ([5.0; 4], 700.0, [50.0, 50.0]),
220            ([1.0, 2.0, 3.0, 4.0], 1.0, [100.0, 100.0]),
221            ([4.0, 3.0, 2.0, 1.0], 1.0, [0.0, 0.0]),
222            ([1.0, 2.0, 1.0, 3.0], 1.0, [200.0 / 3.0, 75.0]),
223            ([-4.0, -2.0, -3.0, -1.0], 1.0, [40.0, 25.0]),
224        ] {
225            for scale in [1.0, 1e-17, 1e12] {
226                let bars: Vec<_> = closes
227                    .iter()
228                    .map(|p| {
229                        let p = p * scale;
230                        Bar::new(chrono::DateTime::UNIX_EPOCH, p, p, p, p, volume)
231                    })
232                    .collect();
233                let mut study = MoneyFlowIndex::new(2);
234                study.calculate(&bars);
235                assert_eq!(study.values().value(0, 1), None);
236                for (bar, want) in expected.into_iter().enumerate() {
237                    let got = study.values().value(0, bar + 2).unwrap();
238                    assert!(
239                        (got - want).abs() < 1e-9,
240                        "{closes:?}, {volume}, {scale}: {got}"
241                    );
242                }
243            }
244        }
245    }
246}