Code Along: Full System

Put it all together: custom strategy, real sizing, layered risk, validation

This is the capstone. Everything from the previous walkthroughs — strategy, portfolio sizing, risk, data — combined into one system, then validated with a bootstrap confidence interval.

Step 1: Write a custom strategy

This strategy combines momentum and mean-reversion signals, scaling confidence by volatility. It also checks portfolio state before trading.

python
import math
from horizon import Signal, Strategy
from horizon.asset_classes import Equity
from horizon.features import RealizedVol, Return, Zscore

class AdaptiveDual(Strategy):
    """Dual-signal strategy: momentum + mean reversion, vol-scaled."""
    name = "adaptive_dual"
    asset_classes = [Equity]
    features = {
        "z": Zscore(window=20),
        "r20": Return(window=20),
        "vol": RealizedVol(window=60),
    }

    def evaluate(self, f, universe, ctx):
        if ctx.portfolio.drawdown_pct > 0.04:
            return []  # strategy-level pause

        signals = []
        for m in universe:
            z, r20, vol = f.z[m.id], f.r20[m.id], f.vol[m.id]
            if any(math.isnan(v) for v in [z, r20, vol]):
                continue

            # Mean-reversion: fade z-score extremes in range-bound markets
            mr = -z * 0.5 if abs(z) > 2.0 and abs(r20) < 0.04 else 0.0
            # Momentum: follow strong recent returns
            mom = math.copysign(1.0, r20) * 0.5 if abs(r20) > 0.03 else 0.0

            combined = mr + mom
            if abs(combined) < 0.3:
                continue

            vol_scale = max(0.5, min(2.0, 0.20 / max(vol, 0.05)))
            signals.append(Signal.from_score(
                market=m, score=combined,
                edge_per_stdev=int(40 * vol_scale), horizon="5d",
                expected_expected_vol_bps=max(vol * 10_000, 100),
                reason=f"z={z:.1f} r20={r20:+.1%} vol={vol:.1%}",
                features={"z": z, "r20": r20, "vol": vol},
            ))
        return signals

The two sub-signals can agree or conflict. Vol scaling adjusts edge estimates — low vol means more conviction. The features={...} dict stores raw values for post-hoc IC analysis.

Step 2: Kelly sizing with transaction costs

python
from horizon.portfolio import KellyOptimizer

portfolio = KellyOptimizer(
    kelly_fraction=0.20,
    max_gross_leverage=1.2,
    transaction_cost_bps=8,
)

One-fifth Kelly. Full Kelly assumes perfect edge estimates, which you never have. The 8 bps cost drag prevents the optimizer from taking tiny-edge trades that lose money to fees.

Step 3: Three tiers of risk protection

python
from horizon.risk import DrawdownGuard, OrderRisk, RiskConfig, StopLoss
from horizon.risk.drawdown import DrawdownAction

risk = RiskConfig(
    per_order=OrderRisk(max_order_notional_usd=25_000, rate_limit_per_sec=20),
    max_gross_leverage=1.2,
    stop_loss=StopLoss(per_position_pct=0.05, trailing_pct=0.03),
    drawdown=[
        DrawdownGuard(daily_pct=0.03, action=DrawdownAction.HaltNew),
        DrawdownGuard(weekly_pct=0.08, action=DrawdownAction.ReduceHalf),
        DrawdownGuard(monthly_pct=0.15, action=DrawdownAction.Flatten),
    ],
)

The stop has two parts: 5% hard stop plus 3% trailing (locks in gains). The drawdown tiers escalate: pause at 3% daily, halve at 8% weekly, flatten at 15% monthly.

Step 4: Regime-switching stress data

python
from horizon.data import SyntheticRegimes

data = SyntheticRegimes(
    market_ids=["AAPL", "MSFT", "NVDA", "GOOGL", "AMZN"],
    n_bars=504,
    regimes=[
        (0.30, 0.20, 0.15),    # uptrend, low vol
        (0.25, 0.00, 0.30),    # choppy, high vol
        (0.20, -0.35, 0.40),   # CRASH
        (0.25, 0.15, 0.20),    # recovery
    ],
    seed=2025,
)

Each tuple: (weight, annualized_drift, annualized_vol). The crash regime is the real test of whether your risk config works.

Step 5: Run the backtest

python
import horizon as hz
from horizon.asset_classes import AssetClass, Equity
from horizon.discovery import StaticUniverse
from horizon.discovery.base import Market

tickers = ["AAPL", "MSFT", "NVDA", "GOOGL", "AMZN"]
universe = StaticUniverse([
    Market(id=t, asset_class=AssetClass.Equity) for t in tickers
])

result = hz.run(
    mode="backtest",
    strategies=[AdaptiveDual()],
    asset_classes=[Equity],
    universe=universe,
    portfolio=portfolio,
    risk=risk,
    data_source=data,
    backtest=hz.BacktestConfig(initial_cash_usd=100_000),
)

print(f"Trades:       {result.n_trades}")
print(f"Total return: {result.total_return:+.2%}")
print(f"Sharpe:       {result.sharpe:+.3f}")
print(f"Max drawdown: {result.max_drawdown:.2%}")

Step 6: Validate with Bootstrap

A single Sharpe number means nothing without a confidence interval.

python
from horizon.validate import Bootstrap

equity = [e for _, e in result.equity_curve]
returns = [(equity[i] - equity[i-1]) / equity[i-1]
           for i in range(1, len(equity))]

bs = Bootstrap(metrics=["sharpe"], n_samples=500, seed=42)
bs_result = bs.run(returns=returns)
lo, hi = bs_result.ci("sharpe", conf=0.95)
print(f"\nSharpe 95% CI: [{lo:+.3f}, {hi:+.3f}]")

