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

python
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

  1. Each strategy evaluates independently: they see the same universe + features but produce their own signals
  2. Signals are tagged with the originating strategy’s name
  3. All signals are deposited in the shared SignalStore
  4. The portfolio optimizer sees the combined list: it allocates capital across ALL signals, treating them as one unified opinion set
  5. 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:

python
for trade in result.trades:
    print(f"{trade.strategy_id}: {trade.market_id} P&L={trade.realized_pnl:+.2f}")

Sum by strategy:

python
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:

List them Each strategy deposits signals independently. Portfolio sees them as separate sources. Attribution per strategy.
Wrap in Ensemble `Ensemble([s1, s2, s3], weights=[...])` aggregates children with per-child weighting. Attribution rolls up to the ensemble.

Both work. See Ensemble for when each is appropriate.

Next