Ehlers Error-Correcting EMA (ECEMA)

Parameters: length = 20 | gain_limit = 50

Overview

The Ehlers Error Correcting EMA (ECEMA), developed by John Ehlers, enhances the traditional exponential moving average by dynamically adjusting its gain parameter to minimize prediction error at each bar. Unlike standard EMAs that use a fixed smoothing factor, ECEMA tests multiple gain values within a bounded range and selects the one that produces the smallest error between the current price and the filtered output. This optimization occurs independently at each bar, allowing the indicator to adapt continuously to changing market conditions. The algorithm maintains the smoothness characteristic of exponential averaging while achieving superior responsiveness through intelligent gain selection.

ECEMA begins with a baseline EMA calculation, then applies an error correction term that adjusts the output based on the difference between current price and the previous ECEMA value. The gain limit parameter controls the adaptation range, with higher limits allowing more aggressive adjustments. Typical gain limits range from 30 to 70, representing maximum gains of 3.0 to 7.0 in tenths. The indicator tests discrete gain values within this range, evaluating each to find the optimal balance between tracking accuracy and noise reduction. This error minimization approach makes ECEMA particularly effective at following price during trends while avoiding overshooting during reversals.

Traders use ECEMA as an advanced alternative to traditional moving averages for trend identification and signal generation. The adaptive gain selection helps the indicator stay closer to price during important moves while maintaining stability during consolidations. Crossovers between price and ECEMA generate cleaner signals than fixed EMAs, as the error correction mechanism reduces lag without introducing excessive noise. Many systematic traders employ ECEMA in multi timeframe strategies, using longer period settings for trend direction and shorter periods for timing entries. The indicator also serves excellently as a dynamic trailing stop, as its adaptive nature keeps it appropriately distanced from price based on current volatility and trend strength.

Implementation Examples

Get started with ECEMA in a few lines:

use vectorta::indicators::moving_averages::ehlers_ecema::{ehlers_ecema, EhlersEcemaInput, EhlersEcemaParams};
use vectorta::utilities::data_loader::{Candles, read_candles_from_csv};

// From a price slice
let prices = vec![100.0, 102.0, 101.5, 103.0, 105.0, 104.5];
let params = EhlersEcemaParams { length: Some(20), gain_limit: Some(50), pine_compatible: Some(false), confirmed_only: Some(false) };
let input = EhlersEcemaInput::from_slice(&prices, params);
let result = ehlers_ecema(&input)?;

// From candles with default params (length=20, gain_limit=50; source="close")
let candles: Candles = read_candles_from_csv("data/sample.csv")?;
let input = EhlersEcemaInput::with_default_candles(&candles);
let result = ehlers_ecema(&input)?;

for v in result.values { println!("ecema: {}", v); }

API Reference

Input Methods
// From price slice
EhlersEcemaInput::from_slice(&[f64], EhlersEcemaParams) -> EhlersEcemaInput

// From candles with custom source
EhlersEcemaInput::from_candles(&Candles, &str, EhlersEcemaParams) -> EhlersEcemaInput

// From candles with default params (source="close")
EhlersEcemaInput::with_default_candles(&Candles) -> EhlersEcemaInput
Parameters Structure
pub struct EhlersEcemaParams {
    pub length: Option<usize>,          // Default: 20
    pub gain_limit: Option<usize>,      // Default: 50 (±5.0 in tenths)
    pub pine_compatible: Option<bool>,  // Default: false
    pub confirmed_only: Option<bool>,   // Default: false
}
Output Structure
pub struct EhlersEcemaOutput {
    pub values: Vec<f64>, // ECEMA values
}
Validation, Warmup & NaNs
  • Empty input → EhlersEcemaError::EmptyInputData; all NaNs → EhlersEcemaError::AllValuesNaN.
  • length > 0 and length ≤ len(data); otherwise InvalidPeriod { period, data_len }.
  • Require at least length valid values after the first finite; otherwise NotEnoughValidData { needed, valid }.
  • gain_limit > 0; otherwise InvalidGainLimit { gain_limit }.
  • Warmup: regular mode writes NaN until first + length − 1; Pine mode emits from the first valid bar.
  • confirmed_only=true uses the previous bar as source; affects both batch and streaming modes.
Error Handling
use vectorta::indicators::moving_averages::ehlers_ecema::{ehlers_ecema, EhlersEcemaError};

