Low Pass Channel (LPC)

Parameters: cutoff_type = adaptive | fixed_period = 20 | max_cycle_limit = 60 | cycle_mult = 1 | tr_mult = 1

Overview

The Low Pass Channel (LPC) attenuates higher frequency price oscillations and volatility with minimal lag using a single pole IIR low pass filter that smooths the price series while preserving trend information. Channel bands are constructed by applying the same filter to the True Range values and then adding and subtracting this filtered volatility from the smoothed price centerline. When operating in adaptive mode, the indicator uses John Ehlers' Instantaneous Frequency Measurement technique to detect the dominant price cycle through Hilbert Transform calculations, automatically adjusting the filter cutoff to match market conditions. Traders employ LPC to identify smooth trend direction while the channel bands provide dynamic support and resistance levels that expand and contract based on filtered volatility. Unlike simple moving average channels, LPC's single pole filter design offers superior frequency response characteristics, effectively removing market noise while maintaining responsiveness to genuine price movements.

Defaults: cutoff_type='adaptive', fixed_period=20, max_cycle_limit=60, cycle_mult=1.0, tr_mult=1.0.

Implementation Examples

Compute LPC from candles or raw OHLC + source slices:

use vector_ta::indicators::lpc::{lpc, LpcInput, LpcParams};
use vector_ta::utilities::data_loader::{Candles, read_candles_from_csv};

// Using raw slices (high, low, close, src)
let high = vec![101.0, 103.0, 102.0];
let low = vec![ 99.0, 100.0, 100.5];
let close = vec![100.5, 102.0, 101.2];
let src = close.clone();
let params = LpcParams { cutoff_type: Some("adaptive".into()), fixed_period: Some(20), max_cycle_limit: Some(60), cycle_mult: Some(1.0), tr_mult: Some(1.0) };
let input = LpcInput::from_slices(&high, &low, &close, &src, params);
let out = lpc(&input)?;

// Using Candles with defaults (source = "close")
let candles: Candles = read_candles_from_csv("data/sample.csv")?;
let input = LpcInput::with_default_candles(&candles);
let out = lpc(&input)?;

// Access components
for i in 0..out.filter.len() {
    println!("filter={}, high={}, low={}", out.filter[i], out.high_band[i], out.low_band[i]);
}

API Reference

Input Methods
// From raw OHLC + source slices
LpcInput::from_slices(&[f64], &[f64], &[f64], &[f64], LpcParams) -> LpcInput

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

// From candles with defaults (source = "close")
LpcInput::with_default_candles(&Candles) -> LpcInput
Parameters Structure
#[derive(Debug, Clone, Default)]
pub struct LpcParams {
    pub cutoff_type: Option<String>, // Default: "adaptive"
    pub fixed_period: Option<usize>, // Default: 20
    pub max_cycle_limit: Option<usize>, // Default: 60
    pub cycle_mult: Option<f64>,    // Default: 1.0
    pub tr_mult: Option<f64>,       // Default: 1.0
}
Output Structure
pub struct LpcOutput {
    pub filter: Vec<f64>,
    pub high_band: Vec<f64>,
    pub low_band: Vec<f64>,
}
Validation, Warmup & NaNs
  • All four inputs must have equal length; otherwise LpcError::MissingData.
  • cutoff_type must be 'adaptive' or 'fixed'; otherwise LpcError::InvalidCutoffType.
  • fixed_period > 0 and fixed_period ≤ data_len; otherwise LpcError::InvalidPeriod.
  • There must be at least 2 valid points after the first finite bar; otherwise LpcError::NotEnoughValidData.
  • If any input slice is empty: LpcError::EmptyInputData. If all values are NaN: LpcError::AllValuesNaN.
  • Warmup: indices before the first fully‑finite bar are NaN. At the first valid index, filter = src[first] and bands are seeded from high[first]-low[first].
Error Handling
use vector_ta::indicators::lpc::{lpc, LpcError};

match lpc(&input) {
    Ok(output) => process(output.filter, output.high_band, output.low_band),
    Err(LpcError::EmptyInputData) =>
        eprintln!("lpc: input slice is empty"),
    Err(LpcError::AllValuesNaN) =>
        eprintln!("lpc: all values are NaN"),
    Err(LpcError::InvalidPeriod { period, data_len }) =>
        eprintln!("lpc: invalid period: {} (len = {})", period, data_len),
    Err(LpcError::NotEnoughValidData { needed, valid }) =>
        eprintln!("lpc: need {} valid points, got {}", needed, valid),
    Err(LpcError::InvalidCutoffType { cutoff_type }) =>
        eprintln!("lpc: invalid cutoff type: {}", cutoff_type),
    Err(LpcError::MissingData) =>
        eprintln!("lpc: required OHLC data missing or mismatched lengths"),
}

Python Bindings

Basic Usage

Compute LPC from NumPy arrays (defaults: adaptive, period 20, max_cycle_limit 60, multipliers 1.0):

import numpy as np
from vector_ta import lpc

high = np.array([101.0, 103.0, 102.0], dtype=np.float64)
low = np.array([ 99.0, 100.0, 100.5], dtype=np.float64)
close = np.array([100.5, 102.0, 101.2], dtype=np.float64)
src = close.copy()

# Defaults (adaptive cutoff)
filt, hi, lo = lpc(high, low, close, src)

# Fixed cutoff with period 20
filt, hi, lo = lpc(high, low, close, src,
                   cutoff_type='fixed', fixed_period=20,
                   max_cycle_limit=60, cycle_mult=1.0, tr_mult=1.0,
                   kernel='auto')

print(f"filter[0:3] = {filt[:3]}")
Streaming Real-time Updates
from vector_ta import LpcStream

