Keltner Channel Width Oscillator

Parameters: length = 20 | multiplier = 2 | use_exponential = true | bands_style = Average True Range | atr_length = 10

Overview

Keltner Channel Width Oscillator tracks how wide a Keltner-style envelope is relative to its centerline. Instead of returning upper and lower bands directly, it compresses the channel into a width ratio so you can monitor expansion and contraction as a standalone oscillator. That makes it useful for volatility regime detection, breakout preparation, and comparing width behavior across symbols without relying on absolute price scale.

This implementation returns the raw width ratio as `kbw` and a moving average of that width as `kbw_sma`. The centerline can be built with either SMA or EMA, and the channel width can be derived from Average True Range, raw True Range, or simple high-low Range. Candle input defaults to close as the source series, while slice input accepts a separate source array directly.

Defaults: length = 20, multiplier = 2.0, use_exponential = true, bands_style = "Average True Range", and atr_length = 10.

Implementation Examples

Compute raw channel width and the width SMA from candles or explicit OHLC plus source arrays.

use vector_ta::indicators::keltner_channel_width_oscillator::{
    keltner_channel_width_oscillator,
    KeltnerChannelWidthOscillatorInput,
    KeltnerChannelWidthOscillatorParams,
};
use vector_ta::utilities::data_loader::{Candles, read_candles_from_csv};

let output = keltner_channel_width_oscillator(
    &KeltnerChannelWidthOscillatorInput::from_slices(
        &high,
        &low,
        &close,
        &close,
        KeltnerChannelWidthOscillatorParams::default(),
    ),
)?;

let candles: Candles = read_candles_from_csv("data/sample.csv")?;
let candle_output = keltner_channel_width_oscillator(
    &KeltnerChannelWidthOscillatorInput::with_default_candles(&candles),
)?;

println!("{:?}", output.kbw.last());
println!("{:?}", candle_output.kbw_sma.last());

API Reference

Input Methods
// From candles
KeltnerChannelWidthOscillatorInput::from_candles(&Candles, &str, KeltnerChannelWidthOscillatorParams) -> KeltnerChannelWidthOscillatorInput

// From slices
KeltnerChannelWidthOscillatorInput::from_slices(&[f64], &[f64], &[f64], &[f64], KeltnerChannelWidthOscillatorParams) -> KeltnerChannelWidthOscillatorInput

// From candles with default params and source "close"
KeltnerChannelWidthOscillatorInput::with_default_candles(&Candles) -> KeltnerChannelWidthOscillatorInput
Parameters Structure
pub struct KeltnerChannelWidthOscillatorParams {
    pub length: Option<usize>,
    pub multiplier: Option<f64>,
    pub use_exponential: Option<bool>,
    pub bands_style: Option<String>,
    pub atr_length: Option<usize>,
}
Output Structure
pub struct KeltnerChannelWidthOscillatorOutput {
    pub kbw: Vec<f64>,
    pub kbw_sma: Vec<f64>,
}
Validation, Warmup & NaNs
  • High, low, close, and source arrays must be non-empty, equal in length, and contain at least one valid bar.
  • length and atr_length must be positive, and multiplier must be finite and non-negative.
  • Supported band styles are Average True Range, True Range, and Range.
  • Average True Range mode can require more valid bars than the other two styles because ATR warmup is separate.
  • Streaming resets on invalid bars only through update_reset_on_nan(); plain update() just returns None.
Builder, Streaming & Batch APIs
// Builder
KeltnerChannelWidthOscillatorBuilder::new()
    .length(usize)
    .multiplier(f64)
    .use_exponential(bool)
    .bands_style(&str)
    .atr_length(usize)
    .kernel(Kernel)
    .apply(&Candles)

KeltnerChannelWidthOscillatorBuilder::new()
    .apply_slices(&[f64], &[f64], &[f64], &[f64])
    .into_stream()

