Multi-outcome markets must satisfy Σ p_i = 1. When they don’t, the excess is overround — remove it with multiplicative normalization. Combinatorial markets over k binary variables create 2^k outcomes; Hanson’s LMSR prices them without exponential liquidity. Conditional markets satisfy P(A) = Σ P(A|B_i) × P(B_i) — any violation is an arbitrage signal.

Why This Matters for Agents

Most real prediction markets are not binary. “Who wins the 2028 presidential primary?” has 8+ candidates. “Which party wins AND by what margin?” combines two variables into a joint outcome space. An autonomous agent that only handles YES/NO contracts ignores the richest source of mispricing on Polymarket and Kalshi.

This is Layer 3 — Trading. Multi-outcome and conditional market math sits between the agent’s price-reading pipeline (covered in Prediction Market Math 101) and its intelligence module in Layer 4, where Bayesian updating refines probability estimates across correlated contracts. An agent operating on the Agent Betting Stack needs three capabilities this guide provides: (1) extract true probabilities from n-outcome markets with overround, (2) understand combinatorial market structure to avoid the exponential blowup trap, and (3) detect consistency violations between conditional and unconditional markets that create arbitrage.

The Math

The n-Outcome Completeness Condition

A market with n mutually exclusive, collectively exhaustive outcomes must satisfy:

Σ p_i = 1    for i = 1, 2, ..., n

where p_i is the true probability of outcome i. In a zero-vig market, the prices equal these probabilities directly. In a real market, prices include overround:

Σ price_i = 1 + O

where O is the overround (the market maker’s margin). O > 0 in every real market.

Overround in Multi-Outcome Markets

For a binary market, overround is simple: YES + NO - $1.00. For n outcomes, the formula is identical in principle but the magnitude scales differently. A 10-outcome market might have each outcome’s price inflated by the same absolute amount, or proportionally — the market maker’s mechanism determines the distribution.

Three vig-removal methods handle this:

1. Multiplicative normalization (simplest):

p_true_i = price_i / Σ price_j

Assumes the overround is distributed proportionally to each outcome’s price. Works well when no outcome is heavily favored.

2. Shin’s method (accounts for insider trading):

Shin’s model assumes a fraction z of trades come from informed insiders. The true probability satisfies:

p_true_i = (√(z² + 4(1-z) × price_i × S) - z) / (2(1-z))

where S = Σ price_j is the book sum. Shin’s method pulls more overround from longshots and less from favorites — matching empirical sportsbook behavior.

3. Power method (handles favorite-longshot bias):

Find exponent k such that:

Σ price_i^k = 1

Then p_true_i = price_i^k. This corrects for the well-documented favorite-longshot bias where longshots are systematically overpriced.

Combinatorial Markets: The Exponential Blowup

A combinatorial prediction market lets traders bet on joint outcomes across multiple questions. Consider three binary questions:

  • Q1: Will the Democrats win the presidency? (D or R)
  • Q2: Will the Senate flip? (Flip or Hold)
  • Q3: Will GDP growth exceed 3%? (Yes or No)

The joint outcome space has 2³ = 8 states:

State  | President | Senate | GDP>3%
-------|-----------|--------|-------
  1    |    D      |  Flip  |  Yes
  2    |    D      |  Flip  |  No
  3    |    D      |  Hold  |  Yes
  4    |    D      |  Hold  |  No
  5    |    R      |  Flip  |  Yes
  6    |    R      |  Flip  |  No
  7    |    R      |  Hold  |  Yes
  8    |    R      |  Hold  |  No

With 3 binary questions, 8 states is manageable. With 10 binary questions, you get 2^10 = 1,024 states. With 20, you get 2^20 = 1,048,576 states. A traditional order book for each state is infeasible — no market has enough liquidity to maintain a million separate books.

This is the exponential blowup problem, and it’s the central challenge of combinatorial prediction markets.

Hanson’s LMSR: Solving the Blowup

Robin Hanson’s Logarithmic Market Scoring Rule (LMSR) solves the exponential blowup by replacing discrete order books with a continuous cost function. For an outcome space with n states, the cost function is:

C(q) = b × ln(Σ exp(q_i / b))    for i = 1, ..., n

where q_i is the number of outstanding shares of outcome i and b is the liquidity parameter controlling price sensitivity. The price of a share of outcome i is:

