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
from horizon.features import SMA, EMA
SMA. Simple Moving Average
SMA(window: int = 20, market: str | None = None)
Computes the arithmetic mean of the last window prices:
SMA(window) = Σ prices[-window:] / window
Behavior
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
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
EMA(
span: int = 20,
alpha: float | None = None,
market: str | None = None,
)
spanintalphafloat | NoneMath
EMA(t) = α × price(t) + (1 - α) × EMA(t-1)
With α = 2 / (span + 1). Recent prices get exponentially more weight than older ones.
Behavior
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
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
| Property | SMA | EMA |
|---|---|---|
| Weighting | Equal across window | Exponential, recent-weighted |
| Lag | High | Lower |
| Responsiveness | Slow | Fast |
| Stability | Very stable | Slightly noisier |
| Common use | Baseline trend filter | Signal 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
Pitfalls
Tests
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)