A smooth market series is easier to read than a noisy one, but smoothing usually arrives late. The Double Exponential Moving Average (DEMA) changes that trade-off. It combines two Exponential Moving Average (EMA) states so the final line reacts sooner than a plain EMA with the same alpha.
The calculation is short:
The consequences are not. DEMA needs two initialization stages, has a longer warm-up, and can overshoot because some of its effective historical weights are negative.
That limitation is the first comparison in the interactive lab. A synthetic step rises from 0 to 10; with span 3, DEMA reaches 10.625 even though the input remains 10. The value is valid under the formula. It is also the clearest warning that DEMA is not a conventional convex average.
This tutorial builds the algorithm from its components, implements it in Python and TypeScript, and tests what “reduced lag” does—and does not—mean.
The outcome we want
Suppose a metric is rising steadily. A plain EMA follows it with a persistent offset. We want to retain recursive smoothing while shrinking that offset.
Patrick G. Mulloy introduced the one-parameter DEMA in the January 1994 article “Smoothing Data With Faster Moving Averages”. The maintained TA-Lib definition expresses it through two nested EMA layers:
DEMA is descriptive smoothing. It is not evidence that the next observation will move in the direction of the line.
First understand the correction
Rewrite the formula:
EMA2 is smoother and usually slower than EMA1. Their gap estimates how far
the first smoother has moved away from its own smoothed history. DEMA adds that
gap back once.
At observation five in the worked example:
The correction is:
and:
The word “double” refers to the two EMA layers. It does not mean doubling the input, alpha, or period.
The canonical EMA convention
This package uses the same convention in both layers.
For span :
Each EMA uses:
and is seeded with the Simple Moving Average (SMA) of its first inputs.
EMA1 consumes source values. EMA2 consumes only available EMA1 values.
That distinction determines the warm-up.
Why DEMA needs observations
The first EMA1 exists at source observation . The second layer then
needs EMA1 states:
So the first EMA2, and therefore the first DEMA, appears at:
With span 3, observations 1 and 2 warm EMA1; observations 3 and 4 warm
EMA2; observation 5 produces the first DEMA.
A complete worked example
Use:
and values:
| Observation | Input | EMA1 | EMA2 | DEMA | State |
|---|---|---|---|---|---|
| 1 | 10 | null | null | null | Warming EMA1 |
| 2 | 13 | null | null | null | Warming EMA1 |
| 3 | 12 | 11.666667 | null | null | Warming EMA2 |
| 4 | 15 | 13.333333 | null | null | Warming EMA2 |
| 5 | 14 | 13.666667 | 12.888889 | 14.444444 | Ready |
| 6 | 18 | 15.833333 | 14.361111 | 17.305556 | Ready |
| 7 | 17 | 16.416667 | 15.388889 | 17.444444 | Ready |
| 8 | 20 | 18.208333 | 16.798611 | 19.618056 | Ready |
At observation 7, the current input is 17 while DEMA is 17.444444. DEMA has crossed above the input after the preceding rise. That is expected overshoot.
Open the interactive DEMA playground to begin at the overshoot comparison. Then choose the worked, ramp, and reversal scenarios:
- the step reveals overshoot clearly;
- the ramp shows the steady offset shrinking after initialization;
- the reversal shows that turning points still have transient delay.
The chart scales only from observations already revealed. Future points do not set the visible axis, so the causal display does not leak the later path.
The effective weights explain the behavior
Let:
After initialization effects decay, the lag- EMA coefficient is:
Applying EMA twice produces:
Combining the layers gives:
For the current observation:
When , this exceeds the plain EMA coefficient . A contemporary letter in the December 1994 Stocks & Commodities archive used this fact to warn that equal nominal periods are not automatically equal-smoothness comparisons.
As grows, the bracket eventually becomes negative. That is the source of a crucial difference:
- EMA weights are non-negative and form a convex average;
- DEMA coefficients sum to one but are not all non-negative;
- DEMA can therefore move beyond recent inputs or component states.
For span 3, alpha is 0.5. The settled coefficients at lags 0 through 5 are
0.75, 0.25, 0.0625, 0, -0.015625, and -0.015625. The negative
tail begins at lag 4. These are filter coefficients, not portfolio weights,
and they describe the settled filter rather than the SMA-seeded startup path.
In the default synthetic step comparison, the aligned ready outputs are:
| Observation | Input | EMA1 | EMA2 | DEMA | Interpretation |
|---|---|---|---|---|---|
| 5 | 0 | 0 | 0 | 0 | Both seeds are ready |
| 6 | 10 | 5 | 2.5 | 7.5 | DEMA reacts faster than EMA1 |
| 7 | 10 | 7.5 | 5 | 10 | DEMA reaches the new level |
| 8 | 10 | 8.75 | 6.875 | 10.625 | DEMA overshoots a flat input |
The table isolates the mechanism. It does not represent a market event or a claim that the response is desirable for a trading strategy.
What “reduced lag” means
For an indefinitely extended linear ramp, the steady-state first moment of the DEMA coefficients is zero:
This cancels the constant offset that a plain EMA retains behind a ramp.
It does not mean:
- zero delay at every frequency;
- no startup transient;
- no lag at reversals;
- no noise amplification;
- knowledge of a future observation.
“Zero lag” without those qualifications is an overstatement.
A reproducible implementation
The core state machine is:
validate span and values
alpha = 2 / (span + 1)
for each source value:
update or seed EMA1
if EMA1 is unavailable:
emit warming_ema1
continue
feed EMA1 into EMA2
update or seed EMA2
if EMA2 is unavailable:
emit warming_ema2
continue
emit 2 * EMA1 - EMA2
The Python implementation and TypeScript implementation both return aligned component states. Null warm-up output is deliberate: inventing early values would silently choose another seed convention.
Data rules matter as much as the formula
The production contract is one ordered, homogeneous series. The delivered Python and TypeScript functions are compact numeric batch kernels: they accept only values and span. They validate what they receive, but timestamp, identity, calendar, adjustment-basis, and provisionality checks belong in an upstream wrapper.
- Values must be finite and non-null.
- Timestamps must be unique and increasing.
- Missing rows do not become zero or create decay steps.
- Unit, frequency, calendar, identity, and adjustment basis remain stable.
- Price adjustment happens before smoothing.
- Recursive states retain full precision.
If an old observation changes, both EMA layers and every later DEMA value must be replayed. The delivered batch kernel replays from the beginning. A separate streaming wrapper may replay from a verified earlier checkpoint, but that checkpoint must contain both EMA states—not only the displayed DEMA.
What the tests prove
Both languages consume one shared fixture. The suite checks:
- all worked
EMA1,EMA2, and DEMA values; - first output at ;
- span-one identity;
- constant-input preservation;
- step-response overshoot;
- translation and scale equivariance;
- rejection of invalid spans and values;
- source immutability;
- coefficient unity gain and zero steady-state first moment;
- visual syntax, controls, accessibility, and fixture agreement.
Passing those tests proves that the implementation matches the declared definition. It does not prove that a DEMA crossover predicts returns.
Compare neighboring methods carefully
| Method | Construction | Warm-up under SMA seeds | Main behavior |
|---|---|---|---|
| SMA | Equal finite-window weights | Abrupt window entry and exit | |
| EMA | One recursive exponential layer | Convex and lagging | |
| DEMA | Less ramp lag, possible overshoot | ||
| TEMA | Three-layer binomial combination | Further compensation, more complexity |
Comparing all four with the same integer span answers “what does this parameter do in each formula?” It does not hold smoothness or frequency response equal.
Practical use and misuse
DEMA can be useful when:
- observations arrive continuously;
- constant-memory updates matter;
- plain EMA lag is operationally costly;
- some overshoot is acceptable and measured;
- the parameter is evaluated against a defined objective.
It can be misleading when:
- a close visual fit is treated as predictive;
- span is tuned on the evaluation period;
- costs and execution delay are ignored;
- adjusted and unadjusted data are mixed;
- different initialization rules are compared as if identical.
Why the default case is synthetic
A historical close series can make the calculation feel concrete, but a provider interval alone does not explain a market cause or validate a signal. This tutorial therefore uses a synthetic step to isolate negative-weight overshoot exactly. A future dated example should be published only from a redistributable official source with the price basis, retrieval time, and raw observations documented separately from author-derived EMA and DEMA values.
Summary
DEMA combines:
or equivalently:
Two SMA-seeded layers create a warm-up. The correction reduces steady-state ramp lag, while negative older coefficients allow overshoot. Those are two sides of the same design.
Next, TEMA extends the same lag-compensation idea to three EMA layers.
For the canonical contract, exact example, code, tests, evidence, and visual QA, see the topic README and references.
Rendered from the canonical Mermaid sources linked by this article.
DEMA calculation flow
Purpose: show why two separately seeded EMA layers create three visible states.
The first layer accepts source values; the second accepts only available EMA1
states. With an SMA seed of length span in each layer, the first ready DEMA
appears at source observation 2 × span − 1.
Takeaway: warm-up alignment is part of the algorithm, not a display choice.
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.
DEMA-01 — Smoothing Data With Faster Moving Averages
- Organization or authors: Patrick G. Mulloy; Technical Analysis of Stocks & Commodities
- Source type: Original practitioner publication introducing DEMA1
- Publication or effective date: January 1994
- Version: Volume 12, issue 1, pages 11–19; archive synopsis accessed 2026-07-20
- URL: Publisher archive
- Jurisdiction: General securities technical analysis
- Supports: DEMA's provenance; its purpose as exponential smoothing with less lag than a plain EMA; descriptive rather than inferential use.
- Limitations: The complete article is paywalled. Its reported trading example is historical and does not establish predictive or out-of-sample value.
DEMA-02 — TA-Lib DEMA definition
- Organization or authors: TA-Lib project
- Source type: Maintained official technical documentation and reference implementation
- Publication or effective date: Continuously maintained
- Version: Web documentation and main-branch C source accessed 2026-07-20
- URL: DEMA documentation and C implementation
- Jurisdiction: Open-source technical documentation
- Supports:
EMA1 = EMA(input, period),EMA2 = EMA(EMA1, period),DEMA = 2 × EMA1 − EMA2; period-one identity; two EMA lookbacks; SMA seeding in the default implementation. - Limitations: TA-Lib compatibility modes and unstable-period settings are library conventions. This package specifies its own complete output alignment and validation contract.
DEMA-03 — Follow-up discussion of equivalent period
- Organization or authors: Letter published by Technical Analysis of Stocks & Commodities
- Source type: Contemporary technical criticism
- Publication or effective date: December 1994
- Version: Volume 12, issue 12, pages 537–541; archive synopsis accessed 2026-07-20
- URL: Letters to S&C
- Jurisdiction: General securities technical analysis
- Supports: For the same alpha, DEMA places weight
alpha × (2 − alpha)on the current value versusalphafor EMA; a nominal EMA period is not automatically a smoothness-equivalent DEMA period. - Limitations: The archive exposes the letter synopsis rather than the full exchange. It does not define a universal period-matching criterion.
DEMA-04 — EMA foundation package
- Organization or authors: The Fintech Builder
- Source type: Canonical local topic package
- Publication or effective date: Reviewed 2026-07-20
- Version: D07-F01-A02
- URL: EMA package
- Jurisdiction: Educational implementation contract
- Supports: Finance span conversion, SMA initialization, missing-value policy, recursive precision, and time-series provenance inherited by both DEMA layers.
- Limitations: Local design contract rather than an external authority.
Evidence and design reconciliation
Sourced facts:
- Mulloy introduced the one-parameter DEMA in 1994 to reduce lag relative to a plain EMA.
- Maintained TA-Lib documentation defines DEMA as
2 × EMA1 − EMA2. - A period of one returns the input.
- With the same alpha, DEMA's newest-observation coefficient is larger than EMA's.
Derived results independently verified in this package:
- With SMA seeding in each layer, the first DEMA appears at observation
2 × span − 1. - The steady-state impulse coefficient at lag
kisalpha × (1 − alpha)^k × [2 − alpha(k + 1)]. - Some older coefficients are negative, so DEMA is not a convex average and may overshoot.
- The steady-state first moment of those coefficients is zero, explaining exact tracking of an indefinitely extended linear ramp after transients decay.
Explicit implementation choices:
- Both EMA layers use
alpha = 2 / (span + 1)and the same positive integer span. - Each EMA layer uses an SMA seed over its first
spanvalid inputs. - Missing and non-finite values are rejected; absent timestamps do not create updates.
- Warm-up outputs are null and recursive state is never rounded.
Claims intentionally not made:
- “Reduced lag” is not “no delay at every frequency.”
- Equal span labels do not guarantee equal smoothness across EMA and DEMA.
- DEMA is not a forecast, trading rule, or evidence of profitable signals.
Historical-case publication boundary
The audit identified a dated overshoot interval as potentially useful, but a provider series must have explicit redistribution permission and exact price basis before values are copied into this public package. No provider response or private research bundle is treated as public factual evidence here. The synthetic step is retained because it isolates the mathematical failure mode without attaching an unsupported event narrative.
Full dependency-light reference implementations in both supported languages.
/** Canonical SMA-seeded Double Exponential Moving Average for D07-F01-A05. */
export class DEMAValidationError extends Error {
constructor(message: string) {
super(message);
this.name = "DEMAValidationError";
}
}
export type DEMAStatus = "warming_ema1" | "warming_ema2" | "ready";
export interface DEMAPoint {
readonly value: number;
readonly ema1: number | null;
readonly ema2: number | null;
readonly dema: number | null;
readonly status: DEMAStatus;
}
export function calculateDemaComponents(
values: readonly number[],
span: number,
): DEMAPoint[] {
if (!Number.isSafeInteger(span) || span < 1) {
throw new DEMAValidationError("span must be a positive integer.");
}
if (!Array.isArray(values)) {
throw new DEMAValidationError("values must be an array.");
}
const numbers = values.map((value, index) => {
if (typeof value !== "number" || !Number.isFinite(value)) {
throw new DEMAValidationError(
`values[${index}] must be a finite number.`,
);
}
return value;
});
const alpha = 2 / (span + 1);
const ema1Seed: number[] = [];
const ema2Seed: number[] = [];
let ema1: number | null = null;
let ema2: number | null = null;
const output: DEMAPoint[] = [];
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, dema: 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, dema: null, status: "warming_ema2"});
} else {
output.push({value, ema1, ema2, dema: 2 * ema1 - ema2, status: "ready"});
}
}
return output;
}
export function calculateDema(
values: readonly number[],
span: number,
): Array<number | null> {
return calculateDemaComponents(values, span).map((point) => point.dema);
}
/** Return settled infinite-history coefficients, not SMA-seeded warm-up weights. */
export function demaSteadyStateWeights(
span: number,
length: number,
): number[] {
if (!Number.isSafeInteger(span) || span < 1) {
throw new DEMAValidationError("span must be a positive integer.");
}
if (!Number.isSafeInteger(length) || length < 0) {
throw new DEMAValidationError("length must be a non-negative integer.");
}
const alpha = 2 / (span + 1);
const q = 1 - alpha;
return Array.from(
{length},
(_, lag) => alpha * q ** lag * (2 - alpha * (lag + 1)),
);
}
The embedded lab now expands to its full document height, keeping the article as the only scroll surface.