Voss Predictive Filter (VOSS)

Parameters: period = 20 | predict = 3 | bandwidth = 0.25

Overview

John Ehlers introduced the Voss Predictive Filter in August 2019, adapting Henning U. Voss's mathematical work on negative delay filters into a practical trading tool. The indicator generates two outputs: a two-pole band-pass filter that tracks cyclical price movements, and a predictive line that anticipates future price direction by subtracting weighted historical values from the current filtered signal. While the filter cannot actually see into the future, its negative group delay provides signals earlier than conventional indicators, potentially offering traders an edge through advanced warning of price turns. Ehlers reduced Voss's complex mathematical framework into just six lines of code, making sophisticated signal processing accessible to traders. The predictive component works by analyzing the cyclic nature of price movements and projecting where the cycle should go next based on its current phase. Traders typically watch for crossovers between the predictive line and the filter line to identify potential entry and exit points, with the predictive line leading the filter during trend changes.

Defaults: period=20, predict=3, bandwidth=0.25.

Implementation Examples

Compute VOSS from prices or candles:

use vector_ta::indicators::voss::{voss, VossInput, VossParams};
use vector_ta::utilities::data_loader::{Candles, read_candles_from_csv};

// From a price slice
let prices = vec![100.0, 101.5, 103.0, 102.0, 104.5];
let params = VossParams { period: Some(20), predict: Some(3), bandwidth: Some(0.25) };
let input = VossInput::from_slice(&prices, params);
let out = voss(&input)?; // out.voss and out.filt (Vec<f64>)

// From candles with defaults (period=20, predict=3, bandwidth=0.25; source="close")
let candles: Candles = read_candles_from_csv("data/sample.csv")?;
let input = VossInput::with_default_candles(&candles);
let out = voss(&input)?;

// Access both outputs
for (v, f) in out.voss.iter().zip(out.filt.iter()) {
    println!("voss: {v}, filt: {f}");
}

API Reference

Input Methods
// From price slice
VossInput::from_slice(&[f64], VossParams) -> VossInput

// From candles with custom source
VossInput::from_candles(&Candles, &str, VossParams) -> VossInput

// From candles with defaults (close, period=20, predict=3, bandwidth=0.25)
VossInput::with_default_candles(&Candles) -> VossInput
Parameters Structure
#[derive(Debug, Clone)]
pub struct VossParams {
    pub period: Option<usize>,   // Default: 20
    pub predict: Option<usize>,  // Default: 3
    pub bandwidth: Option<f64>,  // Default: 0.25
}
Output Structure
#[derive(Debug, Clone)]
pub struct VossOutput {
    pub voss: Vec<f64>, // predictive series
    pub filt: Vec<f64>, // two-pole filter
}
Validation, Warmup & NaNs
  • period > 0; period <= data.len(). Otherwise VossError::InvalidPeriod.
  • Input must contain at least one finite value; else VossError::AllValuesNaN.
  • Warmup length = first_non_nan + max(period, 5, 3×predict); if not enough valid points, VossError::NotEnoughValidData.
  • Outputs are NaN during warmup (prefix is filled via allocation helpers).
  • Streaming: VossStream::update returns None until warmup completes; then (voss, filt).
Error Handling
use vector_ta::indicators::voss::{voss, VossError};

match voss(&input) {
    Ok(out) => use_outputs(out.voss, out.filt),
    Err(VossError::EmptyInputData) => eprintln!("Input data is empty"),
    Err(VossError::AllValuesNaN) => eprintln!("All input values are NaN"),
    Err(VossError::InvalidPeriod { period, data_len }) =>
        eprintln!("Invalid period {} for data length {}", period, data_len),
    Err(VossError::NotEnoughValidData { needed, valid }) =>
        eprintln!("Need {} valid points, only {} found", needed, valid),
}

Python Bindings

Basic Usage

Calculate VOSS (returns both voss and filt):

import numpy as np
from vector_ta import voss

prices = np.array([100.0, 101.5, 103.0, 102.0, 104.5], dtype=np.float64)

# Defaults: period=20, predict=3, bandwidth=0.25
voss_vals, filt_vals = voss(prices)

# Custom parameters and optional kernel
voss_vals, filt_vals = voss(prices, period=20, predict=3, bandwidth=0.25, kernel="auto")

