Quant Library

Prebuilt strategies. TSMomentum, BollingerMeanRev, MovingAverageCross, RSIMeanRev, Ensemble

Imports

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

python
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

python
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).

python
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

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

python
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

python
if cross > 0:
    Direction.Increase
elif cross < 0:
    Direction.Decrease

RSIMeanRev

Mean reversion on RSI. Buy when oversold, sell when overbought.

python
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

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

python
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_bps by 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

python
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:

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

Next