quantwave_core/indicators/incremental/
mom.rs1use crate::indicators::incremental::utils::RingBuffer;
4use crate::traits::Next;
5
6macro_rules! impl_lookback_momentum {
7 ($name:ident, $compute:ident) => {
8 #[derive(Debug, Clone)]
9 #[allow(non_camel_case_types)]
10 pub struct $name {
11 pub timeperiod: usize,
12 history: RingBuffer<f64>,
13 bar_count: usize,
14 }
15
16 impl $name {
17 pub fn new(timeperiod: usize) -> Self {
18 let cap = timeperiod.saturating_add(1).max(1);
19 Self {
20 timeperiod,
21 history: RingBuffer::with_capacity(cap),
22 bar_count: 0,
23 }
24 }
25
26 #[inline]
27 fn lagged(&self) -> Option<f64> {
28 let n = self.bar_count;
29 if n <= self.timeperiod {
30 return None;
31 }
32 let idx = n - 1 - self.timeperiod;
33 self.history.get(idx).copied()
34 }
35 }
36
37 impl Next<f64> for $name {
38 type Output = f64;
39
40 fn next(&mut self, input: f64) -> Self::Output {
41 self.history.push_back(input);
42 self.bar_count += 1;
43
44 let Some(prev) = self.lagged() else {
45 return f64::NAN;
46 };
47 $compute(input, prev)
48 }
49 }
50 };
51}
52
53#[inline]
54fn mom_compute(cur: f64, prev: f64) -> f64 {
55 cur - prev
56}
57
58#[inline]
59fn roc_compute(cur: f64, prev: f64) -> f64 {
60 if prev != 0.0 {
61 ((cur - prev) / prev) * 100.0
62 } else {
63 0.0
64 }
65}
66
67#[inline]
68fn rocp_compute(cur: f64, prev: f64) -> f64 {
69 if prev != 0.0 {
70 (cur - prev) / prev
71 } else {
72 0.0
73 }
74}
75
76#[inline]
77fn rocr_compute(cur: f64, prev: f64) -> f64 {
78 if prev != 0.0 {
79 cur / prev
80 } else {
81 0.0
82 }
83}
84
85#[inline]
86fn rocr100_compute(cur: f64, prev: f64) -> f64 {
87 if prev != 0.0 {
88 (cur / prev) * 100.0
89 } else {
90 0.0
91 }
92}
93
94impl_lookback_momentum!(MOM, mom_compute);
95impl_lookback_momentum!(ROC, roc_compute);
96impl_lookback_momentum!(ROCP, rocp_compute);
97impl_lookback_momentum!(ROCR, rocr_compute);
98impl_lookback_momentum!(ROCR100, rocr100_compute);
99
100#[cfg(test)]
101mod tests {
102 use super::*;
103 use proptest::prelude::*;
104
105 fn assert_momentum_parity<I, F>(mut indicator: I, input: &[f64], batch: F)
106 where
107 I: Next<f64, Output = f64>,
108 F: Fn(&[f64], usize) -> Result<Vec<f64>, talib_rs::error::TaError>,
109 {
110 let timeperiod = 14;
111 let streaming: Vec<f64> = input.iter().map(|&x| indicator.next(x)).collect();
112 let batch = batch(input, timeperiod).unwrap_or_else(|_| vec![f64::NAN; input.len()]);
113 for (s, b) in streaming.iter().zip(batch.iter()) {
114 if s.is_nan() {
115 assert!(b.is_nan());
116 } else if !b.is_nan() {
117 approx::assert_relative_eq!(s, b, epsilon = 1e-10);
118 }
119 }
120 }
121
122 proptest! {
123 #[test]
124 fn mom_parity(input in prop::collection::vec(1.0..100.0, 10..100)) {
125 let period = 14;
126 assert_momentum_parity(MOM::new(period), &input, talib_rs::momentum::mom);
127 }
128
129 #[test]
130 fn roc_parity(input in prop::collection::vec(1.0..100.0, 10..100)) {
131 let period = 14;
132 assert_momentum_parity(ROC::new(period), &input, talib_rs::momentum::roc);
133 }
134
135 #[test]
136 fn rocp_parity(input in prop::collection::vec(1.0..100.0, 10..100)) {
137 let period = 14;
138 assert_momentum_parity(ROCP::new(period), &input, talib_rs::momentum::rocp);
139 }
140
141 #[test]
142 fn rocr_parity(input in prop::collection::vec(1.0..100.0, 10..100)) {
143 let period = 14;
144 assert_momentum_parity(ROCR::new(period), &input, talib_rs::momentum::rocr);
145 }
146
147 #[test]
148 fn rocr100_parity(input in prop::collection::vec(1.0..100.0, 10..100)) {
149 let period = 14;
150 assert_momentum_parity(ROCR100::new(period), &input, talib_rs::momentum::rocr100);
151 }
152 }
153}