Multi-Strategy Runs
Running multiple strategies in one backtest with unified portfolio sizing
One of Horizon’s core benefits: run multiple strategies simultaneously, and the portfolio optimizer sees the combined signal list. Cross-strategy attribution is automatic via the strategy_id tag on every signal.
Pattern
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,
MovingAverageCrossStrategy,
RSIMeanRev,
TSMomentum,
)
from horizon.risk import RiskProfile
result = hz.run(
mode="backtest",
strategies=[
TSMomentum(lookback=20, edge_bps=50),
BollingerMeanRev(window=20, entry_z=2.0, edge_bps=40),
MovingAverageCrossStrategy(fast=10, slow=30, edge_bps=30),
RSIMeanRev(window=14, edge_bps=35),
],
asset_classes=[Equity],
universe=StaticUniverse([
Market(id=t, asset_class=AssetClass.Equity)
for t in ["AAPL", "MSFT", "NVDA", "GOOGL", "AMZN"]
]),
portfolio=KellyOptimizer(kelly_fraction=0.20), # diluted. 4 strategies
risk=RiskProfile.moderate(),
data_source=SyntheticGBM(
["AAPL", "MSFT", "NVDA", "GOOGL", "AMZN"], n_bars=252, seed=42,
),
backtest=hz.BacktestConfig(initial_cash_usd=100_000),
)
What happens each tick
- Each strategy evaluates independently: they see the same universe + features but produce their own signals
- Signals are tagged with the originating strategy’s name
- All signals are deposited in the shared
SignalStore - The portfolio optimizer sees the combined list: it allocates capital across ALL signals, treating them as one unified opinion set
- Conflict resolution is automatic: if two strategies want opposite positions on the same market, Kelly math produces a small net target; the book isn’t doubled up
Attribution
Every trade carries its strategy_id:
for trade in result.trades:
print(f"{trade.strategy_id}: {trade.market_id} P&L={trade.realized_pnl:+.2f}")
Sum by strategy:
from collections import defaultdict
pnl_by_strategy = defaultdict(float)
for t in result.trades:
pnl_by_strategy[t.strategy_id] += t.realized_pnl
for strategy, pnl in sorted(pnl_by_strategy.items(), key=lambda x: -x[1]):
print(f"{strategy:30s} ${pnl:+.2f}")
Dilute Kelly for multi-strategy
When running N strategies, each strategy’s signals hit the optimizer. If you use kelly_fraction=0.25 for one strategy, you’d want roughly 0.25 / N per strategy when running N simultaneously, otherwise the combined book is N× oversized.
Typical starting values:
- 1 strategy:
kelly_fraction=0.25 - 2 strategies:
kelly_fraction=0.15 - 4 strategies:
kelly_fraction=0.10 - 8+ strategies:
kelly_fraction=0.06
Or use CarverSystematic(annual_vol_target=0.15) and let vol targeting do the dilution automatically.
Ensembling vs listing
Two ways to run multiple strategies:
Both work. See Ensemble for when each is appropriate.