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