stream = LpcStream(cutoff_type='fixed', fixed_period=20, max_cycle_limit=60, cycle_mult=1.0, tr_mult=1.0)
for (h, l, c, s) in ohlc_src_feed:
    result = stream.update(h, l, c, s)
    if result is not None:
        filt, hi, lo = result
        handle(filt, hi, lo)
Batch Parameter Optimization

Sweep fixed periods and multipliers across a grid:

import numpy as np
from vector_ta import lpc_batch

high, low, close, src = ...  # NumPy float64 arrays of equal length

res = lpc_batch(
    high, low, close, src,
    fixed_period_range=(10, 20, 5),
    cycle_mult_range=(1.0, 1.0, 0.0),
    tr_mult_range=(1.0, 1.5, 0.5),
    cutoff_type='fixed', max_cycle_limit=60, kernel='auto'
)

print(res['values'].shape)  # (rows, cols) with rows = combos*3 (filter, high, low)
print(res['fixed_periods'], res['cycle_mults'], res['tr_mults'])
print(res['order'])  # ['filter', 'high', 'low']
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 lpc_cuda_batch_dev, lpc_cuda_many_series_one_param_dev

# One series (float32)
high_f32 = np.asarray(load_high(), dtype=np.float32)
low_f32 = np.asarray(load_low(), dtype=np.float32)
close_f32 = np.asarray(load_close(), dtype=np.float32)
src_f32 = np.asarray(load_series(), dtype=np.float32)

dev = lpc_cuda_batch_dev(
    high_f32=high_f32,
    low_f32=low_f32,
    close_f32=close_f32,
    src_f32=src_f32,
    fixed_period_range=(5, 30, 5),
    cycle_mult_range=(0.5, 2.0, 0.5),
    tr_mult_range=(0.5, 2.0, 0.5),
    cutoff_type=14,
    max_cycle_limit=14,
    device_id=0,
)

# Many series (time-major)
high_tm_f32 = np.asarray(load_high_time_major_matrix(), dtype=np.float32)
low_tm_f32 = np.asarray(load_low_time_major_matrix(), dtype=np.float32)
close_tm_f32 = np.asarray(load_close_time_major_matrix(), dtype=np.float32)
src_tm_f32 = np.asarray(load_series_time_major_matrix(), dtype=np.float32)

dev_tm = lpc_cuda_many_series_one_param_dev(
    high_tm_f32=high_tm_f32,
    low_tm_f32=low_tm_f32,
    close_tm_f32=close_tm_f32,
    src_tm_f32=src_tm_f32,
    cutoff_type=14,
    fixed_period=14,
    tr_mult=1.0,
    device_id=0,
)

JavaScript / WASM

Basic Usage
import { lpc } from 'vectorta-wasm';

const high = new Float64Array([101.0, 103.0, 102.0]);
const low = new Float64Array([ 99.0, 100.0, 100.5]);
const close = new Float64Array([100.5, 102.0, 101.2]);
const src = close;

// Returns flat array: [filter..., high..., low...]
const values = lpc(high, low, close, src, 'fixed', 20, 60, 1.0, 1.0);
const len = src.length;
const filter = values.slice(0, len);
const highBand = values.slice(len, 2*len);
const lowBand = values.slice(2*len, 3*len);
Memory-Efficient Operations

Use zero-copy *_alloc/*_into for large arrays:

import { lpc_alloc, lpc_free, lpc_into, memory } from 'vectorta-wasm';

const len = src.length;
// Allocate output buffers in WASM memory
const fPtr = lpc_alloc(len);
const hPtr = lpc_alloc(len);
const lPtr = lpc_alloc(len);

// Copy inputs into WASM memory
const hIn = lpc_alloc(len), lIn = lpc_alloc(len), cIn = lpc_alloc(len), sIn = lpc_alloc(len);
new Float64Array(memory.buffer, hIn, len).set(high);
new Float64Array(memory.buffer, lIn, len).set(low);
new Float64Array(memory.buffer, cIn, len).set(close);
new Float64Array(memory.buffer, sIn, len).set(src);

// Compute directly into pre-allocated buffers
lpc_into(hIn, lIn, cIn, sIn, fPtr, hPtr, lPtr, len, 'fixed', 20, 60, 1.0, 1.0);

// Read results
const filter = new Float64Array(memory.buffer, fPtr, len).slice();
const highBand = new Float64Array(memory.buffer, hPtr, len).slice();
const lowBand = new Float64Array(memory.buffer, lPtr, len).slice();

// Free
[fPtr, hPtr, lPtr, hIn, lIn, cIn, sIn].forEach(ptr => lpc_free(ptr, len));
Batch Processing

Sweep parameter ranges and get a rows×cols matrix:

import { lpc_batch } from 'vectorta-wasm';

const cfg = {
  fixed_period_range: [10, 20, 5],
  cycle_mult_range: [1.0, 1.0, 0.0],
  tr_mult_range: [1.0, 1.5, 0.5],
  cutoff_type: 'fixed',
  max_cycle_limit: 60,
};

const out = lpc_batch(high, low, close, src, cfg);
console.log(out.rows, out.cols, out.order); // order: ['filter','high','low']
console.log(out.fixed_periods, out.cycle_mults, out.tr_mults);

CUDA Bindings (Rust)

use vector_ta::cuda::CudaLpc;
use vector_ta::indicators::lpc::LpcBatchRange;

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

let high: [f32] = /* ... */;
let low: [f32] = /* ... */;
let close: [f32] = /* ... */;
let src: [f32] = /* ... */;
let range = LpcBatchRange::default();

let out = cuda.lpc_batch_dev(&high, &low, &close, &src, &range)?;
let _ = out;

Performance Analysis

Comparison:
View:
Loading chart...

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

Related Indicators