Ehlers Error-Correcting EMA (ECEMA)
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 > 0andlength ≤ len(data); otherwiseInvalidPeriod { period, data_len }.- Require at least
lengthvalid values after the first finite; otherwiseNotEnoughValidData { needed, valid }. gain_limit > 0; otherwiseInvalidGainLimit { gain_limit }.- Warmup: regular mode writes
NaNuntilfirst + length − 1; Pine mode emits from the first valid bar. confirmed_only=trueuses 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
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
Arnaud Legoux Moving Average
Moving average indicator
Compound Ratio Moving Average (CoRa Wave)
Moving average indicator
High-Low Correlation
Technical analysis indicator
Centered Weighted Moving Average
Moving average indicator
Double Exponential Moving Average
Moving average indicator
Ehlers Distance Coefficient Filter
Moving average indicator