Ehlers Undersampled Double Moving Average

Parameters: fast_length = 6 | slow_length = 12 | sample_length = 5

Overview

Ehlers Undersampled Double Moving Average is a two-line smoother built around a simple idea: do not let every incoming bar immediately change the filter state. Instead, the indicator samples the source only on the chosen cadence, holds that sampled value between sample points, and then feeds the held stream into two Hann-weighted smoothing filters. The result is a fast line and a slow line that can behave more like a deliberate stepwise signal pair than a continuously updated moving-average crossover.

That undersampling stage matters as much as the two smoothing lengths. Shortening the sample cadence makes the lines react more often, while lengthening it causes each accepted sample to persist for more bars before the next update. In candle mode the implementation defaults to `hlcc4`, so the pair is naturally centered on a blended price rather than a raw close.

Defaults: Ehlers Undersampled Double Moving Average uses `fast_length = 6`, `slow_length = 12`, `sample_length = 5`, and defaults candle input to `hlcc4`.

Implementation Examples

Compute the fast and slow lines from a raw slice or from candle `hlcc4` data.

use vector_ta::indicators::moving_averages::ehlers_undersampled_double_moving_average::{
    ehlers_undersampled_double_moving_average,
    EhlersUndersampledDoubleMovingAverageInput,
    EhlersUndersampledDoubleMovingAverageParams,
};
use vector_ta::utilities::data_loader::{Candles, read_candles_from_csv};

let values = vec![100.0, 100.7, 101.2, 100.9, 101.8, 102.1, 101.6, 102.4];

let output = ehlers_undersampled_double_moving_average(
    &EhlersUndersampledDoubleMovingAverageInput::from_slice(
        &values,
        EhlersUndersampledDoubleMovingAverageParams {
            fast_length: Some(6),
            slow_length: Some(12),
            sample_length: Some(5),
        },
    )
)?;

let candles: Candles = read_candles_from_csv("data/sample.csv")?;
let candle_output = ehlers_undersampled_double_moving_average(
    &EhlersUndersampledDoubleMovingAverageInput::with_default_candles(&candles)
)?;

println!("fast = {:?}", output.fast.last());
println!("slow = {:?}", candle_output.slow.last());

API Reference

Input Methods
// From candles and a named source field
EhlersUndersampledDoubleMovingAverageInput::from_candles(
    &Candles,
    &str,
    EhlersUndersampledDoubleMovingAverageParams,
) -> EhlersUndersampledDoubleMovingAverageInput

// From a raw slice
EhlersUndersampledDoubleMovingAverageInput::from_slice(
    &[f64],
    EhlersUndersampledDoubleMovingAverageParams,
) -> EhlersUndersampledDoubleMovingAverageInput

// From candles with default parameters
EhlersUndersampledDoubleMovingAverageInput::with_default_candles(&Candles)
    -> EhlersUndersampledDoubleMovingAverageInput
Parameters Structure
pub struct EhlersUndersampledDoubleMovingAverageParams {
    pub fast_length: Option<usize>,   // default 6
    pub slow_length: Option<usize>,   // default 12
    pub sample_length: Option<usize>, // default 5
}
Output Structure
pub struct EhlersUndersampledDoubleMovingAverageOutput {
    pub fast: Vec<f64>,
    pub slow: Vec<f64>,
}
Validation, Warmup & NaNs
  • The input slice must be non-empty and contain at least one finite value or the function returns EmptyInputData or AllValuesNaN.
  • fast_length, slow_length, and sample_length must each be in 1..=4096.
  • Batch range definitions validate the same bounds and reject invalid integer sweeps for each axis.
  • Single-run output is NaN-prefixed only through the first valid source index; once finite input begins, the filters start emitting values immediately.
  • Streaming returns None until the first finite update arrives, then returns Some((fast, slow)) on each later call.
  • Destination slices for in-place APIs must match the input length exactly.
  • Batch mode rejects unsupported kernels through InvalidKernelForBatch.
Builder, Streaming & Batch APIs
// Builder
EhlersUndersampledDoubleMovingAverageBuilder::new()
    .fast_length(usize)
    .slow_length(usize)
    .sample_length(usize)
    .kernel(Kernel)
    .apply_slice(&[f64])

