D07-F01-A06 / Complete engineering topic

Triple Exponential Moving Average (TEMA): Layered Warm-Up and Signed Weights

A production-minded guide to Triple Exponential Moving Average (TEMA): Layered Warm-Up and Signed Weights.

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

Smoothing makes a noisy time series easier to read, but it also makes sustained changes arrive late. The Triple Exponential Moving Average (TEMA) combines three recursive EMA layers to reduce that lag:

T=3E(1)3E(2)+E(3)T=3E^{(1)}-3E^{(2)}+E^{(3)}

where E(1)E^{(1)} smooths the source, E(2)E^{(2)} smooths E(1)E^{(1)}, and E(3)E^{(3)} smooths E(2)E^{(2)}.

TEMA is not simply the third EMA. It is the signed combination of all three. That combination is more responsive, but it also creates a longer startup and can overshoot. This tutorial builds the complete calculation and makes that trade-off visible.

The outcome we want

A practitioner wants a causal trend estimate that follows sustained movement more closely than an equal-span EMA. A builder wants exact seed, warm-up, missing-data, and replay rules so Python and TypeScript produce the same path.

Patrick G. Mulloy's 1994 follow-up “Smoothing Data With Less Lag” developed the triple-EMA extension. The maintained TA-Lib definition records the same three-layer formula.

This is descriptive smoothing. “Follows sooner” does not mean “knows what happens next.”

Build the three layers

For span nn:

α=2n+1\alpha=\frac{2}{n+1}

Each layer begins with the arithmetic mean of its first nn available inputs and then updates recursively:

Et=Et1+α(ytEt1)E_t=E_{t-1}+\alpha(y_t-E_{t-1})

EMA1 consumes source values. EMA2 consumes only ready EMA1 values. EMA3 consumes only ready EMA2 values.

TEMA's three layers and final combination

The word “triple” counts the nested layers. It does not mean triple the span, alpha, or source value.

Why warm-up lasts 3n23n-2

The first EMA1 appears at source observation nn. The first EMA2 appears after nn EMA1 states, at 2n12n-1. The third layer needs nn EMA2 states, so the first TEMA appears at:

3n23n-2
Rendering system map…

With span 3, observations 1–2 warm EMA1, 3–4 warm EMA2, 5–6 warm EMA3, and observation 7 is the first ready output.

See the correction, not just the formula

Rewrite TEMA:

T=E(1)+2(E(1)E(2))(E(2)E(3))T=E^{(1)}+2(E^{(1)}-E^{(2)})-(E^{(2)}-E^{(3)})

EMA2 trails EMA1 during a sustained rise, and EMA3 trails EMA2. The formula extends EMA1 away from the slower second layer twice, then removes the residual gap between the second and third layers.

That is a lag correction, not a forecast. It uses current and past state only.

A complete span-three example

Use:

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

and α=1/2\alpha=1/2.

Obs.InputEMA1EMA2EMA3TEMAStage
110nullnullnullnullEMA1
213nullnullnullnullEMA1
31211.6667nullnullnullEMA2
41513.3333nullnullnullEMA2
51413.666712.8889nullnullEMA3
61815.833314.3611nullnullEMA3
71716.416715.388914.213017.2963Ready
82018.208316.798615.505819.7350Ready
91918.604217.701416.603619.3119Ready
102220.302119.001717.802721.7037Ready

At observation 7:

E(1)=19712,E(2)=27718,E(3)=1535108E^{(1)}=\frac{197}{12},\quad E^{(2)}=\frac{277}{18},\quad E^{(3)}=\frac{1535}{108} T=4672717.2963T=\frac{467}{27}\approx17.2963

The current input is 17, so TEMA is already above it.

Input and smoothing paths in the worked example

Open the interactive TEMA playground and advance one observation at a time. The step scenario makes overshoot especially clear: span-three TEMA rises to 10.625 after a jump from 0 to 10.

Effective weights explain the trade-off

Let q=1αq=1-\alpha. After initialization effects decay, the lag-kk coefficient is:

wk=αqk[33α(k+1)+α22(k+1)(k+2)]w_k=\alpha q^k\left[ 3-3\alpha(k+1)+ \frac{\alpha^2}{2}(k+1)(k+2) \right]

