Equity Mean Reversion
Bollinger-based mean reversion with Kelly sizing + moderate risk
A complete working example: mean-reverting strategy on a universe of US tech equities.
The full file
python
"""examples/equity_mean_reversion.py"""
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
from horizon.risk import RiskProfile, StopLoss
def main():
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,
mu=0.10,
sigma=0.22,
seed=42,
)
result = hz.run(
mode="backtest",
strategies=[
BollingerMeanRev(window=20, entry_z=2.0, edge_bps=60, horizon_days=3),
],
asset_classes=[Equity],
universe=universe,
portfolio=KellyOptimizer(
kelly_fraction=0.25,
max_gross_leverage=1.0,
transaction_cost_bps=5,
),
risk=RiskProfile.moderate().override(
stop_loss=StopLoss(per_position_pct=0.08, trailing_pct=0.05),
),
data_source=data,
backtest=hz.BacktestConfig(initial_cash_usd=100_000),
)
print("=" * 60)
print("EQUITY MEAN REVERSION BACKTEST")
print("=" * 60)
print(f" Ticks: {len(result.equity_curve)}")
print(f" Trades: {result.n_trades}")
print(f" Start equity: ${result.equity_curve[0][1]:,.2f}")
print(f" End equity: ${result.equity_curve[-1][1]:,.2f}")
print(f" Peak equity: ${max(e for _, e in result.equity_curve):,.2f}")
print(f" Total return: {result.total_return:+.2%}")
print(f" Sharpe: {result.sharpe:+.3f}")
print(f" Sortino: {result.sortino:+.3f}")
print(f" Max drawdown: {result.max_drawdown:.2%}")
if __name__ == "__main__":
main()
Run it
bash
PYTHONPATH=. python3 examples/equity_mean_reversion.py
What it does
Universe
5 US tech tickers wrapped in
Market objects with asset_class=Equity.Data
SyntheticGBM generates 252 daily bars of GBM for each ticker with mu=10%, sigma=22%, seeded at 42. The same seed always produces the same price paths.Strategy
BollingerMeanRev reads BollingerZ(window=20): distance from rolling mean in stddev units. When |z| > 2, emits an opposite-direction signal (long when below, short when above).Portfolio
KellyOptimizer with quarter Kelly, 1.0 gross leverage, 5 bps transaction cost assumption. Converts each signal to USD notional using edge / vol² math.Risk
RiskProfile.moderate() gives the standard 10% stop loss, 5/10/20% stacked drawdown guards, and kill switch at 25% drawdown. We override the stop to 8% per position with 5% trailing.Run
hz.run(mode="backtest", ...) iterates the historical bars, runs the full pipeline each tick, produces a BacktestResult with equity curve, trades, and summary metrics.Expected output
============================================================
EQUITY MEAN REVERSION BACKTEST
============================================================
Ticks: 252
Trades: ~300 - 600
Start equity: $100,000.00
End equity: $95,000 - $100,000
Peak equity: ~$100,000
Total return: ~0% (GBM has no real MR edge)
Sharpe: ~-0.5 to 0
Sortino: ~-0.5 to 0
Max drawdown: ~5% - 15%
Numbers may vary slightly by seed. Pure GBM has no exploitable mean reversion, so the strategy is expected to churn transaction costs and show negative-to-neutral Sharpe. The risk enforcement is the observable win: stops + drawdown guards bound max DD.
Variations
More aggressive sizing
python
portfolio=KellyOptimizer(
kelly_fraction=0.5, # half Kelly. more aggressive
max_gross_leverage=1.5, # up to 1.5x gross
)
Expect wider swings in both directions.
Tighter risk
python
risk=RiskConfig(
max_gross_leverage=1.0,
stop_loss=StopLoss(per_position_pct=0.03), # 3% stops
drawdown=[
DrawdownGuard(daily_pct=0.03, action=DrawdownAction.HaltNew),
DrawdownGuard(weekly_pct=0.06, action=DrawdownAction.Flatten),
],
)
Expect lower Sharpe (more stops firing) but bounded drawdown.
Regime-shift data
python
from horizon.data import SyntheticRegimes
data = SyntheticRegimes(
market_ids=["AAPL", "MSFT", "NVDA", "GOOGL", "AMZN"],
n_bars=300,
regimes=[
(0.4, 0.20, 0.15), # uptrend
(0.3, 0.0, 0.30), # chop
(0.3, -0.30, 0.40), # crash
],
seed=42,
)
Tests the strategy across multiple market environments.