Band-Pass Filter (Ehlers)

Parameters: period = 20 | bandwidth = 0.3 (1e-7–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 vector_ta::indicators::bandpass::{bandpass, BandPassInput, BandPassParams};
use vector_ta::utilities::data_loader::{Candles, read_candles_from_csv};

// From a price slice (needs enough data for derived hp_period)
let prices: Vec<f64> = vec![/* ... */];
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 or period > data_len; InvalidBandwidth if bandwidth is not in (0,1] or non-finite.
  • Derived checks: HpPeriodTooSmall or TriggerPeriodTooSmall if computed periods < 2.
  • Data validity: EmptyInputData if empty; AllValuesNaN if no finite values; NotEnoughValidData if there are fewer than period finite values after the first finite input.
  • Note: the internal high-pass stage uses a derived hp_period ≈ round(4·period/bandwidth); if hp_period exceeds your data length you may see HighPassError.
  • 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 vector_ta::indicators::bandpass::{bandpass, BandPassError};

match bandpass(&input) {
    Ok(out) => use_outputs(out),
    Err(BandPassError::EmptyInputData) =>
        eprintln!("empty input"),
    Err(BandPassError::AllValuesNaN) =>
        eprintln!("all input values are NaN"),
    Err(BandPassError::InvalidPeriod { period, data_len }) =>
        eprintln!("invalid period: {period} (data_len={data_len})"),
    Err(BandPassError::NotEnoughValidData { needed, valid }) =>
        eprintln!("need {needed} valid points, got {valid}"),
    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::HighPassError(e)) =>
        eprintln!("HighPass error: {}", e),
    Err(BandPassError::OutputLengthMismatch { expected, got }) =>
        eprintln!("Output length mismatch: expected {expected}, got {got}"),
    Err(e) => eprintln!("BandPass failed: {e}"),
}

Python Bindings

Basic Usage

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

import numpy as np
from vector_ta 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 vector_ta 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 vector_ta 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 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 bandpass_cuda_batch_dev, bandpass_cuda_many_series_one_param_dev

# One series (float32)
close_f32 = np.asarray(load_close(), dtype=np.float32)

dev = bandpass_cuda_batch_dev(
    close_f32=close_f32,
    period_range=(5, 30, 5),
    bandwidth_range=(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 = bandpass_cuda_many_series_one_param_dev(
    data_tm_f32=data_tm_f32,
    period=14,
    bandwidth=1.0,
    device_id=0,
)

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);

CUDA Bindings (Rust)

use vector_ta::cuda::CudaBandpassBatchResult;
use vector_ta::indicators::bandpass::BandPassBatchRange;

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

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

let out = cuda.bandpass_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-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