A canonical, well-specified, cross-language (Python + TypeScript) reference implementation of the Exponential Moving Average (EMA) as used in financial technical analysis — with a streaming online engine and Yahoo Finance test cases.
📖 Full article (canonical): Exponential Moving Average — The Fintech Builder
This repository is the runnable, production-oriented companion to that article. The article teaches the concept; this repo is the code you install and build on.
🧭 Browse all algorithms: Awesome FinTech Algorithms — the full index of the library. 🗂️ This algorithm's domain: Technical Indicators › Trend Smoothing
| Catalog topic | D07-F01-A02 |
| Domain | D07 — Technical Indicators |
| Family | D07-F01 — Trend Smoothing |
| Difficulty | 2 / 5 |
| Languages | Python, TypeScript |
| Prerequisite | Simple Moving Average (SMA) |
- What is an EMA?
- Why this implementation
- Install
- Quickstart
- Streaming (live feeds)
- Yahoo Finance use cases
- The mathematics
- Worked example (exact)
- Variants
- API reference
- Input contract & edge cases
- Testing
- Related algorithms
- References
- License
An Exponential Moving Average is a causal recursive smoother that moves a
fraction α of the way from its previous state toward the current observation:
E_t = E_{t-1} + α · (x_t − E_{t-1}) (forecast-error form)
E_t = α · x_t + (1 − α) · E_{t-1} (convex-combination form)
For the finance-style span parameter n: α = 2 / (n + 1).
Compared with a Simple Moving Average of the same length, an EMA responds more strongly to recent observations while retaining a geometrically decaying memory of the past. It is a building block for MACD, the McClellan Oscillator, DEMA/TEMA, and exponentially weighted volatility estimators.
EMA does not predict the next value. It is computed from current and past accepted data and therefore lags abrupt changes.
Most "EMA in 3 lines" snippets quietly disagree on the parts that actually matter. This package is explicit about all of them:
- SMA-seeded, span-parameterized canonical EMA with an explicit warm-up
(
nullbefore the seed) — the convention finance charts use. - Full-precision recursive state — internal state is never rounded.
- Strict validation — missing / non-finite values are rejected, never silently skipped; inputs are never mutated.
- Named comparison variants (adjusted finite-history, time-aware decay) that never silently replace the canonical kernel.
- A streaming engine (
StreamingEMA) that yields the identical numbers as the batch kernel, one observation at a time, with checkpoint/restore. - Cross-language parity — the Python and TypeScript suites assert the same
acceptance fractions (
35/3, 40/3, 41/3, 95/6).
Python
pip install fintech-ema # core, zero dependencies
pip install "fintech-ema[yahoo]" # + live Yahoo Finance (yfinance)TypeScript / JavaScript (Node ≥ 20)
npm install fintech-ema
npm install yahoo-finance2 # optional, for live Yahoo FinancePython
from fintech_ema import ema, StreamingEMA, alpha_from_span
prices = [10, 13, 12, 15, 14, 18]
ema(prices, span=3)
# [None, None, 11.666…, 13.333…, 13.666…, 15.833…]
alpha_from_span(3) # 0.5TypeScript
import { ema, StreamingEMA, alphaFromSpan } from "fintech-ema";
const prices = [10, 13, 12, 15, 14, 18];
ema(prices, 3);
// [null, null, 11.666…, 13.333…, 13.666…, 15.833…]
alphaFromSpan(3); // 0.5StreamingEMA holds O(1) state and accepts one tick at a time — ideal for live
tickers, dashboards, and low-memory pipelines. It produces the same numbers as
the batch ema kernel and can be checkpointed for restart or correction
recovery.
from fintech_ema import StreamingEMA
stream = StreamingEMA(span=10)
for tick in feed: # your live data source
value = stream.update(tick) # None during warm-up, then the EMA state
if stream.ready:
publish(value)
checkpoint = stream.state_dict() # JSON-serializable
resumed = StreamingEMA.from_state_dict(checkpoint)import { StreamingEMA } from "fintech-ema";
const stream = new StreamingEMA(10);
for (const tick of feed) {
const value = stream.update(tick);
if (stream.ready) publish(value);
}
const resumed = StreamingEMA.fromStateDict(stream.stateDict());The package ships a Yahoo Finance integration for real-data demos, kept separate from the test path so continuous integration never flakes on network or rate limits.
- Unit tests run offline against a committed synthetic OHLCV fixture that
mimics the Yahoo schema (
Date,Open,High,Low,Close,Adj Close,Volume). - Live downloads are opt-in and go through the optional dependency
(
yfinancefor Python,yahoo-finance2for TypeScript).
from fintech_ema import ema, load_close_series, fetch_close_series
# Offline (deterministic): from any Yahoo-schema CSV or a pandas DataFrame
closes = load_close_series("data.csv") # prefers "Adj Close"
ema(closes, span=20)
# Live (opt-in): requires pip install "fintech-ema[yahoo]"
closes = fetch_close_series("AAPL", period="6mo", interval="1d")
ema(closes, span=20)import { ema, loadCloseSeries, fetchCloseSeries } from "fintech-ema";
const closes = loadCloseSeries("data.csv"); // offline
ema(closes, 20);
const live = await fetchCloseSeries("AAPL", { interval: "1d" }); // opt-in
ema(live, 20);Data note: the committed fixtures are synthetic and exist only to exercise the load → EMA path. They do not represent, and should not be cited as, real market observations.
Span ↔ alpha ↔ half-life
α = 2 / (n + 1) n = 2/α − 1
α = 1 − exp(−ln2 / h) h = ln(0.5) / ln(1 − α) (for 0 < α < 1)
Exponential weights. Expanding the recurrence, the observation k steps
back receives weight w_k = α(1 − α)^k, so influence decays geometrically but
never becomes exactly zero (in exact arithmetic). A span greater than 1 is an
equivalent period, not a hard lookback window. Span 1 is the identity
boundary: α = 1, so E_t = x_t.
Seed. The canonical first state at observation n is the simple average of
the first n valid values: E_n = (x_1 + … + x_n) / n. Output before that is
null.
Span 3 ⇒ α = 2/4 = 1/2. Input: 10, 13, 12, 15, 14, 18.
| # | value x_t |
EMA | status |
|---|---|---|---|
| 1 | 10 | null |
warming |
| 2 | 13 | null |
warming |
| 3 | 12 | 35/3 = 11.6666… |
ready (seed) |
| 4 | 15 | 40/3 = 13.3333… |
ready |
| 5 | 14 | 41/3 = 13.6666… |
ready |
| 6 | 18 | 95/6 = 15.8333… |
ready |
Seed: E₃ = (10+13+12)/3 = 35/3. Step 4: e₄ = 15 − 35/3 = 10/3,
α·e₄ = 5/3, E₄ = 35/3 + 5/3 = 40/3. These exact fractions are the shared
acceptance values asserted by both language test suites.
| Variant | Definition | Function |
|---|---|---|
| Canonical finance EMA | α = 2/(n+1), SMA seed, recursive |
ema |
| Adjusted finite-history | normalize available exponential weights | adjusted_ema / adjustedEma |
| Time-aware | α_t from elapsed time, first-value seed |
time_aware_ema / timeAwareEma |
| Streaming | online O(1) state, identical to canonical | StreamingEMA |
Deliberately out of scope: centered/future-looking smoothing, automatic parameter optimization, Holt/Holt–Winters, DEMA/TEMA/KAMA, and any crossover or trading rule.
Both languages expose the same surface (Python snake_case, TS camelCase):
| Purpose | Python | TypeScript |
|---|---|---|
| Canonical EMA | ema(values, span) |
ema(values, span) |
| Adjusted variant | adjusted_ema(values, span) |
adjustedEma(values, span) |
| Time-aware variant | time_aware_ema(values, elapsed, half_life) |
timeAwareEma(values, elapsed, halfLife) |
| Streaming engine | StreamingEMA(span) |
new StreamingEMA(span) |
| span → α | alpha_from_span(span) |
alphaFromSpan(span) |
| α → span | span_from_alpha(alpha) |
spanFromAlpha(alpha) |
| half-life → α | alpha_from_half_life(h) |
alphaFromHalfLife(h) |
| α → half-life | half_life_from_alpha(alpha) |
halfLifeFromAlpha(alpha) |
| Yahoo (offline) | load_close_series(src) |
loadCloseSeries(path) |
| Yahoo (live) | fetch_close_series(symbol) |
fetchCloseSeries(symbol) |
| Errors | EMAValidationError |
EMAValidationError |
- Ordered, homogeneous series. One numeric value per accepted observation, in a stable unit and adjustment basis. Mixing raw and adjusted prices in one EMA history is invalid.
- Missing / non-finite → rejected, not silently skipped. Numeric
0is a valid observation. - Seed dependence. Different initialization rules produce different early values — this package's choice (SMA seed) travels with the data.
- Revisions. For
0 < α < 1, correcting a past value changes every later EMA value; recompute from the start or from a verified checkpoint. Span 1 is the overwrite boundary. - Precision. Never persist a rounded display value and reuse it as state.
Python (30 tests; live Yahoo test deselected by default)
cd python
pip install -e ".[dev]"
pytest # offline
pytest -m live # also run the live Yahoo Finance downloadTypeScript (19 tests, zero runtime dependencies)
cd typescript
npm install
npm test # node --test with native type stripping
npm run build # emit dist/ for publishingD07-F01-A01— Simple Moving Average (supplies the seed)D07-F01-A04— Wilder RMA (α = 1/n)D07-F01-A05— DEMA (nested EMA states)D04-F02-A01— Traditional McClellan Oscillator (5% vs 10% EMA of breadth)
Method sources: NIST Engineering Statistics Handbook (single exponential
smoothing), pandas ewm documentation, and StockCharts moving-average
methodology. The recurrence and parameter conversions are sourced; the SMA seed,
missing-data policy, and output contract are explicit package choices. Full
citations are in the companion article.
MIT © The Fintech Builder. Part of the 100 FinTech Algorithms library.