quantwave_core/indicators/incremental/
simple.rs1use crate::traits::Next;
4use crate::utils::RingBuffer;
5
6#[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#[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#[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}