D07-F01-A07 / Complete engineering topic

Hull MA

A production-minded guide to Hull MA.

D07 · TECHNICAL INDICATORS
D07-F01-A07Canonical / Tested / Open
D07 / D07-F01

A moving average makes a noisy path easier to read, but it pays for that smoothness by arriving late. The Hull Moving Average attacks that trade-off with an unusual move: it first creates an intentionally aggressive estimate, then smooths that estimate with a short final window.

That construction makes HMA fast and visually smooth. It also makes HMA easy to implement incorrectly. Two charting platforms can both display “HMA(11)” while using different half-period lengths. An implementation can also emit its first value too early by smoothing an incomplete raw series.

This tutorial fixes those ambiguities. You will build the original integer-truncated form, calculate its exact readiness, and see why overshoot is a valid result rather than a bug.

The idea in one equation

For a selected window nn, define:

h=max(1,n/2),r=max(1,n)h=\max(1,\lfloor n/2\rfloor),\qquad r=\max(1,\lfloor\sqrt n\rfloor)

Then:

HMAn(x)=WMAr(2WMAh(x)WMAn(x))\operatorname{HMA}_{n}(x) = \operatorname{WMA}_{r} \left( 2\operatorname{WMA}_{h}(x)-\operatorname{WMA}_{n}(x) \right)

Every WMA is trailing and newest-heavy. Within an mm-observation window, the oldest value receives weight 1 and the newest receives weight mm.

The equation contains three jobs:

  1. WMA(h) creates a responsive short baseline.
  2. 2 × WMA(h) − WMA(n) extrapolates away from the slower baseline.
  3. WMA(r) smooths that aggressive raw path.

Three WMA stages turn the source into Hull MA

Why subtracting an average can reduce lag

Imagine that a source is rising. The short WMA will usually sit above the long WMA because it concentrates on newer, larger values. Their difference is a rough measure of how far the long average trails:

correction=WMAh(x)WMAn(x)\text{correction} = \operatorname{WMA}_{h}(x)-\operatorname{WMA}_{n}(x)

HMA's raw stage adds that correction to the short WMA:

R=WMAh(x)+correctionR = \operatorname{WMA}_{h}(x)+\text{correction}

This is extrapolation. Alan Hull's original explanation uses a sequence from 0 to 9: a long midpoint is 4.5, a short midpoint is 7, and adding their difference to the short midpoint gives 9.5. That deliberate overcompensation helps offset the lag added by the final smoothing stage.

The word “helps” matters. HMA has no access to future observations. It remains a causal transformation of historical data, and reduced lag is not a promise of prediction.

The rounding decision that changes the algorithm

Half of an odd window and the square root of most windows are fractional. Weighted averages need integer lengths, so an implementation must choose a policy.

Alan Hull's published formula uses:

Integer(n/2)andInteger(n)\operatorname{Integer}(n/2) \quad\text{and}\quad \operatorname{Integer}(\sqrt n)

For positive periods, this package interprets Integer as truncation:

n=11h=5,r=3n=11 \Rightarrow h=5,\quad r=3

StockCharts documents a different, nearest-integer policy:

n=11h=6,r=3n=11 \Rightarrow h=6,\quad r=3

Neither label reveals this difference. A production data contract must record the rounding policy, and a test should use an odd window such as 11 so a nearest-rounding implementation cannot accidentally pass.

The interactive lab lets you toggle both policies. With an even window or a convenient square root, the curves may agree. Choose n = 11 to isolate the half-window difference; choose n = 7 to change both derived lengths and delay nearest-mode readiness by one observation.

Warm-up is longer than the selected window

The long WMA first becomes available at observation nn, creating the first raw Hull value. But the final WMA needs rr consecutive raw values.

Therefore:

first ready observation=n+r1\text{first ready observation}=n+r-1

and:

null warm-up count=n+r2\text{null warm-up count}=n+r-2

For n = 5, the root length is 2. The first raw value appears at observation 5, but the first final HMA appears at observation 6.

For n = 20, the root length is 4. The first HMA appears at observation 23, not observation 20.

Returning null during warm-up preserves alignment and distinguishes “not yet defined” from a legitimate zero.

Work through HMA(5)

Use:

10, 13, 12, 15, 14, 18, 17, 20, 19, 2210,\ 13,\ 12,\ 15,\ 14,\ 18,\ 17,\ 20,\ 19,\ 22

