Skip to content

HMA ​

Hull Moving Average — Alan Hull's WMA(2·WMA(n/2) − WMA(n), √n), a near-lag-free trend filter that combines a fast Wma(n/2), a slow Wma(n), and a final smoothing pass.

Quick reference ​

FieldValue
FamilyMoving Averages
Input typef64 (single close)
Output typef64
Output rangeunbounded; tracks the input price scale
Default parametersperiod is required (no default in either binding)
Warmup period (warmup_period())period + round(√period).max(1) − 1 — exact first-emission index
InterpretationNear-zero-lag trend line with an inherent smoothing step.

Formula ​

half   = max(period / 2, 1)                       // integer division
smooth = max(round(sqrt(period)), 1)              // nearest integer, floor at 1

raw_t  = 2 * WMA(price, half)_t  -  WMA(price, period)_t
HMA_t  = WMA(raw, smooth)_t

The "magic" is the 2·WMA(n/2) − WMA(n) step: the fast WMA leads the slow WMA on a trend, so doubling the fast and subtracting the slow produces a series that is ahead of the input by roughly the WMA lag. The final WMA(…, √n) then smooths the resulting overshoot back down to a clean line. For period = 9 this gives half = 4, smooth = 3; for period = 14, half = 7, smooth = 4.

Parameters ​

NameTypeDefaultValid rangeDescription
periodusizenone>= 1Top-level lookback. The inner WMA periods are derived from it. period = 0 errors with Error::PeriodZero.

(Python class wickra.HMA(period) has no #[pyo3(signature)] default; pass period explicitly.)

Inputs / Outputs ​

From crates/wickra-core/src/indicators/hma.rs:

rust
use wickra::{Indicator, Hma};
// Hma: Input = f64, Output = f64
const _: fn(&mut Hma, f64) -> Option<f64> = <Hma as Indicator>::update;

Python returns float | None (streaming) / array.array('d') (batch, NaN for warmup). Node returns number | null / Array<number> with NaN.

Warmup ​

warmup_period() returns:

period + round(sqrt(period)).max(1) - 1

which gives 11 for Hma::new(9), 17 for Hma::new(14), 19 for Hma::new(16). This figure is exact: the first non-None output lands on input warmup_period() (index warmup_period() - 1).

The number reflects how the three inner WMAs warm up in parallel: the slow WMA(period) emits at input period, then the smoothing WMA(√period) needs √period − 1 more inputs on top.

rust
use wickra::{Indicator, Wma};

// HMA feeds two raw WMAs and smooths their 2·half − full diff:
let mut half_wma = Wma::new(7)?;
let mut full_wma = Wma::new(14)?;
let mut smooth_wma = Wma::new(4)?;
let input = 100.0;
let h = half_wma.update(input);
let f = full_wma.update(input);
let _hma = match (h, f) {
    (Some(h), Some(f)) => smooth_wma.update(2.0 * h - f),
    _ => None,
};

half_wma and full_wma receive every input, so full_wma emits at input period (not later). The 2·half − full diff then flows into smooth_wma, which needs round(√period) of those — giving a first emission at exactly period + round(√period) − 1.

periodround(√period)warmup_period()First emission (input #)
931111
1441717
1641919

This is pinned by the first_emission_matches_warmup_period test in hma.rs: the first call that returns Some is exactly at warmup_period() - 1 (0-indexed).

Edge cases ​

  • Constant series. Feeding [10.0; n] produces Some(10.0) once the chain is warm. All three WMAs converge to 10, so raw = 2·10 − 10 = 10, then WMA(10, smooth) = 10. The unit test constant_series_yields_constant_hma pins this with Hma::new(9) over 80 constants.
  • NaN / infinity inputs. Inherited from the inner Wma: non-finite inputs are silently dropped at the half/full WMA boundary and never reach the 2·h − f arithmetic.
  • Reset. hma.reset() resets all three internal WMAs; the next update starts a full warmup countdown.

Examples ​

Rust ​

rust
use wickra::{BatchExt, Hma, Indicator};

fn main() -> Result<(), Box<dyn std::error::Error>> {
    let mut hma = Hma::new(9)?;
    let prices: Vec<f64> = (1..=20).map(f64::from).collect();
    let out: Vec<Option<f64>> = hma.batch(&prices);
    println!("warmup_period = {}", hma.warmup_period());
    println!("{:?}", out);
    Ok(())
}

Output:

warmup_period = 11
[None, None, None, None, None, None, None, None, None, None, Some(11.0), Some(12.0), Some(13.0), Some(14.0), Some(15.0), Some(16.0), Some(17.0), Some(18.0), Some(19.0), Some(20.0)]

The first Some lands at index 10 (the 11th input) — exactly warmup_period() - 1, as the Warmup section explains. On the linear ramp 1, 2, …, 20, HMA tracks price exactly with no visible lag.

Python ​

python
import numpy as np
import wickra as ta

hma = ta.HMA(9)
out = hma.batch(np.arange(1.0, 21.0))
print("warmup_period =", hma.warmup_period())
print(out)

Output:

warmup_period = 11
[nan nan nan nan nan nan nan nan nan nan 11. 12. 13. 14. 15. 16. 17. 18.
 19. 20.]

Node ​

javascript
const ta = require('wickra');
const hma = new ta.HMA(9);
const prices = Array.from({ length: 20 }, (_, i) => i + 1);
console.log(hma.batch(prices));
console.log('warmupPeriod:', hma.warmupPeriod());

Output:

[
  NaN, NaN, NaN, NaN, NaN, NaN,
  NaN, NaN, NaN, NaN,  11,  12,
   13,  14,  15,  16,  17,  18,
   19,  20
]
warmupPeriod: 11

Interpretation ​

Hma is the lag-reduction trend filter that does not require you to choose between responsiveness and noise: the final WMA(√period) pass is a built-in smoothing step that prevents the kind of whipsaw a Tema of the same period would produce on noisy data. On clean trending data it sits effectively on top of price; on choppy data the smoothing pass keeps the line readable.

The textbook signal is colour-coded slope: HMA turning up = uptrend, turning down = downtrend. Crossover patterns (Hma(9) vs Hma(20)) also work and tend to be cleaner than the equivalent EMA pair.

Prefer Hma over Dema / Tema when your data is noisy enough that the lag-reduction in those would manifest as whipsaws. Prefer Tema / Dema on cleaner data where you want one fewer smoothing step.

Common pitfalls ​

  • Mis-reading the warmup as a lag. warmup_period() is the exact first-emission index (Hma::new(9).warmup_period() == 11, first Some at the 11th input), so it can be used directly for Chain alignment. The leading None/NaN values are warmup, not lag — once HMA emits it tracks price with near-zero lag.
  • Picking period = 2 or 3. The inner half = period / 2 is an integer division floored at 1. For period = 2, half = 1, smooth = 1, and you essentially end up with Wma(2·price − WMA(2)) which is a sharp, noisy line. HMA is designed for period >= 9 or so; for shorter lookbacks reach for Ema(period) or Wma(period) instead.

References ​

Alan Hull, "How to Reduce Lag in a Moving Average", 2005 — the original HMA derivation, hosted on Hull's site at https://alanhull.com/the-hull-moving-average/.

See also ​