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 EmptyInputData or AllValuesNaN.
  • Requires at least 14 valid points after the first finite value; else NotEnoughValidData {{ needed: 14, valid }}.
  • Warmup with 1‑bar lag: wma1 = first + 7, wma2 = first + 13, predict = first + 13, trigger = first + 16; earlier indices are NaN.
  • Zero‑copy APIs validate output sizes and may return InvalidPeriod on mismatch.
  • Streaming returns None until buffers fill; then Some((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

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