With n = 5:

h=2,r=2h=2,\qquad r=2

At observation 5:

WMA2=1(15)+2(14)3=14.333333\operatorname{WMA}_{2} = \frac{1(15)+2(14)}{3} =14.333333\ldots WMA5=1(10)+2(13)+3(12)+4(15)+5(14)15=13.466666\operatorname{WMA}_{5} = \frac{1(10)+2(13)+3(12)+4(15)+5(14)}{15} =13.466666\ldots R5=2(14.333333)13.466666=15.2R_5=2(14.333333\ldots)-13.466666\ldots=15.2

This is only one raw value, so final HMA is still null.

At observation 6:

WMA2=16.666666\operatorname{WMA}_{2}=16.666666\ldots WMA5=15.2\operatorname{WMA}_{5}=15.2 R6=18.133333R_6=18.133333\ldots

Now the final two-period WMA has two raw members:

HMA6=1(15.2)+2(18.133333)3=17.155555\operatorname{HMA}_6 = \frac{1(15.2)+2(18.133333\ldots)}{3} =17.155555\ldots

The final stage softens the raw extrapolation while keeping more weight on its newest value.

The raw stage responds most aggressively and the final stage smooths it

Why HMA can leave the source range

A WMA alone is a convex average. Its non-negative weights sum to one, so its value stays between the minimum and maximum of its window.

The raw Hull stage breaks that property:

2WMAhWMAn2\operatorname{WMA}_{h}-\operatorname{WMA}_{n}

The negative long-WMA term creates negative effective weights for part of the source history. The total effective weight still preserves constants, but the result is no longer range-bounded.

For n = 5, the first ready result makes those signed weights explicit:

HMA5,6=145x1445x2745x3+0x4+35x5+23x6\operatorname{HMA}_{5,6} = -\frac{1}{45}x_1 -\frac{4}{45}x_2 -\frac{7}{45}x_3 +0x_4 +\frac{3}{5}x_5 +\frac{2}{3}x_6

The coefficients sum to one, but the first three are negative. Apply them to the step [0, 0, 0, 0, 10, 10] and the first ready HMA is 38/3, or 12.6667. The source never exceeds 10. This is a reproducible consequence of the signed filter, not evidence of a future observation or a forecast.

The exact signed weights produce a first-ready step value above the source ceiling

That leads to two important tests:

  • a constant input must remain constant once ready;
  • a sharp step can produce an HMA above the step's high or below its low.

Do not test HMA with a “must stay inside the window” invariant. That invariant belongs to a plain WMA, not to an extrapolating filter.

A trustworthy implementation

The implementation should validate the complete input before calculating any stage. It should reject booleans, nulls, infinities, NaN, numeric strings, and nonpositive windows.

The batch algorithm is:

Plain text
validate window and every observation
half = max(1, floor(window / 2))
root = max(1, floor(sqrt(window)))

short = WMA(values, half)
long  = WMA(values, window)
raw   = 2 * short - long where both are ready
hma   = WMA(consecutive ready raw values, root)
align hma to the source with null warm-up

The provided Python and TypeScript versions use the same rolling-WMA update, so all three stages run in linear time. They return the component series as well as the final HMA. Exposing components makes rounding and alignment bugs much easier to diagnose.

Data quality and revision behavior

A fixed-period HMA advances once per accepted observation. It does not know whether two observations are one minute or three days apart. The input contract must therefore declare the series frequency and calendar.

Missing data is not zero. Silently skipping a missing scheduled bar changes the meaning of “five periods,” while inserting a zero fabricates a market observation. Handle the gap upstream or define a separate irregular-time algorithm.

One ready HMA can depend on as many as:

n+r1n+r-1

source observations. Correcting a historical value can alter multiple raw values and then multiple final values. Recompute the affected suffix from a trusted checkpoint; do not patch only the output at the corrected timestamp.

Likewise, an HMA is provisional whenever unresolved provisional observations remain inside its effective support.

Interpretation without overclaiming

HMA is useful when a reader values a smoother that reacts faster than a plain moving average with the same nominal period. Its slope and turning points can help describe an observed trend.

