use std::collections::VecDeque;
use crate::error::{Error, Result};
use crate::indicators::ema::Ema;
use crate::traits::Indicator;
#[derive(Debug, Clone)]
pub struct PolarizedFractalEfficiency {
period: usize,
smoothing: usize,
closes: VecDeque<f64>,
prev_close: Option<f64>,
segments: VecDeque<f64>,
segment_sum: f64,
ema: Ema,
}
impl PolarizedFractalEfficiency {
pub fn new(period: usize, smoothing: usize) -> Result<Self> {
if period == 0 {
return Err(Error::PeriodZero);
}
Ok(Self {
period,
smoothing,
closes: VecDeque::with_capacity(period + 1),
prev_close: None,
segments: VecDeque::with_capacity(period),
segment_sum: 0.0,
ema: Ema::new(smoothing)?,
})
}
pub const fn periods(&self) -> (usize, usize) {
(self.period, self.smoothing)
}
}
impl Indicator for PolarizedFractalEfficiency {
type Input = f64;
type Output = f64;
fn update(&mut self, close: f64) -> Option<f64> {
if let Some(prev) = self.prev_close {
let diff = close - prev;
let segment = diff.mul_add(diff, 1.0).sqrt();
self.segment_sum += segment;
self.segments.push_back(segment);
if self.segments.len() > self.period {
self.segment_sum -= self.segments.pop_front().unwrap_or(0.0);
}
}
self.prev_close = Some(close);
self.closes.push_back(close);
if self.closes.len() > self.period + 1 {
self.closes.pop_front();
}
if self.closes.len() <= self.period {
return None;
}
let oldest = *self.closes.front().unwrap_or(&close);
let net = close - oldest;
let direction = if net > 0.0 {
1.0
} else if net < 0.0 {
-1.0
} else {
0.0
};
let span = self.period as f64;
let straight = net.mul_add(net, span * span).sqrt();
let raw = 100.0 * direction * straight / self.segment_sum;
self.ema.update(raw)
}
fn reset(&mut self) {
self.closes.clear();
self.prev_close = None;
self.segments.clear();
self.segment_sum = 0.0;
self.ema.reset();
}
fn warmup_period(&self) -> usize {
self.period + self.smoothing
}
fn is_ready(&self) -> bool {
self.ema.is_ready()
}
fn name(&self) -> &'static str {
"PolarizedFractalEfficiency"
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::traits::BatchExt;
use approx::assert_relative_eq;
#[test]
fn rejects_zero_period() {
assert!(matches!(
PolarizedFractalEfficiency::new(0, 5),
Err(Error::PeriodZero)
));
assert!(matches!(
PolarizedFractalEfficiency::new(10, 0),
Err(Error::PeriodZero)
));
}
#[test]
fn accessors_and_metadata() {
let pfe = PolarizedFractalEfficiency::new(10, 5).unwrap();
assert_eq!(pfe.periods(), (10, 5));
assert_eq!(pfe.warmup_period(), 15);
assert_eq!(pfe.name(), "PolarizedFractalEfficiency");
assert!(!pfe.is_ready());
}
#[test]
fn warmup_emits_after_period_plus_smoothing() {
let mut pfe = PolarizedFractalEfficiency::new(4, 2).unwrap();
let inputs: Vec<f64> = (0..10).map(f64::from).collect();
let out = pfe.batch(&inputs);
assert!(out[4].is_none());
assert!(out[5].is_some());
}
#[test]
fn perfect_uptrend_is_strongly_positive() {
let mut pfe = PolarizedFractalEfficiency::new(5, 3).unwrap();
let inputs: Vec<f64> = (0..30).map(f64::from).collect();
let last = pfe.batch(&inputs).last().unwrap().unwrap();
assert!(last > 99.0, "pfe {last} should be near +100");
}
#[test]
fn perfect_downtrend_is_strongly_negative() {
let mut pfe = PolarizedFractalEfficiency::new(5, 3).unwrap();
let inputs: Vec<f64> = (0..30).map(|i| -f64::from(i)).collect();
let last = pfe.batch(&inputs).last().unwrap().unwrap();
assert!(last < -99.0, "pfe {last} should be near -100");
}
#[test]
fn flat_market_returns_zero() {
let mut pfe = PolarizedFractalEfficiency::new(5, 3).unwrap();
let inputs = [10.0; 20];
let last = pfe.batch(&inputs).last().unwrap().unwrap();
assert_relative_eq!(last, 0.0, epsilon = 1e-12);
}
#[test]
fn choppy_market_is_inefficient() {
let mut pfe = PolarizedFractalEfficiency::new(5, 3).unwrap();
let inputs: Vec<f64> = (0..40)
.map(|i| if i % 2 == 0 { 100.0 } else { 102.0 })
.collect();
let last = pfe.batch(&inputs).last().unwrap().unwrap();
assert!(
last.abs() < 60.0,
"choppy pfe {last} should be far from +-100"
);
}
#[test]
fn reset_clears_state() {
let mut pfe = PolarizedFractalEfficiency::new(5, 3).unwrap();
let inputs: Vec<f64> = (0..30).map(f64::from).collect();
pfe.batch(&inputs);
assert!(pfe.is_ready());
pfe.reset();
assert!(!pfe.is_ready());
assert_eq!(pfe.periods(), (5, 3));
}
#[test]
fn batch_equals_streaming() {
let inputs: Vec<f64> = (0..80)
.map(|i| 100.0 + (f64::from(i) * 0.3).sin() * 5.0)
.collect();
let mut a = PolarizedFractalEfficiency::new(10, 5).unwrap();
let mut b = PolarizedFractalEfficiency::new(10, 5).unwrap();
assert_eq!(
a.batch(&inputs),
inputs.iter().map(|x| b.update(*x)).collect::<Vec<_>>()
);
}
}