Chandelier Exit (CE)
period = 22 | mult = 3 (0.5–5) | use_close = true Overview
Chandelier Exit, developed by Chuck LeBeau, creates dynamic trailing stops that adapt to market volatility by combining Average True Range (ATR) with rolling price extremes. The indicator calculates two stop levels simultaneously. For long positions, it finds the highest value over the lookback period and subtracts a multiple of ATR to position the stop below the market. Short position stops use the lowest value plus a multiple of ATR to trail above current prices. This dual approach allows traders to follow trends in either direction while maintaining stops at a volatility adjusted distance.
The system switches between long and short regimes based on which stop level price crosses. When price breaks above the short stop, the indicator shifts to long mode and begins tracking the long stop. Conversely, dropping below the long stop triggers short mode. This automatic regime detection helps traders identify potential trend reversals while the trailing mechanism locks in profits as trends mature. The ATR multiplier, typically set between 2.5 and 3.5, determines how much breathing room positions receive before stops trigger.
Unlike fixed percentage trailing stops, Chandelier Exits respond to changing market conditions. During volatile periods, stops move further from price to avoid premature exits from normal fluctuations. When volatility contracts, stops tighten to protect more of the accumulated gains. Traders can choose between using closing prices or high low extremes for the rolling window calculation, with closes providing smoother stops and highs lows offering tighter trend following. The indicator excels in trending markets where it allows profitable positions to run while systematically limiting losses.
Implementation Examples
Compute CE from slices or candles (defaults: period=22, mult=3.0, use_close=true):
use vectorta::indicators::chandelier_exit::{chandelier_exit, ChandelierExitInput, ChandelierExitParams};
use vectorta::utilities::data_loader::{Candles, read_candles_from_csv};
// From slices (high, low, close)
let high = vec![/* ... */];
let low = vec![/* ... */];
let close= vec![/* ... */];
let params = ChandelierExitParams { period: Some(22), mult: Some(3.0), use_close: Some(true) };
let input = ChandelierExitInput::from_slices(&high, &low, &close, params);
let out = chandelier_exit(&input)?; // out.long_stop, out.short_stop
// From candles with defaults
let candles: Candles = read_candles_from_csv("data/sample.csv")?;
let input = ChandelierExitInput::with_default_candles(&candles);
let out = chandelier_exit(&input)?; API Reference
Input Methods ▼
// From candles
ChandelierExitInput::from_candles(&Candles, ChandelierExitParams) -> ChandelierExitInput
// From slices (high, low, close)
ChandelierExitInput::from_slices(&[f64], &[f64], &[f64], ChandelierExitParams) -> ChandelierExitInput
// With defaults (period=22, mult=3.0, use_close=true)
ChandelierExitInput::with_default_candles(&Candles) -> ChandelierExitInput Parameters Structure ▼
#[derive(Debug, Clone)]
pub struct ChandelierExitParams {
pub period: Option<usize>, // Default: 22
pub mult: Option<f64>, // Default: 3.0
pub use_close: Option<bool>, // Default: true
} Output Structure ▼
pub struct ChandelierExitOutput {
pub long_stop: Vec<f64>, // Long stop values (NaN when inactive)
pub short_stop: Vec<f64>, // Short stop values (NaN when inactive)
} Validation, Warmup & NaNs ▼
period > 0andperiod ≤ len; otherwiseChandelierExitError::InvalidPeriod.- Slice lengths must match (
high/low/close); elseChandelierExitError::InconsistentDataLengths. - First valid index depends on
use_close(close-only vs. min of high/low/close). If none,AllValuesNaN. - Needs at least
periodpoints after the first valid; elseNotEnoughValidData. - Warmup: indices
[.. first + period − 1]areNaNfor both stops. - Extrema windows skip NaNs; an all-NaN window yields
NaNat that index. - ATR uses Wilder’s RMA with seed at the first full window; ATR failures are surfaced as
AtrError(String).
Error Handling ▼
use vectorta::indicators::chandelier_exit::{chandelier_exit, ChandelierExitError};
match chandelier_exit(&input) {
Ok(output) => { /* use output.long_stop / output.short_stop */ }
Err(ChandelierExitError::EmptyInputData) => eprintln!("Input is empty"),
Err(ChandelierExitError::AllValuesNaN) => eprintln!("All values are NaN"),
Err(ChandelierExitError::InvalidPeriod { period, data_len }) =>
eprintln!("Invalid period {} for data length {}", period, data_len),
Err(ChandelierExitError::NotEnoughValidData { needed, valid }) =>
eprintln!("Need {} points after first valid, got {}", needed, valid),
Err(ChandelierExitError::InconsistentDataLengths { high_len, low_len, close_len }) =>
eprintln!("Inconsistent lengths: high={}, low={}, close={}", high_len, low_len, close_len),
Err(ChandelierExitError::AtrError(msg)) => eprintln!("ATR error: {}", msg),
} Python Bindings
Basic Usage ▼
Compute CE long/short stops from NumPy arrays:
import numpy as np
from vectorta import chandelier_exit
# High/Low/Close inputs
high = np.array([...], dtype=float)
low = np.array([...], dtype=float)
close= np.array([...], dtype=float)
# Defaults: period=22, mult=3.0, use_close=True
long_stop, short_stop = chandelier_exit(high, low, close)
# Or specify parameters (optional kernel: "auto", "avx2", ...)
long_stop, short_stop = chandelier_exit(high, low, close, period=20, mult=2.5, use_close=False, kernel="auto")
print(long_stop.shape, short_stop.shape) Streaming Real-time Updates ▼
Stream OHLC bars and receive masked stops after warmup:
import numpy as np
from vectorta import ChandelierExitStreamPy
stream = ChandelierExitStreamPy(period=22, mult=3.0, use_close=True)
for h, l, c in ohlc_feed:
result = stream.update(h, l, c)
if result is not None:
long_stop, short_stop = result
if not np.isnan(long_stop):
pass # long stop active
if not np.isnan(short_stop):
pass # short stop active Batch Parameter Optimization ▼
Run a sweep over (period, mult):
import numpy as np
from vectorta import chandelier_exit_batch
high = np.array([...], dtype=float)
low = np.array([...], dtype=float)
close= np.array([...], dtype=float)
results = chandelier_exit_batch(
high, low, close,
period_range=(10, 30, 5),
mult_range=(2.0, 4.0, 0.5),
use_close=True,
kernel="auto"
)
print(results["values"].shape) # (2 * num_combos, len)
print(results["periods"]) # tested periods
print(results["mults"]) # tested multipliers
print(results["use_close"]) # bools per combo CUDA Acceleration ▼
CUDA support for Chandelier Exit is coming soon. The API will mirror other CUDA-enabled indicators.
# Coming soon: CUDA-accelerated CE calculations
# Refer to other indicators for the planned API patterns. JavaScript/WASM Bindings
Basic Usage ▼
Compute CE in JS/TS:
import { ce_js } from 'vectorta-wasm';
const high = new Float64Array([/* ... */]);
const low = new Float64Array([/* ... */]);
const close= new Float64Array([/* ... */]);
// Returns { values: Float64Array, rows: 2, cols: len }
const { values, rows, cols } = ce_js(high, low, close, 22, 3.0, true);
// First row: long_stop, second row: short_stop
const long = values.slice(0, cols);
const short = values.slice(cols);
interface CeResult { values: Float64Array; rows: number; cols: number; } Memory-Efficient Operations ▼
Use zero-copy pointers for large datasets:
import { memory, ce_alloc, ce_free, ce_into } from 'vectorta-wasm';
const len = close.length;
// Allocate and copy inputs
const hPtr = ce_alloc(len);
const lPtr = ce_alloc(len);
const cPtr = ce_alloc(len);
new Float64Array(memory.buffer, hPtr, len).set(high);
new Float64Array(memory.buffer, lPtr, len).set(low);
new Float64Array(memory.buffer, cPtr, len).set(close);
// Allocate output for [long.., short..]
const outPtr = ce_alloc(2 * len);
// Call into WASM; ce_into writes contiguous [long, short]
ce_into(hPtr, lPtr, cPtr, outPtr, len, 22, 3.0, true);
// Read results
const out = new Float64Array(memory.buffer, outPtr, 2 * len).slice();
const long = out.slice(0, len);
const short = out.slice(len);
// Free allocations
ce_free(hPtr, len);
ce_free(lPtr, len);
ce_free(cPtr, len);
ce_free(outPtr, 2 * len); Batch Processing ▼
Batch over (period, mult) combinations:
import { ce_batch, ce_batch_into, ce_alloc, ce_free, memory } from 'vectorta-wasm';
// High-level API returns { values, combos, rows, cols }
const cfg = { period_range: [10, 30, 5], mult_range: [2.0, 4.0, 0.5], use_close: true };
const { values, combos, rows, cols } = ce_batch(high, low, close, cfg);
// Or into a pre-allocated buffer (rows returned)
const rowsOut = ce_batch_into(high_ptr, low_ptr, close_ptr, len, outPtr, 10, 30, 5, 2.0, 4.0, 0.5, true);
// Result layout: row pairs per combo -> [long_row0, short_row0, long_row1, short_row1, ...] 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.