Skip to main content
Multi-outcome events are prediction markets with more than two possible outcomes - for example, “Who wins the 2024 election?” with candidates Trump, Biden, DeSantis, etc. Each outcome is a separate binary contract with its own YES/NO tokens. Horizon models these as an Event containing multiple Outcome objects, layered on top of the existing binary Market model. Zero breaking changes to existing strategies.

Core Concepts

Each Outcome is a Binary Contract

Every outcome in a multi-outcome event is itself a standard binary contract. You can:
  • Buy YES on an outcome (bet it will happen)
  • Sell YES on an outcome (exit a YES position)
  • Buy NO on an outcome (bet it won’t happen)
  • Sell NO on an outcome (exit a NO position)
The full Side × OrderSide matrix works exactly like single-market trading.

Event Parity

For a well-formed event, the sum of all outcome YES prices should be approximately 1.0. If the sum deviates significantly, there may be an arbitrage opportunity:
  • Sum under 1.0: Buy all outcomes = guaranteed profit (after fees)
  • Sum over 1.0: Sell all outcomes (buy all NOs) = guaranteed profit (after fees)

Types

Outcome

A single named outcome in a multi-outcome event.
from horizon import Outcome, Side

outcome = Outcome(
    name="Trump",
    market_id="trump-wins-2024",
    yes_token_id="token_yes_123",
    no_token_id="token_no_456",
    yes_price=0.55,
)

outcome.name             # "Trump"
outcome.market_id        # "trump-wins-2024"
outcome.yes_price        # 0.55
outcome.token_id(Side.Yes)  # "token_yes_123"
outcome.token_id(Side.No)   # "token_no_456"

Event

Groups outcomes under a shared event/condition.
from horizon import Event, Outcome

trump = Outcome(name="Trump", market_id="trump-win", yes_token_id="t1", no_token_id="t2", yes_price=0.55)
biden = Outcome(name="Biden", market_id="biden-win", yes_token_id="t3", no_token_id="t4", yes_price=0.30)
desantis = Outcome(name="DeSantis", market_id="desantis-win", yes_token_id="t5", no_token_id="t6", yes_price=0.10)

event = Event(
    id="election-2024",
    name="Who wins the 2024 election?",
    outcomes=[trump, biden, desantis],
    neg_risk=True,
    exchange="polymarket",
    condition_id="0xabc...",
)

event.outcome_count()        # 3
event.outcome_names()        # ["Trump", "Biden", "DeSantis"]
event.outcome_by_name("Trump")  # Outcome(name="Trump", ...)

Event → Markets

Event.to_markets() converts each outcome into a standard Market with event_id and outcome_name set:
markets = event.to_markets()
# Returns 3 Market objects, each with:
#   market.event_id = "election-2024"
#   market.outcome_name = "Trump" / "Biden" / "DeSantis"
#   market.neg_risk = True
#   market.yes_token_id / no_token_id set from the Outcome
These markets work with all existing engine methods - submit_order, submit_quotes, tick, etc.

Discovery

Discover multi-outcome events from Polymarket:
from horizon import discover_events

events = discover_events(
    exchange="polymarket",
    query="election",
    active=True,
    limit=10,
)

for event in events:
    print(f"{event.name} ({event.outcome_count()} outcomes)")
    for name in event.outcome_names():
        outcome = event.outcome_by_name(name)
        print(f"  {name}: {outcome.yes_price:.0%}")
discover_events queries the Polymarket Gamma API and groups markets by condition_id into events. Each market question is parsed to extract the outcome name.

Engine Event Management

Registering Events

Register events on the engine to enable event-level risk limits and exposure tracking:
engine = hz.Engine()

# Register event with its outcome market IDs
engine.register_event("election-2024", ["trump-win", "biden-win", "desantis-win"])

# Or from an Event object
event = discover_events("polymarket", query="election")[0]
markets = event.to_markets()
engine.register_event(event.id, [m.id for m in markets])

Event Exposure & Positions

# Total exposure across all outcome markets in the event
exposure = engine.event_exposure("election-2024")

# All positions in the event
positions = engine.event_positions("election-2024")

# Reverse lookup: which event does a market belong to?
event_id = engine.market_event_id("trump-win")  # "election-2024"

# All registered events
events = engine.registered_events()  # {"election-2024": ["trump-win", ...]}

