Skip to main content

egui_charts/studies/builtin/
market_facilitation.rs

1use super::clamp_param;
2use crate::model::Bar;
3use crate::studies::palette;
4use crate::studies::{Indicator, IndicatorValue, IndicatorValues};
5use egui::Color32;
6
7/// Market Facilitation Index (MFI / BW MFI)
8/// Developed by Bill Williams
9/// Measures price movement per unit of volume
10#[derive(Clone)]
11pub struct MarketFacilitationIndex {
12    /// Divisor for volume (typically 1 for crypto, 1000000 for stocks)
13    divisor: f64,
14    values: IndicatorValues,
15    color: Color32,
16    visible: bool,
17}
18
19impl MarketFacilitationIndex {
20    /// Create with the default plot colors.
21    pub fn new() -> Self {
22        Self {
23            divisor: 1.0,
24            values: IndicatorValues::new(),
25            color: palette::UP, // Teal
26            visible: true,
27        }
28    }
29
30    /// Set the volume divisor, clamped into `f64::EPSILON` to `f64::MAX`, NaN to `f64::EPSILON`.
31    pub fn with_divisor(mut self, divisor: f64) -> Self {
32        self.divisor = clamp_param(divisor, f64::EPSILON, f64::MAX);
33        self
34    }
35
36    /// Set the plot color.
37    pub fn with_color(mut self, color: Color32) -> Self {
38        self.color = color;
39        self
40    }
41}
42
43impl Default for MarketFacilitationIndex {
44    fn default() -> Self {
45        Self::new()
46    }
47}
48
49impl Indicator for MarketFacilitationIndex {
50    crate::studies::inputs::study_inputs!(self;
51        divisor: number("Divisor", f64::EPSILON, f64::MAX)
52    ; {});
53
54    fn name(&self) -> &str {
55        "MFI BW"
56    }
57
58    fn desc(&self) -> &str {
59        "Market Facilitation Index - Price movement per volume"
60    }
61
62    fn calculate(&mut self, data: &[Bar]) {
63        self.update(data, 0);
64    }
65
66    fn update(&mut self, data: &[Bar], from: usize) {
67        let from = from.min(self.values.len()).min(data.len());
68        self.values.truncate(from);
69        for bar in &data[from..] {
70            let range = bar.high - bar.low;
71            let volume = bar.volume / self.divisor;
72
73            if volume == 0.0 {
74                self.values.push(IndicatorValue::None);
75            } else {
76                let mfi = range / volume;
77                self.values.push(IndicatorValue::Single(mfi));
78            }
79        }
80    }
81
82    fn is_incremental(&self) -> bool {
83        true
84    }
85
86    fn values(&self) -> &IndicatorValues {
87        &self.values
88    }
89
90    fn colors(&self) -> Vec<Color32> {
91        vec![self.color]
92    }
93
94    fn set_colors(&mut self, colors: Vec<Color32>) {
95        if !colors.is_empty() {
96            self.color = colors[0];
97        }
98    }
99
100    fn is_overlay(&self) -> bool {
101        false
102    }
103
104    fn is_visible(&self) -> bool {
105        self.visible
106    }
107
108    fn set_visible(&mut self, visible: bool) {
109        self.visible = visible;
110    }
111
112    fn clone_box(&self) -> Box<dyn Indicator> {
113        Box::new(self.clone())
114    }
115
116    fn line_names(&self) -> Vec<String> {
117        vec!["MFI".to_string()]
118    }
119}
120
121#[cfg(test)]
122mod tests {
123    use super::*;
124    use chrono::Utc;
125
126    fn make_bar(high: f64, low: f64, volume: f64) -> Bar {
127        Bar {
128            time: Utc::now(),
129            open: (high + low) / 2.0,
130            high,
131            low,
132            close: (high + low) / 2.0,
133            volume,
134            colors: None,
135            payload: None,
136        }
137    }
138
139    #[test]
140    fn test_mfi_calculation() {
141        let mut mfi = MarketFacilitationIndex::new();
142
143        let data = vec![
144            make_bar(102.0, 98.0, 1000.0),  // Range=4, MFI=0.004
145            make_bar(105.0, 100.0, 2000.0), // Range=5, MFI=0.0025
146        ];
147
148        mfi.calculate(&data);
149
150        assert_eq!(mfi.values.len(), 2);
151
152        let IndicatorValue::Single(v) = mfi.values.to_rows()[0] else {
153            panic!("no value: {:?}", mfi.values.to_rows()[0]);
154        };
155        assert!((v - 0.004).abs() < 0.0001);
156    }
157
158    #[test]
159    fn test_mfi_zero_volume() {
160        let mut mfi = MarketFacilitationIndex::new();
161
162        let data = vec![make_bar(102.0, 98.0, 0.0)];
163
164        mfi.calculate(&data);
165
166        assert!(matches!(mfi.values.to_rows()[0], IndicatorValue::None));
167    }
168}