Code Along: Multi-Asset Basket

Trade equities and prediction markets in the same backtest

Most trading systems are single-asset-class. This one isn’t. In this walkthrough you’ll run equities and prediction markets through the same pipeline, with separate strategies for each, and a single portfolio optimizer that sees everything.

Step 1: Define a mixed universe

We need markets from two different worlds: equities (AAPL, MSFT, a crypto perp) and a prediction market contract.

python
from horizon.asset_classes import AssetClass, Equity, Prediction, Crypto
from horizon.discovery import StaticUniverse
from horizon.discovery.base import Market

markets = [
    Market(id="AAPL", asset_class=AssetClass.Equity),
    Market(id="MSFT", asset_class=AssetClass.Equity),
    Market(id="BTC-PERP", asset_class=AssetClass.Crypto),
    Market(id="TRUMP-2028", asset_class=AssetClass.Prediction),
]
universe = StaticUniverse(markets)

Each Market is tagged with its asset class. This tag is what lets strategies declare which markets they care about.

Step 2: Write an equity strategy

This one uses a moving average crossover. It only sees equity and perpetual markets because of the asset_classes declaration.

python
from horizon.quant import MovingAverageCrossStrategy

class EquityMR(MovingAverageCrossStrategy):
    """MA crossover on equities and perps."""
    name = "equity_trend"
    asset_classes = [Equity, Crypto]
    fast = 10
    slow = 30
    edge_bps = 50
    horizon_days = 5

When the engine iterates the universe, EquityMR only receives AAPL, MSFT, and BTC-PERP. It never sees TRUMP-2028. The filtering happens automatically based on asset_classes.

Step 3: Write a prediction market strategy

Prediction markets trade differently. Prices are probabilities (0 to 1). This strategy buys when the market price looks too low relative to a simple estimate.

python
import math
from horizon import Signal, Strategy

class PredictionTrader(Strategy):
    """Simple probability-based prediction market trader."""
    name = "pred_trader"
    asset_classes = [Prediction]
    features = {}

    def evaluate(self, f, universe, ctx):
        signals = []
        for m in universe:
            price = ctx.prices.get(m.id)
            if price is None or math.isnan(price):
                continue

            # Simple contrarian: buy below 0.30, sell above 0.70
            if price < 0.30:
                signals.append(Signal.from_score(
                    market=m,
                    score=1.0,
                    edge_per_stdev=80,
                    horizon="7d",
                    reason=f"underpriced at {price:.2f}",
                ))
            elif price > 0.70:
                signals.append(Signal.from_score(
                    market=m,
                    score=-1.0,
                    edge_per_stdev=80,
                    horizon="7d",
                    reason=f"overpriced at {price:.2f}",
                ))
        return signals

This strategy only receives TRUMP-2028. It never sees equities.

Step 4: Wire up the portfolio and risk

The portfolio optimizer sees signals from both strategies. It doesn’t care which strategy produced them — it just sees a list of signals with edge estimates and allocates capital.

python
import horizon as hz
from horizon.data import SyntheticGBM
from horizon.portfolio import KellyOptimizer
from horizon.risk import RiskProfile

result = hz.run(
    mode="backtest",
    strategies=[EquityMR(), PredictionTrader()],
    asset_classes=[Equity, Crypto, Prediction],
    universe=universe,
    portfolio=KellyOptimizer(
        kelly_fraction=0.20,
        max_gross_leverage=1.5,
        transaction_cost_bps=5,
    ),
    risk=RiskProfile.moderate(),
    data_source=SyntheticGBM(
        market_ids=["AAPL", "MSFT", "BTC-PERP", "TRUMP-2028"],
        n_bars=252,
        seed=99,
    ),
    backtest=hz.BacktestConfig(initial_cash_usd=100_000),
)

Two things to notice:

  1. strategies=[EquityMR(), PredictionTrader()] — both run every tick, but each only sees its own markets.
  2. asset_classes=[Equity, Crypto, Prediction] — the top-level declaration tells the engine which asset classes are in play.

Step 5: Read the results

python
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%}")

# Per-strategy attribution
from collections import Counter
counts = Counter(t.strategy_id for t in result.trades if t.strategy_id)
for sid, n in counts.most_common():
    pnl = sum(t.realized_pnl for t in result.trades if t.strategy_id == sid)
    print(f"  {sid}: {n} trades, P&L={pnl:+.2f}")

Every trade carries a strategy_id tag. You can split by strategy to see which one contributed what. If the equity strategy is profitable but the prediction strategy isn’t (or vice versa), you see it immediately.

How the asset class filtering works

When you set asset_classes = [Equity, Crypto] on a strategy, the engine filters the universe before calling evaluate(). Your strategy only sees matching markets. Features are only computed for those markets. Two strategies with different asset_classes never interfere at the signal level — conflict resolution happens at the portfolio level.

Full file

python
# codealong_multi_asset.py
import math
from collections import Counter

import horizon as hz
from horizon import Signal, Strategy
from horizon.asset_classes import AssetClass, Equity, Prediction, Crypto
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 MovingAverageCrossStrategy
from horizon.risk import RiskProfile


class EquityMR(MovingAverageCrossStrategy):
    name = "equity_trend"
    asset_classes = [Equity, Crypto]
    fast = 10
    slow = 30
    edge_bps = 50
    horizon_days = 5


class PredictionTrader(Strategy):
    name = "pred_trader"
    asset_classes = [Prediction]
    features = {}

    def evaluate(self, f, universe, ctx):
        signals = []
        for m in universe:
            price = ctx.prices.get(m.id)
            if price is None or math.isnan(price):
                continue
            if price < 0.30:
                signals.append(Signal.from_score(
                    market=m, score=1.0, edge_per_stdev=80,
                    horizon="7d", reason=f"underpriced at {price:.2f}",
                ))
            elif price > 0.70:
                signals.append(Signal.from_score(
                    market=m, score=-1.0, edge_per_stdev=80,
                    horizon="7d", reason=f"overpriced at {price:.2f}",
                ))
        return signals


def main():
    markets = [
        Market(id="AAPL", asset_class=AssetClass.Equity),
        Market(id="MSFT", asset_class=AssetClass.Equity),
        Market(id="BTC-PERP", asset_class=AssetClass.Crypto),
        Market(id="TRUMP-2028", asset_class=AssetClass.Prediction),
    ]

    result = hz.run(
        mode="backtest",
        strategies=[EquityMR(), PredictionTrader()],
        asset_classes=[Equity, Crypto, Prediction],
        universe=StaticUniverse(markets),
        portfolio=KellyOptimizer(
            kelly_fraction=0.20,
            max_gross_leverage=1.5,
            transaction_cost_bps=5,
        ),
        risk=RiskProfile.moderate(),
        data_source=SyntheticGBM(
            market_ids=["AAPL", "MSFT", "BTC-PERP", "TRUMP-2028"],
            n_bars=252,
            seed=99,
        ),
        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%}")

    counts = Counter(t.strategy_id for t in result.trades if t.strategy_id)
    for sid, n in counts.most_common():
        pnl = sum(t.realized_pnl for t in result.trades if t.strategy_id == sid)
        print(f"  {sid}: {n} trades, P&L={pnl:+.2f}")


if __name__ == "__main__":
    main()

Run it

bash
PYTHONPATH=. python3 codealong_multi_asset.py

Next