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 { cur / prev } else { 0.0 }
79}
80
81#[inline]
82fn rocr100_compute(cur: f64, prev: f64) -> f64 {
83 if prev != 0.0 {
84 (cur / prev) * 100.0
85 } else {
86 0.0
87 }
88}
89
90impl_lookback_momentum!(MOM, mom_compute);
91impl_lookback_momentum!(ROC, roc_compute);
92impl_lookback_momentum!(ROCP, rocp_compute);
93impl_lookback_momentum!(ROCR, rocr_compute);
94impl_lookback_momentum!(ROCR100, rocr100_compute);
95
96#[cfg(test)]
97mod tests {
98 use super::*;
99 use proptest::prelude::*;
100
101 fn assert_momentum_parity<I, F>(mut indicator: I, input: &[f64], batch: F)
102 where
103 I: Next<f64, Output = f64>,
104 F: Fn(&[f64], usize) -> Result<Vec<f64>, talib_rs::error::TaError>,
105 {
106 let timeperiod = 14;
107 let streaming: Vec<f64> = input.iter().map(|&x| indicator.next(x)).collect();
108 let batch = batch(input, timeperiod).unwrap_or_else(|_| vec![f64::NAN; input.len()]);
109 for (s, b) in streaming.iter().zip(batch.iter()) {
110 if s.is_nan() {
111 assert!(b.is_nan());
112 } else if !b.is_nan() {
113 approx::assert_relative_eq!(s, b, epsilon = 1e-10);
114 }
115 }
116 }
117
118 proptest! {
119 #[test]
120 fn mom_parity(input in prop::collection::vec(1.0..100.0, 10..100)) {
121 let period = 14;
122 assert_momentum_parity(MOM::new(period), &input, talib_rs::momentum::mom);
123 }
124
125 #[test]
126 fn roc_parity(input in prop::collection::vec(1.0..100.0, 10..100)) {
127 let period = 14;
128 assert_momentum_parity(ROC::new(period), &input, talib_rs::momentum::roc);
129 }
130
131 #[test]
132 fn rocp_parity(input in prop::collection::vec(1.0..100.0, 10..100)) {
133 let period = 14;
134 assert_momentum_parity(ROCP::new(period), &input, talib_rs::momentum::rocp);
135 }
136
137 #[test]
138 fn rocr_parity(input in prop::collection::vec(1.0..100.0, 10..100)) {
139 let period = 14;
140 assert_momentum_parity(ROCR::new(period), &input, talib_rs::momentum::rocr);
141 }
142
143 #[test]
144 fn rocr100_parity(input in prop::collection::vec(1.0..100.0, 10..100)) {
145 let period = 14;
146 assert_momentum_parity(ROCR100::new(period), &input, talib_rs::momentum::rocr100);
147 }
148 }
149}