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()