D07-F01-A03 / Complete engineering topic

Weighted Moving Average (WMA): Newest-Heavy Linear Smoothing

A production-minded guide to Weighted Moving Average (WMA): Newest-Heavy Linear Smoothing.

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

A Simple Moving Average asks every observation in its window to contribute equally. A Weighted Moving Average asks a more specific question:

What is the window’s mean if newer observations receive progressively greater influence?

Take three values:

10, 13, 1210,\ 13,\ 12

Give the oldest value multiplier 1, the middle value 2, and the newest value 3:

1(10)+2(13)+3(12)1+2+3=726=12\frac{1(10)+2(13)+3(12)}{1+2+3} =\frac{72}{6} =12

That is the canonical three-observation WMA used in this topic. The multiplication is easy. Most implementation bugs come from the surrounding choices: weight direction, normalization, warm-up, window movement, missing data, and confusing WMA with other weighted indicators.

What you will build

By the end, you will be able to:

  • map values to linear recency multipliers;
  • normalize those multipliers into weights that sum to one;
  • calculate WMA directly and with a constant-time rolling update;
  • distinguish WMA from SMA, EMA, VWMA, and arbitrary-weight averages;
  • implement matching Python and TypeScript versions;
  • validate the result with invariants rather than one final number.

The required prerequisite is Simple Moving Average. You need to understand a trailing fixed-count window and full-window warm-up. EMA is useful comparison context, but WMA does not require recursive exponential state.

“Weighted” does not define the weights

The general weighted mean is:

xˉw=iwixiiwi\bar{x}_w=\frac{\sum_i w_ix_i}{\sum_iw_i}

NIST's weighted-mean reference and NumPy's maintained average documentation both document this numerator-over-total-weight structure. But that formula does not tell us where the weights come from.

Weights might represent:

  • observation recency;
  • trading volume;
  • measurement precision;
  • portfolio exposure;
  • survey frequency;
  • an arbitrary model specification.

For technical-analysis WMA, the common linear convention assigns the smallest multiplier to the oldest observation and increases by one toward the newest. TradingView's moving-average documentation presents this exact schedule, and its Pine reference describes weights falling in arithmetic progression with age.

This package therefore makes the naming contract explicit:

Plain text
WMA = trailing linearly weighted moving average
oldest multiplier = 1
newest multiplier = window

An arbitrary-weight moving average is a broader algorithm. A reversed schedule is a valid weighted mean, but it answers the opposite recency question.

The triangular denominator

For a window of length nn, the raw multipliers are:

1,2,,n1,2,\ldots,n

Their total is the triangular number:

Dn=1+2++n=n(n+1)2D_n=1+2+\cdots+n=\frac{n(n+1)}2

The canonical WMA is:

WMAt(n)=1xtn+1+2xtn+2++nxtDn\operatorname{WMA}_t^{(n)} =\frac{1x_{t-n+1}+2x_{t-n+2}+\cdots+nx_t}{D_n}

For n=3n=3:

D3=3(4)2=6D_3=\frac{3(4)}2=6

and the normalized weights are:

16, 26, 36\frac16,\ \frac26,\ \frac36

They sum to one. The newest value contributes half of the final result.

Linear WMA weight ladder showing values 10, 13, and 12 receiving multipliers 1, 2, and 3

The multipliers rise from oldest to newest. Open the weight ladder at full size.

Notice the distinction:

  • 1, 2, 3 are raw multipliers;
  • 1/6, 2/6, 3/6 are normalized weights;
  • 10, 26, 36 are weighted products;
  • 72 is the weighted sum;
  • 12 is the normalized WMA.

Forgetting to divide by six returns 72, which has the wrong scale and is not an average.

The full six-observation example

Continue the D07 fixture:

[10,13,12,15,14,18][10,13,12,15,14,18]

with window = 3.

ObservationWindowWeighted calculationWMA
110Not readynull
210, 13Not readynull
310, 13, 12(10+26+36)/6(10+26+36)/612
413, 12, 15(13+24+45)/6(13+24+45)/613.6667
512, 15, 14(12+30+42)/6(12+30+42)/614
615, 14, 18(15+28+54)/6(15+28+54)/616.1667

