wickra_core/indicators/
pgo.rs1use crate::error::{Error, Result};
4use crate::indicators::ema::Ema;
5use crate::indicators::sma::Sma;
6use crate::indicators::true_range::TrueRange;
7use crate::ohlcv::Candle;
8use crate::traits::Indicator;
9
10#[derive(Debug, Clone)]
41pub struct Pgo {
42 period: usize,
43 sma: Sma,
44 tr: TrueRange,
45 ema_tr: Ema,
46 current: Option<f64>,
47}
48
49impl Pgo {
50 pub fn new(period: usize) -> Result<Self> {
53 if period == 0 {
54 return Err(Error::PeriodZero);
55 }
56 if period > crate::error::MAX_PERIOD {
57 return Err(Error::InvalidPeriod {
58 message: crate::error::PERIOD_ABOVE_MAX,
59 });
60 }
61 Ok(Self {
62 period,
63 sma: Sma::new(period)?,
64 tr: TrueRange::new(),
65 ema_tr: Ema::new(period)?,
66 current: None,
67 })
68 }
69
70 pub const fn period(&self) -> usize {
72 self.period
73 }
74}
75
76impl Indicator for Pgo {
77 type Input = Candle;
78 type Output = f64;
79
80 #[inline]
81 fn update(&mut self, candle: Candle) -> Option<f64> {
82 let mean = self.sma.update(candle.close);
83 let tr = self.tr.update(candle).expect("TrueRange always emits");
86 let ema_tr = self.ema_tr.update(tr);
87 let mean = mean?;
88 let ema_tr = ema_tr?;
89 if ema_tr <= 0.0 {
90 return self.current;
93 }
94 let value = (candle.close - mean) / ema_tr;
95 self.current = Some(value);
96 Some(value)
97 }
98
99 fn reset(&mut self) {
100 self.sma.reset();
101 self.tr.reset();
102 self.ema_tr.reset();
103 self.current = None;
104 }
105
106 #[inline]
107 fn warmup_period(&self) -> usize {
108 self.period
111 }
112
113 #[inline]
114 fn is_ready(&self) -> bool {
115 self.current.is_some()
116 }
117
118 #[inline]
119 fn name(&self) -> &'static str {
120 "PGO"
121 }
122}
123
124#[cfg(test)]
125mod tests {
126 use super::*;
127 use crate::traits::BatchExt;
128 use approx::assert_relative_eq;
129
130 fn candle(close: f64, high: f64, low: f64, ts: i64) -> Candle {
131 Candle::new(close, high, low, close, 1.0, ts).unwrap()
132 }
133
134 #[test]
135 fn rejects_zero_period() {
136 assert!(matches!(Pgo::new(0), Err(Error::PeriodZero)));
137 }
138
139 #[test]
140 fn accessors_and_metadata() {
141 let mut p = Pgo::new(14).unwrap();
142 assert_eq!(p.period(), 14);
143 assert_eq!(p.warmup_period(), 14);
144 assert_eq!(p.name(), "PGO");
145 assert!(!p.is_ready());
146 for i in 0..14 {
147 p.update(candle(10.0, 11.0, 9.0, i));
148 }
149 assert!(p.is_ready());
150 }
151
152 #[test]
153 fn flat_close_yields_zero_numerator() {
154 let mut p = Pgo::new(5).unwrap();
157 let mut out = None;
158 for i in 0..20 {
159 out = p.update(candle(10.0, 11.0, 9.0, i));
160 }
161 let v = out.unwrap();
162 assert_relative_eq!(v, 0.0, epsilon = 1e-12);
163 }
164
165 #[test]
166 fn warmup_emits_first_value_at_period() {
167 let mut p = Pgo::new(3).unwrap();
168 for i in 0..2 {
169 assert_eq!(p.update(candle(10.0, 11.0, 9.0, i)), None);
170 }
171 assert!(p.update(candle(10.0, 11.0, 9.0, 2)).is_some());
172 }
173
174 #[test]
175 fn close_above_mean_is_positive() {
176 let mut p = Pgo::new(5).unwrap();
178 for i in 0..20 {
179 let c = 10.0 + f64::from(i);
180 p.update(candle(c, c + 0.5, c - 0.5, i64::from(i)));
181 }
182 let last = p.update(candle(40.0, 40.5, 39.5, 20)).expect("PGO is warm");
184 assert!(
185 last > 0.0,
186 "PGO on rising series should be positive: {last}"
187 );
188 }
189
190 #[test]
191 fn zero_tr_holds_value() {
192 let mut p = Pgo::new(3).unwrap();
195 p.update(candle(10.0, 10.0, 10.0, 0));
196 p.update(candle(10.0, 10.0, 10.0, 1));
197 let v = p.update(candle(10.0, 10.0, 10.0, 2));
198 assert!(v.is_none(), "expected hold, got {v:?}");
201 }
202
203 #[test]
204 fn batch_equals_streaming() {
205 let candles: Vec<Candle> = (0..60_i64)
206 .map(|i| {
207 let c = 100.0 + (i as f64 * 0.3).sin() * 8.0;
208 candle(c, c + 1.0, c - 1.0, i)
209 })
210 .collect();
211 let batch = Pgo::new(14).unwrap().batch(&candles);
212 let mut b = Pgo::new(14).unwrap();
213 let streamed: Vec<_> = candles.iter().map(|c| b.update(*c)).collect();
214 assert_eq!(batch, streamed);
215 }
216
217 #[test]
218 fn reset_clears_state() {
219 let mut p = Pgo::new(5).unwrap();
220 for i in 0..20 {
221 p.update(candle(10.0, 11.0, 9.0, i));
222 }
223 assert!(p.is_ready());
224 p.reset();
225 assert!(!p.is_ready());
226 assert_eq!(p.update(candle(10.0, 11.0, 9.0, 0)), None);
227 }
228}