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 {
23 (close - open) / hl
24 } else {
25 0.0
26 }
27 }
28}
29
30#[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#[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}