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.
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.
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.
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.
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:
strategies=[EquityMR(), PredictionTrader()]— both run every tick, but each only sees its own markets.asset_classes=[Equity, Crypto, Prediction]— the top-level declaration tells the engine which asset classes are in play.
Step 5: Read the results
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
# 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
PYTHONPATH=. python3 codealong_multi_asset.py