Event Parity Check

Check whether outcome prices sum to ~1.0 using feed data:
result = engine.event_parity_check("election-2024", threshold=0.02)
if result and result.is_violation:
    print(f"Parity violation: sum={result.yes_price:.4f}, deviation={result.deviation:.4f}")

Event Risk Limits

Set max_position_per_event to cap total exposure across all outcomes in an event:
from horizon import RiskConfig

config = RiskConfig(
    max_position_per_market=100.0,
    max_position_per_event=200.0,  # None = disabled (default)
)

hz.run(
    risk=config,
    ...
)
hz.Risk() does not support max_position_per_event. Use RiskConfig directly for event-level position limits.
When a market is registered in an event, orders are checked against both the per-market and per-event limits. Markets not in any event are unaffected.
Event risk limits are optional. Set max_position_per_event=None (the default) to disable event-level risk checks entirely.

Running with Events

Pass events directly to hz.run():
import horizon as hz

events = hz.discover_events("polymarket", query="election", limit=5)

def fair_value(ctx: hz.Context) -> float:
    feed = ctx.feeds.get("poly", hz.context.FeedData())
    return feed.price

def quoter(ctx: hz.Context, fair: float) -> list[hz.Quote]:
    # Access the current event
    if ctx.event:
        print(f"Quoting {ctx.market.outcome_name} in {ctx.event.name}")
    return hz.quotes(fair, spread=0.04, size=5)

hz.run(
    name="multi_outcome_mm",
    exchange=hz.Polymarket(private_key="0x..."),
    events=events,                              # Pass events instead of markets
    feeds={"poly": hz.PolymarketBook("...")},
    pipeline=[fair_value, quoter],
    risk=hz.RiskConfig(max_position_per_market=50, max_position_per_event=150),
    mode="live",
)
When events is provided:
  1. Each event is registered on the engine automatically
  2. Each outcome is converted to a Market via to_markets()
  3. The pipeline runs once per outcome per cycle
  4. ctx.event is set to the current event being processed
  5. ctx.market.event_id and ctx.market.outcome_name are populated
events and markets are independent parameters. Events expand into markets automatically - don’t list the same market IDs in both.

Arbitrage Detection

Detect event-level arbitrage opportunities using EventArbitrageOpportunity:
from horizon import EventArbitrageOpportunity

# The engine's event_parity_check returns a ParityResult.
# For full arbitrage analysis with fee awareness, use the Rust detect_event_arbitrage:
result = engine.event_parity_check("election-2024", threshold=0.02)
if result and result.is_violation:
    if result.deviation > 0:
        print("Overround: sell all outcomes for guaranteed profit")
    else:
        print("Underround: buy all outcomes for guaranteed profit")
FieldTypeDescription
event_idstrEvent identifier
exchangestrExchange name
price_sumfloatSum of all outcome YES prices
overroundfloatprice_sum - 1.0
directionstr"buy_all" or "sell_all"
net_edgefloatEdge after fees
is_executableboolWhether the edge exceeds fees

Example: Multi-Outcome Market Maker

import horizon as hz

def fair_value(ctx: hz.Context) -> float:
    """Use feed price as fair value for each outcome."""
    feed = ctx.feeds.get(ctx.market.id, hz.context.FeedData())
    if feed.price > 0:
        return feed.price
    return 0.5  # Default

def event_aware_quoter(ctx: hz.Context, fair: float) -> list[hz.Quote]:
    """Quote with event-aware inventory management."""
    # Per-outcome inventory
    market_pos = ctx.inventory.net_for_market(ctx.market.id)

    # Total event inventory (if in an event)
    event_pos = 0.0
    if ctx.event:
        event_market_ids = [o.market_id for o in ctx.event.outcomes]
        event_pos = ctx.inventory.net_for_event(event_market_ids)

    # Skew based on inventory
    skew = market_pos * 0.002 + event_pos * 0.001
    spread = 0.04

    return hz.quotes(fair - skew, spread, size=5)

events = hz.discover_events("polymarket", query="election")

hz.run(
    name="event_mm",
    exchange=hz.Polymarket(private_key="0x..."),
    events=events,
    pipeline=[fair_value, event_aware_quoter],
    risk=hz.RiskConfig(max_position_per_market=50, max_position_per_event=150),
    mode="live",
    dashboard=True,
)