EhlersUndersampledDoubleMovingAverageBuilder::new()
    .apply(&Candles)

EhlersUndersampledDoubleMovingAverageBuilder::new()
    .into_stream()

// Stream
EhlersUndersampledDoubleMovingAverageStream::try_new(
    EhlersUndersampledDoubleMovingAverageParams
)
EhlersUndersampledDoubleMovingAverageStream::update(f64)
    -> Option<(f64, f64)>
EhlersUndersampledDoubleMovingAverageStream::reset()

// Batch
EhlersUndersampledDoubleMovingAverageBatchBuilder::new()
    .fast_length_range(start, end, step)
    .slow_length_range(start, end, step)
    .sample_length_range(start, end, step)
    .kernel(Kernel)
    .apply_slice(&[f64])

EhlersUndersampledDoubleMovingAverageBatchBuilder::new()
    .apply_candles(&Candles)
    .apply_candles_source(&Candles, &str)
Error Handling
pub enum EhlersUndersampledDoubleMovingAverageError {
    EmptyInputData,
    AllValuesNaN,
    InvalidFastLength { fast_length: usize },
    InvalidSlowLength { slow_length: usize },
    InvalidSampleLength { sample_length: usize },
    OutputLengthMismatch { expected: usize, got: usize },
    InvalidFastLengthRange { start: usize, end: usize, step: usize },
    InvalidSlowLengthRange { start: usize, end: usize, step: usize },
    InvalidSampleLengthRange { start: usize, end: usize, step: usize },
    InvalidKernelForBatch(Kernel),
}

Python Bindings

Python exposes a tuple-returning single-run function, a streaming class, and a batch function. The single-run binding returns separate NumPy arrays for the fast and slow lines. Batch returns both output matrices plus the tested fast, slow, and sample-length grids.

import numpy as np
from vector_ta import (
    ehlers_undersampled_double_moving_average,
    ehlers_undersampled_double_moving_average_batch,
    EhlersUndersampledDoubleMovingAverageStream,
)

data = np.asarray(close_values, dtype=np.float64)

fast, slow = ehlers_undersampled_double_moving_average(
    data,
    fast_length=6,
    slow_length=12,
    sample_length=5,
    kernel="auto",
)

stream = EhlersUndersampledDoubleMovingAverageStream(6, 12, 5)
print(stream.update(data[-1]))

batch = ehlers_undersampled_double_moving_average_batch(
    data,
    fast_length_range=(4, 8, 2),
    slow_length_range=(10, 16, 2),
    sample_length_range=(3, 5, 1),
    kernel="auto",
)

print(batch["fast_values"].shape, batch["slow_values"].shape)
print(batch["fast_lengths"], batch["slow_lengths"], batch["sample_lengths"])
print(batch["rows"], batch["cols"])

JavaScript/WASM Bindings

The WASM layer exposes an object-returning single-run wrapper, an object-returning batch wrapper, and lower-level allocation and in-place exports. The standard JavaScript path returns one object containing separate fast and slow arrays, while batch returns flattened matrices, tested parameter combos, and the rows and cols shape.

import init, {
  ehlers_undersampled_double_moving_average_js,
  ehlers_undersampled_double_moving_average_batch_js,
} from "/pkg/vector_ta.js";

await init();

const data = new Float64Array(closeValues);

const lines = ehlers_undersampled_double_moving_average_js(data, 6, 12, 5);
console.log(lines.fast, lines.slow);

const batch = ehlers_undersampled_double_moving_average_batch_js(data, {
  fast_length_range: [4, 8, 2],
  slow_length_range: [10, 16, 2],
  sample_length_range: [3, 5, 1],
});

console.log(batch.fast_values, batch.slow_values, batch.combos, batch.rows, batch.cols);

CUDA Bindings (Rust)

Additional details for the CUDA bindings can be found inside the VectorTA repository.

Performance Analysis

Comparison:
View:
Placeholder data (no recorded benchmarks for this indicator)

Across sizes, Rust CPU runs about 1.14× faster than Tulip C in this benchmark.

Loading chart...

AMD Ryzen 9 9950X (CPU) | NVIDIA RTX 4090 (GPU)

Related Indicators