Keltner Channels

Parameters: period = 20 | multiplier = 2 | ma_type = ema

Overview

Keltner Channels create dynamic price envelopes that expand and contract based on market volatility, combining a moving average centerline with ATR based bands to identify trend direction, overbought and oversold levels, and breakout opportunities. The middle band tracks the trend using an exponential moving average while the outer bands are positioned at a multiple of Average True Range above and below, creating channels that widen during volatile periods and narrow during consolidations. When price hugs the upper band, it signals strong bullish momentum that often continues as long as price remains above the middle band, while sustained movement along the lower band indicates bearish pressure. Traders watch for channel squeezes where volatility contracts and bands narrow, setting up explosive breakout moves when price finally escapes the compressed range. The indicator particularly excels at filtering trends, as price tends to walk along one band during strong directional moves while bouncing between bands during ranging markets. Additionally, Keltner Channels form the foundation of the famous squeeze indicator when combined with Bollinger Bands, identifying low volatility setups that precede significant price expansions, making them invaluable for both trend following and breakout strategies.

Implementation Examples

Get upper, middle, and lower bands in a few lines:

use vectorta::indicators::keltner::{keltner, KeltnerInput, KeltnerParams};
use vectorta::utilities::data_loader::{Candles, read_candles_from_csv};

// Using with OHLC + source slices
let high   = vec![101.0, 102.0, 103.5, 103.0, 104.2];
let low    = vec![ 99.5, 100.8, 101.7, 101.9, 102.5];
let close  = vec![100.5, 101.6, 102.8, 102.2, 103.6];
let source = close.clone(); // e.g., close as source

let params = KeltnerParams { period: Some(20), multiplier: Some(2.0), ma_type: Some("ema".into()) };
let input = KeltnerInput::from_slice(&high, &low, &close, &source, params);
let out = keltner(&input)?;

// Using with Candles (defaults: source="close", period=20, multiplier=2.0, ma_type="ema")
let candles: Candles = read_candles_from_csv("data/sample.csv")?;
let input = KeltnerInput::with_default_candles(&candles);
let out = keltner(&input)?;

// Access the bands
for i in 0..out.middle_band.len() {
    println!("upper={}, middle={}, lower={}", out.upper_band[i], out.middle_band[i], out.lower_band[i]);
}

API Reference

Input Methods
// From OHLC + source slices
KeltnerInput::from_slice(&[f64], &[f64], &[f64], &[f64], KeltnerParams) -> KeltnerInput

// From candles with custom source key (e.g., "close")
KeltnerInput::from_candles(&Candles, &str, KeltnerParams) -> KeltnerInput

// From candles with default params (source="close", period=20, multiplier=2.0, ma_type="ema")
KeltnerInput::with_default_candles(&Candles) -> KeltnerInput
Parameters Structure
pub struct KeltnerParams {
    pub period: Option<usize>,    // Default: 20
    pub multiplier: Option<f64>,  // Default: 2.0
    pub ma_type: Option<String>,  // Default: "ema"
}
Output Structure
pub struct KeltnerOutput {
    pub upper_band: Vec<f64>,
    pub middle_band: Vec<f64>,
    pub lower_band: Vec<f64>,
}
Validation, Warmup & NaNs
  • period > 0 and period ≤ len; otherwise KeltnerError::KeltnerInvalidPeriod { period, data_len }.
  • Warmup requires at least period valid points after the first finite close value; else KeltnerError::KeltnerNotEnoughValidData { needed, valid }.
  • Indices before warm = first_valid + period − 1 are set to NaN in all three bands.
  • Leading all-NaN input yields KeltnerError::KeltnerAllValuesNaN. Candle field selection errors surface as KeltnerError::KeltnerMaError(..).
  • Streaming: update() returns None until warmup completes, then yields (upper, middle, lower) per bar.
Error Handling
use vectorta::indicators::keltner::{keltner, KeltnerError};

match keltner(&input) {
    Ok(output) => process_bands(output.upper_band, output.middle_band, output.lower_band),
    Err(KeltnerError::KeltnerEmptyData) => eprintln!("empty data"),
    Err(KeltnerError::KeltnerInvalidPeriod { period, data_len }) => {
        eprintln!("invalid period: {period} (len={data_len})");
    }
    Err(KeltnerError::KeltnerNotEnoughValidData { needed, valid }) => {
        eprintln!("not enough valid data: need {needed}, have {valid}");
    }
    Err(KeltnerError::KeltnerAllValuesNaN) => eprintln!("all values NaN"),
    Err(KeltnerError::KeltnerMaError(msg)) => eprintln!("MA/candle error: {msg}"),
}