The full-precision output is:

Plain text
[null, null, 12, 13.666666666666666, 14, 16.166666666666668]

The first two outputs are null because a three-observation WMA needs three values. Partial-window weighting would rebuild both the schedule and denominator during warm-up. That is a different contract.

Weight orientation is semantic

At observation 3, canonical weights are:

Plain text
value:   10  13  12
weight:   1   2   3

Reverse them:

Plain text
value:   10  13  12
weight:   3   2   1

The reversed result is:

3(10)+2(13)+1(12)6=686=11.3333\frac{3(10)+2(13)+1(12)}6 =\frac{68}{6} =11.3333

Both results are normalized weighted means. Only the first is this package’s canonical recency-weighted WMA.

This is why an API should not accept an undocumented array called weights. It should either expose a named policy or record the exact vector orientation.

Moving the window efficiently

Direct calculation performs nn multiplications for every ready output. That is easy to audit, but a rolling update can do constant work per new observation.

Define the unweighted window sum:

Ut=xtn+1++xtU_t=x_{t-n+1}+\cdots+x_t

and the linear weighted sum:

Lt=1xtn+1++nxtL_t=1x_{t-n+1}+\cdots+nx_t

When the window advances:

Lt=Lt1Ut1+nxtL_t=L_{t-1}-U_{t-1}+nx_t

Why subtract the prior unweighted sum? Every retained observation shifts one position toward the oldest edge, so every retained multiplier decreases by one. Subtracting the whole old window applies that one-unit decrease and also removes the outgoing oldest value.

For observation 4:

L3=72,U3=10+13+12=35L_3=72,\qquad U_3=10+13+12=35 L4=7235+3(15)=82L_4=72-35+3(15)=82

The unweighted sum moves normally:

U4=35+1510=40U_4=35+15-10=40

Then:

WMA4(3)=826=13.6667\operatorname{WMA}_4^{(3)}=\frac{82}{6}=13.6667

The first ready window initializes both sums. Every later window updates them.

Rendering system map…

WMA versus SMA

At observation 6, the active values are 15, 14, and 18.

SMA:

15+14+183=15.6667\frac{15+14+18}{3}=15.6667

WMA:

1(15)+2(14)+3(18)6=16.1667\frac{1(15)+2(14)+3(18)}6=16.1667

WMA finishes closer to the newest value, 18, because that observation receives half the total normalized weight.

Raw values with matching three-observation SMA and newest-heavy WMA paths

The two averages use the same observations and window; only the weights differ. Open the comparison at full size.

This does not establish that WMA is better. It establishes that WMA responds to a different weighting preference.

Historical example: one EFFR window, two weight schedules

To see that preference on a real series without turning the example into a performance claim, use five Effective Federal Funds Rate (EFFR) observations published by the Federal Reserve Bank of New York. The rates in this table are provider observations in percent. The SMA, products, and WMA are calculations made for this article.

Effective dateEFFR observed by the providerWMA multiplier
2024-09-165.33%1
2024-09-175.33%2
2024-09-185.33%3
2024-09-194.83%4
2024-09-204.83%5

The equal-weight calculation is:

SMA(5)=5.33+5.33+5.33+4.83+4.835=5.13%\operatorname{SMA}^{(5)}=\frac{5.33+5.33+5.33+4.83+4.83}{5}=5.13\%

The newest-heavy calculation is:

WMA(5)=1(5.33)+2(5.33)+3(5.33)+4(4.83)+5(4.83)15=5.03%\operatorname{WMA}^{(5)} =\frac{1(5.33)+2(5.33)+3(5.33)+4(4.83)+5(4.83)}{15} =5.03\%

The WMA ends closer to the newest observed rate, 4.83%, because the two newest observations carry 9 of the 15 weight units. That is the visible value of the method: the recency preference is explicit and auditable. It is not evidence that WMA predicted the change, explains why it occurred, or produces a tradable signal.

