Chandelier Exit (CE)

Parameters: 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 > 0 and period ≤ len; otherwise ChandelierExitError::InvalidPeriod.
  • Slice lengths must match (high/low/close); else ChandelierExitError::InconsistentDataLengths.
  • First valid index depends on use_close (close-only vs. min of high/low/close). If none, AllValuesNaN.
  • Needs at least period points after the first valid; else NotEnoughValidData.
  • Warmup: indices [.. first + period − 1] are NaN for both stops.
  • Extrema windows skip NaNs; an all-NaN window yields NaN at 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

Comparison:
View:
Loading chart...

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.

Related Indicators