Your First Strategy

Build a strategy from scratch, step by step

This tutorial walks through writing a strategy from scratch. By the end you’ll have a working mean-reversion strategy with real features, real risk enforcement, and real backtest metrics.

What we’re building

A simple z-score mean reversion strategy:

  • When a stock’s 20-day log return z-score drops below -2, go long
  • When it rises above +2, go short
  • Size each position using quarter-Kelly
  • Apply a 5% stop loss and a 5% daily drawdown guard

Step 1. Scaffold the file

python
# my_first_strategy.py
import horizon as hz
from horizon import Strategy, Signal
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.features import Zscore, RealizedVol
from horizon.portfolio import KellyOptimizer
from horizon.risk import DrawdownGuard, RiskConfig, StopLoss
from horizon.risk.drawdown import DrawdownAction

Everything comes from horizon. No pandas, no numpy (unless you want them), no broker SDKs.

Step 2. Define the strategy

python
class MyMeanReversion(Strategy):
    name = "my_mr"
    asset_classes = [Equity]
    features = {
        "z": Zscore(window=20),
        "vol": RealizedVol(window=60),
    }

    def evaluate(self, f, universe):
        signals = []
        for market in universe:
            z = f.z[market.id]
            vol = f.vol[market.id]
            if abs(z) > 2.0:
                signals.append(
                    Signal.from_score(
                        market=market,
                        score=-z,                       # negative z → long
                        edge_per_stdev=20,               # 20 bps per σ above threshold
                        horizon="3d",
                        expected_expected_vol_bps=max(vol * 100, 50),
                        reason=f"z={z:.2f}",
                    )
                )
        return signals

What each piece does

Step 3. Build the run config

python
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=[MyMeanReversion],
        asset_classes=[Equity],
        universe=universe,
        portfolio=KellyOptimizer(
            kelly_fraction=0.25,
            max_gross_leverage=1.0,
            transaction_cost_bps=5,
        ),
        risk=RiskConfig(
            max_gross_leverage=1.0,
            stop_loss=StopLoss(per_position_pct=0.05),
            drawdown=[
                DrawdownGuard(daily_pct=0.05, action=DrawdownAction.HaltNew),
                DrawdownGuard(weekly_pct=0.10, action=DrawdownAction.ReduceHalf),
            ],
        ),
        data_source=data,
        backtest=hz.BacktestConfig(initial_cash_usd=100_000),
    )

Step 4. Print the results

python
    print("=" * 50)
    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()

Step 5. Run it

bash
PYTHONPATH=. python3 my_first_strategy.py

Output (your exact numbers depend on how the seed plays out):

==================================================
  Trades:       ~40-80
  Start equity: $100,000.00
  End equity:   $9x,xxx.xx
  Peak equity:  $10x,xxx.xx
  Total return: ~-3% to +2%
  Sharpe:       ~-0.5 to +0.5
  Sortino:      ~-0.5 to +0.5
  Max drawdown: 3% - 10% (bounded by guards)

What to try next

Full file

python
# my_first_strategy.py
import horizon as hz
from horizon import Strategy, Signal
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.features import RealizedVol, Zscore
from horizon.portfolio import KellyOptimizer
from horizon.risk import DrawdownGuard, RiskConfig, StopLoss
from horizon.risk.drawdown import DrawdownAction


class MyMeanReversion(Strategy):
    name = "my_mr"
    asset_classes = [Equity]
    features = {
        "z": Zscore(window=20),
        "vol": RealizedVol(window=60),
    }

    def evaluate(self, f, universe):
        return [
            Signal.from_score(
                market=m,
                score=-f.z[m.id],
                edge_per_stdev=20,
                horizon="3d",
                expected_expected_vol_bps=max(f.vol[m.id] * 100, 50),
                reason=f"z={f.z[m.id]:.2f}",
            )
            for m in universe
            if abs(f.z[m.id]) > 2.0
        ]


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

    result = hz.run(
        mode="backtest",
        strategies=[MyMeanReversion],
        asset_classes=[Equity],
        universe=universe,
        portfolio=KellyOptimizer(kelly_fraction=0.25, max_gross_leverage=1.0),
        risk=RiskConfig(
            max_gross_leverage=1.0,
            stop_loss=StopLoss(per_position_pct=0.05),
            drawdown=[
                DrawdownGuard(daily_pct=0.05, action=DrawdownAction.HaltNew),
                DrawdownGuard(weekly_pct=0.10, action=DrawdownAction.ReduceHalf),
            ],
        ),
        data_source=SyntheticGBM(
            market_ids=["AAPL", "MSFT", "NVDA", "GOOGL", "AMZN"],
            n_bars=252,
            mu=0.10,
            sigma=0.22,
            seed=42,
        ),
        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 DD: {result.max_drawdown:.2%}")


if __name__ == "__main__":
    main()

Next