Python Bindings

Basic Usage
import numpy as np
from vectorta import keltner, KeltnerStream, keltner_batch

high   = np.array([101.0, 102.0, 103.5, 103.0, 104.2], dtype=float)
low    = np.array([ 99.5, 100.8, 101.7, 101.9, 102.5], dtype=float)
close  = np.array([100.5, 101.6, 102.8, 102.2, 103.6], dtype=float)
source = close.copy()

# Vector output (upper, middle, lower)
upper, middle, lower = keltner(high, low, close, source, period=20, multiplier=2.0, ma_type='ema', kernel=None)

# Streaming
stream = KeltnerStream(period=20, multiplier=2.0, ma_type='ema')
for h, l, c, s in zip(high, low, close, source):
    val = stream.update(h, l, c, s)
    if val is not None:
        up, mid, lo = val

# Batch sweep
result = keltner_batch(
    high, low, close, source,
    period_range=(10, 30, 10),          # 10, 20, 30
    multiplier_range=(1.5, 2.5, 0.5),   # 1.5, 2.0, 2.5
    kernel='auto'
)
U = result['upper']   # shape: [rows, len]
M = result['middle']
L = result['lower']
periods = result['periods']
multipliers = result['multipliers']
CUDA Acceleration

CUDA support for Keltner is currently under development. The API will follow the same pattern as other CUDA-enabled indicators.

# Coming soon: CUDA-accelerated Keltner calculations
# Pattern will mirror batch/stream APIs above.

JavaScript/WASM Bindings

Basic Usage

Calculate Keltner Channels in JavaScript/TypeScript:

import { keltner } from 'vectorta-wasm';

const high   = new Float64Array([101.0, 102.0, 103.5, 103.0, 104.2]);
const low    = new Float64Array([ 99.5, 100.8, 101.7, 101.9, 102.5]);
const close  = new Float64Array([100.5, 101.6, 102.8, 102.2, 103.6]);
const source = close; // e.g., close as source

const result = keltner(high, low, close, source, 20, 2.0, 'ema');
// result: { values: Float64Array, rows: 3, cols: N }
const N = result.cols;
const upper  = result.values.slice(0, N);
const middle = result.values.slice(N, 2*N);
const lower  = result.values.slice(2*N);
Memory-Efficient Operations

Use zero-copy operations for large datasets:

import { keltner_alloc, keltner_free, keltner_into, memory } from 'vectorta-wasm';

const N = close.length;

// Allocate WASM memory for inputs and outputs
const hPtr = keltner_alloc(N);
const lPtr = keltner_alloc(N);
const cPtr = keltner_alloc(N);
const sPtr = keltner_alloc(N);
const upPtr = keltner_alloc(N);
const midPtr = keltner_alloc(N);
const lowPtr = keltner_alloc(N);

// Copy input data into WASM memory
new Float64Array(memory.buffer, hPtr, N).set(high);
new Float64Array(memory.buffer, lPtr, N).set(low);
new Float64Array(memory.buffer, cPtr, N).set(close);
new Float64Array(memory.buffer, sPtr, N).set(source);

// Compute directly into pre-allocated output buffers
// Args: high_ptr, low_ptr, close_ptr, source_ptr, upper_ptr, middle_ptr, lower_ptr, len, period, multiplier, ma_type
keltner_into(hPtr, lPtr, cPtr, sPtr, upPtr, midPtr, lowPtr, N, 20, 2.0, 'ema');

// Read results and copy out
const upper  = new Float64Array(memory.buffer, upPtr, N).slice();
const middle = new Float64Array(memory.buffer, midPtr, N).slice();
const lower  = new Float64Array(memory.buffer, lowPtr, N).slice();

// Free all allocations
for (const ptr of [hPtr, lPtr, cPtr, sPtr, upPtr, midPtr, lowPtr]) keltner_free(ptr, N);
Batch Processing

Test numerous period/multiplier combinations:

import { keltner_batch } from 'vectorta-wasm';

const cfg = {
  period_range: [10, 30, 10],       // start, end, step
  multiplier_range: [1.5, 2.5, 0.5],
  ma_type: 'ema'
};

const out = keltner_batch(high, low, close, source, cfg);
// out: { upper: Float64Array, middle: Float64Array, lower: Float64Array, combos, rows, cols }
// Each band is [rows * cols] flattened: row-major by parameter combo

Performance Analysis

Comparison:
View:
Loading chart...

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

Related Indicators