For the current observation:

w0=1(1α)3w_0=1-(1-\alpha)^3

This is larger than the current coefficient for equal-alpha EMA or DEMA. Farther back, some coefficients are negative. The coefficients still sum to one, but TEMA is not a convex average and can move outside the recent range.

Span-three TEMA effective weights

For span 3, lag 3 has weight −0.03125. That negative coefficient does not mean the market observation was “negative” or bearish. It means the settled filter subtracts part of that older observation. This is why a constant input is preserved—the full coefficient sequence sums to one—while a changing input can be overshot.

The first two steady-state coefficient moments cancel. That reduces the low-order polynomial offset that remains in simpler exponential smoothers after transients. It does not promise:

  • zero phase delay at every frequency;
  • no startup or reversal transient;
  • less noise;
  • no overshoot;
  • predictive information.

Implement the state machine

Plain text
validate span and every source value
alpha = 2 / (span + 1)

for each value:
    seed or update EMA1
    if unavailable: emit warming_ema1; continue

    seed or update EMA2 from EMA1
    if unavailable: emit warming_ema2; continue

    seed or update EMA3 from EMA2
    if unavailable: emit warming_ema3; continue

    emit EMA3 + 3 * (EMA1 - EMA2)

The Python implementation and TypeScript implementation expose all three layers. Null warm-up positions are part of the contract, not missing work.

Data and restart rules

The source must be finite, ordered, and homogeneous.

  • Null is not zero.
  • A missing row creates no decay step.
  • Do not silently sort or deduplicate observations.
  • Keep unit, frequency, identity, calendar, and adjustment basis stable.
  • Never round recursive state.

A historical correction affects all three states and every later TEMA. Replay the suffix from a verified checkpoint. That checkpoint must preserve the three EMA states, unfinished seed progress, span, event identity, and provenance; the displayed TEMA alone cannot resume the filter.

Test the definition, not a trading story

Both implementations consume one synthetic fixture. The tests verify every component, the 3n − 2 alignment, span-one identity, constant preservation, short input, affine equivariance, invalid data, immutability, and a deliberate step overshoot.

Passing those tests means the code implements this declared TEMA. It does not mean a TEMA crossover predicts returns.

Why there is no selected historical “success” story

A named rally or reversal would make the chart feel concrete, but applying TEMA to that path would only produce author-derived arithmetic. It would not show that TEMA predicted the move, improved a decision, or remained useful after costs. A reproducible market example would also need exact symbol and venue identity, session dates, adjustment basis, corporate-action checks, provider retrieval time, and a clear split between provider observations and derived TEMA values.

This lesson therefore uses controlled synthetic paths to isolate warm-up, negative weights, overshoot, and reversal behavior. A later empirical study should predefine its task and evaluation window rather than choose a dramatic event after seeing the result.

Compare fairly

MethodConstructionSMA-seeded warm-upCharacter
EMAOne exponential layernnConvex, persistent lag
DEMA2EMA1EMA22EMA1-EMA22n12n-1First correction, possible overshoot
TEMA3EMA13EMA2+EMA33EMA1-3EMA2+EMA33n23n-2Stronger correction, longer startup
Hull MAWeighted moving-average constructionDefinition-dependentFinite-window lag reduction

Equal integer spans do not equalize smoothness or frequency response.

Summary and next step

TEMA combines three nested EMA states:

T=3E(1)3E(2)+E(3)T=3E^{(1)}-3E^{(2)}+E^{(3)}

Three SMA seeds create a 3n23n-2 warm-up. Stronger current weighting reduces low-order steady-state lag, while signed older weights permit overshoot. Those are two sides of the same mechanism.

Next, Hull Moving Average approaches lag reduction with weighted moving averages and a square-root-length final smoother.

For the canonical contract, exact fixture, code, tests, evidence, and visual QA, see the topic README and references.

TEMA calculation flow

Rendering system map…

With an SMA seed in every layer, span nn first emits TEMA at source observation 3n23n-2.

ReferencesPrimary sources and evidence notes

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