price_i = ∂C/∂q_i = exp(q_i / b) / Σ exp(q_j / b)

This is exactly the softmax function. The prices automatically sum to 1, satisfy non-negativity, and respond smoothly to trades.

The key insight: a trader buying shares of outcome i doesn’t need to interact with a specific counterparty or order book. The market maker (the cost function itself) computes the cost of moving from state q to state q’ and charges:

Trade cost = C(q') - C(q)

For a combinatorial market over k binary variables with 2^k states, the LMSR cost function decomposes. A trade on a marginal event (e.g., “Democrats win the presidency” regardless of Senate or GDP) affects all states where that marginal holds. The cost of that marginal trade can be computed in O(2^(k-1)) time — still exponential, but Hanson showed that with independence assumptions or bounded tree-width, this drops to polynomial time.

The liquidity parameter b controls the trade-off:

  • Large b: prices move slowly, market maker absorbs more risk, deeper liquidity
  • Small b: prices move quickly, market maker takes less risk, more responsive to information
  • Maximum market maker loss is bounded: b × ln(n), where n is the number of outcomes

Conditional Probability Markets

A conditional market prices P(A|B) — the probability of event A given that event B occurs. On Polymarket, conditional markets resolve as follows:

  • If B occurs and A occurs: pays $1.00
  • If B occurs and A does not occur: pays $0.00
  • If B does not occur: contract voids (refunds at purchase price)

The chain rule of probability connects conditional and unconditional markets:

P(A ∩ B) = P(A|B) × P(B)

And the law of total probability provides a consistency check:

P(A) = Σ P(A|B_i) × P(B_i)

where {B_1, B_2, …, B_m} is a partition of the sample space.

If the unconditional market for A prices it at $0.60, and the conditional markets price:

P(A|B_1) = 0.80,  P(B_1) = 0.50
P(A|B_2) = 0.35,  P(B_2) = 0.50

Then the law of total probability gives:

P(A) = 0.80 × 0.50 + 0.35 × 0.50 = 0.575

The unconditional market says $0.60 but the conditional markets imply $0.575. The $0.025 discrepancy — if it exceeds transaction costs — is an arbitrage signal.

The Chain Rule in Market Terms

Given three related markets on Polymarket:

  • Market M_B: “Will Biden run?” priced at P(B) = $0.30
  • Market M_A|B: “Will Biden win the nomination given he runs?” priced at P(A|B) = $0.85
  • Market M_A: “Will Biden win the nomination?” priced at P(A) = $0.28

Consistency requires:

P(A) ≥ P(A|B) × P(B)    (since P(A) = P(A|B)×P(B) + P(A|¬B)×P(¬B) ≥ P(A|B)×P(B))

Check: 0.85 × 0.30 = 0.255. The unconditional market at $0.28 exceeds this, which is consistent (the gap of $0.025 represents the probability of Biden winning the nomination without running, which could be near zero — e.g., via draft or write-in).

If instead P(A) = $0.20, that would violate P(A) ≥ P(A|B) × P(B) since 0.20 < 0.255. That’s an arbitrage.

Worked Examples

Example 1: Polymarket 2028 Republican Primary

A Polymarket “2028 Republican Presidential Nominee” market might show:

DeSantis:    $0.32
Vance:       $0.28
Haley:       $0.15
Ramaswamy:   $0.09
Scott:       $0.07
Other:       $0.12
─────────────────────
Sum:         $1.03
Overround:   3.0%

Applying the three vig-removal methods:

Outcome      Raw     Multiplicative   Shin (z=0.03)   Power (k≈0.971)
─────────────────────────────────────────────────────────────────────
DeSantis     $0.32     31.07%           31.15%           31.08%
Vance        $0.28     27.18%           27.27%           27.19%
Haley        $0.15     14.56%           14.68%           14.59%
Ramaswamy    $0.09      8.74%            8.88%            8.78%
Scott        $0.07      6.80%            6.94%            6.84%
Other        $0.12     11.65%           11.77%           11.69%
─────────────────────────────────────────────────────────────────────
Sum          $1.03    100.00%          100.00%          100.00%

With only 3% overround, all three methods produce nearly identical results. The differences matter more in sportsbook markets with 8-15% overround.

Example 2: Conditional Markets — Party Wins Given Nominee

Suppose Polymarket has these related markets:

Unconditional market: “Which party wins the 2028 presidential election?”

  • Democrats: $0.48
  • Republicans: $0.54 (sum = $1.02, overround 2%)

Conditional market: “Democrats win given Biden is nominee?”

  • P(D wins | Biden nominee) = $0.42

Nominee market: “Will Biden be the Democratic nominee?”

  • P(Biden nominee) = $0.30

Conditional market: “Democrats win given Harris is nominee?”

  • P(D wins | Harris nominee) = $0.55

Nominee market: “Will Harris be the Democratic nominee?”

  • P(Harris nominee) = $0.45

All other Democratic nominees combined:

  • P(D wins | Other nominee) ≈ $0.50
  • P(Other nominee) = 1 - 0.30 - 0.45 = $0.25

Using the law of total probability:

P(D wins) = P(D|Biden) × P(Biden) + P(D|Harris) × P(Harris) + P(D|Other) × P(Other)
P(D wins) = 0.42 × 0.30 + 0.55 × 0.45 + 0.50 × 0.25
P(D wins) = 0.126 + 0.2475 + 0.125
P(D wins) = 0.4985

The unconditional market (after vig removal) implies P(D wins) = 0.48 / 1.02 = 0.4706. The conditional markets imply 0.4985. That’s a $0.028 discrepancy. After Polymarket’s ~2% winner fee, the break-even cost for the arb trade is approximately $0.02 per contract, so this $0.028 gap could yield a small profit.

Example 3: Kalshi Categorical Event Markets

Kalshi’s “Number of Fed Rate Cuts in 2026” market with categorical outcomes:

0 cuts:    22¢   ($0.22)
1 cut:     35¢   ($0.35)
2 cuts:    28¢   ($0.28)
3+ cuts:   18¢   ($0.18)
──────────────────────────
Sum:      103¢   ($1.03)
Overround: 3.0%

An agent computing expected cuts (treating 3+ as exactly 3 for a lower bound):

E[cuts] = 0 × (0.22/1.03) + 1 × (0.35/1.03) + 2 × (0.28/1.03) + 3 × (0.18/1.03)
E[cuts] = 0 + 0.3398 + 0.5437 + 0.5243
E[cuts] ≈ 1.41 cuts

If the agent’s macro model predicts 2.1 cuts (based on economic indicators), there’s a significant divergence from market pricing — the market is more hawkish than the model. The agent would buy the 2-cut and 3+-cut contracts, hedged against the 0-cut and 1-cut contracts.

Implementation

import numpy as np
from scipy.optimize import brentq
from dataclasses import dataclass, field
from typing import Optional


@dataclass
class MultiOutcomeMarket:
    """Represents a multi-outcome prediction market with vig removal."""
    outcomes: dict[str, float]  # outcome_name -> raw_price
    platform: str = "unknown"
    market_id: str = ""

    @property
    def raw_sum(self) -> float:
        return sum(self.outcomes.values())

    @property
    def overround(self) -> float:
        return self.raw_sum - 1.0

    @property
    def overround_pct(self) -> float:
        return self.overround * 100

    def true_probs_multiplicative(self) -> dict[str, float]:
        """Remove overround via multiplicative normalization.
        Assumes vig is distributed proportionally to each price."""
        s = self.raw_sum
        return {k: v / s for k, v in self.outcomes.items()}

    def true_probs_power(self) -> dict[str, float]:
        """Remove overround via the power method.
        Finds k such that Σ price_i^k = 1."""
        prices = np.array(list(self.outcomes.values()))
        names = list(self.outcomes.keys())

        if np.isclose(prices.sum(), 1.0, atol=1e-6):
            return dict(zip(names, prices))

        def objective(k: float) -> float:
            return np.sum(prices ** k) - 1.0

        # k < 1 when sum > 1 (overround), k > 1 when sum < 1 (underround)
        k = brentq(objective, 0.01, 10.0)
        true_probs = prices ** k
        return dict(zip(names, true_probs))

    def true_probs_shin(self, z: float = 0.03) -> dict[str, float]:
        """Remove overround via Shin's method.
        z = assumed fraction of insider/informed trading volume."""
        prices = list(self.outcomes.values())
        names = list(self.outcomes.keys())
        s = sum(prices)
        result = {}
        for name, price in zip(names, prices):
            numerator = (z ** 2 + 4 * (1 - z) * price * s) ** 0.5 - z
            denominator = 2 * (1 - z)
            result[name] = numerator / denominator
        # Normalize to exactly 1.0
        total = sum(result.values())
        return {k: v / total for k, v in result.items()}

    def detect_arbitrage(self, fee_rate: float = 0.0) -> dict:
        """Check for arbitrage if sum of prices < 1.0 after fees."""
        total = self.raw_sum
        cost = total * (1 + fee_rate)
        if cost < 1.0:
            return {
                "arb_exists": True,
                "cost": cost,
                "guaranteed_profit": 1.0 - cost,
                "roi_pct": ((1.0 - cost) / cost) * 100,
            }
        return {
            "arb_exists": False,
            "overround": self.overround,
            "overround_pct": self.overround_pct,
        }


@dataclass
class ConditionalMarketTree:
    """Detects consistency violations between conditional and unconditional markets."""
    unconditional_price: float  # P(A)
    conditionals: dict[str, tuple[float, float]]  # condition_name -> (P(A|B_i), P(B_i))
    fee_rate: float = 0.02

    def implied_unconditional(self) -> float:
        """Compute P(A) from conditional markets using law of total probability.
        P(A) = Σ P(A|B_i) × P(B_i)"""
        return sum(p_a_given_b * p_b for p_a_given_b, p_b in self.conditionals.values())

    def consistency_gap(self) -> float:
        """Difference between market P(A) and conditional-implied P(A)."""
        return self.unconditional_price - self.implied_unconditional()

    def is_arbitrageable(self) -> dict:
        """Check if the gap exceeds transaction costs."""
        gap = self.consistency_gap()
        abs_gap = abs(gap)
        # Approximate transaction cost: fee on both legs
        tx_cost = self.fee_rate * 2
        if abs_gap > tx_cost:
            if gap > 0:
                direction = "unconditional overpriced — sell A, buy conditional replication"
            else:
                direction = "unconditional underpriced — buy A, sell conditional replication"
            return {
                "arbitrageable": True,
                "gap": gap,
                "abs_gap": abs_gap,
                "tx_cost": tx_cost,
                "net_edge": abs_gap - tx_cost,
                "direction": direction,
            }
        return {
            "arbitrageable": False,
            "gap": gap,
            "abs_gap": abs_gap,
            "tx_cost": tx_cost,
        }


def lmsr_cost(q: np.ndarray, b: float) -> float:
    """Hanson's LMSR cost function.
    C(q) = b * ln(Σ exp(q_i / b))

    Args:
        q: array of outstanding shares per outcome
        b: liquidity parameter (higher = deeper market)

    Returns:
        Current cost state of the market
    """
    # Use log-sum-exp trick for numerical stability
    q_scaled = q / b
    max_q = np.max(q_scaled)
    return b * (max_q + np.log(np.sum(np.exp(q_scaled - max_q))))


def lmsr_prices(q: np.ndarray, b: float) -> np.ndarray:
    """Compute LMSR prices (softmax of q/b).

    Args:
        q: array of outstanding shares per outcome
        b: liquidity parameter

    Returns:
        Array of prices summing to 1.0
    """
    q_scaled = q / b
    max_q = np.max(q_scaled)
    exp_q = np.exp(q_scaled - max_q)
    return exp_q / np.sum(exp_q)


def lmsr_trade_cost(
    q_before: np.ndarray,
    outcome_idx: int,
    shares: float,
    b: float,
) -> float:
    """Compute cost of buying `shares` of outcome `outcome_idx`.

    Args:
        q_before: current share state
        outcome_idx: which outcome to buy
        shares: number of shares (positive = buy, negative = sell)
        b: liquidity parameter

    Returns:
        Cost of the trade (positive = agent pays)
    """
    q_after = q_before.copy()
    q_after[outcome_idx] += shares
    return lmsr_cost(q_after, b) - lmsr_cost(q_before, b)


def monitor_conditional_consistency(
    unconditional_prices: dict[str, float],
    conditional_markets: dict[str, dict[str, tuple[float, float]]],
    fee_rate: float = 0.02,
) -> list[dict]:
    """Monitor consistency across a set of related conditional markets.

    Args:
        unconditional_prices: {event_name: market_price}
        conditional_markets: {event_name: {condition: (P(A|B), P(B))}}
        fee_rate: platform fee rate

    Returns:
        List of arbitrage opportunities found
    """
    opportunities = []
    for event, unc_price in unconditional_prices.items():
        if event not in conditional_markets:
            continue
        tree = ConditionalMarketTree(
            unconditional_price=unc_price,
            conditionals=conditional_markets[event],
            fee_rate=fee_rate,
        )
        result = tree.is_arbitrageable()
        if result["arbitrageable"]:
            opportunities.append({
                "event": event,
                "unconditional_price": unc_price,
                "implied_from_conditionals": tree.implied_unconditional(),
                **result,
            })
    return opportunities


# --- Demo: Multi-outcome vig removal ---
if __name__ == "__main__":
    # Republican primary example
    market = MultiOutcomeMarket(
        outcomes={
            "DeSantis": 0.32,
            "Vance": 0.28,
            "Haley": 0.15,
            "Ramaswamy": 0.09,
            "Scott": 0.07,
            "Other": 0.12,
        },
        platform="Polymarket",
        market_id="republican-nominee-2028",
    )

    print(f"Raw sum: ${market.raw_sum:.2f}")
    print(f"Overround: {market.overround_pct:.1f}%\n")

    mult = market.true_probs_multiplicative()
    power = market.true_probs_power()
    shin = market.true_probs_shin(z=0.03)

    print(f"{'Outcome':<14} {'Raw':>7} {'Mult':>8} {'Power':>8} {'Shin':>8}")
    print("-" * 48)
    for name in market.outcomes:
        print(
            f"{name:<14} ${market.outcomes[name]:.2f}"
            f"  {mult[name]:>6.2%}  {power[name]:>6.2%}  {shin[name]:>6.2%}"
        )

    # --- Demo: LMSR pricing ---
    print("\n--- LMSR Market Maker Demo ---")
    n_outcomes = 8
    b = 100.0  # liquidity parameter
    q = np.zeros(n_outcomes)

    print(f"Initial prices (uniform): {lmsr_prices(q, b)}")
    print(f"Max market maker loss: ${b * np.log(n_outcomes):.2f}")

    # Simulate a trade: buy 10 shares of outcome 0
    cost = lmsr_trade_cost(q, outcome_idx=0, shares=10, b=b)
    q[0] += 10
    new_prices = lmsr_prices(q, b)
    print(f"\nAfter buying 10 shares of outcome 0:")
    print(f"  Trade cost: ${cost:.4f}")
    print(f"  New prices: {np.round(new_prices, 4)}")
    print(f"  Price sum:  {new_prices.sum():.6f}")

    # --- Demo: Conditional consistency check ---
    print("\n--- Conditional Market Consistency Check ---")
    opps = monitor_conditional_consistency(
        unconditional_prices={"Dem wins": 0.47},
        conditional_markets={
            "Dem wins": {
                "Biden nominee": (0.42, 0.30),
                "Harris nominee": (0.55, 0.45),
                "Other nominee": (0.50, 0.25),
            }
        },
        fee_rate=0.02,
    )
    if opps:
        for opp in opps:
            print(f"  Event: {opp['event']}")
            print(f"  Market price: ${opp['unconditional_price']:.3f}")
            print(f"  Implied:      ${opp['implied_from_conditionals']:.3f}")
            print(f"  Gap:          ${opp['abs_gap']:.3f}")
            print(f"  Direction:    {opp['direction']}")
    else:
        print("  No arbitrage opportunities found.")

Limitations and Edge Cases

Conditional market void risk. When the conditioning event B doesn’t occur, conditional contracts void. An agent that replicates an unconditional position using conditional contracts is exposed to capital lockup — funds tied up in conditional contracts that void return no profit, just principal. This opportunity cost is real and isn’t captured in the simple consistency check above.

LMSR subsidization cost. The LMSR market maker subsidizes liquidity — someone (the platform) eats a bounded but guaranteed loss of up to b × ln(n). For Polymarket’s CLOB model, this isn’t an issue since liquidity is provided by external market makers. But any platform running a pure LMSR (rare in 2026) needs to fund this subsidy.

Correlated outcomes break independence. The exponential blowup reduction in Hanson’s combinatorial LMSR relies on outcome independence or bounded tree-width. When outcomes are highly correlated (e.g., “Democrats win the presidency” and “Democrats win the Senate” have ~0.7 correlation), the decomposition is less efficient and the market maker’s loss bounds widen.

Thin conditional markets. Conditional markets on Polymarket often have 5-20x less liquidity than unconditional markets. A $0.03 consistency gap might exist purely because no one has bothered to arb it — the liquidity is too thin to trade meaningful size. An agent should check orderbook depth before executing: a $0.03 edge on $500 of available liquidity is $15 gross — barely worth the gas fees on Polygon.

Overround removal method sensitivity. For low-overround markets (1-3%, typical of prediction markets), all three removal methods produce nearly identical results. The choice matters for sportsbook markets with 8-15% overround, where Shin’s method and the power method diverge meaningfully from multiplicative normalization. See the vig calculation guide for sportsbook-specific methods.

Time-varying conditional prices. The chain rule P(A ∩ B) = P(A|B) × P(B) holds at each instant, but prices across related markets don’t update simultaneously. An agent that detects a $0.03 gap should verify it persists for at least 30-60 seconds before trading, ruling out transient orderbook lag.

FAQ

How do you calculate probabilities in a multi-outcome prediction market?

Sum all outcome prices to get the total. Divide each outcome’s price by that total to get the true implied probability. For example, if four outcomes are priced at $0.45, $0.30, $0.15, and $0.13 (sum = $1.03), the true probability of outcome 1 is $0.45 / $1.03 = 43.7%. The 3% excess is the overround (market maker’s margin).

What is a combinatorial prediction market and why is it hard to build?

A combinatorial prediction market lets traders bet on combinations of multiple variables — like which party wins AND by what margin. The problem is exponential blowup: k binary variables create 2^k joint outcomes. Ten binary questions yield 1,024 possible states. Hanson’s LMSR solves this by maintaining a cost function over the full outcome space without requiring explicit liquidity pools for every combination.

How do conditional probability markets work on Polymarket?

Conditional markets price P(A|B) — the probability of A given that B occurs. On Polymarket, these resolve to $1.00 only if both the condition and the outcome are met, or void if the condition fails. The chain rule connects them: P(A and B) = P(A|B) × P(B). Agents can detect mispricing by checking whether conditional and unconditional market prices satisfy this identity.

How do you detect arbitrage across related multi-outcome prediction markets?

Use the law of total probability: P(A) must equal the sum of P(A|B_i) × P(B_i) across all conditioning events B_i. If the unconditional market price for A diverges from this computed sum beyond transaction costs, an arbitrage opportunity exists. An agent monitors both the conditional and unconditional markets and trades when the discrepancy exceeds fees.

What is Hanson’s Logarithmic Market Scoring Rule for combinatorial markets?

Hanson’s LMSR uses the cost function C(q) = b × ln(Σ exp(q_i/b)), where q_i is the number of shares of outcome i and b is the liquidity parameter. It prices any trade across an exponential outcome space in O(n) time by exploiting the decomposability of the log-sum-exp function, avoiding the need to maintain separate order books for every combination.

What’s Next

This guide covers the math for markets with many outcomes and conditional structure. The natural next steps:

  • Scoring rules that power these markets: Prediction Market Scoring Rules derives the proper scoring rules (logarithmic, quadratic, spherical) that underlie automated market makers like LMSR.
  • Arbitrage detection algorithms: Arbitrage Detection Algorithms covers cross-platform and within-platform arb detection — including the multi-outcome arbitrage checks from this guide at scale. Use the Arbitrage Calculator to run these checks in real time.
  • The market maker math: LMSR and Automated Market Maker Math goes deeper on the cost function properties, bounded loss proofs, and liquidity parameter calibration.
  • API endpoints for pulling multi-outcome data: Prediction Market API Reference documents the Polymarket and Kalshi endpoints for fetching multi-outcome market structures.
  • Sportsbook multi-outcome markets: Offshore sportsbooks offer futures markets (MVP, division winners, season win totals) with much higher overround — the vig-removal methods in this guide are essential for extracting true probabilities from BetOnline or Bovada futures boards.