Features
FeatureStore + built-in feature classes
FeatureStore
from horizon.features import FeatureStore
store = FeatureStore(max_history=1000)
# Each tick
store.update_feeds(feeds, now=now)
# Compute features for a universe
namespace = store.compute(
feature_spec={"z": Zscore(20), "vol": RealizedVol(60)},
universe_market_ids=["AAPL", "MSFT"],
feeds=feeds,
)
# Access
z_aapl = namespace.z["AAPL"]
vol_msft = namespace.vol["MSFT"]
Methods
update_feeds(feeds: dict[str, FeedData], now: datetime | None = None): append new prices to histories, clear per-tick cache. Silently rejects NaN/Inf/≤0 prices.history(market_id: str) -> PriceHistory: get the rolling history for a market.has_market(market_id: str) -> bool: check if a market has any history.compute(feature_spec, universe_market_ids, feeds) -> FeatureNamespace: compute every feature in the spec for every market in the universe.
PriceHistory
@dataclass
class PriceHistory:
max_len: int = 1000
prices: list[float]
log_returns: list[float]
timestamps: list[datetime]
Methods
update(price: float, ts: datetime | None = None): append a new price. Silently rejects NaN/Inf/≤0.last_price() -> float | Nonelast_n_prices(n: int) -> list[float]last_n_returns(n: int) -> list[float]__len__() -> int
Feature base class
from horizon.features.base import Feature, PriceHistory
from horizon.context import FeedData
class Feature(ABC):
def __init__(self, *, market: str | None = None):
"""``market`` pins this feature to a specific market id."""
self.market = market
@abstractmethod
def compute(
self,
market_id: str,
history: PriceHistory,
feeds: dict[str, FeedData],
) -> float:
...
@property
def key(self) -> str:
"""Unique cache key. class name + all attribute params."""
Built-in features
Price
Price(market: str | None = None)
Most recent price for a market. Useful as cross-asset reference (pin with market=...).
Return
Return(window: int = 1, market: str | None = None)
Log return over the last window periods. Returns NaN if insufficient history.
Zscore
Zscore(window: int = 20, market: str | None = None)
Rolling z-score of the most recent log return against the last window returns. Returns 0.0 if the std is 0.
RealizedVol
RealizedVol(
window: int = 60,
annualize: bool = True,
periods_per_year: float = 252.0,
market: str | None = None,
)
Annualized (by default) standard deviation of log returns over the last window periods.
SMA
SMA(window: int = 20, market: str | None = None)
Simple moving average of the last window prices.
EMA
EMA(span: int = 20, alpha: float | None = None, market: str | None = None)
Exponential moving average. If alpha is None, derived from span as 2 / (span + 1).
RSI
RSI(window: int = 14, market: str | None = None)
Wilder’s Relative Strength Index. Returns a value in [0, 100]. Neutral is 50.
BollingerZ
BollingerZ(window: int = 20, market: str | None = None)
Distance from rolling mean price in stddev units. Like Zscore but over price levels rather than returns.
MovingAverageCross
MovingAverageCross(fast: int = 10, slow: int = 30, market: str | None = None)
Normalized distance between a fast and slow SMA: (fast_ma - slow_ma) / slow_ma. Positive in an uptrend, negative in a downtrend.
FeatureNamespace
The object returned from store.compute(). Access features by name, then by market id:
ns = store.compute({"z": Zscore(20), "vol": RealizedVol(60)}, ["AAPL"], feeds)
ns.z["AAPL"] # scalar
ns.vol["AAPL"] # scalar
"z" in ns # True
ns.nonexistent # AttributeError
Writing custom features
Subclass Feature and implement compute:
import math
from horizon.features.base import Feature, PriceHistory
from horizon.context import FeedData
class MaxDrawdownOver(Feature):
def __init__(self, window: int = 20, market: str | None = None):
super().__init__(market=market)
self.window = window
def compute(self, market_id, history, feeds):
prices = history.last_n_prices(self.window)
if len(prices) < self.window:
return float("nan")
peak = prices[0]
max_dd = 0.0
for p in prices:
peak = max(peak, p)
if peak > 0:
max_dd = max(max_dd, (peak - p) / peak)
return max_dd
Use immediately in a strategy: no registration needed.
Tests
21 unit tests in tests/test_features.py cover every feature against hand-calculated expected values.