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 #[inline]
91 fn update(&mut self, input: f64) -> Option<f64> {
92 if !input.is_finite() {
93 return None;
95 }
96 let Some(prev) = self.prev_price else {
97 self.prev_price = Some(input);
98 return None;
99 };
100 self.prev_price = Some(input);
101
102 let roc = if prev == 0.0 {
103 0.0
105 } else {
106 (input / prev - 1.0) * 100.0
107 };
108 let smoothed = self.ema1.update(roc)?;
109 let pmo = self.ema2.update(10.0 * smoothed)?;
110 self.current = Some(pmo);
111 Some(pmo)
112 }
113
114 fn reset(&mut self) {
115 self.prev_price = None;
116 self.ema1.reset();
117 self.ema2.reset();
118 self.current = None;
119 }
120
121 #[inline]
122 fn warmup_period(&self) -> usize {
123 2
126 }
127
128 #[inline]
129 fn is_ready(&self) -> bool {
130 self.current.is_some()
131 }
132
133 #[inline]
134 fn name(&self) -> &'static str {
135 "PMO"
136 }
137}
138
139#[cfg(test)]
140mod tests {
141 use super::*;
142 use crate::traits::BatchExt;
143 use approx::assert_relative_eq;
144
145 #[test]
146 fn new_rejects_zero_period() {
147 assert!(matches!(Pmo::new(0, 20), Err(Error::PeriodZero)));
148 assert!(matches!(Pmo::new(35, 0), Err(Error::PeriodZero)));
149 }
150
151 #[test]
152 fn new_rejects_period_one() {
153 assert!(matches!(Pmo::new(1, 20), Err(Error::InvalidPeriod { .. })));
154 assert!(matches!(Pmo::new(35, 1), Err(Error::InvalidPeriod { .. })));
155 }
156
157 #[test]
161 fn accessors_and_metadata() {
162 let mut pmo = Pmo::new(35, 20).unwrap();
163 assert_eq!(pmo.periods(), (35, 20));
164 assert_eq!(pmo.name(), "PMO");
165 assert_eq!(pmo.value(), None);
166 pmo.update(100.0);
167 pmo.update(101.0);
168 assert!(pmo.value().is_some());
169 }
170
171 #[test]
178 fn zero_previous_price_treats_roc_as_flat() {
179 let mut pmo = Pmo::new(2, 2).unwrap();
180 assert_eq!(pmo.update(0.0), None);
182 let out = pmo.update(50.0).expect("emits");
185 assert_eq!(out, 0.0);
186 }
187
188 #[test]
189 fn first_emission_at_second_update() {
190 let mut pmo = Pmo::new(35, 20).unwrap();
191 assert_eq!(pmo.warmup_period(), 2);
192 assert_eq!(pmo.update(100.0), None);
193 assert!(pmo.update(101.0).is_some());
194 }
195
196 #[test]
197 fn constant_series_yields_zero() {
198 let mut pmo = Pmo::new(35, 20).unwrap();
200 let out = pmo.batch(&[100.0; 60]);
201 for v in out.iter().skip(2).flatten() {
202 assert_relative_eq!(*v, 0.0, epsilon = 1e-12);
203 }
204 }
205
206 #[test]
207 fn steady_uptrend_is_positive() {
208 let mut pmo = Pmo::new(35, 20).unwrap();
209 let prices: Vec<f64> = (1..=120).map(|i| 100.0 * 1.01_f64.powi(i)).collect();
210 let out = pmo.batch(&prices);
211 let last = out.iter().rev().flatten().next().unwrap();
212 assert!(
213 *last > 0.0,
214 "steady uptrend PMO should be positive, got {last}"
215 );
216 }
217
218 #[test]
219 fn ignores_non_finite_input() {
220 let mut pmo = Pmo::new(35, 20).unwrap();
221 let out = pmo.batch(&(1..=60).map(f64::from).collect::<Vec<_>>());
222 let last = *out.last().unwrap();
223 assert!(last.is_some());
224 assert_eq!(pmo.update(f64::NAN), None);
225 assert_eq!(pmo.update(f64::INFINITY), None);
226 }
227
228 #[test]
229 fn reset_clears_state() {
230 let mut pmo = Pmo::new(35, 20).unwrap();
231 pmo.batch(&(1..=60).map(f64::from).collect::<Vec<_>>());
232 assert!(pmo.is_ready());
233 pmo.reset();
234 assert!(!pmo.is_ready());
235 assert_eq!(pmo.update(1.0), None);
236 }
237
238 #[test]
239 fn batch_equals_streaming() {
240 let prices: Vec<f64> = (1..=120)
241 .map(|i| 100.0 + (f64::from(i) * 0.25).sin() * 8.0)
242 .collect();
243 let batch = Pmo::new(35, 20).unwrap().batch(&prices);
244 let mut b = Pmo::new(35, 20).unwrap();
245 let streamed: Vec<_> = prices.iter().map(|p| b.update(*p)).collect();
246 assert_eq!(batch, streamed);
247 }
248}