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 , define:
Then:
Every WMA is trailing and newest-heavy. Within an -observation window, the oldest value receives weight 1 and the newest receives weight .
The equation contains three jobs:
WMA(h)creates a responsive short baseline.2 × WMA(h) − WMA(n)extrapolates away from the slower baseline.WMA(r)smooths that aggressive raw path.
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:
HMA's raw stage adds that correction to the short WMA:
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:
For positive periods, this package interprets Integer as truncation:
StockCharts documents a different, nearest-integer policy:
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 , creating the first raw Hull value. But the final WMA needs consecutive raw values.
Therefore:
and:
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:
With n = 5:
At observation 5:
This is only one raw value, so final HMA is still null.
At observation 6:
Now the final two-period WMA has two raw members:
The final stage softens the raw extrapolation while keeping more weight on its newest value.
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:
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:
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.
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:
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:
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.
Rendered from the canonical Mermaid sources linked by this article.
Hull MA calculation flow
Canonical readiness is n + floor(sqrt(n)) − 1 accepted observations. Every
branch is causal: no future source value participates.
ReferencesPrimary sources and evidence notesExpand the source trail, evidence role, and limitations behind the engineering choices.
Expand the source trail, evidence role, and limitations behind the engineering choices.
Primary definition
-
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:
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
-
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), subtractWMA(n), then smooth withWMA(sqrt(n)). -
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.5to6andsqrt(11)to3. This is recorded as a variant because it differs from Hull'sIntegernotation. -
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.
-
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
| Decision | Package choice | Basis |
|---|---|---|
| Half length | max(1, floor(n / 2)) | Hull's Integer(Period/2) |
| Root length | max(1, floor(sqrt(n))) | Hull's Integer(SquareRoot(Period)) |
| WMA orientation | Weights 1..m, oldest to newest | Standard linear WMA convention |
| Initialization | Full window at every stage | Reproducible trailing-window contract |
| Warm-up | Aligned null values | Distinguishes unavailable from numeric zero |
| Missing input | Reject | Avoids an unstated clock or imputation policy |
| Precision | Full precision internally | Prevents 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)) - 1accepted observations; - aligned null count
n + floor(sqrt(n)) - 2; - effective source support of
n + floor(sqrt(n)) - 1observations; - batch time using rolling WMA updates;
- translation and scale equivariance;
- possible overshoot because
2 × short − longis affine, not convex; - the first-ready
n = 5signed coefficients[-1, -4, -7, 0, 27, 30] / 45and step output38/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.
Full dependency-light reference implementations in both supported languages.
/** 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,
};
}
The embedded lab now expands to its full document height, keeping the article as the only scroll surface.