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:
where smooths the source, smooths , and smooths .
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 :
Each layer begins with the arithmetic mean of its first available inputs and then updates recursively:
EMA1 consumes source values. EMA2 consumes only ready EMA1 values.
EMA3 consumes only ready EMA2 values.
The word “triple” counts the nested layers. It does not mean triple the span, alpha, or source value.
Why warm-up lasts
The first EMA1 appears at source observation . The first EMA2 appears
after EMA1 states, at . The third layer needs EMA2
states, so the first TEMA appears at:
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:
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:
and .
| Obs. | Input | EMA1 | EMA2 | EMA3 | TEMA | Stage |
|---|---|---|---|---|---|---|
| 1 | 10 | null | null | null | null | EMA1 |
| 2 | 13 | null | null | null | null | EMA1 |
| 3 | 12 | 11.6667 | null | null | null | EMA2 |
| 4 | 15 | 13.3333 | null | null | null | EMA2 |
| 5 | 14 | 13.6667 | 12.8889 | null | null | EMA3 |
| 6 | 18 | 15.8333 | 14.3611 | null | null | EMA3 |
| 7 | 17 | 16.4167 | 15.3889 | 14.2130 | 17.2963 | Ready |
| 8 | 20 | 18.2083 | 16.7986 | 15.5058 | 19.7350 | Ready |
| 9 | 19 | 18.6042 | 17.7014 | 16.6036 | 19.3119 | Ready |
| 10 | 22 | 20.3021 | 19.0017 | 17.8027 | 21.7037 | Ready |
At observation 7:
The current input is 17, so TEMA is already above it.
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 . After initialization effects decay, the lag- coefficient is:
For the current observation:
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.
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
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
| Method | Construction | SMA-seeded warm-up | Character |
|---|---|---|---|
| EMA | One exponential layer | Convex, persistent lag | |
| DEMA | First correction, possible overshoot | ||
| TEMA | Stronger correction, longer startup | ||
| Hull MA | Weighted moving-average construction | Definition-dependent | Finite-window lag reduction |
Equal integer spans do not equalize smoothness or frequency response.
Summary and next step
TEMA combines three nested EMA states:
Three SMA seeds create a 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.
Rendered from the canonical Mermaid sources linked by this article.
TEMA calculation flow
With an SMA seed in every layer, span first emits TEMA at source observation .
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.
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- 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
spanavailable 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.
Full dependency-light reference implementations in both supported languages.
/** 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
);
}
The embedded lab now expands to its full document height, keeping the article as the only scroll surface.