SMA & EMA

Simple and exponential moving averages

SMA and EMA are the two classic moving average features. Both smooth a price series; they differ in how much weight they give to recent bars.

Import

python
from horizon.features import SMA, EMA

SMA. Simple Moving Average

python
SMA(window: int = 20, market: str | None = None)

Computes the arithmetic mean of the last window prices:

SMA(window) = Σ prices[-window:] / window

Behavior

python
def compute(self, market_id, history, feeds):
    prices = history.last_n_prices(self.window)
    if len(prices) < self.window:
        return float("nan")
    return sum(prices) / len(prices)

Returns NaN until window bars of history are available.

Example

python
from horizon.features import SMA

features = {
    "sma20": SMA(window=20),
    "sma50": SMA(window=50),
}

def evaluate(self, f, universe):
    # Long when short MA > long MA
    return [
        Signal.increase(m, edge_bps=30)
        for m in universe
        if f.sma20[m.id] > f.sma50[m.id]
    ]

When to use SMA

  • Simple trend filter
  • Pair with a faster signal for entry/exit timing
  • Baseline for comparison against more sophisticated methods

EMA. Exponential Moving Average

python
EMA(
    span: int = 20,
    alpha: float | None = None,
    market: str | None = None,
)
spanint
Effective window. Determines smoothing factor if `alpha` is not given.
alphafloat | None
Smoothing factor. If None, derived from span as `2 / (span + 1)`.

Math

EMA(t) = α × price(t) + (1 - α) × EMA(t-1)

With α = 2 / (span + 1). Recent prices get exponentially more weight than older ones.

Behavior

python
def compute(self, market_id, history, feeds):
    prices = history.last_n_prices(self.span * 3)   # enough burn-in
    if not prices:
        return float("nan")
    ema = prices[0]
    for p in prices[1:]:
        ema = self.alpha * p + (1 - self.alpha) * ema
    return ema

Note: EMA is computed fresh each tick from the last 3 × span prices. This is less efficient than maintaining an incremental EMA but keeps the feature stateless (the store already has the rolling prices; the feature just reads them).

Example. MACD-like

python
from horizon.features import EMA

features = {
    "ema12": EMA(span=12),
    "ema26": EMA(span=26),
}

def evaluate(self, f, universe):
    # MACD line. long when fast EMA > slow EMA
    return [
        Signal.increase(m, edge_bps=30)
        for m in universe
        if f.ema12[m.id] > f.ema26[m.id]
    ]

SMA vs EMA comparison

PropertySMAEMA
WeightingEqual across windowExponential, recent-weighted
LagHighLower
ResponsivenessSlowFast
StabilityVery stableSlightly noisier
Common useBaseline trend filterSignal entry/exit

For a given period, EMA reacts faster to price changes because recent prices dominate the average. SMA is slower to react but more resistant to single-bar noise.

When to use which

SMA is better when... - You want a stable baseline - Noise is high and you want to ignore it - You're using MA for regime filtering
EMA is better when... - You need to react quickly - You're using MA for entry/exit signals - The underlying signal has positive autocorrelation

Pitfalls

Tests

python
def test_known_mean(self) -> None:
    store = FeatureStore()
    for p in [1.0, 2.0, 3.0, 4.0, 5.0]:
        store.update_feeds({"T": FeedData(market_id="T", price=p)})
    sma = SMA(window=5)
    ns = store.compute({"m": sma}, ["T"], {})
    assert ns.m["T"] == 3.0

def test_ema_monotonic_converges(self) -> None:
    store = FeatureStore()
    for _ in range(100):
        store.update_feeds({"T": FeedData(market_id="T", price=50.0)})
    ema = EMA(span=10)
    ns = store.compute({"e": ema}, ["T"], {})
    assert math.isclose(ns.e["T"], 50.0, abs_tol=1e-6)

Next