Ehlers Predictive Moving Average (Ehlers PMA)
Overview
The Ehlers Predictive Moving Average (PMA), developed by John Ehlers, creates a forward looking price estimate by combining double smoothing with mathematical extrapolation to reduce lag beyond traditional moving averages. The indicator first applies a 7 period weighted moving average to price, then calculates another 7 period WMA of that result, creating a double smoothed series. Rather than using this smoothed value directly, PMA extrapolates forward by computing 2 times the first WMA minus the second WMA. This extrapolation projects where price should be if the current trend continues, effectively anticipating future price movement. A final 4 period WMA of the extrapolated values creates a trigger line for generating crossover signals.
The dual line structure of PMA provides both trend prediction and signal generation capabilities. The predict line leads price action, often turning before actual price reversals occur, giving traders advance warning of potential trend changes. The smoother trigger line filters out minor fluctuations in the predict line, reducing false signals while maintaining responsiveness. Crossovers between predict and trigger lines indicate momentum shifts with less lag than traditional moving average crossovers. The fixed internal parameters were optimized by Ehlers through extensive testing, eliminating the need for user adjustment while ensuring consistent performance across different markets.
Traders use Ehlers PMA for early trend detection and timing precise entries and exits. The predictive nature of the indicator helps identify turning points before they become apparent in price action, providing a timing advantage over traditional indicators. Crossovers between the predict and trigger lines generate trading signals, with bullish crosses suggesting longs and bearish crosses indicating shorts. The indicator works particularly well in trending markets where its extrapolation mechanism can accurately project price direction. Many traders combine PMA with momentum oscillators or volatility filters to confirm signals and avoid whipsaws during choppy conditions. The indicator also serves as an innovative trailing stop, staying ahead of price during trends while quickly reversing when momentum shifts.
Implementation Examples
Compute Ehlers PMA (predict and trigger) from prices or candles:
use vectorta::indicators::moving_averages::ehlers_pma::{ehlers_pma, EhlersPmaInput, EhlersPmaParams};
use vectorta::utilities::data_loader::{Candles, read_candles_from_csv};
// 1) From a price slice
let data = vec![100.0, 101.0, 102.0, 101.5, 103.0, 104.0];
let input = EhlersPmaInput::from_slice(&data, EhlersPmaParams::default());
let out = ehlers_pma(&input)?; // out.predict, out.trigger
// 2) From candles with default source ("close")
let candles: Candles = read_candles_from_csv("data/sample.csv")?;
let input = EhlersPmaInput::with_default_candles(&candles);
let out = ehlers_pma(&input)?;
// Iterate results
for (p, t) in out.predict.iter().zip(out.trigger.iter()) {
println!("predict={}, trigger={}", p, t);
} API Reference
Input Methods ▼
// From price slice
EhlersPmaInput::from_slice(&[f64], EhlersPmaParams) -> EhlersPmaInput
// From candles with custom source
EhlersPmaInput::from_candles(&Candles, &str, EhlersPmaParams) -> EhlersPmaInput
// From candles with defaults (source="close")
EhlersPmaInput::with_default_candles(&Candles) -> EhlersPmaInput Parameters Structure ▼
// No configurable parameters
pub struct EhlersPmaParams; // Default: EhlersPmaParams::default() Output Structure ▼
pub struct EhlersPmaOutput {
pub predict: Vec<f64>, // Leading line (NaNs during warmup)
pub trigger: Vec<f64>, // Smoothed signal line
} Validation, Warmup & NaNs ▼
- Errors on
EmptyInputDataorAllValuesNaN. - Requires at least
14valid points after the first finite value; elseNotEnoughValidData {{ needed: 14, valid }}. - Warmup with 1‑bar lag:
wma1 = first + 7,wma2 = first + 13,predict = first + 13,trigger = first + 16; earlier indices areNaN. - Zero‑copy APIs validate output sizes and may return
InvalidPeriodon mismatch. - Streaming returns
Noneuntil buffers fill; thenSome((predict, trigger)).
Error Handling ▼
use vectorta::indicators::moving_averages::ehlers_pma::{ehlers_pma, EhlersPmaError};
match ehlers_pma(&input) {
Ok(out) => process(out.predict, out.trigger),
Err(EhlersPmaError::EmptyInputData) => eprintln!("no data"),
Err(EhlersPmaError::AllValuesNaN) => eprintln!("all values are NaN"),
Err(EhlersPmaError::NotEnoughValidData { needed, valid }) => {
eprintln!("need {needed} valid points, have {valid}");
}
Err(EhlersPmaError::InvalidPeriod { period, data_len }) => {
eprintln!("buffer size mismatch: period={period}, data_len={data_len}");
}
} Python Bindings
Basic Usage ▼
Calculate Ehlers PMA; returns predict and trigger arrays. Optional kernel selects implementation.
import numpy as np
from vectorta import ehlers_pma, ehlers_pma_flat
prices = np.array([100.0, 101.0, 102.0, 101.5, 103.0, 104.0])
# Tuple of (predict, trigger)
predict, trigger = ehlers_pma(prices, kernel="auto")
# Or flat matrix with metadata
flat = ehlers_pma_flat(prices)
assert flat['rows'] == 2 and flat['cols'] == len(prices)
# flat['values'] shape is (2, len)
Streaming Real-time Updates ▼
from vectorta import EhlersPmaStream
stream = EhlersPmaStream()
for price in price_feed:
res = stream.update(price)
if res is not None:
predict, trigger = res
use(predict, trigger)
CUDA Acceleration ▼
Available when installed with CUDA-enabled build. Functions return device arrays.
# from vectorta import (
# ehlers_pma_cuda_batch_dev,
# ehlers_pma_cuda_many_series_one_param_dev,
# )
#
# # One series × many (identical) params (combos inferred from period_range)
# pred_dev, trig_dev = ehlers_pma_cuda_batch_dev(
# data_f32=prices.astype(np.float32),
# period_range=(0, 10, 1), # combos > 0 controls rows
# device_id=0,
# )
#
# # Many series × one param (time-major [T, N])
# pred_dev, trig_dev = ehlers_pma_cuda_many_series_one_param_dev(
# data_tm_f32=portfolio_tm.astype(np.float32),
# device_id=0,
# ) JavaScript/WASM Bindings
Basic Usage ▼
Compute Ehlers PMA in JS/TS. Returns a struct with flattened values and dimensions.
import { ehlers_pma } from 'vectorta-wasm';
const prices = new Float64Array([100, 101, 102, 101.5, 103]);
const result = ehlers_pma(prices); // { values: Float64Array(2*len), rows: 2, cols: len }
// Split rows [predict..., trigger...]
const len = result.cols;
const predict = result.values.slice(0, len);
const trigger = result.values.slice(len);
console.log(predict, trigger); Memory-Efficient Operations ▼
Zero-copy variant using pre-allocated WASM memory.
import { ehlers_pma_alloc, ehlers_pma_free, ehlers_pma_into, memory } from 'vectorta-wasm';
const prices = new Float64Array([/* your data */]);
const n = prices.length;
// Allocate input and output (output holds 2*n)
const inPtr = ehlers_pma_alloc(n);
const outPtr = ehlers_pma_alloc(n);
new Float64Array(memory.buffer, inPtr, n).set(prices);
// Compute directly into pre-allocated memory
ehlers_pma_into(inPtr, outPtr, n);
// Read predict and trigger
const out = new Float64Array(memory.buffer, outPtr, 2*n).slice();
const predict = out.slice(0, n);
const trigger = out.slice(n);
ehlers_pma_free(inPtr, n);
ehlers_pma_free(outPtr, n); 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.