If the CI contains zero, your strategy might not have real edge. If the entire interval is positive, there’s evidence the signal is real.

Step 7: The decision chain

Three questions, three answers:

  1. Is my signal real? Bootstrap CI. If lo > 0, statistically positive.
  2. Is my sizing right? Compare Kelly vs EqualWeight(). If Kelly doesn’t win, your edge estimates are noisy.
  3. Is my risk working? Max drawdown should be bounded by the 15% monthly guard.
python
from horizon.portfolio import EqualWeight

result_ew = hz.run(
    mode="backtest",
    strategies=[AdaptiveDual()],
    asset_classes=[Equity],
    universe=universe,
    portfolio=EqualWeight(),
    risk=risk,
    data_source=data,
    backtest=hz.BacktestConfig(initial_cash_usd=100_000),
)
print(f"\nKelly:       Sharpe={result.sharpe:+.3f}  MaxDD={result.max_drawdown:.2%}")
print(f"EqualWeight: Sharpe={result_ew.sharpe:+.3f}  MaxDD={result_ew.max_drawdown:.2%}")

Full file

python
# codealong_full_system.py
import math
import horizon as hz
from horizon import Signal, Strategy
from horizon.asset_classes import AssetClass, Equity
from horizon.data import SyntheticRegimes
from horizon.discovery import StaticUniverse
from horizon.discovery.base import Market
from horizon.features import RealizedVol, Return, Zscore
from horizon.portfolio import EqualWeight, KellyOptimizer
from horizon.risk import DrawdownGuard, OrderRisk, RiskConfig, StopLoss
from horizon.risk.drawdown import DrawdownAction
from horizon.validate import Bootstrap

class AdaptiveDual(Strategy):
    name = "adaptive_dual"
    asset_classes = [Equity]
    features = {"z": Zscore(window=20), "r20": Return(window=20), "vol": RealizedVol(window=60)}

    def evaluate(self, f, universe, ctx):
        if ctx.portfolio.drawdown_pct > 0.04:
            return []
        signals = []
        for m in universe:
            z, r20, vol = f.z[m.id], f.r20[m.id], f.vol[m.id]
            if any(math.isnan(v) for v in [z, r20, vol]):
                continue
            mr = -z * 0.5 if abs(z) > 2.0 and abs(r20) < 0.04 else 0.0
            mom = math.copysign(1.0, r20) * 0.5 if abs(r20) > 0.03 else 0.0
            combined = mr + mom
            if abs(combined) < 0.3:
                continue
            vol_scale = max(0.5, min(2.0, 0.20 / max(vol, 0.05)))
            signals.append(Signal.from_score(
                market=m, score=combined, edge_per_stdev=int(40 * vol_scale),
                horizon="5d", expected_expected_vol_bps=max(vol * 10_000, 100),
                reason=f"z={z:.1f} r20={r20:+.1%} vol={vol:.1%}",
                features={"z": z, "r20": r20, "vol": vol},
            ))
        return signals

def main():
    tickers = ["AAPL", "MSFT", "NVDA", "GOOGL", "AMZN"]
    universe = StaticUniverse([Market(id=t, asset_class=AssetClass.Equity) for t in tickers])
    data = SyntheticRegimes(market_ids=tickers, n_bars=504, regimes=[
        (0.30, 0.20, 0.15), (0.25, 0.00, 0.30),
        (0.20, -0.35, 0.40), (0.25, 0.15, 0.20),
    ], seed=2025)
    portfolio = KellyOptimizer(kelly_fraction=0.20, max_gross_leverage=1.2, transaction_cost_bps=8)
    risk = RiskConfig(
        per_order=OrderRisk(max_order_notional_usd=25_000, rate_limit_per_sec=20),
        max_gross_leverage=1.2,
        stop_loss=StopLoss(per_position_pct=0.05, trailing_pct=0.03),
        drawdown=[
            DrawdownGuard(daily_pct=0.03, action=DrawdownAction.HaltNew),
            DrawdownGuard(weekly_pct=0.08, action=DrawdownAction.ReduceHalf),
            DrawdownGuard(monthly_pct=0.15, action=DrawdownAction.Flatten),
        ],
    )
    result = hz.run(
        mode="backtest", strategies=[AdaptiveDual()], asset_classes=[Equity],
        universe=universe, portfolio=portfolio, risk=risk,
        data_source=data, backtest=hz.BacktestConfig(initial_cash_usd=100_000),
    )
    print(f"Trades: {result.n_trades}  Return: {result.total_return:+.2%}  "
          f"Sharpe: {result.sharpe:+.3f}  MaxDD: {result.max_drawdown:.2%}")

    equity = [e for _, e in result.equity_curve]
    returns = [(equity[i] - equity[i-1]) / equity[i-1] for i in range(1, len(equity))]
    bs = Bootstrap(metrics=["sharpe"], n_samples=500, seed=42)
    lo, hi = bs.run(returns=returns).ci("sharpe", conf=0.95)
    print(f"Sharpe 95% CI: [{lo:+.3f}, {hi:+.3f}]")

    result_ew = hz.run(
        mode="backtest", strategies=[AdaptiveDual()], asset_classes=[Equity],
        universe=universe, portfolio=EqualWeight(), risk=risk,
        data_source=data, backtest=hz.BacktestConfig(initial_cash_usd=100_000),
    )
    print(f"Kelly: {result.sharpe:+.3f}  EqualWeight: {result_ew.sharpe:+.3f}")

if __name__ == "__main__":
    main()

Run it

bash
PYTHONPATH=. python3 codealong_full_system.py

Next