But visual responsiveness is not a trading result. HMA may:

  • amplify a brief reversal;
  • overshoot after a step;
  • differ across platforms because of rounding;
  • react differently when the sampling frequency changes;
  • look compelling in hindsight without surviving costs.

Alan Hull advises against using HMA crossover signals because crossover logic depends on relative lag. Treat that as design guidance from the indicator's creator, not as proof that another signal rule is profitable.

Why this article does not invent a historical market event

The worked path and step are synthetic so every input and intermediate value can be redistributed and checked. A named historical example would add value only with a traceable provider extract that records the symbol, exchange, session dates, source field, adjustment convention, corporate actions, access date, and usage rights. The article would need to label those rows as provider observations and label the WMA, raw Hull, and HMA columns as author calculations. Even then, the example would show calculation and possible platform divergence—not that an event caused the indicator move, predicted a later price, or produced a profitable decision.

Production checklist

Before publishing or comparing an HMA:

  • declare the source field, adjustments, calendar, and frequency;
  • declare newest-heavy WMA weights;
  • record truncation or another rounding policy;
  • use full-window initialization;
  • preserve aligned null warm-up;
  • retain full precision between stages;
  • expose or log derived half and root lengths;
  • test odd windows and nonsquare windows;
  • test identity, constants, equivariance, and overshoot;
  • replay suffixes after corrections;
  • separate indicator fidelity from strategy validation.

Where to go next

HMA uses fixed derived lengths. The next tutorial, Kaufman's Adaptive Moving Average (KAMA), takes a different route: it changes its smoothing rate based on the observed path's efficiency. Comparing them shows the difference between fixed lag compensation and adaptive smoothing.

Before that comparison, use the interactive lab to select window = 7, switch the rounding policy, and compare both the derived lengths and first-ready observation. Then select the step scenario with window = 5 to reproduce the exact 12.6667 overshoot. Those experiments reveal why an implementation contract matters as much as the headline formula.

Hull MA calculation flow

Rendering system map…

Canonical readiness is n + floor(sqrt(n)) − 1 accepted observations. Every branch is causal: no future source value participates.

ReferencesPrimary sources and evidence notes

Expand the source trail, evidence role, and limitations behind the engineering choices.

Primary definition

  1. Alan Hull, The Hull Moving Average
    https://alanhull.com/the-hull-moving-average/
    Accessed 2026-07-23.

    Hull states that he developed HMA in 2005, explains the lag-compensation intuition, and gives:

    WMAInteger(n)(2WMAInteger(n/2)(x)WMAn(x))\operatorname{WMA}_{\operatorname{Integer}(\sqrt n)} \left( 2\operatorname{WMA}_{\operatorname{Integer}(n/2)}(x) - \operatorname{WMA}_{n}(x) \right)

    This is the authority for the package's three-stage definition and positive-integer truncation policy. His discussion of crossover signals is presented as the creator's interpretation, not as an empirical guarantee.

Independent confirmations

  1. TradingView Help Center, Hull Moving Average
    https://www.tradingview.com/support/solutions/43000589149-hull-moving-average/
    Accessed 2026-07-23.

    Confirms the three calculation stages: doubled WMA(n/2), subtract WMA(n), then smooth with WMA(sqrt(n)).

  2. StockCharts ChartSchool, Hull Moving Average (HMA)
    https://chartschool.stockcharts.com/table-of-contents/technical-indicators-and-overlays/technical-overlays/hull-moving-average-hma
    Accessed 2026-07-23.

    Confirms the three WMA stages and documents a nearest-integer vendor convention. Its example maps 11/2 = 5.5 to 6 and sqrt(11) to 3. This is recorded as a variant because it differs from Hull's Integer notation.

  3. Fidelity Learning Center, Hull Moving Average
    https://www.fidelity.com/learning-center/trading-investing/technical-analysis/technical-indicator-guide/hull-moving-average
    Accessed 2026-07-20.

    Secondary confirmation of the short WMA, long WMA, subtraction, and final square-root-period WMA structure.

  4. TradingView Help Center, Moving Averages
    https://www.tradingview.com/support/solutions/43000502589-moving-averages/
    Accessed 2026-07-20.

    Supports the conventional linearly weighted, newest-heavy WMA primitive used by this package.

Canonical decisions

