Quant Library
Prebuilt strategies. TSMomentum, BollingerMeanRev, MovingAverageCross, RSIMeanRev, Ensemble
Imports
from horizon.quant import (
TSMomentum,
BollingerMeanRev,
MovingAverageCrossStrategy,
RSIMeanRev,
Ensemble,
)
All strategies subclass Strategy and emit real Signal objects. Drop them straight into hz.run(strategies=[...]).
TSMomentum
Time-series momentum. Go long if rolling return > threshold, short if below -threshold.
TSMomentum(
lookback: int = 20,
vol_window: int = 60,
threshold: float = 0.01,
edge_bps: float = 50.0,
horizon_days: int = 5,
)
Features used
Return(window=lookback)RealizedVol(window=vol_window)
Signal logic
if r > threshold:
Direction.Increase, confidence=min(1.0, |r| * 20), edge_bps=edge_bps
elif r < -threshold:
Direction.Decrease, confidence=min(1.0, |r| * 20), edge_bps=edge_bps
BollingerMeanRev
Mean reversion on Bollinger-z (distance from rolling mean in stddev units).
BollingerMeanRev(
window: int = 20,
entry_z: float = 2.0,
vol_window: int = 60,
edge_bps: float = 40.0,
horizon_days: int = 3,
)
Features used
BollingerZ(window=window)RealizedVol(window=vol_window)
Signal logic
if z > entry_z:
Direction.Decrease, edge_bps = edge_bps × (|z| / entry_z)
elif z < -entry_z:
Direction.Increase, edge_bps = edge_bps × (|z| / entry_z)
MovingAverageCrossStrategy
Classic trend-following via fast/slow MA cross.
MovingAverageCrossStrategy(
fast: int = 10,
slow: int = 30,
vol_window: int = 60,
edge_bps: float = 30.0,
horizon_days: int = 5,
)
Features used
MovingAverageCross(fast=fast, slow=slow)RealizedVol(window=vol_window)
Signal logic
if cross > 0:
Direction.Increase
elif cross < 0:
Direction.Decrease
RSIMeanRev
Mean reversion on RSI. Buy when oversold, sell when overbought.
RSIMeanRev(
window: int = 14,
oversold: float = 30.0,
overbought: float = 70.0,
vol_window: int = 60,
edge_bps: float = 35.0,
horizon_days: int = 3,
)
Features used
RSI(window=window)RealizedVol(window=vol_window)
Signal logic
if rsi < oversold:
Direction.Increase, confidence = min(1, (oversold - rsi) / 30)
elif rsi > overbought:
Direction.Decrease, confidence = min(1, (rsi - overbought) / 30)
Ensemble
Combine multiple strategies under one name with per-child weights.
from horizon.quant import Ensemble, TSMomentum, BollingerMeanRev
ensemble = Ensemble(
strategies=[
TSMomentum(lookback=20),
BollingerMeanRev(window=15),
],
weights=[0.6, 0.4],
name="trend_plus_mr",
)
hz.run(strategies=[ensemble], ...)
How it works
- Merges child features (prefixed by child name to avoid collisions)
- On evaluate, calls each child with its own feature namespace view
- Adjusts each signal’s
expected_edge_bpsby the child’s weight - Tags signals with the child’s name for attribution
- Children are instances (not subclasses): users can pre-configure them
Using them all together
import horizon as hz
from horizon.asset_classes import AssetClass, Equity
from horizon.data import SyntheticGBM
from horizon.discovery import StaticUniverse
from horizon.discovery.base import Market
from horizon.portfolio import KellyOptimizer
from horizon.quant import (
BollingerMeanRev,
Ensemble,
MovingAverageCrossStrategy,
TSMomentum,
)
from horizon.risk import RiskProfile
universe = StaticUniverse([
Market(id=t, asset_class=AssetClass.Equity)
for t in ["AAPL", "MSFT", "NVDA", "GOOGL", "AMZN"]
])
data = SyntheticGBM(
market_ids=["AAPL", "MSFT", "NVDA", "GOOGL", "AMZN"],
n_bars=252, seed=42,
)
result = hz.run(
mode="backtest",
strategies=[
TSMomentum(lookback=20, edge_bps=50),
BollingerMeanRev(window=15, entry_z=2.0, edge_bps=40),
MovingAverageCrossStrategy(fast=10, slow=30, edge_bps=30),
],
asset_classes=[Equity],
universe=universe,
portfolio=KellyOptimizer(kelly_fraction=0.2),
risk=RiskProfile.moderate(),
data_source=data,
backtest=hz.BacktestConfig(initial_cash_usd=100_000),
)
Every strategy emits signals into the shared signal store; the portfolio optimizer sees them all and allocates capital across the combined signal list.
Writing your own
Subclass Strategy and use the feature library:
from horizon import Strategy, Signal
from horizon.asset_classes import Equity
from horizon.features import Zscore, RealizedVol
class MyIdea(Strategy):
asset_classes = [Equity]
def __init__(self, window: int = 20):
self.window = window
self.features = {
"z": Zscore(window=window),
"vol": RealizedVol(window=60),
}
self.name = f"MyIdea_{window}"
def evaluate(self, f, universe):
return [
Signal.from_score(m, score=-f.z[m.id], edge_per_stdev=15)
for m in universe
if abs(f.z[m.id]) > 2
]
See Writing strategies for the full recipe.