indexes_rs/v2/mfi/
main.rs

1use crate::v2::mfi::types::{MFIConfig, MFIError, MFIInput, MFIMarketCondition, MFIOutput, MFIState, MoneyFlow};
2
3/// Money Flow Index (MFI) Indicator
4///
5/// MFI is a momentum indicator that uses both price and volume to identify
6/// overbought or oversold conditions. It's often called "Volume-weighted RSI".
7///
8/// Formula:
9/// 1. Typical Price = (High + Low + Close) / 3
10/// 2. Raw Money Flow = Typical Price × Volume
11/// 3. Money Flow Direction: Positive if current TP > previous TP, else Negative
12/// 4. Money Ratio = (Positive Money Flow Sum) / (Negative Money Flow Sum)
13/// 5. MFI = 100 - (100 / (1 + Money Ratio))
14pub struct MFI {
15    state: MFIState,
16}
17
18impl MFI {
19    /// Create a new MFI calculator with default configuration (period=14)
20    pub fn new() -> Self {
21        Self::with_config(MFIConfig::default())
22    }
23
24    /// Create a new MFI calculator with custom period
25    pub fn with_period(period: usize) -> Result<Self, MFIError> {
26        if period == 0 {
27            return Err(MFIError::InvalidPeriod);
28        }
29
30        let config = MFIConfig { period, ..Default::default() };
31        Ok(Self::with_config(config))
32    }
33
34    /// Create a new MFI calculator with custom configuration
35    pub fn with_config(config: MFIConfig) -> Self {
36        Self { state: MFIState::new(config) }
37    }
38
39    /// Calculate MFI for the given input
40    pub fn calculate(&mut self, input: MFIInput) -> Result<MFIOutput, MFIError> {
41        // Validate input
42        self.validate_input(&input)?;
43        self.validate_config()?;
44
45        // Calculate typical price
46        let typical_price = self.calculate_typical_price(&input);
47
48        // Calculate raw money flow
49        let raw_money_flow = typical_price * input.volume;
50
51        // Determine money flow direction
52        let flow_direction = self.determine_flow_direction(typical_price);
53
54        // Create money flow data point
55        let money_flow = MoneyFlow {
56            typical_price,
57            raw_money_flow,
58            flow_direction,
59        };
60
61        // Add to history and update sums
62        self.update_money_flow_history(money_flow);
63
64        // Calculate MFI if we have enough data
65        let mfi = if self.state.has_sufficient_data {
66            self.calculate_mfi_value()?
67        } else {
68            50.0 // Default neutral value when insufficient data
69        };
70
71        // Determine market condition
72        let market_condition = self.determine_market_condition(mfi);
73
74        // Update previous typical price
75        self.state.previous_typical_price = Some(typical_price);
76
77        Ok(MFIOutput {
78            mfi,
79            typical_price,
80            raw_money_flow,
81            flow_direction,
82            market_condition,
83        })
84    }
85
86    /// Calculate MFI for a batch of inputs
87    pub fn calculate_batch(&mut self, inputs: &[MFIInput]) -> Result<Vec<MFIOutput>, MFIError> {
88        inputs.iter().map(|input| self.calculate(*input)).collect()
89    }
90
91    /// Reset the calculator state
92    pub fn reset(&mut self) {
93        self.state = MFIState::new(self.state.config);
94    }
95
96    /// Get current state (for serialization/debugging)
97    pub fn get_state(&self) -> &MFIState {
98        &self.state
99    }
100
101    /// Restore state (for deserialization)
102    pub fn set_state(&mut self, state: MFIState) {
103        self.state = state;
104    }
105
106    /// Get current positive money flow sum
107    pub fn positive_money_flow(&self) -> f64 {
108        self.state.positive_money_flow_sum
109    }
110
111    /// Get current negative money flow sum
112    pub fn negative_money_flow(&self) -> f64 {
113        self.state.negative_money_flow_sum
114    }
115
116    // Private helper methods
117
118    fn validate_input(&self, input: &MFIInput) -> Result<(), MFIError> {
119        // Check for valid prices
120        if !input.high.is_finite() || !input.low.is_finite() || !input.close.is_finite() {
121            return Err(MFIError::InvalidPrice);
122        }
123
124        // Check OHLC relationship
125        if input.high < input.low {
126            return Err(MFIError::InvalidOHLC);
127        }
128
129        if input.close < input.low || input.close > input.high {
130            return Err(MFIError::InvalidOHLC);
131        }
132
133        // Check volume
134        if input.volume < 0.0 {
135            return Err(MFIError::NegativeVolume);
136        }
137
138        Ok(())
139    }
140
141    fn validate_config(&self) -> Result<(), MFIError> {
142        if self.state.config.period == 0 {
143            return Err(MFIError::InvalidPeriod);
144        }
145
146        if self.state.config.overbought <= self.state.config.oversold {
147            return Err(MFIError::InvalidThresholds);
148        }
149
150        if self.state.config.overbought > 100.0 || self.state.config.oversold < 0.0 {
151            return Err(MFIError::InvalidThresholds);
152        }
153
154        Ok(())
155    }
156
157    fn calculate_typical_price(&self, input: &MFIInput) -> f64 {
158        (input.high + input.low + input.close) / 3.0
159    }
160
161    fn determine_flow_direction(&self, current_typical_price: f64) -> f64 {
162        match self.state.previous_typical_price {
163            Some(prev_tp) => {
164                if current_typical_price > prev_tp {
165                    1.0 // Positive money flow
166                } else if current_typical_price < prev_tp {
167                    -1.0 // Negative money flow
168                } else {
169                    0.0 // Neutral (unchanged)
170                }
171            }
172            None => 0.0, // First calculation - neutral
173        }
174    }
175
176    fn update_money_flow_history(&mut self, money_flow: MoneyFlow) {
177        // Remove oldest if at capacity
178        if self.state.money_flows.len() >= self.state.config.period {
179            if let Some(oldest) = self.state.money_flows.pop_front() {
180                // Remove from sums
181                if oldest.flow_direction > 0.0 {
182                    self.state.positive_money_flow_sum -= oldest.raw_money_flow;
183                } else if oldest.flow_direction < 0.0 {
184                    self.state.negative_money_flow_sum -= oldest.raw_money_flow;
185                }
186            }
187        }
188
189        // Add new money flow
190        if money_flow.flow_direction > 0.0 {
191            self.state.positive_money_flow_sum += money_flow.raw_money_flow;
192        } else if money_flow.flow_direction < 0.0 {
193            self.state.negative_money_flow_sum += money_flow.raw_money_flow;
194        }
195
196        self.state.money_flows.push_back(money_flow);
197
198        // Check if we have sufficient data
199        self.state.has_sufficient_data = self.state.money_flows.len() >= self.state.config.period;
200    }
201
202    fn calculate_mfi_value(&self) -> Result<f64, MFIError> {
203        if self.state.negative_money_flow_sum == 0.0 {
204            // All positive money flow
205            return Ok(100.0);
206        }
207
208        if self.state.positive_money_flow_sum == 0.0 {
209            // All negative money flow
210            return Ok(0.0);
211        }
212
213        let money_ratio = self.state.positive_money_flow_sum / self.state.negative_money_flow_sum;
214        let mfi = 100.0 - (100.0 / (1.0 + money_ratio));
215
216        if !mfi.is_finite() {
217            return Err(MFIError::DivisionByZero);
218        }
219
220        Ok(mfi)
221    }
222
223    fn determine_market_condition(&self, mfi: f64) -> MFIMarketCondition {
224        if !self.state.has_sufficient_data {
225            MFIMarketCondition::Insufficient
226        } else if mfi >= self.state.config.overbought {
227            MFIMarketCondition::Overbought
228        } else if mfi <= self.state.config.oversold {
229            MFIMarketCondition::Oversold
230        } else {
231            MFIMarketCondition::Normal
232        }
233    }
234}
235
236impl Default for MFI {
237    fn default() -> Self {
238        Self::new()
239    }
240}
241
242/// Convenience function to calculate MFI for OHLCV data without maintaining state
243pub fn calculate_mfi_simple(highs: &[f64], lows: &[f64], closes: &[f64], volumes: &[f64], period: usize) -> Result<Vec<f64>, MFIError> {
244    let len = highs.len();
245    if len != lows.len() || len != closes.len() || len != volumes.len() {
246        return Err(MFIError::InvalidInput("All price and volume arrays must have same length".to_string()));
247    }
248
249    if len == 0 {
250        return Ok(Vec::new());
251    }
252
253    let mut mfi_calculator = MFI::with_period(period)?;
254    let mut results = Vec::with_capacity(len);
255
256    for i in 0..len {
257        let input = MFIInput {
258            high: highs[i],
259            low: lows[i],
260            close: closes[i],
261            volume: volumes[i],
262        };
263        let output = mfi_calculator.calculate(input)?;
264        results.push(output.mfi);
265    }
266
267    Ok(results)
268}