Skip to main content

quantwave_core/indicators/incremental/
simple.rs

1//! Simple O(1) indicators: BOP, OBV, MFI.
2
3use crate::traits::Next;
4use crate::utils::RingBuffer;
5
6/// Balance Of Power.
7#[derive(Debug, Clone, Default)]
8#[allow(non_camel_case_types)]
9pub struct BOP;
10
11impl BOP {
12    pub fn new() -> Self {
13        Self
14    }
15}
16
17impl Next<(f64, f64, f64, f64)> for BOP {
18    type Output = f64;
19
20    fn next(&mut self, (open, high, low, close): (f64, f64, f64, f64)) -> Self::Output {
21        let hl = high - low;
22        if hl > 0.0 {
23            (close - open) / hl
24        } else {
25            0.0
26        }
27    }
28}
29
30/// On Balance Volume.
31#[derive(Debug, Clone)]
32#[allow(non_camel_case_types)]
33pub struct OBV {
34    prev_close: Option<f64>,
35    acc: f64,
36    started: bool,
37}
38
39impl OBV {
40    pub fn new() -> Self {
41        Self {
42            prev_close: None,
43            acc: 0.0,
44            started: false,
45        }
46    }
47}
48
49impl Next<(f64, f64)> for OBV {
50    type Output = f64;
51
52    fn next(&mut self, (close, volume): (f64, f64)) -> Self::Output {
53        if !self.started {
54            self.acc = volume;
55            self.prev_close = Some(close);
56            self.started = true;
57            return self.acc;
58        }
59        let pc = self.prev_close.unwrap();
60        if close > pc {
61            self.acc += volume;
62        } else if close < pc {
63            self.acc -= volume;
64        }
65        self.prev_close = Some(close);
66        self.acc
67    }
68}
69
70/// Money Flow Index.
71#[derive(Debug, Clone)]
72#[allow(non_camel_case_types)]
73pub struct MFI {
74    pub timeperiod: usize,
75    prev_tp: Option<f64>,
76    flow_window: RingBuffer<(f64, f64)>,
77    pos_sum: f64,
78    neg_sum: f64,
79    comparisons: usize,
80}
81
82impl MFI {
83    pub fn new(timeperiod: usize) -> Self {
84        Self {
85            timeperiod,
86            prev_tp: None,
87            flow_window: RingBuffer::with_capacity(timeperiod),
88            pos_sum: 0.0,
89            neg_sum: 0.0,
90            comparisons: 0,
91        }
92    }
93
94    #[inline]
95    fn mfi_from(pos: f64, neg: f64) -> f64 {
96        if neg > 0.0 {
97            100.0 - (100.0 / (1.0 + pos / neg))
98        } else {
99            100.0
100        }
101    }
102}
103
104impl Next<(f64, f64, f64, f64)> for MFI {
105    type Output = f64;
106
107    fn next(&mut self, (high, low, close, volume): (f64, f64, f64, f64)) -> Self::Output {
108        let period = self.timeperiod;
109        if period < 2 {
110            return f64::NAN;
111        }
112        let tp = (high + low + close) / 3.0;
113        let mf = tp * volume;
114
115        let Some(prev_tp) = self.prev_tp else {
116            self.prev_tp = Some(tp);
117            return f64::NAN;
118        };
119
120        let (pos_add, neg_add) = if tp > prev_tp {
121            (mf, 0.0)
122        } else if tp < prev_tp {
123            (0.0, mf)
124        } else {
125            (0.0, 0.0)
126        };
127        self.prev_tp = Some(tp);
128        self.comparisons += 1;
129
130        if self.flow_window.len() >= period {
131            if let Some((op, on)) = self.flow_window.pop_front() {
132                self.pos_sum -= op;
133                self.neg_sum -= on;
134            }
135        }
136        self.flow_window.push_back((pos_add, neg_add));
137        self.pos_sum += pos_add;
138        self.neg_sum += neg_add;
139
140        if self.comparisons < period {
141            return f64::NAN;
142        }
143        Self::mfi_from(self.pos_sum, self.neg_sum)
144    }
145}
146
147#[cfg(test)]
148mod tests {
149    use super::*;
150    use proptest::prelude::*;
151
152    proptest! {
153        #[test]
154        fn test_mfi_parity(
155            h in prop::collection::vec(1.0..100.0, 1..100),
156            l in prop::collection::vec(1.0..100.0, 1..100),
157            c in prop::collection::vec(1.0..100.0, 1..100),
158            v in prop::collection::vec(1.0..1000.0, 1..100)
159        ) {
160            let len = h.len().min(l.len()).min(c.len()).min(v.len());
161            if len < 20 { return Ok(()); }
162            let period = 14;
163            let mut mfi = MFI::new(period);
164            let streaming: Vec<f64> = (0..len)
165                .map(|i| mfi.next((h[i], l[i], c[i], v[i])))
166                .collect();
167            let batch = talib_rs::momentum::mfi(&h[..len], &l[..len], &c[..len], &v[..len], period)
168                .unwrap_or_else(|_| vec![f64::NAN; len]);
169            for (s, b) in streaming.iter().zip(batch.iter()) {
170                if s.is_nan() { assert!(b.is_nan()); }
171                else { approx::assert_relative_eq!(s, b, epsilon = 1e-6); }
172            }
173        }
174    }
175}