match ehlers_ecema(&input) {
    Ok(output) => process(output.values),
    Err(EhlersEcemaError::EmptyInputData) => eprintln!("empty input"),
    Err(EhlersEcemaError::AllValuesNaN) => eprintln!("all values are NaN"),
    Err(EhlersEcemaError::InvalidPeriod { period, data_len }) =>
        eprintln!("invalid period {} for data length {}", period, data_len),
    Err(EhlersEcemaError::NotEnoughValidData { needed, valid }) =>
        eprintln!("need {} valid values after first finite; got {}", needed, valid),
    Err(EhlersEcemaError::InvalidGainLimit { gain_limit }) =>
        eprintln!("gain_limit must be > 0; got {}", gain_limit),
    Err(EhlersEcemaError::EmaError(e)) => eprintln!("ema error: {}", e),
}

Python Bindings

Basic Usage

Calculate ECEMA using NumPy arrays (defaults: length=20, gain_limit=50):

import numpy as np
from vectorta import ehlers_ecema

prices = np.array([100.0, 102.0, 101.5, 103.0, 105.0, 104.5], dtype=float)

out = ehlers_ecema(prices, length=20, gain_limit=50, pine_compatible=False, confirmed_only=False, kernel="auto")
print(out)
Streaming Real-time Updates

Incremental updates with EhlersEcemaStream:

from vectorta import EhlersEcemaStream

stream = EhlersEcemaStream(length=20, gain_limit=50, pine_compatible=False, confirmed_only=False)
for price in price_feed:
    value = stream.update(price)  # returns None during warmup (non-Pine)
    if value is not None:
        print("ecema:", value)
Batch Parameter Optimization

Sweep length and gain_limit:

import numpy as np
from vectorta import ehlers_ecema_batch

prices = np.array([...], dtype=float)

res = ehlers_ecema_batch(
    prices,
    length_range=(10, 30, 5),
    gain_limit_range=(30, 70, 10),
    kernel="auto"
)

print(res["values"].shape)  # (num_combos, len(prices))
print(res["lengths"])       # tested lengths
print(res["gain_limits"])   # tested gain limits
CUDA Acceleration

CUDA support for ECEMA is coming soon. The API will mirror other CUDA-enabled indicators.

JavaScript/WASM Bindings

Basic Usage

Compute ECEMA directly from a Float64Array:

import { ehlers_ecema_js } from 'vectorta-wasm';

const prices = new Float64Array([/* data */]);
const values = ehlers_ecema_js(prices, 20, 50);
console.log(values);
Memory-Efficient Operations

Use zero-copy into/alloc helpers (and extended flags via into_ex):

import { ehlers_ecema_alloc, ehlers_ecema_free, ehlers_ecema_into, ehlers_ecema_into_ex, memory } from 'vectorta-wasm';

const prices = new Float64Array([/* your data */]);
const n = prices.length;

const inPtr = ehlers_ecema_alloc(n);
const outPtr = ehlers_ecema_alloc(n);
new Float64Array(memory.buffer, inPtr, n).set(prices);

// Basic into
ehlers_ecema_into(inPtr, outPtr, n, 20, 50);
const ecema = new Float64Array(memory.buffer, outPtr, n).slice();

// Extended with pine/confirmed flags
ehlers_ecema_into_ex(inPtr, outPtr, n, 20, 50, /*pine=*/false, /*confirmed=*/false);

ehlers_ecema_free(inPtr, n);
ehlers_ecema_free(outPtr, n);
Batch Processing

Use the unified batch API with range config:

import { ehlers_ecema_batch } from 'vectorta-wasm';

const prices = new Float64Array([/* historical prices */]);
const cfg = {
  length_range: [10, 30, 5],
  gain_limit_range: [30, 70, 10],
};

const out = ehlers_ecema_batch(prices, cfg);
// out: { values: Float64Array, combos: {length?: number, gain_limit?: number}[], rows: number, cols: number }

// Reshape into matrix
const rows = out.rows, cols = out.cols;
const matrix = [] as number[][];
for (let r = 0; r < rows; r++) {
  const start = r * cols;
  matrix.push(Array.from(out.values.slice(start, start + cols)));
}

// Access a specific combo row (e.g., length=20, gain_limit=50)
const i = out.combos.findIndex(c => (c.length ?? 20) === 20 && (c.gain_limit ?? 50) === 50);
const ecema20_50 = i >= 0 ? matrix[i] : undefined;

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