Chande Exits (Chandelier Exits)

Parameters: period = 22 | mult = 3 (0.5–5) | direction = long

Overview

Chandelier Exits, developed by Chuck LeBeau, provide volatility based trailing stops that adapt to market conditions by combining Average True Range (ATR) with rolling price extremes. For long positions, the indicator calculates the highest high over the specified period and subtracts a multiple of ATR to create a dynamic stop level below the market. Short positions use the lowest low plus a multiple of ATR to position the stop above current prices. This approach ensures stops remain at a distance proportional to current volatility.

The indicator excels at protecting profits in trending markets while avoiding premature exits during normal price fluctuations. As volatility increases, the stop moves further from price to accommodate larger swings. When volatility contracts, the stop tightens to preserve more gains. Typical ATR multipliers range from 2.5 to 3.5, with higher values providing more breathing room but potentially giving back larger portions of unrealized profits.

Traders primarily use Chandelier Exits for position management rather than entry signals. The indicator works particularly well with trend following systems where it allows winners to run while cutting losses systematically. Unlike fixed percentage stops, Chandelier Exits adjust to market character, making them effective across different instruments and market conditions. The trailing nature ensures the stop only moves in the favorable direction, locking in profits as trends extend.

Implementation Examples

Get adaptive trailing stops in a few lines:

use vectorta::indicators::chande::{chande, ChandeInput, ChandeParams};
use vectorta::utilities::data_loader::{Candles, read_candles_from_csv};

// From high/low/close slices
let high = vec![/* ... */];
let low = vec![/* ... */];
let close = vec![/* ... */];
let params = ChandeParams { period: Some(22), mult: Some(3.0), direction: Some("long".into()) };
let input = ChandeInput::from_slices(&high, &low, &close, params);
let result = chande(&input)?; // result.values: Vec<f64>

// From Candles with defaults (period=22, mult=3.0, direction="long")
let candles: Candles = read_candles_from_csv("data/sample.csv")?;
let input = ChandeInput::with_default_candles(&candles);
let result = chande(&input)?;

API Reference

Input Methods
// From high/low/close slices
ChandeInput::from_slices(&[f64], &[f64], &[f64], ChandeParams) -> ChandeInput

// From candles
ChandeInput::from_candles(&Candles, ChandeParams) -> ChandeInput

// From candles with default params (period=22, mult=3.0, direction="long")
ChandeInput::with_default_candles(&Candles) -> ChandeInput
Parameters Structure
pub struct ChandeParams {
    pub period: Option<usize>,   // Default: 22
    pub mult: Option<f64>,       // Default: 3.0
    pub direction: Option<String>, // Default: "long" ("long" or "short")
}
Output Structure
pub struct ChandeOutput {
    pub values: Vec<f64>, // trailing stop levels (per direction)
}
Validation, Warmup & NaNs
  • Requires non-empty high/low/close and equal lengths; else ChandeError::EmptyInputData or DataLengthMismatch.
  • First finite triple index is used; if none, ChandeError::AllValuesNaN.
  • period > 0 and period ≤ len; otherwise ChandeError::InvalidPeriod.
  • Need at least period valid points after first finite; otherwise ChandeError::NotEnoughValidData.
  • direction must be "long" or "short"; otherwise ChandeError::InvalidDirection.
  • Warmup: indices [0 .. first+period-2] are NaN via alloc_with_nan_prefix.
  • Streaming: ChandeStream::update returns None until warmup completes; then Some(value) each step.
Error Handling
use vectorta::indicators::chande::{chande, ChandeError};

match chande(&input) {
    Ok(out) => process(out.values),
    Err(ChandeError::EmptyInputData) => eprintln!("empty input"),
    Err(ChandeError::AllValuesNaN) => eprintln!("no finite values"),
    Err(ChandeError::InvalidPeriod { period, data_len }) => eprintln!("invalid period {} for len {}", period, data_len),
    Err(ChandeError::NotEnoughValidData { needed, valid }) => eprintln!("need {} valid points, got {}", needed, valid),
    Err(ChandeError::DataLengthMismatch { h, l, c }) => eprintln!("len mismatch: h={}, l={}, c={}", h, l, c),
    Err(ChandeError::InvalidDirection { direction }) => eprintln!("invalid direction: {}", direction),
} 

Python Bindings

Basic Usage
import numpy as np
from vectorta import chande, ChandeStream

# High/Low/Close arrays
high = np.array([...], dtype=np.float64)
low = np.array([...], dtype=np.float64)
close = np.array([...], dtype=np.float64)

# Single run
values = chande(high, low, close, period=22, mult=3.0, direction="long")

# Streaming
stream = ChandeStream(22, 3.0, "long")
out = []
for h, l, c in zip(high, low, close):
    v = stream.update(h, l, c)
    out.append(np.nan if v is None else v)
Batch Processing
import numpy as np
from vectorta import chande_batch

high = np.array([...], dtype=np.float64)
low = np.array([...], dtype=np.float64)
close = np.array([...], dtype=np.float64)

# Ranges as (start, end, step)
res = chande_batch(high, low, close, (14, 30, 2), (2.0, 4.0, 0.5), "long")

# Access fields
values = res['values']        # shape: (num_combos, len)
periods = res['periods']      # list of periods for each row
mults = res['mults']          # list of mults for each row
directions = res['directions']
CUDA Acceleration

CUDA support for this indicator is coming soon.

JavaScript/WASM Bindings

Basic Usage

Calculate Chande Exits in JavaScript/TypeScript:

import { chande_js } from 'vectorta-wasm';

const high = new Float64Array([/* ... */]);
const low = new Float64Array([/* ... */]);
const close = new Float64Array([/* ... */]);

// period, mult, direction ("long" or "short")
const values = chande_js(high, low, close, 22, 3.0, "long");
console.log('Chande values:', values);
Memory-Efficient Operations

Use zero-copy operations for large datasets:

import { chande_alloc, chande_free, chande_into, memory } from 'vectorta-wasm';

const len = close.length;
const hPtr = chande_alloc(len);
const lPtr = chande_alloc(len);
const cPtr = chande_alloc(len);
const outPtr = chande_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);

chande_into(hPtr, lPtr, cPtr, outPtr, len, 22, 3.0, "long");
const stops = new Float64Array(memory.buffer, outPtr, len).slice();

chande_free(hPtr, len);
chande_free(lPtr, len);
chande_free(cPtr, len);
chande_free(outPtr, len);
Batch Processing

Test multiple parameter combinations:

import { chande_batch_js } from 'vectorta-wasm';

const out = chande_batch_js(
  high, low, close,
  14, 30, 2,  // period range
  2.0, 4.0, 0.5, // mult range
  "long"
);

// Returned object contains: { values: Float64Array, periods: Float64Array, mults: Float64Array, directions: string[], rows, cols }
console.log(out.rows, out.cols);

Performance Analysis

Comparison:
View:
Loading chart...

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

Related Indicators