Provenance: New York Fed endpoint rates/all/search.json, queried with startDate=2024-09-13, endDate=2024-09-26, and type=rate; retrieved 2026-07-22T23:23:25Z. Provider data may be revised. The machine-readable example separates provider fields from derived arithmetic, and the source boundary is recorded in S08.

WMA versus EMA

Both WMA and EMA emphasize recent observations, but their memory is fundamentally different.

WMA:

  • has exactly nn active observations;
  • uses a fixed linear ramp inside the window;
  • drops the oldest value to zero at the boundary;
  • can be recomputed from only the active window.

EMA:

  • updates recursively;
  • uses geometrically decaying influence;
  • retains theoretical influence from all post-seed history;
  • depends on prior state and initialization.

Calling EMA “a type of weighted average” is mathematically reasonable. Calling it the same algorithm as finite linear WMA is not.

WMA is not VWMA

A Volume-Weighted Moving Average assigns weights from a second series:

VWMAt=pivivi\operatorname{VWMA}_t =\frac{\sum p_iv_i}{\sum v_i}

Its weights are volumes, not positions. VWMA requires paired price and volume observations, missing-volume rules, and a zero-volume denominator policy.

WMA needs only one numeric series because its multipliers are generated from position.

The data contract

The numeric core accepts finite values and a positive integer window. A production time-series wrapper also needs:

  • strictly increasing unique timestamps;
  • stable series identity and unit;
  • a declared calendar and frequency;
  • a stable value basis, such as adjusted close;
  • source finality and revision provenance.

A missing timestamp does not become a zero-weight or zero-value row. It simply creates no accepted observation. Numeric zero, when valid for the series, remains a normal value.

For equity prices, mixing adjusted and unadjusted observations can turn a split into a false trend. For currency data, changing conversion policy changes the unit being averaged. Those are input failures, not moving-average behaviors.

Python implementation

The readable optimized structure is:

Python
denominator = window * (window + 1) / 2
window_sum = sum(values[:window])
weighted_sum = sum(
    weight * value
    for weight, value in enumerate(values[:window], start=1)
)
output.append(weighted_sum / denominator)

for index in range(window, len(values)):
    incoming = values[index]
    outgoing = values[index - window]
    weighted_sum = weighted_sum - window_sum + window * incoming
    window_sum = window_sum + incoming - outgoing
    output.append(weighted_sum / denominator)

The implementation validates all input before emitting results. That prevents partial output from escaping before a later invalid value is discovered. It also rejects an overflowed rolling sum rather than publishing a non-finite WMA from otherwise finite but extreme inputs.

TypeScript implementation

TypeScript mirrors the same state:

TypeScript
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];
}

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;
}

Both language implementations run against the same JSON fixtures.

Tests that reveal real mistakes

Direct versus optimized

Independently calculate every ready window using explicit products and compare it with the recurrence. This catches update-order errors.

The weighted update must use the prior window_sum. Updating the unweighted sum first changes the formula.

Window one identity

WMAt(1)=xt\operatorname{WMA}_t^{(1)}=x_t

Constant-series preservation

For any constant cc:

WMA([c,c,,c])=c\operatorname{WMA}([c,c,\ldots,c])=c

Translation invariance

WMA(x+c)=WMA(x)+c\operatorname{WMA}(x+c)=\operatorname{WMA}(x)+c

Range invariant

Every ready output lies between the active window’s minimum and maximum because normalized weights are positive and sum to one.

Monotone-window comparison

For a nondecreasing window, newest-heavy linear WMA cannot be below the SMA of the same values. Reversed weights can violate this expected direction, making the property useful for catching orientation mistakes.

Numerical behavior

The implementation keeps full floating-point precision. Round only when presenting a result.

Long rolling streams can accumulate small binary floating-point differences because the recurrence repeatedly adds and subtracts values. An implementation with strict error bounds can periodically rebuild both sums from the retained window. The policy must be documented and applied consistently across languages.

Explore the weight schedule

Open the WMA Weight Lab.

