wickra_core/indicators/
polarized_fractal_efficiency.rs1use std::collections::VecDeque;
4
5use crate::error::{Error, Result};
6use crate::indicators::ema::Ema;
7use crate::traits::Indicator;
8
9#[derive(Debug, Clone)]
42pub struct PolarizedFractalEfficiency {
43 period: usize,
44 smoothing: usize,
45 closes: VecDeque<f64>,
46 prev_close: Option<f64>,
47 segments: VecDeque<f64>,
48 segment_sum: f64,
49 ema: Ema,
50}
51
52impl PolarizedFractalEfficiency {
53 pub fn new(period: usize, smoothing: usize) -> Result<Self> {
60 if period == 0 {
61 return Err(Error::PeriodZero);
62 }
63 if period > crate::error::MAX_PERIOD {
64 return Err(Error::InvalidPeriod {
65 message: crate::error::PERIOD_ABOVE_MAX,
66 });
67 }
68 Ok(Self {
69 period,
70 smoothing,
71 closes: VecDeque::with_capacity(period + 1),
72 prev_close: None,
73 segments: VecDeque::with_capacity(period),
74 segment_sum: 0.0,
75 ema: Ema::new(smoothing)?,
76 })
77 }
78
79 pub const fn periods(&self) -> (usize, usize) {
81 (self.period, self.smoothing)
82 }
83}
84
85impl Indicator for PolarizedFractalEfficiency {
86 type Input = f64;
87 type Output = f64;
88
89 #[inline]
90 fn update(&mut self, close: f64) -> Option<f64> {
91 if !close.is_finite() {
92 return None;
93 }
94 if let Some(prev) = self.prev_close {
95 let diff = close - prev;
96 let segment = diff.mul_add(diff, 1.0).sqrt();
97 self.segment_sum += segment;
98 self.segments.push_back(segment);
99 if self.segments.len() > self.period {
100 self.segment_sum -= self.segments.pop_front().unwrap_or(0.0);
101 }
102 }
103 self.prev_close = Some(close);
104
105 self.closes.push_back(close);
106 if self.closes.len() > self.period + 1 {
107 self.closes.pop_front();
108 }
109 if self.closes.len() <= self.period {
110 return None;
111 }
112
113 let oldest = *self.closes.front().unwrap_or(&close);
114 let net = close - oldest;
115 let direction = if net > 0.0 {
116 1.0
117 } else if net < 0.0 {
118 -1.0
119 } else {
120 0.0
121 };
122 let span = self.period as f64;
123 let straight = net.mul_add(net, span * span).sqrt();
124 let raw = 100.0 * direction * straight / self.segment_sum;
125 self.ema.update(raw)
126 }
127
128 fn reset(&mut self) {
129 self.closes.clear();
130 self.prev_close = None;
131 self.segments.clear();
132 self.segment_sum = 0.0;
133 self.ema.reset();
134 }
135
136 #[inline]
137 fn warmup_period(&self) -> usize {
138 self.period + self.smoothing
139 }
140
141 #[inline]
142 fn is_ready(&self) -> bool {
143 self.ema.is_ready()
144 }
145
146 #[inline]
147 fn name(&self) -> &'static str {
148 "PolarizedFractalEfficiency"
149 }
150}
151
152#[cfg(test)]
153mod tests {
154 use super::*;
155 use crate::traits::BatchExt;
156 use approx::assert_relative_eq;
157
158 #[test]
159 fn rejects_zero_period() {
160 assert!(matches!(
161 PolarizedFractalEfficiency::new(0, 5),
162 Err(Error::PeriodZero)
163 ));
164 assert!(matches!(
165 PolarizedFractalEfficiency::new(10, 0),
166 Err(Error::PeriodZero)
167 ));
168 }
169
170 #[test]
171 fn accessors_and_metadata() {
172 let pfe = PolarizedFractalEfficiency::new(10, 5).unwrap();
173 assert_eq!(pfe.periods(), (10, 5));
174 assert_eq!(pfe.warmup_period(), 15);
175 assert_eq!(pfe.name(), "PolarizedFractalEfficiency");
176 assert!(!pfe.is_ready());
177 }
178
179 #[test]
180 fn warmup_emits_after_period_plus_smoothing() {
181 let mut pfe = PolarizedFractalEfficiency::new(4, 2).unwrap();
182 let inputs: Vec<f64> = (0..10).map(f64::from).collect();
185 let out = pfe.batch(&inputs);
186 assert!(out[4].is_none());
187 assert!(out[5].is_some());
188 }
189
190 #[test]
191 fn perfect_uptrend_is_strongly_positive() {
192 let mut pfe = PolarizedFractalEfficiency::new(5, 3).unwrap();
195 let inputs: Vec<f64> = (0..30).map(f64::from).collect();
196 let last = pfe.batch(&inputs).last().unwrap().unwrap();
197 assert!(last > 99.0, "pfe {last} should be near +100");
198 }
199
200 #[test]
201 fn perfect_downtrend_is_strongly_negative() {
202 let mut pfe = PolarizedFractalEfficiency::new(5, 3).unwrap();
203 let inputs: Vec<f64> = (0..30).map(|i| -f64::from(i)).collect();
204 let last = pfe.batch(&inputs).last().unwrap().unwrap();
205 assert!(last < -99.0, "pfe {last} should be near -100");
206 }
207
208 #[test]
209 fn flat_market_returns_zero() {
210 let mut pfe = PolarizedFractalEfficiency::new(5, 3).unwrap();
212 let inputs = [10.0; 20];
213 let last = pfe.batch(&inputs).last().unwrap().unwrap();
214 assert_relative_eq!(last, 0.0, epsilon = 1e-12);
215 }
216
217 #[test]
218 fn choppy_market_is_inefficient() {
219 let mut pfe = PolarizedFractalEfficiency::new(5, 3).unwrap();
222 let inputs: Vec<f64> = (0..40)
223 .map(|i| if i % 2 == 0 { 100.0 } else { 102.0 })
224 .collect();
225 let last = pfe.batch(&inputs).last().unwrap().unwrap();
226 assert!(
227 last.abs() < 60.0,
228 "choppy pfe {last} should be far from +-100"
229 );
230 }
231
232 #[test]
233 fn reset_clears_state() {
234 let mut pfe = PolarizedFractalEfficiency::new(5, 3).unwrap();
235 let inputs: Vec<f64> = (0..30).map(f64::from).collect();
236 pfe.batch(&inputs);
237 assert!(pfe.is_ready());
238 pfe.reset();
239 assert!(!pfe.is_ready());
240 assert_eq!(pfe.periods(), (5, 3));
241 }
242
243 #[test]
244 fn batch_equals_streaming() {
245 let inputs: Vec<f64> = (0..80)
246 .map(|i| 100.0 + (f64::from(i) * 0.3).sin() * 5.0)
247 .collect();
248 let mut a = PolarizedFractalEfficiency::new(10, 5).unwrap();
249 let mut b = PolarizedFractalEfficiency::new(10, 5).unwrap();
250 assert_eq!(
251 a.batch(&inputs),
252 inputs.iter().map(|x| b.update(*x)).collect::<Vec<_>>()
253 );
254 }
255}