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