DecisionPackage choiceBasis
Half lengthmax(1, floor(n / 2))Hull's Integer(Period/2)
Root lengthmax(1, floor(sqrt(n)))Hull's Integer(SquareRoot(Period))
WMA orientationWeights 1..m, oldest to newestStandard linear WMA convention
InitializationFull window at every stageReproducible trailing-window contract
Warm-upAligned null valuesDistinguishes unavailable from numeric zero
Missing inputRejectAvoids an unstated clock or imputation policy
PrecisionFull precision internallyPrevents stage-rounding drift

Independent derivations

The following are derived in this package rather than quoted from a source:

  • first HMA readiness at n + floor(sqrt(n)) - 1 accepted observations;
  • aligned null count n + floor(sqrt(n)) - 2;
  • effective source support of n + floor(sqrt(n)) - 1 observations;
  • O(T)O(T) batch time using rolling WMA updates;
  • translation and scale equivariance;
  • possible overshoot because 2 × short − long is affine, not convex;
  • the first-ready n = 5 signed coefficients [-1, -4, -7, 0, 27, 30] / 45 and step output 38/3;
  • finite correction propagation through the nested windows.

These derivations are checked by shared fixtures and executable tests.

Historical-example evidence gate

No named security or dated market event is asserted in this package. Adding one requires a traceable provider extract with symbol and exchange identity, session dates, source-field and adjustment definitions, corporate-action context, access date, and redistribution permission. Provider observations must be separated from author calculations. A historical calculation example does not by itself establish causality, prediction, or strategy performance.

Non-claims

The package does not claim that:

  • HMA predicts future prices;
  • reduced visual lag guarantees lower phase delay at every frequency;
  • HMA turning points are profitable signals;
  • one period setting transfers across instruments or frequencies;
  • nearest-integer and truncation implementations are interchangeable;
  • a responsive historical fit survives costs or out-of-sample evaluation.
hullMa.ts
/** Canonical Hull moving average for D07-F01-A07. */

export class HullMAValidationError extends Error {
  constructor(message: string) {
    super(message);
    this.name = "HullMAValidationError";
  }
}

export interface HullMAResult {
  halfLength: number;
  rootLength: number;
  shortWma: Array<number | null>;
  longWma: Array<number | null>;
  rawHull: Array<number | null>;
  hullMa: Array<number | null>;
}

function wma(values: readonly number[], window: number): Array<number | null> {
  const output: Array<number | null> = Array(
    Math.min(window - 1, values.length),
  ).fill(null);
  if (values.length < window) return output;

  const denominator = (window * (window + 1)) / 2;
  let windowSum = 0;
  let weightedSum = 0;
  for (let index = 0; index < window; index += 1) {
    windowSum += values[index];
    weightedSum += (index + 1) * values[index];
  }
  output.push(weightedSum / denominator);

  for (let index = window; index < values.length; index += 1) {
    const incoming = values[index];
    const outgoing = values[index - window];
    weightedSum = weightedSum - windowSum + window * incoming;
    windowSum = windowSum + incoming - outgoing;
    output.push(weightedSum / denominator);
  }
  return output;
}

export function calculateHullMa(
  values: readonly number[],
  window: number,
): HullMAResult {
  if (!Number.isSafeInteger(window) || window < 1) {
    throw new HullMAValidationError("window must be a positive safe integer.");
  }
  if (!Array.isArray(values)) {
    throw new HullMAValidationError("values must be an array.");
  }

  const numbers = values.map((value, index) => {
    if (typeof value !== "number" || !Number.isFinite(value)) {
      throw new HullMAValidationError(
        `values[${index}] must be a finite number.`,
      );
    }
    return value;
  });

  const halfLength = Math.max(1, Math.floor(window / 2));
  const rootLength = Math.max(1, Math.floor(Math.sqrt(window)));
  const shortWma = wma(numbers, halfLength);
  const longWma = wma(numbers, window);
  const rawHull = shortWma.map((short, index) => {
    const long = longWma[index];
    return short === null || long === null ? null : 2 * short - long;
  });
  const rawValues = rawHull.filter((value): value is number => value !== null);
  const hullMa = [
    ...Array(Math.min(window - 1, numbers.length)).fill(null),
    ...wma(rawValues, rootLength),
  ];

  return {
    halfLength,
    rootLength,
    shortWma,
    longWma,
    rawHull,
    hullMa,
  };
}
Full-height labhull ma playgroundOpen full screen