use crate::indicators::metadata::{IndicatorMetadata, ParamDef};
use crate::traits::Next;
use crate::indicators::bandpass::BandPass;
use std::collections::VecDeque;
#[derive(Debug, Clone)]
pub struct VossPredictor {
bandpass: BandPass,
order: usize,
voss_history: VecDeque<f64>,
}
impl VossPredictor {
pub fn new(period: usize, predict: usize) -> Self {
let order = 3 * predict;
Self {
bandpass: BandPass::new(period, 0.25), order,
voss_history: VecDeque::with_capacity(order + 1),
}
}
}
impl Default for VossPredictor {
fn default() -> Self {
Self::new(20, 3)
}
}
impl Next<f64> for VossPredictor {
type Output = (f64, f64);
fn next(&mut self, input: f64) -> Self::Output {
let filt = self.bandpass.next(input);
let mut sum_c = 0.0;
if self.order > 0 {
for count in 0..self.order {
let idx = self.order - count;
let val = if idx <= self.voss_history.len() {
self.voss_history[idx - 1]
} else {
0.0
};
sum_c += ((count + 1) as f64 / self.order as f64) * val;
}
}
let voss = ((3.0 + self.order as f64) / 2.0) * filt - sum_c;
self.voss_history.push_front(voss);
if self.voss_history.len() > self.order {
self.voss_history.pop_back();
}
(filt, voss)
}
}
pub const VOSS_PREDICTOR_METADATA: IndicatorMetadata = IndicatorMetadata {
name: "VossPredictor",
description: "A predictive filter with negative group delay for band-limited signals.",
params: &[
ParamDef { name: "period", default: "20", description: "Center period of the BandPass filter" },
ParamDef { name: "predict", default: "3", description: "Number of bars of prediction" },
],
formula_source: "https://github.com/lavs9/quantwave/blob/main/references/Ehlers%20Papers/A%20PEEK%20INTO%20THE%20FUTURE.pdf",
formula_latex: r#"
\[
Filt = \text{BandPass}(Price, Period, 0.25)
\]
\[
Order = 3 \cdot Predict
\]
\[
SumC = \sum_{n=0}^{Order-1} \frac{n+1}{Order} Voss_{t-(Order-n)}
\]
\[
Voss = \frac{3 + Order}{2} Filt - SumC
\]
"#,
gold_standard_file: "voss_predictor.json",
category: "Ehlers DSP",
};
#[cfg(test)]
mod tests {
use super::*;
use crate::traits::Next;
use crate::test_utils::{load_gold_standard_tuple, assert_indicator_parity_tuple};
use proptest::prelude::*;
#[test]
fn test_voss_gold_standard() {
let case = load_gold_standard_tuple("voss_predictor");
let vp = VossPredictor::new(20, 3);
assert_indicator_parity_tuple(vp, &case.input, &case.expected);
}
#[test]
fn test_voss_basic() {
let mut vp = VossPredictor::default();
let inputs = vec![10.0, 11.0, 12.0, 13.0, 14.0, 15.0];
for input in inputs {
let (filt, voss) = vp.next(input);
assert!(!filt.is_nan());
assert!(!voss.is_nan());
}
}
proptest! {
#[test]
fn test_voss_parity(
inputs in prop::collection::vec(1.0..100.0, 60..120),
) {
let period = 20;
let predict = 3;
let mut vp = VossPredictor::new(period, predict);
let streaming_results: Vec<(f64, f64)> = inputs.iter().map(|&x| vp.next(x)).collect();
let mut batch_results = Vec::with_capacity(inputs.len());
let mut bp = BandPass::new(period, 0.25);
let order = 3 * predict;
let mut v_hist = VecDeque::new();
for &input in &inputs {
let filt = bp.next(input);
let mut sum_c = 0.0;
for count in 0..order {
let idx = order - count;
let val = if idx <= v_hist.len() {
v_hist[idx - 1]
} else {
0.0
};
sum_c += ((count + 1) as f64 / order as f64) * val;
}
let voss = ((3.0 + order as f64) / 2.0) * filt - sum_c;
v_hist.push_front(voss);
if v_hist.len() > order {
v_hist.pop_back();
}
batch_results.push((filt, voss));
}
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);
}
}
}
}