---
title: "Multi-Outcome Events"
description: "Trade multi-outcome prediction markets with event grouping, parity checks, and event-level risk limits."
icon: "diagram-project"
---

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.

```python
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.

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

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

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

<Note>
  `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.
</Note>

## Engine Event Management

### Registering Events

Register events on the engine to enable event-level risk limits and exposure tracking:

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

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

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

```python
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,
    ...
)
```

<Note>
  `hz.Risk()` does not support `max_position_per_event`. Use `RiskConfig` directly for event-level position limits.
</Note>

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.

<Tip>
  Event risk limits are optional. Set `max_position_per_event=None` (the default) to disable event-level risk checks entirely.
</Tip>

## Running with Events

Pass events directly to `hz.run()`:

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

<Warning>
  `events` and `markets` are independent parameters. Events expand into markets automatically - don't list the same market IDs in both.
</Warning>

## Arbitrage Detection

Detect event-level arbitrage opportunities using `EventArbitrageOpportunity`:

```python
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")
```

| Field | Type | Description |
|-------|------|-------------|
| `event_id` | `str` | Event identifier |
| `exchange` | `str` | Exchange name |
| `price_sum` | `float` | Sum of all outcome YES prices |
| `overround` | `float` | `price_sum - 1.0` |
| `direction` | `str` | `"buy_all"` or `"sell_all"` |
| `net_edge` | `float` | Edge after fees |
| `is_executable` | `bool` | Whether the edge exceeds fees |

## Example: Multi-Outcome Market Maker

```python
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,
)
```
