Band-Pass Filter (Ehlers)
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:
InvalidPeriodifperiod < 2orperiod > data_len;InvalidBandwidthif bandwidth is not in(0,1]or non-finite. - Derived checks:
HpPeriodTooSmallorTriggerPeriodTooSmallif computed periods < 2. - Data validity:
EmptyInputDataif empty;AllValuesNaNif no finite values;NotEnoughValidDataif there are fewer thanperiodfinite values after the first finite input. - Note: the internal high-pass stage uses a derived
hp_period ≈ round(4·period/bandwidth); ifhp_periodexceeds your data length you may seeHighPassError. - Warmup: outputs are
NaNuntil the HP stage stabilizes (≥2 bars); trigger requires additional warmup. - Normalization: decaying peak with
k=0.991keepsbp_normalizedwithin ~[-1, 1]. - Streaming:
BandPassStream::updatereturnsNaNfor 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
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.