// Stream
KeltnerChannelWidthOscillatorStream::try_new(KeltnerChannelWidthOscillatorParams)
KeltnerChannelWidthOscillatorStream::update(f64, f64, f64, f64) -> Option<(f64, f64)>
KeltnerChannelWidthOscillatorStream::update_reset_on_nan(f64, f64, f64, f64) -> Option<(f64, f64)>

// Batch
KeltnerChannelWidthOscillatorBatchBuilder::new()
    .length_range(usize, usize, usize)
    .multiplier_range(f64, f64, f64)
    .atr_length_range(usize, usize, usize)
    .use_exponential(bool)
    .bands_style(&str)
    .apply_slices(&[f64], &[f64], &[f64], &[f64])
Error Handling
pub enum KeltnerChannelWidthOscillatorError {
    EmptyInputData,
    DataLengthMismatch,
    AllValuesNaN,
    InvalidLength { length: usize, data_len: usize },
    InvalidAtrLength { atr_length: usize, data_len: usize },
    InvalidMultiplier { multiplier: f64 },
    InvalidBandsStyle { bands_style: String },
    NotEnoughValidData { needed: usize, valid: usize },
    OutputLengthMismatch { expected: usize, got: usize },
    InvalidRange { start: String, end: String, step: String },
    InvalidKernelForBatch(Kernel),
}

Python Bindings

Python exposes a scalar function, a stream class, and a batch sweep. The scalar function returns two arrays: `kbw` and `kbw_sma`. The batch function returns those reshaped matrices together with the tested lengths, multipliers, ATR lengths, band styles, and grid dimensions.

import numpy as np
from vector_ta import (
    keltner_channel_width_oscillator,
    keltner_channel_width_oscillator_batch,
    KeltnerChannelWidthOscillatorStream,
)

high = np.asarray(high_values, dtype=np.float64)
low = np.asarray(low_values, dtype=np.float64)
close = np.asarray(close_values, dtype=np.float64)

kbw, kbw_sma = keltner_channel_width_oscillator(
    high,
    low,
    close,
    close,
    length=20,
    multiplier=2.0,
    use_exponential=True,
    bands_style="Average True Range",
    atr_length=10,
    kernel="auto",
)

stream = KeltnerChannelWidthOscillatorStream()
print(stream.update(101.0, 99.5, 100.2, 100.2))

batch = keltner_channel_width_oscillator_batch(
    high,
    low,
    close,
    close,
    length_range=(18, 22, 2),
    multiplier_range=(1.5, 2.5, 0.5),
    atr_length_range=(10, 14, 2),
    use_exponential=True,
    bands_style="Average True Range",
    kernel="auto",
)

print(batch["kbw"].shape)
print(batch["bands_styles"])

JavaScript/WASM Bindings

The WASM layer exposes scalar, batch, and into-buffer helpers. The scalar binding returns an object with `kbw` and `kbw_sma`. The batch binding returns those flattened arrays plus `rows`, `cols`, and the tested parameter `combos`.

import init, {
  keltner_channel_width_oscillator_js,
  keltner_channel_width_oscillator_batch_js,
} from "@vectoralpha/vector_ta";

await init();

const single = keltner_channel_width_oscillator_js(
  high,
  low,
  close,
  close,
  20,
  2.0,
  true,
  "Average True Range",
  10,
);

const batch = keltner_channel_width_oscillator_batch_js(high, low, close, close, {
  length_range: [18, 22, 2],
  multiplier_range: [1.5, 2.5, 0.5],
  atr_length_range: [10, 14, 2],
  use_exponential: true,
  bands_style: "Average True Range",
});

console.log(single.kbw);
console.log(batch.rows, batch.cols);
console.log(batch.combos[0]);

CUDA Bindings (Rust)

Additional details for the CUDA bindings can be found inside the VectorTA repository.

Performance Analysis

Comparison:
View:
Placeholder data (no recorded benchmarks for this indicator)

Across sizes, Rust CPU runs about 1.14× faster than Tulip C in this benchmark.

Loading chart...

AMD Ryzen 9 9950X (CPU) | NVIDIA RTX 4090 (GPU)

Related Indicators