Ehlers FM Demodulator
period = 30 Overview
Ehlers FM Demodulator works from the intrabar difference between close and open rather than from a single source price alone. The raw open-close move is first scaled and clamped, then passed through a recursive filter governed by the chosen period. The result is a smoothed oscillatory waveform intended to behave more like a demodulated cycle component than a classic trend-following line.
Because the initial step clamps the scaled open-close move into a fixed range, very large single-bar differences do not dominate the output indefinitely. That makes the indicator more focused on recurrent waveform shape than on raw magnitude spikes. The output is a single continuous series, so this page stays closer to a filtered signal tool than to a multi-line confirmation study.
Defaults: Ehlers FM Demodulator uses `period = 30` and reads candle `open` plus `close`.
Implementation Examples
Compute the demodulated waveform from open/close slices or from candle open/close fields.
use vector_ta::indicators::ehlers_fm_demodulator::{
ehlers_fm_demodulator,
EhlersFmDemodulatorInput,
EhlersFmDemodulatorParams,
};
use vector_ta::utilities::data_loader::{Candles, read_candles_from_csv};
let output = ehlers_fm_demodulator(&EhlersFmDemodulatorInput::from_slices(
&open,
&close,
EhlersFmDemodulatorParams { period: Some(30) },
))?;
let candles: Candles = read_candles_from_csv("data/sample.csv")?;
let candle_output = ehlers_fm_demodulator(
&EhlersFmDemodulatorInput::with_default_candles(&candles)
)?;
println!("value = {:?}", output.values.last());
println!("series length = {}", candle_output.values.len()); API Reference
Input Methods ▼
// From candles using named open and close sources
EhlersFmDemodulatorInput::from_candles(
&Candles,
&str,
&str,
EhlersFmDemodulatorParams,
) -> EhlersFmDemodulatorInput
// From open/close slices
EhlersFmDemodulatorInput::from_slices(
&[f64],
&[f64],
EhlersFmDemodulatorParams,
) -> EhlersFmDemodulatorInput
// From candles with default parameters
EhlersFmDemodulatorInput::with_default_candles(&Candles)
-> EhlersFmDemodulatorInput Parameters Structure ▼
pub struct EhlersFmDemodulatorParams {
pub period: Option<usize>, // default 30
} Output Structure ▼
pub struct EhlersFmDemodulatorOutput {
pub values: Vec<f64>,
} Validation, Warmup & NaNs ▼
- Open and close inputs must both be non-empty and have identical lengths.
periodmust be greater than zero and no larger than the input length.- The required valid run after the first finite open/close pair is
period - 2bars, with a minimum of one valid bar. - The standard output is NaN-prefixed through
first_valid + period - 3. - Streaming resets on any non-finite open or close input and returns
Noneuntil warmup is rebuilt. - The raw open-close derivative is internally scaled and clamped, so very large single-bar differences saturate before recursive smoothing.
- Batch mode rejects invalid integer ranges and unsupported kernels.
Builder, Streaming & Batch APIs ▼
// Builder
EhlersFmDemodulatorBuilder::new()
.period(usize)
.kernel(Kernel)
.apply_slices(&[f64], &[f64])
EhlersFmDemodulatorBuilder::new()
.apply(&Candles)
EhlersFmDemodulatorBuilder::new()
.into_stream()
// Stream
EhlersFmDemodulatorStream::try_new(EhlersFmDemodulatorParams)
EhlersFmDemodulatorStream::update(open: f64, close: f64) -> Option<f64>
EhlersFmDemodulatorStream::reset()
EhlersFmDemodulatorStream::period() -> usize
// Batch
EhlersFmDemodulatorBatchBuilder::new()
.period_range(start, end, step)
.period_static(usize)
.kernel(Kernel)
.apply_slices(&[f64], &[f64])
EhlersFmDemodulatorBatchBuilder::new()
.apply_candles(&Candles) Error Handling ▼
pub enum EhlersFmDemodulatorError {
EmptyInputData,
DataLengthMismatch { open_len: usize, close_len: usize },
AllValuesNaN,
InvalidPeriod { period: usize, data_len: usize },
NotEnoughValidData { needed: usize, valid: usize },
OutputLengthMismatch { expected: usize, got: usize },
InvalidKernelForBatch(Kernel),
InvalidRange { start: usize, end: usize, step: usize },
} Python Bindings
Python exposes an array-returning single-run function, a streaming class, and a batch function. The single-run binding returns one NumPy array of demodulated values. Batch returns the values matrix plus the tested periods.
import numpy as np
from vector_ta import (
ehlers_fm_demodulator,
ehlers_fm_demodulator_batch,
EhlersFmDemodulatorStream,
)
open_ = np.asarray(open_values, dtype=np.float64)
close = np.asarray(close_values, dtype=np.float64)
values = ehlers_fm_demodulator(
open_,
close,
period=30,
kernel="auto",
)
stream = EhlersFmDemodulatorStream(30)
print(stream.update(open_[-1], close[-1]))
batch = ehlers_fm_demodulator_batch(
open_,
close,
period_range=(20, 40, 10),
kernel="auto",
)
print(batch["periods"], batch["rows"], batch["cols"]) JavaScript/WASM Bindings
The WASM layer exposes a single-run array-returning helper, a unified batch helper, and pointer-oriented in-place
exports. The batch export is published to JavaScript as ehlers_fm_demodulator_batch even though the
underlying Rust function name is ehlers_fm_demodulator_batch_unified_js.
import init, {
ehlers_fm_demodulator_js,
ehlers_fm_demodulator_batch,
} from "/pkg/vector_ta.js";
await init();
const open_ = new Float64Array(openValues);
const close = new Float64Array(closeValues);
const values = ehlers_fm_demodulator_js(open_, close, 30);
console.log(values);
const batch = ehlers_fm_demodulator_batch(open_, close, {
period_range: [20, 40, 10],
});
console.log(batch.values, batch.periods, batch.rows, batch.cols); CUDA Bindings (Rust)
Additional details for the CUDA bindings can be found inside the VectorTA repository.
Performance Analysis
Across sizes, Rust CPU runs about 1.14× faster than Tulip C in this benchmark.
AMD Ryzen 9 9950X (CPU) | NVIDIA RTX 4090 (GPU)