wickra_core/indicators/
pmo.rs1use crate::error::{Error, Result};
4use crate::traits::Indicator;
5
6use super::Ema;
7
8#[derive(Debug, Clone)]
39pub struct Pmo {
40 smoothing1: usize,
41 smoothing2: usize,
42 prev_price: Option<f64>,
43 ema1: Ema,
44 ema2: Ema,
45 current: Option<f64>,
46}
47
48impl Pmo {
49 pub fn new(smoothing1: usize, smoothing2: usize) -> Result<Self> {
57 if smoothing1 == 0 || smoothing2 == 0 {
58 return Err(Error::PeriodZero);
59 }
60 if smoothing1 < 2 || smoothing2 < 2 {
61 return Err(Error::InvalidPeriod {
62 message: "PMO smoothing periods must be >= 2",
63 });
64 }
65 Ok(Self {
66 smoothing1,
67 smoothing2,
68 prev_price: None,
69 ema1: Ema::with_alpha(2.0 / smoothing1 as f64)?,
70 ema2: Ema::with_alpha(2.0 / smoothing2 as f64)?,
71 current: None,
72 })
73 }
74
75 pub const fn periods(&self) -> (usize, usize) {
77 (self.smoothing1, self.smoothing2)
78 }
79
80 pub const fn value(&self) -> Option<f64> {
82 self.current
83 }
84}
85
86impl Indicator for Pmo {
87 type Input = f64;
88 type Output = f64;
89
90 fn update(&mut self, input: f64) -> Option<f64> {
91 if !input.is_finite() {
92 return self.current;
94 }
95 let Some(prev) = self.prev_price else {
96 self.prev_price = Some(input);
97 return None;
98 };
99 self.prev_price = Some(input);
100
101 let roc = if prev == 0.0 {
102 0.0
104 } else {
105 (input / prev - 1.0) * 100.0
106 };
107 let smoothed = self.ema1.update(roc)?;
108 let pmo = self.ema2.update(10.0 * smoothed)?;
109 self.current = Some(pmo);
110 Some(pmo)
111 }
112
113 fn reset(&mut self) {
114 self.prev_price = None;
115 self.ema1.reset();
116 self.ema2.reset();
117 self.current = None;
118 }
119
120 fn warmup_period(&self) -> usize {
121 2
124 }
125
126 fn is_ready(&self) -> bool {
127 self.current.is_some()
128 }
129
130 fn name(&self) -> &'static str {
131 "PMO"
132 }
133}
134
135#[cfg(test)]
136mod tests {
137 use super::*;
138 use crate::traits::BatchExt;
139 use approx::assert_relative_eq;
140
141 #[test]
142 fn new_rejects_zero_period() {
143 assert!(matches!(Pmo::new(0, 20), Err(Error::PeriodZero)));
144 assert!(matches!(Pmo::new(35, 0), Err(Error::PeriodZero)));
145 }
146
147 #[test]
148 fn new_rejects_period_one() {
149 assert!(matches!(Pmo::new(1, 20), Err(Error::InvalidPeriod { .. })));
150 assert!(matches!(Pmo::new(35, 1), Err(Error::InvalidPeriod { .. })));
151 }
152
153 #[test]
157 fn accessors_and_metadata() {
158 let mut pmo = Pmo::new(35, 20).unwrap();
159 assert_eq!(pmo.periods(), (35, 20));
160 assert_eq!(pmo.name(), "PMO");
161 assert_eq!(pmo.value(), None);
162 pmo.update(100.0);
163 pmo.update(101.0);
164 assert!(pmo.value().is_some());
165 }
166
167 #[test]
174 fn zero_previous_price_treats_roc_as_flat() {
175 let mut pmo = Pmo::new(2, 2).unwrap();
176 assert_eq!(pmo.update(0.0), None);
178 let out = pmo.update(50.0).expect("emits");
181 assert_eq!(out, 0.0);
182 }
183
184 #[test]
185 fn first_emission_at_second_update() {
186 let mut pmo = Pmo::new(35, 20).unwrap();
187 assert_eq!(pmo.warmup_period(), 2);
188 assert_eq!(pmo.update(100.0), None);
189 assert!(pmo.update(101.0).is_some());
190 }
191
192 #[test]
193 fn constant_series_yields_zero() {
194 let mut pmo = Pmo::new(35, 20).unwrap();
196 let out = pmo.batch(&[100.0; 60]);
197 for v in out.iter().skip(2).flatten() {
198 assert_relative_eq!(*v, 0.0, epsilon = 1e-12);
199 }
200 }
201
202 #[test]
203 fn steady_uptrend_is_positive() {
204 let mut pmo = Pmo::new(35, 20).unwrap();
205 let prices: Vec<f64> = (1..=120).map(|i| 100.0 * 1.01_f64.powi(i)).collect();
206 let out = pmo.batch(&prices);
207 let last = out.iter().rev().flatten().next().unwrap();
208 assert!(
209 *last > 0.0,
210 "steady uptrend PMO should be positive, got {last}"
211 );
212 }
213
214 #[test]
215 fn ignores_non_finite_input() {
216 let mut pmo = Pmo::new(35, 20).unwrap();
217 let out = pmo.batch(&(1..=60).map(f64::from).collect::<Vec<_>>());
218 let last = *out.last().unwrap();
219 assert!(last.is_some());
220 assert_eq!(pmo.update(f64::NAN), last);
221 assert_eq!(pmo.update(f64::INFINITY), last);
222 }
223
224 #[test]
225 fn reset_clears_state() {
226 let mut pmo = Pmo::new(35, 20).unwrap();
227 pmo.batch(&(1..=60).map(f64::from).collect::<Vec<_>>());
228 assert!(pmo.is_ready());
229 pmo.reset();
230 assert!(!pmo.is_ready());
231 assert_eq!(pmo.update(1.0), None);
232 }
233
234 #[test]
235 fn batch_equals_streaming() {
236 let prices: Vec<f64> = (1..=120)
237 .map(|i| 100.0 + (f64::from(i) * 0.25).sin() * 8.0)
238 .collect();
239 let batch = Pmo::new(35, 20).unwrap().batch(&prices);
240 let mut b = Pmo::new(35, 20).unwrap();
241 let streamed: Vec<_> = prices.iter().map(|p| b.update(*p)).collect();
242 assert_eq!(batch, streamed);
243 }
244}