use crate::indicators::metadata::{IndicatorMetadata, ParamDef};
use crate::traits::Next;
use std::f64::consts::PI;
#[derive(Debug, Clone)]
pub struct ChannelCycle {
period: usize,
alpha: f64,
beta: f64,
price_window: Vec<f64>,
bp_history: [f64; 2],
detrended_prev: [f64; 2],
count: usize,
}
impl ChannelCycle {
pub fn new(period: usize) -> Self {
let delta = 0.1;
let beta = (2.0 * PI / period as f64).cos();
let gamma = 1.0 / (4.0 * PI * delta / period as f64).cos();
let alpha = gamma - (gamma * gamma - 1.0).sqrt();
Self {
period,
alpha,
beta,
price_window: Vec::with_capacity(period),
bp_history: [0.0; 2],
detrended_prev: [0.0; 2],
count: 0,
}
}
}
impl Default for ChannelCycle {
fn default() -> Self {
Self::new(20)
}
}
impl Next<f64> for ChannelCycle {
type Output = (f64, f64);
fn next(&mut self, input: f64) -> Self::Output {
self.count += 1;
self.price_window.push(input);
if self.price_window.len() > self.period {
self.price_window.remove(0);
}
if self.price_window.len() < self.period {
return (0.0, 0.0);
}
let mut high = f64::MIN;
let mut low = f64::MAX;
for &p in &self.price_window {
if p > high { high = p; }
if p < low { low = p; }
}
let detrended = if high != low {
(input - low) / (high - low) - 0.5
} else {
0.0
};
let bp = 0.5 * (1.0 - self.alpha) * (detrended - self.detrended_prev[1])
+ self.beta * (1.0 + self.alpha) * self.bp_history[0]
- self.alpha * self.bp_history[1];
let omega = 2.0 * PI / self.period as f64;
let leading = (bp - self.bp_history[0]) / omega;
self.bp_history[1] = self.bp_history[0];
self.bp_history[0] = bp;
self.detrended_prev[1] = self.detrended_prev[0];
self.detrended_prev[0] = detrended;
(bp, leading)
}
}
pub const CHANNEL_CYCLE_METADATA: IndicatorMetadata = IndicatorMetadata {
name: "ChannelCycle",
description: "Extracts cyclic components and a leading function using channel-normalized bandpass filtering.",
params: &[
ParamDef {
name: "period",
default: "20",
description: "Channel and Bandpass period",
},
],
formula_source: "https://github.com/lavs9/quantwave/blob/main/references/Ehlers%20Papers/InferringTradingStrategies.pdf",
formula_latex: r#"
\[
Detrended = \frac{Price - Low}{High - Low} - 0.5
\]
\[
BP = \text{Bandpass}(Detrended, Period)
\]
\[
Leading = \frac{BP - BP_{t-1}}{2\pi/Period}
\]
"#,
gold_standard_file: "channel_cycle.json",
category: "Ehlers DSP",
};
#[cfg(test)]
mod tests {
use super::*;
use crate::traits::Next;
use proptest::prelude::*;
#[test]
fn test_channel_cycle_basic() {
let mut cc = ChannelCycle::new(20);
for i in 0..100 {
let (s, l) = cc.next(100.0 + (i as f64 * 0.1).sin());
assert!(!s.is_nan());
assert!(!l.is_nan());
}
}
proptest! {
#[test]
fn test_channel_cycle_parity(
inputs in prop::collection::vec(1.0..100.0, 100..200),
) {
let period = 20;
let mut cc = ChannelCycle::new(period);
let streaming_results: Vec<(f64, f64)> = inputs.iter().map(|&x| cc.next(x)).collect();
let mut batch_results = Vec::with_capacity(inputs.len());
let delta = 0.1;
let beta = (2.0 * PI / period as f64).cos();
let gamma = 1.0 / (4.0 * PI * delta / period as f64).cos();
let alpha = gamma - (gamma * gamma - 1.0).sqrt();
let omega = 2.0 * PI / period as f64;
let mut detrended_vals = Vec::new();
let mut bp_vals = vec![0.0; inputs.len() + 2];
let mut d_vals = vec![0.0; inputs.len() + 2];
for (i, &input) in inputs.iter().enumerate() {
let start = if i >= period - 1 { i + 1 - period } else { 0 };
let window = &inputs[start..i + 1];
if window.len() < period {
batch_results.push((0.0, 0.0));
detrended_vals.push(0.0);
continue;
}
let mut high = f64::MIN;
let mut low = f64::MAX;
for &p in window {
if p > high { high = p; }
if p < low { low = p; }
}
let detrended = if high != low {
(input - low) / (high - low) - 0.5
} else {
0.0
};
detrended_vals.push(detrended);
let idx = i + 2;
d_vals[idx] = detrended;
let bp = 0.5 * (1.0 - alpha) * (d_vals[idx] - d_vals[idx-2])
+ beta * (1.0 + alpha) * bp_vals[idx-1]
- alpha * bp_vals[idx-2];
bp_vals[idx] = bp;
let leading = (bp - bp_vals[idx-1]) / omega;
batch_results.push((bp, leading));
}
for (s, b) in streaming_results.iter().zip(batch_results.iter()) {
approx::assert_relative_eq!(s.0, b.0, epsilon = 1e-10);
approx::assert_relative_eq!(s.1, b.1, epsilon = 1e-10);
}
}
}
}