Band-Pass Filter (Ehlers)

Parameters: period = 20 | bandwidth = 0.3 (0–1)

Overview

The Bandpass Filter extracts specific cyclical frequencies from price data by eliminating both long term trends and short term noise, revealing hidden market rhythms that drive price oscillations. John Ehlers developed this two stage filtering process where a high pass filter first removes trend bias, then a resonant bandpass isolates the target frequency determined by the period parameter. The bandwidth setting controls selectivity, with lower values creating sharper filters that focus on precise cycles, while higher values capture broader frequency ranges. The filter outputs four signals: raw bandpass values showing cycle amplitude, normalized readings constrained between -1 and 1 for consistent comparison, a trigger line for timing entries, and discrete signals marking cycle transitions. Traders use bandpass filters to identify dominant market cycles, time reversals when price reaches cycle extremes, and confirm trend changes when the filter crosses its trigger line. The indicator excels in ranging markets where cycles repeat predictably but produces false signals during strong trending moves that overwhelm cyclical behavior.

Implementation Examples

Compute Band-Pass from slices or candles:

use vectorta::indicators::bandpass::{bandpass, BandPassInput, BandPassParams};
use vectorta::utilities::data_loader::{Candles, read_candles_from_csv};

// From a price slice
let prices = vec![100.0, 101.5, 103.0, 102.2, 104.7];
let params = BandPassParams { period: Some(20), bandwidth: Some(0.3) };
let input = BandPassInput::from_slice(&prices, params);
let out = bandpass(&input)?;

// From Candles with defaults (period=20, bandwidth=0.3; source="close")
let candles: Candles = read_candles_from_csv("data/sample.csv")?;
let input = BandPassInput::with_default_candles(&candles);
let out = bandpass(&input)?;

// Access outputs
println!("bp[last] = {}", out.bp.last().unwrap());
println!("bpn[last] = {}", out.bp_normalized.last().unwrap());
println!("signal[last] = {}", out.signal.last().unwrap());
println!("trigger[last] = {}", out.trigger.last().unwrap());

API Reference

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

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

// From candles with default params (close, period=20, bandwidth=0.3)
BandPassInput::with_default_candles(&Candles) -> BandPassInput
Parameters Structure
pub struct BandPassParams {
    pub period: Option<usize>,   // Default: 20 (must be >= 2)
    pub bandwidth: Option<f64>,  // Default: 0.3 (in [0,1])
}
Output Structure
pub struct BandPassOutput {
    pub bp: Vec<f64>,            // raw band-pass values
    pub bp_normalized: Vec<f64>, // normalized [-1, 1]
    pub signal: Vec<f64>,        // { -1, 0, 1 } from trigger crossover
    pub trigger: Vec<f64>,       // high-pass of bp_normalized
}
Validation, Warmup & NaNs
  • Errors: InvalidPeriod if period < 2; InvalidBandwidth if not [0,1] or non-finite.
  • Derived checks: HpPeriodTooSmall or TriggerPeriodTooSmall if computed periods < 2.
  • Data length: NotEnoughData if len(data) < period or empty.
  • Warmup: outputs are NaN until the HP stage stabilizes (≥2 bars); trigger requires additional warmup.
  • Normalization: decaying peak with k=0.991 keeps bp_normalized within ~[-1, 1].
  • Streaming: BandPassStream::update returns NaN for the first two updates, then finite values.
Error Handling
use vectorta::indicators::bandpass::{bandpass, BandPassError};

match bandpass(&input) {
    Ok(out) => use_outputs(out),
    Err(BandPassError::NotEnoughData { data_len, period }) =>
        eprintln!("Not enough data: len={}, period={}", data_len, period),
    Err(BandPassError::InvalidPeriod { period }) =>
        eprintln!("Invalid period: {}", period),
    Err(BandPassError::InvalidBandwidth { bandwidth }) =>
        eprintln!("Invalid bandwidth: {}", bandwidth),
    Err(BandPassError::HpPeriodTooSmall { hp_period }) =>
        eprintln!("Derived hp_period too small: {}", hp_period),
    Err(BandPassError::TriggerPeriodTooSmall { trigger_period }) =>
        eprintln!("Derived trigger_period too small: {}", trigger_period),
    Err(BandPassError::DestLenMismatch) =>
        eprintln!("Destination slice length mismatch"),
    Err(BandPassError::HighPassError(e)) =>
        eprintln!("HighPass error: {}", e),
}

Python Bindings

Basic Usage

Compute Band-Pass from NumPy arrays (defaults: period=20, bandwidth=0.3):

import numpy as np
from vectorta import bandpass

prices = np.array([100.0, 101.5, 103.0, 102.2, 104.7], dtype=float)

