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