print(voss_vals.shape, filt_vals.shape)
Streaming Real-time Updates
from vector_ta import VossStream

stream = VossStream(period=20, predict=3, bandwidth=0.25)
for price in price_feed:
    pair = stream.update(price)  # None during warmup
    if pair is not None:
        voss_val, filt_val = pair
        consume(voss_val, filt_val)
Batch Parameter Sweep
import numpy as np
from vector_ta import voss_batch

prices = np.array([...], dtype=np.float64)

res = voss_batch(
    prices,
    period_range=(10, 30, 5),
    predict_range=(1, 4, 1),
    bandwidth_range=(0.10, 0.40, 0.10),
    kernel="auto"
)

# res is a dict with 'voss', 'filt', 'periods', 'predicts', 'bandwidths'
print(res['voss'].shape, res['filt'].shape)
CUDA Acceleration

CUDA helpers are available when the Python package is built with CUDA support. Inputs must be float32; outputs are device arrays (DLPack / __cuda_array_interface__ compatible).

import numpy as np
from vector_ta import voss_cuda_batch_dev, voss_cuda_many_series_one_param_dev

# One series (float32)
data_f32 = np.asarray(load_data(), dtype=np.float32)

dev = voss_cuda_batch_dev(
    data_f32=data_f32,
    period=(5, 30, 5),
    predict=(2, 20, 2),
    bandwidth=(0.5, 2.0, 0.5),
    device_id=0,
)

# Many series (time-major)
data_tm_f32 = np.asarray(load_data_time_major_matrix(), dtype=np.float32)

dev_tm = voss_cuda_many_series_one_param_dev(
    data_tm_f32=data_tm_f32,
    period=14,
    predict=14,
    bandwidth=1.0,
    device_id=0,
)

JavaScript/WASM Bindings

Basic Usage

Calculate VOSS in JavaScript/TypeScript:

import { voss_js } from 'vectorta-wasm';

const prices = new Float64Array([100, 101.5, 103, 102, 104.5]);

// Returns an object with { voss, filt }
const out = await voss_js(prices, 20, 3, 0.25);
console.log(out.voss.length, out.filt.length);
Memory-Efficient Operations

Zero‑copy in/out using WASM memory and voss_into:

import { voss_alloc, voss_free, voss_into, memory } from 'vectorta-wasm';

const prices = new Float64Array([/* your data */]);
const n = prices.length;

const inPtr = voss_alloc(n);
const vossPtr = voss_alloc(n);
const filtPtr = voss_alloc(n);

// Copy input into WASM memory
new Float64Array(memory.buffer, inPtr, n).set(prices);

// Args: in_ptr, voss_ptr, filt_ptr, len, period, predict, bandwidth
await voss_into(inPtr, vossPtr, filtPtr, n, 20, 3, 0.25);

// Read results (slice to copy)
const vossVals = new Float64Array(memory.buffer, vossPtr, n).slice();
const filtVals = new Float64Array(memory.buffer, filtPtr, n).slice();

voss_free(inPtr, n);
voss_free(vossPtr, n);
voss_free(filtPtr, n);
Batch Processing

Compute many parameter combos efficiently:

import { voss_batch as voss_batch_js, voss_batch_metadata_js } from 'vectorta-wasm';

const prices = new Float64Array([/* historical prices */]);

// Metadata: triples [period, predict, bandwidth, ...]
const md = await voss_batch_metadata_js(10, 30, 5, 1, 4, 1, 0.1, 0.4, 0.1);

// Unified batch API (returns { voss, filt, combos, rows, cols })
const out = await voss_batch_js(prices, {
  period_range: [10, 30, 5],
  predict_range: [1, 4, 1],
  bandwidth_range: [0.1, 0.4, 0.1]
});

console.log(out.rows, out.cols, out.combos.length);

CUDA Bindings (Rust)

use vector_ta::cuda::CudaVoss;
use vector_ta::indicators::voss::VossBatchRange;

let cuda = CudaVoss::new(0)?;

let data_f32: [f32] = /* ... */;
let sweep = VossBatchRange::default();

let out = cuda.voss_batch_dev(&data_f32, &sweep)?;
let _ = out;

Performance Analysis

Comparison:
View:
Loading chart...

AMD Ryzen 9 9950X (CPU) | NVIDIA RTX 4090 (GPU) | Benchmarks: 2026-01-08

Related Indicators