Access date for web sources: 2026-07-23. All package data is synthetic.

TEMA-01 — Smoothing Data With Faster Moving Averages

  • Organization or authors: Patrick G. Mulloy; Technical Analysis of Stocks & Commodities
  • Source type: Original practitioner publication introducing DEMA
  • Publication date: January 1994
  • Version: Volume 12, issue 1; archive synopsis accessed 2026-07-23
  • URL: Publisher archive
  • Supports: Provenance of the lag-corrected multiple-EMA family and its descriptive smoothing purpose.
  • Limitations: The complete article is paywalled; historical examples do not establish predictive value.

TEMA-02 — Smoothing Data With Less Lag

  • Organization or authors: Patrick G. Mulloy; Technical Analysis of Stocks & Commodities
  • Source type: Original practitioner follow-up covering TEMA
  • Publication date: February 1994
  • Version: Volume 12, issue 2; archive synopsis accessed 2026-07-23
  • URL: Publisher archive
  • Supports: TEMA's provenance and extension from single and double to triple EMA combinations.
  • Limitations: The complete article is paywalled. This package does not inherit any trading-performance claim.

TEMA-03 — TA-Lib TEMA definition and reference implementation

  • Organization or authors: TA-Lib project
  • Source type: Maintained official technical documentation and open-source implementation
  • Publication date: Continuously maintained
  • Version: Documentation and main-branch C source accessed 2026-07-23
  • URL: TEMA documentation and C implementation
  • Supports: EMA1, EMA2, EMA3, TEMA = 3EMA1 − 3EMA2 + EMA3; distinction from EMA3; the same period in all layers; three EMA lookbacks; arithmetic-mean EMA seeds under default compatibility.
  • Limitations: TA-Lib documentation and source revisions can differ in accepted minimum period. Compatibility and unstable-period settings are library conventions. This package independently declares span-one identity, validation, aligned nulls, and error behavior.

TEMA-04 — TC2000 TEMA methodology

  • Organization or authors: Worden / TC2000
  • Source type: Official platform methodology
  • Publication date: Continuously maintained
  • Version: Help article accessed 2026-07-23
  • URL: Triple Exponential Moving Average methodology
  • Supports: Independent documentation of 3EMA1 − 3EMA2 + EMA3, Mulloy attribution, and distinction between TEMA and TRIX.
  • Limitations: Platform syntax is product-specific and does not determine this package's seed or missing-data policy.

TEMA-05 — Local EMA and DEMA foundation

  • Organization or authors: The Fintech Builder
  • Source type: Canonical local topic packages
  • Publication date: Reviewed 2026-07-20
  • Version: D07-F01-A02 and D07-F01-A05
  • URL: EMA and DEMA
  • Supports: Finance span conversion, SMA initialization, recursive precision, and the preceding two-layer correction.
  • Limitations: Local educational contracts rather than external authorities.

Evidence and design reconciliation

Sourced facts:

  • Mulloy's 1994 work introduced the DEMA/TEMA lag-correction family.
  • Maintained TA-Lib and TC2000 documentation define TEMA as 3EMA1 − 3EMA2 + EMA3.
  • TEMA is not the third EMA layer and is not TRIX.
  • Current TA-Lib documentation describes period one as the identity case; accepted minimums can vary by vendor release.

Derived results independently verified in this package:

  • SMA seeding in each layer places the first TEMA at observation 3 × span − 2.
  • The steady-state lag-kk coefficient is alpha × (1 − alpha)^k × [3 − 3alpha(k + 1) + alpha²(k + 1)(k + 2)/2].
  • The newest coefficient is 1 − (1 − alpha)³.
  • Some historical coefficients are negative, so TEMA is not a convex average and can overshoot.
  • The coefficients have unity gain and cancel the first two steady-state moments, reducing low-order polynomial lag after transients.

Explicit implementation choices:

  • All three layers use alpha = 2 / (span + 1) and the same positive integer span.
  • Each layer uses an SMA seed over its first span available inputs.
  • Span one is accepted as this package's explicit algebraic identity case; it is not claimed as a universal vendor minimum.
  • Missing and non-finite values are rejected; absent timestamps create no updates.
  • Warm-up output is null, component state is exposed, and recursive state is never rounded.

