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 Ok(Self {
57 period,
58 sma: Sma::new(period)?,
59 tr: TrueRange::new(),
60 ema_tr: Ema::new(period)?,
61 current: None,
62 })
63 }
64
65 pub const fn period(&self) -> usize {
67 self.period
68 }
69}
70
71impl Indicator for Pgo {
72 type Input = Candle;
73 type Output = f64;
74
75 fn update(&mut self, candle: Candle) -> Option<f64> {
76 let mean = self.sma.update(candle.close);
77 let tr = self.tr.update(candle).expect("TrueRange always emits");
80 let ema_tr = self.ema_tr.update(tr);
81 let mean = mean?;
82 let ema_tr = ema_tr?;
83 if ema_tr <= 0.0 {
84 return self.current;
87 }
88 let value = (candle.close - mean) / ema_tr;
89 self.current = Some(value);
90 Some(value)
91 }
92
93 fn reset(&mut self) {
94 self.sma.reset();
95 self.tr.reset();
96 self.ema_tr.reset();
97 self.current = None;
98 }
99
100 fn warmup_period(&self) -> usize {
101 self.period
104 }
105
106 fn is_ready(&self) -> bool {
107 self.current.is_some()
108 }
109
110 fn name(&self) -> &'static str {
111 "PGO"
112 }
113}
114
115#[cfg(test)]
116mod tests {
117 use super::*;
118 use crate::traits::BatchExt;
119 use approx::assert_relative_eq;
120
121 fn candle(close: f64, high: f64, low: f64, ts: i64) -> Candle {
122 Candle::new(close, high, low, close, 1.0, ts).unwrap()
123 }
124
125 #[test]
126 fn rejects_zero_period() {
127 assert!(matches!(Pgo::new(0), Err(Error::PeriodZero)));
128 }
129
130 #[test]
131 fn accessors_and_metadata() {
132 let mut p = Pgo::new(14).unwrap();
133 assert_eq!(p.period(), 14);
134 assert_eq!(p.warmup_period(), 14);
135 assert_eq!(p.name(), "PGO");
136 assert!(!p.is_ready());
137 for i in 0..14 {
138 p.update(candle(10.0, 11.0, 9.0, i));
139 }
140 assert!(p.is_ready());
141 }
142
143 #[test]
144 fn flat_close_yields_zero_numerator() {
145 let mut p = Pgo::new(5).unwrap();
148 let mut out = None;
149 for i in 0..20 {
150 out = p.update(candle(10.0, 11.0, 9.0, i));
151 }
152 let v = out.unwrap();
153 assert_relative_eq!(v, 0.0, epsilon = 1e-12);
154 }
155
156 #[test]
157 fn warmup_emits_first_value_at_period() {
158 let mut p = Pgo::new(3).unwrap();
159 for i in 0..2 {
160 assert_eq!(p.update(candle(10.0, 11.0, 9.0, i)), None);
161 }
162 assert!(p.update(candle(10.0, 11.0, 9.0, 2)).is_some());
163 }
164
165 #[test]
166 fn close_above_mean_is_positive() {
167 let mut p = Pgo::new(5).unwrap();
169 for i in 0..20 {
170 let c = 10.0 + f64::from(i);
171 p.update(candle(c, c + 0.5, c - 0.5, i64::from(i)));
172 }
173 let last = p.update(candle(40.0, 40.5, 39.5, 20)).expect("PGO is warm");
175 assert!(
176 last > 0.0,
177 "PGO on rising series should be positive: {last}"
178 );
179 }
180
181 #[test]
182 fn zero_tr_holds_value() {
183 let mut p = Pgo::new(3).unwrap();
186 p.update(candle(10.0, 10.0, 10.0, 0));
187 p.update(candle(10.0, 10.0, 10.0, 1));
188 let v = p.update(candle(10.0, 10.0, 10.0, 2));
189 assert!(v.is_none(), "expected hold, got {v:?}");
192 }
193
194 #[test]
195 fn batch_equals_streaming() {
196 let candles: Vec<Candle> = (0..60_i64)
197 .map(|i| {
198 let c = 100.0 + (i as f64 * 0.3).sin() * 8.0;
199 candle(c, c + 1.0, c - 1.0, i)
200 })
201 .collect();
202 let batch = Pgo::new(14).unwrap().batch(&candles);
203 let mut b = Pgo::new(14).unwrap();
204 let streamed: Vec<_> = candles.iter().map(|c| b.update(*c)).collect();
205 assert_eq!(batch, streamed);
206 }
207
208 #[test]
209 fn reset_clears_state() {
210 let mut p = Pgo::new(5).unwrap();
211 for i in 0..20 {
212 p.update(candle(10.0, 11.0, 9.0, i));
213 }
214 assert!(p.is_ready());
215 p.reset();
216 assert!(!p.is_ready());
217 assert_eq!(p.update(candle(10.0, 11.0, 9.0, 0)), None);
218 }
219}