quantwave_core/indicators/
pma.rs1use crate::indicators::metadata::{IndicatorMetadata, ParamDef};
2use crate::traits::Next;
3use std::collections::VecDeque;
4
5#[derive(Debug, Clone)]
12pub struct ProjectedMovingAverage {
13 length: usize,
14 window: VecDeque<f64>,
15 slope_history: VecDeque<f64>,
16 sum_x: f64,
17 sum_x2: f64,
18}
19
20impl ProjectedMovingAverage {
21 pub fn new(length: usize) -> Self {
22 let mut sum_x = 0.0;
23 let mut sum_x2 = 0.0;
24 for i in 1..=length {
25 let x = i as f64;
26 sum_x += x;
27 sum_x2 += x * x;
28 }
29 Self {
30 length,
31 window: VecDeque::with_capacity(length),
32 slope_history: VecDeque::from(vec![0.0; 3]),
33 sum_x,
34 sum_x2,
35 }
36 }
37}
38
39impl Default for ProjectedMovingAverage {
40 fn default() -> Self {
41 Self::new(20)
42 }
43}
44
45impl Next<f64> for ProjectedMovingAverage {
46 type Output = (f64, f64);
47
48 fn next(&mut self, input: f64) -> Self::Output {
49 self.window.push_front(input);
50 if self.window.len() > self.length {
51 self.window.pop_back();
52 }
53
54 if self.window.len() < self.length {
55 return (input, input);
56 }
57
58 let mut sum_y = 0.0;
59 let mut sum_xy = 0.0;
60
61 for i in 0..self.length {
62 let y = self.window[i];
63 let x = (i + 1) as f64;
64 sum_y += y;
65 sum_xy += x * y;
66 }
67
68 let n = self.length as f64;
69 let denom = n * self.sum_x2 - self.sum_x * self.sum_x;
70 let slope = if denom != 0.0 {
71 -(n * sum_xy - self.sum_x * sum_y) / denom
72 } else {
73 0.0
74 };
75 let sma = sum_y / n;
76 let pma = sma + slope * n / 2.0;
77
78 self.slope_history.pop_back();
79 self.slope_history.push_front(slope);
80
81 let predict = pma + 0.5 * (slope - self.slope_history[2]) * n;
82
83 (pma, predict)
84 }
85}
86
87pub const PROJECTED_MOVING_AVERAGE_METADATA: IndicatorMetadata = IndicatorMetadata {
88 name: "Projected Moving Average",
89 description: "A lag-compensated moving average that uses linear regression slope to project the average forward.",
90 usage: "Use as a predictive moving average that uses linear regression projection to anticipate where price will be rather than where it has been, reducing effective lag.",
91 keywords: &["moving-average", "prediction", "ehlers", "zero-lag"],
92 ehlers_summary: "The Projected Moving Average uses linear regression over the lookback window to project the best-fit line forward to the current bar. This predictive approach shifts the MA output toward the leading edge of price movement, achieving reduced lag compared to conventional MAs of the same period.",
93 params: &[ParamDef {
94 name: "length",
95 default: "20",
96 description: "Calculation length",
97 }],
98 formula_source: "https://github.com/lavs9/quantwave/blob/main/references/traderstipsreference/TRADERS’%20TIPS%20-%20MARCH%202025.html",
99 formula_latex: r#"
100\[
101Slope = -\frac{n \sum xy - \sum x \sum y}{n \sum x^2 - (\sum x)^2}
102\]
103\[
104PMA = SMA + Slope \cdot \frac{n}{2}
105\]
106\[
107Predict = PMA + 0.5 \cdot (Slope - Slope_{t-2}) \cdot n
108\]
109"#,
110 gold_standard_file: "pma.json",
111 category: "Ehlers DSP",
112};
113
114#[cfg(test)]
115mod tests {
116 use super::*;
117 use crate::traits::Next;
118 use proptest::prelude::*;
119
120 #[test]
121 fn test_pma_basic() {
122 let mut pma = ProjectedMovingAverage::new(20);
123 let inputs = vec![10.0; 40];
124 for input in inputs {
125 let (p, pr) = pma.next(input);
126 assert_eq!(p, 10.0);
127 assert_eq!(pr, 10.0);
128 }
129 }
130
131 proptest! {
132 #[test]
133 fn test_pma_parity(
134 inputs in prop::collection::vec(1.0..100.0, 40..100),
135 ) {
136 let length = 20;
137 let mut pma = ProjectedMovingAverage::new(length);
138 let streaming_results: Vec<(f64, f64)> = inputs.iter().map(|&x| pma.next(x)).collect();
139
140 let mut batch_results = Vec::with_capacity(inputs.len());
142 let mut slope_hist = vec![0.0; 3];
143 let mut sum_x = 0.0;
144 let mut sum_x2 = 0.0;
145 for i in 1..=length {
146 let x = i as f64;
147 sum_x += x;
148 sum_x2 += x * x;
149 }
150
151 for i in 0..inputs.len() {
152 if i < length - 1 {
153 batch_results.push((inputs[i], inputs[i]));
154 continue;
155 }
156
157 let mut sum_y = 0.0;
158 let mut sum_xy = 0.0;
159 for j in 0..length {
160 let y = inputs[i - j];
161 let x = (j + 1) as f64;
162 sum_y += y;
163 sum_xy += x * y;
164 }
165
166 let n = length as f64;
167 let denom = n * sum_x2 - sum_x * sum_x;
168 let slope = if denom != 0.0 {
169 -(n * sum_xy - sum_x * sum_y) / denom
170 } else {
171 0.0
172 };
173 let sma = sum_y / n;
174 let pma_val = sma + slope * n / 2.0;
175
176 slope_hist.insert(0, slope);
177 if slope_hist.len() > 3 {
178 slope_hist.pop();
179 }
180
181 let predict = pma_val + 0.5 * (slope - slope_hist[2]) * n;
182 batch_results.push((pma_val, predict));
183 }
184
185 for (s, b) in streaming_results.iter().zip(batch_results.iter()) {
186 approx::assert_relative_eq!(s.0, b.0, epsilon = 1e-10);
187 approx::assert_relative_eq!(s.1, b.1, epsilon = 1e-10);
188 }
189 }
190 }
191}