The governed definitions this build depends on. Read them first if a term is unfamiliar.
- PrimaryExponential Moving AverageAn Exponential Moving Average recursively gives recent observations greater influence than progressively older observations.
- ImportantCausal Knowledge CutoffThe time boundary that prevents information unavailable at that moment from influencing a calculation.
- ImportantEMA Period-Label EquivalenceEMA period-label equivalence maps a conventional period label to its smoothing constant.
- ImportantNo-Imputation Missing-Session PolicyA policy that blocks or pauses calculation instead of converting a missing required session into a zero observation.
- ImportantProcessing CheckpointA persisted valid state and provenance bundle from which deterministic processing can safely resume.
- ImportantReady PointThe first eligible observation at which every state required for an indicator output has been initialized.
- ImportantRecursive StateA stored calculation value that a recurrence carries from one accepted observation into the next update.
- ImportantRevision LineageAn ordered chain connecting each correction to the exact earlier record it supersedes.
- ImportantSMA-Seeded EMA InitializationAn EMA initialization policy that uses the arithmetic mean of the first eligible observations as the initial recursive state.
- ImportantSmoothing ConstantA smoothing constant controls how far an exponential state moves toward each new observation.
- ImportantSuffix RecalculationRecomputing a corrected observation and every later output whose recursive state may depend on it.
- ImportantWarm-Up PeriodThe initial span in which an indicator is accumulating enough eligible history to satisfy its declared initialization policy.
- Mentioned10% TrendThe faster McClellan exponential trend that assigns 10 percent weight to the current breadth input.
- Mentioned5% TrendThe slower McClellan exponential trend that assigns 5 percent weight to the current breadth input.
- MentionedTraditional McClellan OscillatorA breadth oscillator equal to the 10% Trend minus the 5% Trend of daily advancing issues minus declining issues.
An ordinary average answers a clear question, but it does not update elegantly when data arrives continuously.
An Exponential Moving Average (EMA) maintains one state. When a new observation arrives, it moves part of the way from the previous state toward the new value:
That single recurrence powers price overlays, MACD, DEMA, TEMA, exponentially weighted risk models, and both McClellan Oscillator variants.
The arithmetic is compact. A reproducible EMA still needs answers to several questions:
- How is chosen?
- How is the first state initialized?
- Are missing observations skipped, rejected, or treated as elapsed decay?
- Does "20-period" mean only 20 observations matter?
- Is the calculation recursive or normalized over the available history?
This article makes every one of those choices explicit.
The intuition: move a fraction of the gap
Suppose the prior EMA is 11.6667 and the current value is 15.
The gap is:
If , apply half of that gap:
Then:
The EMA does not jump all the way to 15. It does not stay at 11.6667 either. It moves by the declared response fraction.
The two equivalent formulas
The forecast-error form is:
Define the current gap:
Then:
The weighted-average form is:
These are algebraically identical.
For , the coefficients are non-negative and sum to one. The update is therefore a convex combination:
If an implementation produces a new EMA beyond both the prior EMA and current value under this canonical rule, something is wrong.
NIST's exponential-smoothing guidance documents the same geometric-decay structure and emphasizes that initialization affects later values. Its forecasting notation uses a one-step index shift; this article uses the current-observation technical-indicator form, where updates .
Converting a span to alpha
Finance charting systems commonly ask for a span or period , then derive:
Examples:
| Span | Alpha | Prior-state persistence |
|---|---|---|
| 1 | 1.000000 | 0.000000 |
| 3 | 0.500000 | 0.500000 |
| 9 | 0.200000 | 0.800000 |
| 19 | 0.100000 | 0.900000 |
| 39 | 0.050000 | 0.950000 |
This relationship is documented in maintained StockCharts methodology and McClellan Financial's EMA explanation.
It also explains the dependency you identified:
- a McClellan 10% Trend is an EMA with , conventionally labeled 19-period;
- a McClellan 5% Trend uses , conventionally labeled 39-period.
The inverse conversion is:
Span is not a finite window
A 20-period SMA retains exactly 20 observations. When the next value arrives, the oldest value drops out completely.
EMA behaves differently.
Repeated substitution gives:
The coefficient on a value lags back is:
For , the weight shrinks geometrically as increases and does not become exactly zero in exact arithmetic. The boundary (span 1) is the exception: the current value receives all the weight and prior influence is zero.
For , the successive new-observation weights are:
For :
This is why calling something a "19-day EMA" does not mean day 20 has vanished. The number 19 is an equivalent span label for the 10% smoothing constant.
Initialization is part of the definition
The recurrence needs a prior EMA. There is no universal seed.
This article uses a common finance convention: seed the EMA with the Simple Moving Average (SMA) of the first valid observations.
Then recurse for .
Pre-seed EMA values are null.
StockCharts documents this SMA-seeded construction. NIST documents multiple legitimate starting-value approaches and notes that initialization matters more when is smaller.
Other conventions include:
First-observation seed
This produces an immediate streaming state but different early values.
Adjusted finite-history weights
This explicitly normalizes the weights available so far.
Prior checkpoint
A trusted state from earlier history can initialize a resumed calculation.
Estimated model level
Forecasting systems may estimate the initial level jointly with other parameters.
These variants converge over time under stable input, but they are not identical near the start. Seed policy must travel with the output.
A six-observation example
Use span 3:
Input values:
| Observation | Value | EMA | State |
|---|---|---|---|
| 1 | 10 | null | Warming |
| 2 | 13 | null | Warming |
| 3 | 12 | 11.666667 | SMA seed |
| 4 | 15 | 13.333333 | Recursive |
| 5 | 14 | 13.666667 | Recursive |
| 6 | 18 | 15.833333 | Recursive |
The seed is:
Observation 4:
Observation 5:
Observation 6:
The path shows two distinct features:
- the first two EMA values are unavailable during the declared warm-up;
- after seeding, the EMA follows the raw series without matching each jump.
Open the interactive EMA playground to reveal the observations causally. Change the span to see alpha and lag change, compare three explicitly labeled initialization conventions, and test the rule that a mixed price basis must be rejected rather than smoothed.
Why EMA lags
Suppose the EMA currently equals , and every future observation becomes a constant .
After updates:
The remaining gap is:
It shrinks geometrically.
A larger alpha closes the gap faster:
- more responsive;
- less smooth;
- more sensitive to short-lived changes.
A smaller alpha retains more prior state:
- smoother;
- slower;
- more sensitive to initialization for longer.
Neither setting is universally better. It depends on the measurement objective.
Half-life gives another way to describe decay
For , the observation-space half-life is:
This is the number of equal update intervals required for influence to fall by half.
The reverse conversion is:
pandas' official exponentially weighted API documents alpha, span, center of mass, and half-life as alternative ways to specify decay. It also documents adjusted versus recursive calculations, missing-value weight policies, and time-aware half-life.
Those settings are not mere syntax aliases when missing data or irregular timestamps are involved.
Missing values: no update is not a zero update
Suppose the previous EMA is 10 and .
If the next real observation is zero:
The state changes.
If the observation is missing and no update occurs:
The state does not change.
Replacing missing data with zero is therefore a substantive modeling choice.
The canonical policy is:
- input values must be finite and non-null;
- a missing row does not advance state;
- the algorithm never fills a missing timestamp with zero;
- upstream imputation, if any, must be declared in the source series.
Libraries can implement missing positions differently. pandas, for example, distinguishes absolute-position and relative-position weighting through ignore_na. Reproducibility requires persisting the selected behavior.
Equal observations versus equal time
A fixed-alpha EMA assumes each accepted observation represents one equal decay step.
That works naturally for a complete daily-session series or evenly sampled intraday bars. It can be misleading when gaps vary.
For a time-aware half-life and elapsed time :
Then:
A larger time gap creates more decay and a larger current-value weight.
This is a different algorithm from applying one fixed-alpha update per row. The package's canonical EMA uses observation-space decay; the time-aware form is an explicit variant.
Data engineering before smoothing
EMA cannot repair an incoherent source series.
Stable value basis
For prices, declare whether the input is:
- raw close;
- split-adjusted close;
- total-return-adjusted value;
- another field.
Do not mix bases inside one EMA history.
Stable units
Currency, percentage points, issue counts, volume, and returns are not interchangeable. EMA preserves the source unit.
Strict order
Timestamps must be unique and increasing. Silent sorting can hide upstream errors and change checkpoint replay.
Corporate actions
A split in an unadjusted price series creates a discontinuity that the EMA will smooth as though it were an economic price move. Adjustment belongs upstream.
Finality
If an observation can be revised, an EMA state that still depends on it remains provisional. With , that dependence persists in exact arithmetic; with , the next accepted value overwrites it.
Corrections persist when alpha is below one
For , correcting changes and every later exact EMA value.
The correction's influence decays by powers of , but it never becomes exactly zero. Exact recomputation therefore starts from:
- the beginning; or
- a verified checkpoint immediately before the corrected point.
A checkpoint should persist:
- full-precision EMA state;
- observation count;
- last timestamp;
- span and alpha;
- seed and missing-data policies;
- series identity, unit, calendar, frequency, and value basis;
- provisionality.
Do not resume from a rounded chart label. Recursive rounding creates cumulative drift.
Span 1 is the important exception. It gives and , so a correction affects its own observation but not the next state. Calling the correction suffix "infinite" without this boundary would be incorrect.
A reproducible algorithm
Pseudocode:
require span is a positive integer
alpha = 2 / (span + 1)
count = 0
seed_sum = 0
ema = null
for observation in strict order:
validate finite value and stable series metadata
count += 1
if count < span:
seed_sum += value
emit warming point with null EMA
continue
if count == span:
seed_sum += value
ema = seed_sum / span
else:
ema = ema + alpha * (value - ema)
emit ready point with full-precision EMA
After warm-up, the algorithm requires constant time and constant persistent state per observation.
Python and TypeScript implementations
The Python implementation and TypeScript implementation execute the same numerical kernels against one shared fixture. Both include the canonical SMA-seeded recurrence plus explicitly named adjusted finite-history and elapsed-time variants. They reject invalid numeric inputs and avoid rounding recursive state. Timestamp order, series identity, value basis, finality, and checkpoint provenance belong to the production adapter because the small array functions do not receive those fields.
What to test
Parameter behavior
- span is a positive integer;
- ;
- span 1 returns the current value;
- direct-alpha variants reject values outside .
Warm-up
- pre-seed values are null;
- the first EMA equals the exact SMA seed;
- first-value and adjusted variants are not confused with the canonical series.
Recurrence
- worked-example values;
- convex-bound invariant;
- constant-input convergence;
- translation and scale equivariance;
- no internal rounding.
Data contract
- null, NaN, and infinity rejection;
- duplicate and unordered timestamps;
- mixed units or value bases;
- missing row versus numeric zero;
- observation-space versus time-aware differences.
State operations
- checkpoint resume;
- historical correction replay;
- provisional suffix propagation;
- source non-mutation.
These tests prove formula fidelity. They do not prove that an EMA crossover predicts returns.
EMA, SMA, and WMA
| Method | Weight shape | Memory | Main implementation trait |
|---|---|---|---|
| SMA | Equal weights | Finite window | Oldest value drops out |
| EMA | Geometric weights | Infinite decaying memory | One recursive state |
| WMA | Explicit weights | Usually finite | Full declared weight vector |
EMA is often more responsive than an equal-span SMA because it gives more influence to recent values. It is still a lagging transformation of observed data.
Connection to the McClellan Oscillator
The Traditional McClellan Oscillator keeps two EMA states of Net Advances:
Everything in that calculation now has a clear meaning:
- is alpha for the fast EMA;
- is prior-state persistence;
- is alpha for the slow EMA;
- 19 and 39 are equivalent span labels;
- each state has initialization and revision history;
- the oscillator is the gap between those two EMA states.
This EMA package is therefore the missing conceptual dependency for the McClellan article.
Rendered from the canonical Mermaid sources linked by this article.
EMA calculation flow
This flow shows the canonical span-parameterized, SMA-seeded EMA lifecycle.
Takeaway: missing or rejected values do not advance state, and the canonical first output appears only when the seed window is complete.
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-20. The package uses only synthetic example data.
Review note (2026-07-23): a named historical price window was deliberately deferred. The current authoritative sources establish methodology but do not provide a redistribution-ready, point-in-time price series with one declared adjustment basis. The synthetic fixture isolates seed and decay mechanics without presenting a provider observation as a market fact. A future case should preserve the provider, symbol, exchange, exact sessions, adjustment basis, retrieval time, and derived-versus-observed boundary.
EMA-01 — NIST single exponential smoothing
- Organization or authors: National Institute of Standards and Technology
- Source type: Engineering Statistics Handbook
- Publication or effective date: Continuously maintained handbook
- Version: Web edition accessed 2026-07-20
- URL: Single Exponential Smoothing
- Jurisdiction: United States standards and statistical guidance
- Supports: Exponentially decreasing weights; recurrence; ; geometric expansion; importance and alternatives of initialization.
- Limitations: Its primary notation is one-step-ahead forecasting, so the observation index is shifted relative to the current-observation technical-indicator EMA used here.
EMA-02 — NIST Dataplot exponential smoothing
- Organization or authors: National Institute of Standards and Technology
- Source type: Official statistical-software reference
- Publication or effective date: Implementation 2000; page last updated 2003
- Version: Web edition accessed 2026-07-20
- URL: Dataplot Exponential Smoothing
- Jurisdiction: United States
- Supports: Current-observation recurrence ; first-value initialization; smoothing-parameter range and intuition.
- Limitations: Uses a first-observation seed rather than this package's finance-style SMA seed.
EMA-03 — pandas exponentially weighted calculations
- Organization or authors: pandas project
- Source type: Official library documentation
- Publication or effective date: pandas 3.0.3 documentation
- Version: Accessed 2026-07-20
- URL: pandas Series.ewm
- Jurisdiction: Open-source technical documentation
- Supports: Conversions among
alpha, span, center of mass, and half-life; adjusted normalized-weight versus recursive calculations; missing-value weight policies; time-aware decay. - Limitations: Library defaults are implementation conventions, not universal financial definitions. The package does not promise byte-for-byte pandas parity.
EMA-04 — StockCharts moving-average methodology
- Organization or authors: StockCharts.com
- Source type: Maintained financial technical-analysis methodology
- Publication or effective date: Continuously maintained
- Version: Web edition accessed 2026-07-20
- URL: Moving Averages — Simple and Exponential
- Jurisdiction: General market analysis
- Supports: EMA as a recent-weighted recursive average; finance convention ; simple-average initialization; old observations retaining influence; moving averages as lagging transformations rather than direction predictors.
- Limitations: Chart-platform behavior and defaults are vendor-specific.
EMA-05 — McClellan EMA calculation
- Organization or authors: McClellan Financial Publications
- Source type: First-party indicator methodology
- Publication or effective date: Not stated
- Version: Web edition accessed 2026-07-20
- URL: Exponential Moving Averages Calculation
- Jurisdiction: United States market breadth
- Supports: Direct recurrence for 10% and 5% trends; span conversion; equivalence to 19- and 39-period EMA labels; infinite decaying memory.
- Limitations: Application-specific to McClellan indicators and not a general missing-data or seed specification.
Evidence and design reconciliation
Sourced facts:
- EMA uses a recursive convex update with exponentially decreasing historical influence.
- The smoothing constant controls responsiveness.
- Span is commonly converted with .
- Initialization materially affects early values.
- Adjusted, recursive, missing-value, and time-aware library variants can differ.
Explicit package choices:
- The canonical topic is a current-observation filter, not a one-step-ahead forecast.
- Span is a positive integer and alpha is derived as .
- The first EMA is the simple average of the first
spanvalid observations. - Pre-seed output is null.
- Missing values are invalid and never silently filled; omitted timestamps do not update observation-space state.
- Internal state is not rounded.
- For , exact corrections recompute the dependent suffix or resume from a verified earlier checkpoint; span 1 is explicitly excepted.
Claims intentionally not made:
- EMA is not presented as a price predictor.
- No crossover or trading rule is asserted.
- A shorter span is not universally superior to a longer span.
Full dependency-light reference implementations in both supported languages.
/** Canonical SMA-seeded exponential moving average for D07-F01-A02. */
export class EMAValidationError extends Error {
constructor(message: string) {
super(message);
this.name = "EMAValidationError";
}
}
export function alphaFromSpan(span: number): number {
if (!Number.isSafeInteger(span) || span < 1) {
throw new EMAValidationError("span must be a positive integer.");
}
return 2 / (span + 1);
}
function validatedValues(values: readonly number[], name = "values"): number[] {
if (!Array.isArray(values)) {
throw new EMAValidationError(`${name} must be an array.`);
}
return values.map((value, index) => {
if (typeof value !== "number" || !Number.isFinite(value)) {
throw new EMAValidationError(`${name}[${index}] must be a finite number.`);
}
return value;
});
}
export function calculateEma(
values: readonly number[],
span: number,
): Array<number | null> {
const alpha = alphaFromSpan(span);
const numbers = validatedValues(values);
const output: Array<number | null> = [];
let seedSum = 0;
let ema: number | null = null;
numbers.forEach((value, zeroBasedIndex) => {
const observation = zeroBasedIndex + 1;
if (observation <= span) seedSum += value;
if (observation < span) {
output.push(null);
} else if (observation === span) {
ema = seedSum / span;
output.push(ema);
} else {
ema = (ema as number) + alpha * (value - (ema as number));
output.push(ema);
}
});
return output;
}
export function calculateAdjustedEma(
values: readonly number[],
span: number,
): number[] {
const alpha = alphaFromSpan(span);
const numbers = validatedValues(values);
const decay = 1 - alpha;
let weightedSum = 0;
let weightTotal = 0;
return numbers.map((value) => {
weightedSum = value + decay * weightedSum;
weightTotal = 1 + decay * weightTotal;
return weightedSum / weightTotal;
});
}
export function calculateTimeAwareEma(
values: readonly number[],
elapsedUnits: readonly number[],
halfLife: number,
): number[] {
const numbers = validatedValues(values);
const intervals = validatedValues(elapsedUnits, "elapsedUnits");
if (numbers.length !== intervals.length) {
throw new EMAValidationError("values and elapsedUnits must have equal length.");
}
if (!Number.isFinite(halfLife) || halfLife <= 0) {
throw new EMAValidationError("halfLife must be a finite positive number.");
}
if (numbers.length === 0) return [];
if (intervals[0] !== 0) {
throw new EMAValidationError("elapsedUnits[0] must be zero.");
}
if (intervals.slice(1).some((interval) => interval <= 0)) {
throw new EMAValidationError("later elapsed intervals must be positive.");
}
let state = numbers[0];
const output = [state];
for (let index = 1; index < numbers.length; index += 1) {
const alphaT = 1 - Math.exp(-Math.log(2) * intervals[index] / halfLife);
state += alphaT * (numbers[index] - state);
output.push(state);
}
return output;
}
The embedded lab now expands to its full document height, keeping the article as the only scroll surface.