Skip to main content
Pro Feature — This requires a Pro or Ultra subscription. Get started at horizon.mathematicalcompany.com
When a prediction market event has multiple outcomes (e.g., 4 candidates), their prices should sum to $1.00. Deviations create arbitrage opportunities.

Full Code

"""Multi-outcome arb: 4-candidate election event."""

import horizon as hz


# Register a multi-outcome event
event = hz.Event(
    event_id="presidential-election-2028",
    outcomes=[
        hz.Outcome(market_id="candidate-a", name="Candidate A"),
        hz.Outcome(market_id="candidate-b", name="Candidate B"),
        hz.Outcome(market_id="candidate-c", name="Candidate C"),
        hz.Outcome(market_id="candidate-d", name="Candidate D"),
    ],
)

# One-shot scan for mispricing
engine = hz.Engine()
engine.register_event(event)

# Start feeds for all outcomes
for outcome in event.outcomes:
    engine.start_feed(
        outcome.market_id,
        "polymarket_book",
        config_json=f'{{"condition_id": "{outcome.market_id}"}}',
    )

# Scan for arb
result = hz.event_arb_scanner(
    engine,
    event_id="presidential-election-2028",
    min_edge=0.005,
)

if result:
    print(f"Event arb found!")
    print(f"  Sum of asks: ${result.total_ask:.4f}")
    print(f"  Edge: ${result.edge:.4f} per full set")
    print(f"  Recommended sizes: {result.sizes}")
else:
    print("No multi-outcome arb found")

How It Works

  1. Event groups related outcomes that must sum to $1.00
  2. event_arb_scanner() sums the best ask across all outcomes
  3. If total_ask < 1.00, buying all outcomes locks in a risk-free profit of 1.00 - total_ask
  4. Sizing is proportional: cheaper outcomes get more contracts to balance dollar exposure

Live Pipeline

Run the scanner continuously inside hz.run():
"""Continuous multi-outcome arb scanner."""

import horizon as hz
from horizon.context import FeedData


def fair_value(ctx: hz.Context) -> float:
    """Use Candidate A feed as primary."""
    feed = ctx.feeds.get("candidate-a", FeedData())
    return feed.price if feed.price > 0 else 0.25


event = hz.Event(
    event_id="presidential-election-2028",
    outcomes=[
        hz.Outcome(market_id="candidate-a", name="Candidate A"),
        hz.Outcome(market_id="candidate-b", name="Candidate B"),
        hz.Outcome(market_id="candidate-c", name="Candidate C"),
        hz.Outcome(market_id="candidate-d", name="Candidate D"),
    ],
)

scanner = hz.event_arb_sweep

hz.run(
    name="multi_outcome_arb",
    events=[event],
    markets=["candidate-a", "candidate-b", "candidate-c", "candidate-d"],
    feeds={
        "candidate-a": hz.PolymarketBook("candidate-a"),
        "candidate-b": hz.PolymarketBook("candidate-b"),
        "candidate-c": hz.PolymarketBook("candidate-c"),
        "candidate-d": hz.PolymarketBook("candidate-d"),
    },
    pipeline=[fair_value],
    risk=hz.Risk(max_position=100, max_drawdown_pct=3),
    interval=1.0,
    mode="paper",
)

Run It

python examples/multi_outcome_arb.py

Composite Scanner

Combine parity, cross-exchange, and multi-outcome scanning for maximum coverage:
scanner = hz.composite_arb(
    methods=[
        hz.ArbMethodConfig(method="parity", market_id="candidate-a", feed_name="candidate-a"),
        hz.ArbMethodConfig(
            method="event",
            event_id="presidential-election-2028",
        ),
        hz.ArbMethodConfig(
            method="cross_exchange",
            market_id="candidate-a",
            exchanges=["polymarket", "kalshi"],
            feed_map={"polymarket": "candidate-a", "kalshi": "kalshi-candidate-a"},
        ),
    ],
    min_edge=0.005,
    auto_execute=True,
)
See Multi-Outcome Arbitrage for the full method reference.