Try:

  1. Step to observation 3 with window 3. Confirm (1×10 + 2×13 + 3×12) ÷ 6 = 12.
  2. Switch to oldest-heavy orientation. Confirm the result changes to 11.3333 and the badge says “Variant.”
  3. Increase the window to 5. Confirm readiness moves to observation 5 and the denominator becomes 15.
  4. Return to the canonical orientation and play through observation 6.

The chart never reveals future WMA values before their observation arrives.

Failure modes

  • Reversing the weight vector
  • Forgetting to normalize by total weight
  • Using a changing denominator during warm-up
  • Treating WMA as EMA or VWMA
  • Updating window_sum before using its prior value in the recurrence
  • Mixing time-based and observation-count windows
  • Treating missing data as zero
  • Mixing adjusted and unadjusted prices
  • Rounding intermediate state
  • Claiming responsiveness proves predictive value

Where WMA leads next

WMA is a direct dependency for Hull Moving Average, which combines WMAs of different lengths before applying another WMA. Understanding orientation and period rounding is essential there.

The next tutorial in Trend Smoothing is Wilder RMA. It returns to recursive state, but uses a smoothing constant of 1/n1/n. That makes it a useful contrast:

  • WMA: finite linear position weights;
  • EMA: geometric recursive weights with α=2/(n+1)\alpha=2/(n+1);
  • Wilder RMA: geometric recursive weights with α=1/n\alpha=1/n.

Summary

A reproducible WMA requires five explicit facts:

  1. the window is trailing and fixed-count;
  2. the oldest multiplier is one;
  3. the newest multiplier is nn;
  4. the denominator is n(n+1)/2n(n+1)/2;
  5. output remains null until the full window exists.

Once those are fixed, formulas, optimized code, tests, visuals, and downstream indicators can agree.

References

The evidence register is available in REFERENCES.md, including NIST’s weighted-mean definition, NumPy’s maintained weighted-average contract, TradingView’s linear finance convention, and TA-Lib’s WMA function classification.

WMA calculation flow

The first ready window initializes both sums. Later windows use the prior unweighted sum to shift every retained multiplier down by one.

Rendering system map…

Takeaway: normalization is unchanged across a run, while the numerator follows the moving window.

ReferencesPrimary sources and evidence notes

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

S01 — NIST Dataplot Weighted Mean

  • Organization: National Institute of Standards and Technology
  • Source type: Official statistical software reference
  • Publication date: March 18, 1997 manual page
  • URL: https://www.itl.nist.gov/div898/software/dataplot/refman2/ch2/weigmean.pdf
  • Accessed: 2026-07-23
  • Supports: General weighted-mean formula wixi/wi\sum w_i x_i / \sum w_i
  • Limitation: Defines weighted means generally, not the finance-specific linear recency schedule

S02 — NumPy average

  • Organization: NumPy Developers
  • Source type: Official maintained library documentation
  • Version viewed: NumPy 2.1 documentation
  • URL: https://numpy.org/doc/2.1/reference/generated/numpy.average.html
  • Accessed: 2026-07-23
  • Supports: Weighted-average calculation, weight-shape requirements, and nonzero denominator
  • Limitation: Does not prescribe which weights a technical-analysis WMA must use

S03 — TradingView Moving Averages

  • Organization: TradingView
  • Source type: Official product methodology/help documentation
  • URL: https://www.tradingview.com/support/solutions/43000502589-moving-averages/
  • Accessed: 2026-07-23
  • Supports: Finance convention that the newest point receives the largest multiplier; the oldest point leaves as the window advances; worked linear weights 1,,n1,\ldots,n
  • Limitation: Product-oriented explanation rather than a formal standard

S04 — TradingView Pine Script ta.wma reference

  • Organization: TradingView
  • Source type: Official language reference
  • Version: Pine Script v6 reference
  • URL: https://www.tradingview.com/pine-script-reference/v6/#fun_ta.wma
  • Accessed: 2026-07-23
  • Supports: WMA factors decreasing in arithmetic progression with age and an equivalent loop implementation
  • Limitation: Product-language reference rather than an independent mathematical standard