Claims intentionally not made:

  • Reduced lag is not zero delay at every frequency.
  • TEMA does not predict a turn or imply a profitable trading rule.
  • Equal nominal spans do not create equal smoothness across different filters.
  • A passing implementation test is not a backtest.
  • A selected historical chart would demonstrate derived arithmetic, not predictive or trading value; this package intentionally uses controlled synthetic evidence.
tema.ts
/** Canonical SMA-seeded Triple Exponential Moving Average for D07-F01-A06. */

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

export type TEMAStatus =
  | "warming_ema1"
  | "warming_ema2"
  | "warming_ema3"
  | "ready";

export interface TEMAPoint {
  value: number;
  ema1: number | null;
  ema2: number | null;
  ema3: number | null;
  tema: number | null;
  status: TEMAStatus;
}

export function calculateTemaComponents(
  values: readonly number[],
  span: number,
): TEMAPoint[] {
  if (!Number.isSafeInteger(span) || span < 1) {
    throw new TEMAValidationError("span must be a positive integer.");
  }
  if (!Array.isArray(values)) {
    throw new TEMAValidationError("values must be an array.");
  }

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

  if (span === 1) {
    return numbers.map((value) => ({
      value,
      ema1: value,
      ema2: value,
      ema3: value,
      tema: value,
      status: "ready",
    }));
  }

  const alpha = 2 / (span + 1);
  const ema1Seed: number[] = [];
  const ema2Seed: number[] = [];
  const ema3Seed: number[] = [];
  let ema1: number | null = null;
  let ema2: number | null = null;
  let ema3: number | null = null;
  const output: TEMAPoint[] = [];

  for (const value of numbers) {
    if (ema1 === null) {
      ema1Seed.push(value);
      if (ema1Seed.length === span) {
        ema1 = ema1Seed.reduce((sum, item) => sum + item, 0) / span;
      }
    } else {
      ema1 += alpha * (value - ema1);
    }

    if (ema1 === null) {
      output.push({
        value, ema1: null, ema2: null, ema3: null, tema: null,
        status: "warming_ema1",
      });
      continue;
    }

    if (ema2 === null) {
      ema2Seed.push(ema1);
      if (ema2Seed.length === span) {
        ema2 = ema2Seed.reduce((sum, item) => sum + item, 0) / span;
      }
    } else {
      ema2 += alpha * (ema1 - ema2);
    }

    if (ema2 === null) {
      output.push({
        value, ema1, ema2: null, ema3: null, tema: null,
        status: "warming_ema2",
      });
      continue;
    }

    if (ema3 === null) {
      ema3Seed.push(ema2);
      if (ema3Seed.length === span) {
        ema3 = ema3Seed.reduce((sum, item) => sum + item, 0) / span;
      }
    } else {
      ema3 += alpha * (ema2 - ema3);
    }

    if (ema3 === null) {
      output.push({
        value, ema1, ema2, ema3: null, tema: null,
        status: "warming_ema3",
      });
    } else {
      output.push({
        value,
        ema1,
        ema2,
        ema3,
        tema: ema3 + (3 * ema1 - 3 * ema2),
        status: "ready",
      });
    }
  }

  return output;
}

export function calculateTema(
  values: readonly number[],
  span: number,
): Array<number | null> {
  return calculateTemaComponents(values, span).map((point) => point.tema);
}

/** Return the settled TEMA filter coefficient at a non-negative lag. */
export function temaSteadyStateWeight(span: number, lag: number): number {
  if (!Number.isSafeInteger(span) || span < 1) {
    throw new TEMAValidationError("span must be a positive integer.");
  }
  if (!Number.isSafeInteger(lag) || lag < 0) {
    throw new TEMAValidationError("lag must be a non-negative integer.");
  }

  const alpha = 2 / (span + 1);
  const decay = 1 - alpha;
  return alpha * decay ** lag * (
    3
    - 3 * alpha * (lag + 1)
    + (alpha ** 2 * (lag + 1) * (lag + 2)) / 2
  );
}
Full-height labtema playgroundOpen full screen