finance_solution/stocks/ta/
mfi.rs1use crate::stocks::ta::common::{opt_cell, require_hlc, validate_positive_volume};
53use crate::stocks::ta::ring::RingF64;
54use crate::util::error::{require_finite, FinanceError, FinanceResult};
55use crate::util::primitives::PeriodLength;
56use crate::{columns_with_strings, print_table_locale_opt};
57
58#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
59pub struct MfiParams {
60 pub period: usize,
61}
62
63impl MfiParams {
64 pub const fn new(period: usize) -> Self {
65 Self { period }
66 }
67
68 pub const fn period_14() -> Self {
69 Self { period: 14 }
70 }
71}
72
73#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
74pub struct ValidatedMfi {
75 params: MfiParams,
76}
77
78impl ValidatedMfi {
79 pub fn new(params: MfiParams) -> FinanceResult<Self> {
80 PeriodLength::new(params.period)?;
81 Ok(Self { params })
82 }
83
84 pub fn params(self) -> MfiParams {
85 self.params
86 }
87
88 pub fn compute(
89 self,
90 high: &[f64],
91 low: &[f64],
92 close: &[f64],
93 volume: &[f64],
94 ) -> FinanceResult<MfiSeries> {
95 mfi_validated(high, low, close, volume, self)
96 }
97}
98
99#[derive(Clone, Debug, PartialEq)]
100pub struct MfiSeries {
101 pub mfi: Vec<Option<f64>>,
102 pub params: MfiParams,
103}
104
105impl MfiSeries {
106 pub fn last(&self) -> Option<f64> {
107 self.mfi.iter().rev().find_map(|x| *x)
108 }
109}
110
111#[derive(Clone, Debug)]
113pub struct MfiState {
114 params: MfiParams,
115 prev_tp: Option<f64>,
116 pos: RingF64,
117 neg: RingF64,
118 last: Option<f64>,
119}
120
121impl MfiState {
122 pub fn new(params: MfiParams) -> FinanceResult<Self> {
123 let _ = ValidatedMfi::new(params)?;
124 Ok(Self {
125 params,
126 prev_tp: None,
127 pos: RingF64::with_capacity(params.period),
128 neg: RingF64::with_capacity(params.period),
129 last: None,
130 })
131 }
132
133 pub fn from_history(
134 params: MfiParams,
135 high: &[f64],
136 low: &[f64],
137 close: &[f64],
138 volume: &[f64],
139 ) -> FinanceResult<Self> {
140 let mut s = Self::new(params)?;
141 let _ = s.push_bars(high, low, close, volume)?;
142 Ok(s)
143 }
144
145 pub fn params(&self) -> MfiParams {
146 self.params
147 }
148
149 pub fn reset(&mut self) {
150 self.prev_tp = None;
151 self.pos.clear();
152 self.neg.clear();
153 self.last = None;
154 }
155
156 pub fn push(
157 &mut self,
158 high: f64,
159 low: f64,
160 close: f64,
161 volume: f64,
162 ) -> FinanceResult<Option<f64>> {
163 require_finite("high", high)?;
164 require_finite("low", low)?;
165 require_finite("close", close)?;
166 require_finite("volume", volume)?;
167 if high < low {
168 return Err(FinanceError::InvalidCashflow {
169 message: "high must be >= low for each bar",
170 });
171 }
172 if volume < 0.0 {
173 return Err(FinanceError::InvalidCashflow {
174 message: "volume must be non-negative",
175 });
176 }
177 let tp = (high + low + close) / 3.0;
178 let rmf = tp * volume;
179 let out = match self.prev_tp {
180 None => {
181 self.prev_tp = Some(tp);
182 self.last = None;
183 None
184 }
185 Some(ptp) => {
186 let (p, n) = if tp > ptp {
187 (rmf, 0.0)
188 } else if tp < ptp {
189 (0.0, rmf)
190 } else {
191 (0.0, 0.0)
192 };
193 let _ = self.pos.push(p);
194 let _ = self.neg.push(n);
195 self.prev_tp = Some(tp);
196 if !self.pos.is_full() {
197 self.last = None;
198 None
199 } else {
200 let pos_sum = self.pos.sum();
201 let neg_sum = self.neg.sum();
202 let mfi = if neg_sum == 0.0 && pos_sum == 0.0 {
203 None
204 } else if neg_sum == 0.0 {
205 Some(100.0)
206 } else {
207 let ratio = pos_sum / neg_sum;
208 Some(100.0 - 100.0 / (1.0 + ratio))
209 };
210 self.last = mfi;
211 mfi
212 }
213 }
214 };
215 Ok(out)
216 }
217
218 pub fn push_bars(
219 &mut self,
220 high: &[f64],
221 low: &[f64],
222 close: &[f64],
223 volume: &[f64],
224 ) -> FinanceResult<Vec<Option<f64>>> {
225 require_hlc(high, low, close)?;
226 validate_positive_volume(volume)?;
227 if close.len() != volume.len() {
228 return Err(FinanceError::LengthMismatch {
229 left: close.len(),
230 right: volume.len(),
231 context: "close/volume",
232 });
233 }
234 let mut out = Vec::with_capacity(close.len());
235 for i in 0..close.len() {
236 out.push(self.push(high[i], low[i], close[i], volume[i])?);
237 }
238 Ok(out)
239 }
240
241 pub fn last(&self) -> Option<f64> {
242 self.last
243 }
244}
245
246pub fn mfi(
247 high: &[f64],
248 low: &[f64],
249 close: &[f64],
250 volume: &[f64],
251 params: MfiParams,
252) -> FinanceResult<MfiSeries> {
253 ValidatedMfi::new(params)?.compute(high, low, close, volume)
254}
255
256fn mfi_validated(
257 high: &[f64],
258 low: &[f64],
259 close: &[f64],
260 volume: &[f64],
261 eng: ValidatedMfi,
262) -> FinanceResult<MfiSeries> {
263 let mut st = MfiState::new(eng.params)?;
264 let mfi = st.push_bars(high, low, close, volume)?;
265 Ok(MfiSeries {
266 mfi,
267 params: eng.params,
268 })
269}
270
271#[derive(Clone, Debug)]
272pub struct MfiSolution {
273 series: MfiSeries,
274 close: Vec<f64>,
275 formula: String,
276}
277
278impl MfiSolution {
279 pub fn series(&self) -> &MfiSeries {
280 &self.series
281 }
282 pub fn formula(&self) -> &str {
283 &self.formula
284 }
285
286 pub fn print_table(&self) {
287 self.print_table_locale_opt(None, None);
288 }
289
290 pub fn print_table_locale(&self, locale: &num_format::Locale, precision: usize) {
291 self.print_table_locale_opt(Some(locale), Some(precision));
292 }
293
294 fn print_table_locale_opt(
295 &self,
296 locale: Option<&num_format::Locale>,
297 precision: Option<usize>,
298 ) {
299 let columns = columns_with_strings(&[
300 ("period", "i", true),
301 ("close", "f", true),
302 ("mfi", "f", true),
303 ]);
304 let data = self
305 .close
306 .iter()
307 .enumerate()
308 .map(|(i, c)| vec![i.to_string(), c.to_string(), opt_cell(self.series.mfi[i])])
309 .collect();
310 print_table_locale_opt(&columns, data, locale, precision);
311 }
312}
313
314pub fn mfi_solution(
326 high: &[f64],
327 low: &[f64],
328 close: &[f64],
329 volume: &[f64],
330 params: MfiParams,
331) -> FinanceResult<MfiSolution> {
332 let series = mfi(high, low, close, volume, params)?;
333 Ok(MfiSolution {
334 series,
335 close: close.to_vec(),
336 formula: format!(
337 "MFI({}) = 100 - 100/(1 + +MF/-MF); TP=(H+L+C)/3; MF=TP*vol",
338 params.period
339 ),
340 })
341}
342
343#[cfg(test)]
344mod tests {
345 use super::*;
346
347 #[test]
348 fn rising_high_mfi() {
349 let n = 40usize;
350 let h: Vec<_> = (0..n).map(|i| 101.0 + i as f64).collect();
351 let l: Vec<_> = (0..n).map(|i| 99.0 + i as f64).collect();
352 let c: Vec<_> = (0..n).map(|i| 100.0 + i as f64).collect();
353 let v = vec![1000.0; n];
354 let s = mfi(&h, &l, &c, &v, MfiParams::period_14()).unwrap();
355 assert!(s.last().unwrap() > 80.0);
356 }
357
358 #[test]
359 fn state_parity() {
360 let n = 35usize;
361 let h: Vec<_> = (0..n).map(|i| 12.0 + (i as f64).sin()).collect();
362 let l: Vec<_> = (0..n).map(|i| 10.0 + (i as f64).sin()).collect();
363 let c: Vec<_> = (0..n).map(|i| 11.0 + (i as f64).sin()).collect();
364 let v: Vec<_> = (0..n).map(|i| 500.0 + i as f64).collect();
365 let p = MfiParams::period_14();
366 let batch = mfi(&h, &l, &c, &v, p).unwrap();
367 let mut st = MfiState::new(p).unwrap();
368 for i in 0..n {
369 let o = st.push(h[i], l[i], c[i], v[i]).unwrap();
370 match (o, batch.mfi[i]) {
371 (None, None) => {}
372 (Some(a), Some(b)) => assert!((a - b).abs() < 1e-9),
373 other => panic!("{other:?}"),
374 }
375 }
376 }
377}