Skip to content

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

1 Commit
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Fintech EMA — Exponential Moving Average Algorithm

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.

Python TypeScript License Tests

📖 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 IndicatorsTrend 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)

Table of contents


What is an EMA?

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.

Why this implementation

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 (null before 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).

Install

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 Finance

Quickstart

Python

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.5

TypeScript

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.5

Streaming (live feeds)

StreamingEMA 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());

Yahoo Finance use cases

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 (yfinance for Python, yahoo-finance2 for 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.

The mathematics

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.

Worked example (exact)

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.

Variants

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.

API reference

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

Input contract & edge cases

  • 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 0 is 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.

Testing

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 download

TypeScript (19 tests, zero runtime dependencies)

cd typescript
npm install
npm test               # node --test with native type stripping
npm run build          # emit dist/ for publishing

Related algorithms

  • D07-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)

References

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.

License

MIT © The Fintech Builder. Part of the 100 FinTech Algorithms library.

Releases

Packages

Contributors

Languages