# Defaults
out = bandpass(prices)

# Custom parameters and kernel selection ("auto", "avx2", "avx512")
out = bandpass(prices, period=20, bandwidth=0.3, kernel="auto")

# out is a dict with 4 numpy arrays
bp = out["bp"]
bpn = out["bp_normalized"]
sig = out["signal"]
trg = out["trigger"]
print(bp[-1], bpn[-1], sig[-1], trg[-1])
Streaming Real-time Updates

Update a Band-Pass stream per tick:

import math
from vectorta import BandPassStream

stream = BandPassStream(20, 0.3)
for price in price_feed:
    v = stream.update(price)
    if math.isnan(v):
        # warmup (first two updates)
        continue
    handle(v)
Batch Parameter Optimization

Test multiple (period, bandwidth) combinations:

import numpy as np
from vectorta import bandpass_batch

prices = np.array([...], dtype=float)
res = bandpass_batch(
    prices,
    period_range=(10, 30, 5),
    bandwidth_range=(0.2, 0.5, 0.1),
    kernel="auto",
)

# res is a dict of 2D arrays shaped [rows, cols]
bp  = res["bp"]
bpn = res["bp_normalized"]
sig = res["signal"]
trg = res["trigger"]

# Metadata
periods = res["periods"]
bandwidths = res["bandwidths"]
CUDA Acceleration

CUDA support for Band-Pass is coming soon. API will follow the pattern used by other CUDA-enabled indicators.

JavaScript/WASM Bindings

Basic Usage

Compute all four outputs in one call:

import { bandpass_js } from 'vectorta-wasm';

const prices = new Float64Array([/* your data */]);
const res = bandpass_js(prices, 20, 0.3);

// res has shape: { values: Float64Array, rows: 4, cols: prices.length }
const values = res.values as Float64Array;
const cols = res.cols as number;
const bp  = values.slice(0, cols);
const bpn = values.slice(cols, 2*cols);
const sig = values.slice(2*cols, 3*cols);
const trg = values.slice(3*cols, 4*cols);
Memory-Efficient Operations

Use zero-copy buffers with explicit allocation:

import { bandpass_alloc, bandpass_free, bandpass_into, memory } from 'vectorta-wasm';

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

// Allocate WASM memory for input and each output
const inPtr  = bandpass_alloc(len);
const bpPtr  = bandpass_alloc(len);
const bpnPtr = bandpass_alloc(len);
const sigPtr = bandpass_alloc(len);
const trgPtr = bandpass_alloc(len);

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

// Compute directly into output buffers
bandpass_into(inPtr, len, /*period=*/20, /*bandwidth=*/0.3, bpPtr, bpnPtr, sigPtr, trgPtr);

// Read results (slice to copy out if needed)
const bp  = new Float64Array(memory.buffer, bpPtr,  len).slice();
const bpn = new Float64Array(memory.buffer, bpnPtr, len).slice();
const sig = new Float64Array(memory.buffer, sigPtr, len).slice();
const trg = new Float64Array(memory.buffer, trgPtr, len).slice();

// Free when done
bandpass_free(inPtr, len);
bandpass_free(bpPtr, len);
bandpass_free(bpnPtr, len);
bandpass_free(sigPtr, len);
bandpass_free(trgPtr, len);
Batch Processing

Evaluate multiple parameter combos:

import { bandpass_batch_js, bandpass_batch_metadata_js } from 'vectorta-wasm';

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

// Get flat [period, bandwidth, period, bandwidth, ...]
const meta = bandpass_batch_metadata_js(10, 30, 5, 0.2, 0.5, 0.1);
const combos = meta.length / 2;

// Compute all combos
const res = bandpass_batch_js(prices, {
  period_range: [10, 30, 5],
  bandwidth_range: [0.2, 0.5, 0.1]
});

// res: { values: Float64Array, combos: number, outputs: 4, cols: number }
const values = res.values as Float64Array;
const cols = res.cols as number;
const rowsPerOutput = combos;

// Block 0..combos: bp rows; next combos: bpn; then signal; then trigger
const bpBlock  = values.slice(0, rowsPerOutput * cols);
const bpnBlock = values.slice(rowsPerOutput * cols, 2 * rowsPerOutput * cols);
const sigBlock = values.slice(2 * rowsPerOutput * cols, 3 * rowsPerOutput * cols);
const trgBlock = values.slice(3 * rowsPerOutput * cols, 4 * rowsPerOutput * cols);

Performance Analysis

Comparison:
View:
Loading chart...

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

CUDA note

In our benchmark workload, the Rust CPU implementation is faster than CUDA for this indicator. Prefer the Rust/CPU path unless your workload differs.

Related Indicators