Keltner Channels
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 > 0andperiod ≤ len; otherwiseKeltnerError::KeltnerInvalidPeriod { period, data_len }.- Warmup requires at least
periodvalid points after the first finiteclosevalue; elseKeltnerError::KeltnerNotEnoughValidData { needed, valid }. - Indices before
warm = first_valid + period − 1are set toNaNin all three bands. - Leading all-NaN input yields
KeltnerError::KeltnerAllValuesNaN. Candle field selection errors surface asKeltnerError::KeltnerMaError(..). - Streaming:
update()returnsNoneuntil 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
AMD Ryzen 9 9950X (CPU) | NVIDIA RTX 4090 (GPU) | Benchmarks: 2026-01-05