Hull Moving Average (HMA)

Parameters: period = 5

Overview

The Hull Moving Average eliminates nearly all lag from traditional moving averages through an ingenious mathematical technique that doubles the weight of recent prices, subtracts out the slower average, then smooths the result with a square root period weighted average. This three step process first calculates a fast WMA at half the period and doubles it, then subtracts a full period WMA to remove lag, finally smoothing this raw difference with a WMA of square root period length to restore stability without reintroducing delay. The result tracks price movements with exceptional responsiveness during trends while maintaining the smoothness needed to filter out market noise and prevent false signals. Traders particularly value HMA for catching trend changes at their inception rather than after significant moves have already occurred, as the indicator turns coincident with price rather than lagging behind like traditional averages. When HMA slopes upward, it confirms bullish momentum with minimal delay, while downward slopes provide early warning of bearish pressure before standard moving averages even begin to turn. The mathematical elegance of using the square root of the period for final smoothing creates an optimal balance between noise reduction and responsiveness that adapts naturally to different timeframes without requiring parameter adjustments.

Implementation Examples

Compute HMA from slices or candles:

use vectorta::indicators::moving_averages::hma::{hma, HmaInput, HmaParams};
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 = HmaParams { period: Some(5) }; // default 5
let input = HmaInput::from_slice(&prices, params);
let result = hma(&input)?;

// From Candles with defaults (source="close", period=5)
let candles: Candles = read_candles_from_csv("data/sample.csv")?;
let input = HmaInput::with_default_candles(&candles);
let result = hma(&input)?;

// Access values
for v in result.values { println!("HMA: {}", v); }

API Reference

Input Methods
// From price slice
HmaInput::from_slice(&[f64], HmaParams) -> HmaInput

// From candles with custom source
HmaInput::from_candles(&Candles, &str, HmaParams) -> HmaInput

// With defaults (source="close", period=5)
HmaInput::with_default_candles(&Candles) -> HmaInput
Parameters Structure
#[derive(Debug, Clone)]
pub struct HmaParams {
    pub period: Option<usize>, // Default: 5
}
Output Structure
pub struct HmaOutput {
    pub values: Vec<f64>, // HMA values (NaN prefix during warmup)
}
Validation, Warmup & NaNs
  • Rejects empty input: HmaError::NoData; rejects all-NaN inputs: HmaError::AllValuesNaN.
  • period > 0 and period ≤ data_len; else HmaError::InvalidPeriod.
  • Requires at least period valid points after the first finite value; else HmaError::NotEnoughValidData.
  • Additional constraints: period/2 > 0 (HmaError::ZeroHalf) and ⌊√period⌋ > 0 (HmaError::ZeroSqrtPeriod).
  • First finite output index is first + period + ⌊√period⌋ − 2; earlier indices are NaN.
Error Handling
use vectorta::indicators::moving_averages::hma::{hma, HmaInput, HmaParams, HmaError};

let input = HmaInput::from_slice(&prices, HmaParams { period: Some(5) });
match hma(&input) {
    Ok(out) => println!("HMA ok: {} values", out.values.len()),
    Err(HmaError::NoData) => eprintln!("hma: no data provided"),
    Err(HmaError::AllValuesNaN) => eprintln!("hma: all values are NaN"),
    Err(HmaError::InvalidPeriod { period, data_len }) =>
        eprintln!("hma: invalid period={} for len={}", period, data_len),
    Err(HmaError::ZeroHalf { period }) =>
        eprintln!("hma: half window is zero for period={}", period),
    Err(HmaError::ZeroSqrtPeriod { period }) =>
        eprintln!("hma: sqrt window is zero for period={}", period),
    Err(HmaError::NotEnoughValidData { needed, valid }) =>
        eprintln!("hma: need {} valid points, have {}", needed, valid),
}

Python Bindings

Basic Usage

Compute HMA with NumPy arrays (period is required):

import numpy as np
from vectorta import hma

prices = np.array([100.0, 101.0, 102.5, 101.8, 103.2], dtype=float)

# Required period; optional kernel: "auto" | "scalar" | "avx2" | "avx512"
vals = hma(prices, period=5, kernel="auto")
print(vals)
Streaming Real-time Updates
from vectorta import HmaStream

stream = HmaStream(period=5)
for price in price_feed:
    y = stream.update(price)
    if y is not None:
        handle(y)
Batch Parameter Sweep
import numpy as np
from vectorta import hma_batch

prices = np.array([...], dtype=float)
out = hma_batch(prices, period_range=(5, 30, 5), kernel="auto")

print(out['values'].shape)  # (rows, len(prices))
print(out['periods'])       # tested periods
CUDA Acceleration

Available when built with CUDA features.

import numpy as np
from vectorta import hma_cuda_batch_dev, hma_cuda_many_series_one_param_dev

# Option 1: One series, many parameters (device buffer returned)
prices_f32 = np.array([...], dtype=np.float32)
period_range = (5, 30, 5)
device_buf, meta = hma_cuda_batch_dev(prices_f32, period_range, device_id=0)
print(meta['periods'])

# Option 2: Many series (time-major), one parameter
tm = np.array([...], dtype=np.float32)  # shape [T, N]
dev_arr = hma_cuda_many_series_one_param_dev(tm, period=20, device_id=0)

JavaScript/WASM Bindings

Basic Usage

Calculate HMA in JavaScript/TypeScript:

import { hma_js } from 'vectorta-wasm';

const prices = new Float64Array([100, 101, 102.5, 101.8, 103.2]);
const values = hma_js(prices, 5);
console.log(values);
Memory-Efficient Operations

Use zero-copy operations for large datasets:

import { hma_alloc, hma_free, hma_into, memory } from 'vectorta-wasm';

const prices = new Float64Array([/* your data */]);
const len = prices.length;

const inPtr = hma_alloc(len);
const outPtr = hma_alloc(len);
new Float64Array(memory.buffer, inPtr, len).set(prices);

// Args: in_ptr, out_ptr, len, period
hma_into(inPtr, outPtr, len, 5);
const values = new Float64Array(memory.buffer, outPtr, len).slice();

hma_free(inPtr, len);
hma_free(outPtr, len);
Batch Processing

Run many period values and get a row-major matrix:

import { hma_batch, hma_batch_into, hma_alloc, hma_free, hma_batch_js, hma_batch_metadata_js, memory } from 'vectorta-wasm';

const prices = new Float64Array([/* historical prices */]);

// Unified helper (returns { values, combos, rows, cols })
const cfg = { period_range: [5, 30, 5] };
const res = hma_batch(prices, cfg);

// Or zero-copy variant allocating output as rows*cols
const [start, end, step] = [5, 30, 5];
const rows = Math.floor((end - start) / step) + 1;
const cols = prices.length;
const outPtr = hma_alloc(rows * cols);
const inPtr = hma_alloc(cols);
new Float64Array(memory.buffer, inPtr, cols).set(prices);

const computedRows = hma_batch_into(inPtr, outPtr, cols, start, end, step);
const flat = new Float64Array(memory.buffer, outPtr, rows * cols).slice();

hma_free(inPtr, cols);
hma_free(outPtr, rows * cols);

// Legacy helpers also exist
const meta = hma_batch_metadata_js(start, end, step);
const flatLegacy = hma_batch_js(prices, start, end, step);

Performance Analysis

Comparison:
View:

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

Loading chart...

AMD Ryzen 9 9950X (CPU) | NVIDIA RTX 4090 (GPU) | Benchmarks: 2026-01-05

Related Indicators