S05 — TA-Lib Functions

  • Organization: TA-Lib project
  • Source type: Official maintained technical-analysis library documentation
  • URL: https://ta-lib.org/functions/
  • Accessed: 2026-07-23
  • Supports: WMA as a distinct overlap-study function alongside SMA, EMA, TRIMA, and other moving averages
  • Limitation: Function index confirms scope but does not fully state the weight formula on that page

S06 — TA-Lib Generic Moving Average

  • Organization: TA-Lib project
  • Source type: Official maintained implementation documentation
  • URL: https://ta-lib.org/functions/ma/
  • Accessed: 2026-07-23
  • Supports: WMA as a selectable moving-average implementation; period one returns the input unchanged
  • Limitation: Dispatcher documentation, not a derivation of WMA

S07 — TradingView Hull Moving Average

S08 — New York Fed Effective Federal Funds Rate data and API observation

  • Organization: Federal Reserve Bank of New York
  • Source type: Official reference-rate methodology and historical data API
  • Data endpoint: https://markets.newyorkfed.org/api/rates/all/search.json
  • Non-secret parameters: startDate=2024-09-13, endDate=2024-09-26, type=rate
  • Methodology URL: https://www.newyorkfed.org/markets/reference-rates/effr
  • API and data hub URL: https://www.newyorkfed.org/markets/data-hub
  • Retrieved: 2026-07-22T23:23:25Z
  • Supports: EFFR observations of 5.33% on 2024-09-16 through 2024-09-18 and 4.83% on 2024-09-19 and 2024-09-20
  • Derived in this package: Five-observation SMA of 5.13% and newest-heavy WMA of 5.03%
  • Limitation: The API response is a provider observation that can be revised. It supports an arithmetic demonstration, not a causal account, forecast, or trading claim.

Claim map and implementation choices

Claim or choiceEvidence
A weighted mean divides the weighted sum by total weightS01, S02
Finance-style WMA gives the newest observation the largest linear multiplierS03, S04
The canonical schedule is oldest 11 through newest nnS03, S04; package convention
window = 1 is the identityS06 and direct formula
WMA is distinct from SMA, EMA, TRIMA, and VWMAS03, S05
Hull MA depends on WMAS07
The five EFFR inputs in the historical arithmetic exampleS08
Full-window null warm-up, finite-value rejection, and display-only roundingExplicit package implementation choices aligned with the D07 family contract

Reproducibility notes

  • Synthetic fixture: [10, 13, 12, 15, 14, 18]
  • Canonical window: 3
  • Weight schedule: oldest to newest [1, 2, 3]
  • Denominator: 1+2+3=61+2+3=6
  • Expected output: [null, null, 12, 82/6, 14, 97/6]
  • Historical observation: New York Fed EFFR values [5.33, 5.33, 5.33, 4.83, 4.83]
  • Historical window: 5; weights oldest-to-newest [1, 2, 3, 4, 5]
  • Derived historical SMA: 5.13; derived historical WMA: 5.03
  • No proprietary implementation or API credential is stored.
wma.ts
/** Canonical linearly weighted moving average for D07-F01-A03. */

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

function requireFiniteState(...values: number[]): void {
  if (!values.every(Number.isFinite)) {
    throw new WMAValidationError(
      "calculation exceeded the finite floating-point range.",
    );
  }
}

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

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

  const output: Array<number | null> = Array(
    Math.min(window - 1, numbers.length),
  ).fill(null);
  if (numbers.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 += numbers[index];
    weightedSum += (index + 1) * numbers[index];
  }
  requireFiniteState(windowSum, weightedSum);
  output.push(weightedSum / denominator);

  for (let index = window; index < numbers.length; index += 1) {
    const incoming = numbers[index];
    const outgoing = numbers[index - window];
    weightedSum = weightedSum - windowSum + window * incoming;
    windowSum = windowSum + incoming - outgoing;
    requireFiniteState(windowSum, weightedSum);
    output.push(weightedSum / denominator);
  }
  return output;
}
Full-height labwma playgroundOpen full screen