28 min read

> "A contract is a meeting of minds, but a prediction market contract is a meeting of beliefs — priced in dollars and settled by reality."

Chapter 4: Contracts, Payoffs, and Market Mechanics

"A contract is a meeting of minds, but a prediction market contract is a meeting of beliefs — priced in dollars and settled by reality." — Adapted from legal tradition

In the previous chapters, you learned what prediction markets are, why they aggregate information effectively, and how probability connects to price. Now it is time to get concrete. What exactly are you buying and selling on a prediction market? How does a trade move from an idea in your head to cash in your account? What happens when the event resolves?

This chapter is the mechanical heart of the book. We will disassemble the prediction market engine piece by piece — contracts, orders, positions, settlement — and show you how every gear fits together. By the end, you will be able to read any prediction market contract, calculate your payoff under every possible outcome, and trace a trade through its entire lifecycle from placement to settlement.

We will build Python classes for every major concept, so you can simulate and experiment on your own machine. The code is not a toy: the patterns here mirror how real trading systems represent contracts and orders internally.


4.1 Binary (Yes/No) Contracts

4.1.1 Definition

A binary contract (also called a binary option, all-or-nothing contract, or digital contract) is the simplest prediction market instrument. It asks a question that has exactly two possible outcomes: Yes or No.

Examples of binary contract questions:

  • "Will the Federal Reserve raise interest rates at the March 2026 meeting?"
  • "Will it snow in London on Christmas Day 2026?"
  • "Will Company X report quarterly revenue above $10 billion?"
  • "Will the Mars Sample Return mission launch before 2030?"

Each binary contract has a resolution date (or resolution window) — the point in time when the answer becomes known — and resolution criteria — the precise rules that determine whether the outcome is Yes or No.

4.1.2 Payoff Structure

The payoff structure of a binary contract is elegantly simple:

Outcome Payoff per Contract
Yes $1.00
No $0.00

If you hold a Yes contract and the event occurs, you receive $1.00. If it does not occur, you receive nothing. The price you pay for the contract (say, $0.65) represents the market's implied probability of the event (65%).

Your profit or loss is:

$$\text{P\&L} = \text{Payoff} - \text{Purchase Price}$$

For a Yes contract purchased at $0.65: - If Yes: P&L = $1.00 - $0.65 = +$0.35 - If No: P&L = $0.00 - $0.65 = -$0.65

4.1.3 Buying Yes vs. Buying No

Every binary contract implicitly creates two tradeable sides:

  • Buying Yes at price $p$ means you pay $p$ and receive $1$ if the event happens.
  • Buying No at price $q$ means you pay $q$ and receive $1$ if the event does NOT happen.

On a well-functioning exchange, these two prices are complementary:

$$p_{\text{Yes}} + p_{\text{No}} = 1$$

If Yes is trading at $0.65, then No should be trading at $0.35. If you buy No at $0.35: - If No (event does not happen): P&L = $1.00 - $0.35 = +$0.65 - If Yes (event happens): P&L = $0.00 - $0.35 = -$0.35

Notice the symmetry: one person's maximum profit is the other's maximum loss.

In practice, the sum $p_{\text{Yes}} + p_{\text{No}}$ may slightly exceed 1 due to the bid-ask spread. We will address this in Section 4.9.

4.1.4 Shorting a Binary Contract

Shorting (or selling short) a Yes contract is economically equivalent to buying a No contract. When you short a Yes contract at price $p$:

  • You receive $p$ upfront.
  • You must pay $1$ if the event happens (your obligation).
  • You keep the $p$ if the event does not happen.

Your payoff when shorting Yes at price $p$: - If Yes: P&L = $p - $1.00$ - If No: P&L = $p - $0.00 = p$

For shorting Yes at $0.65: - If Yes: P&L = $0.65 - $1.00 = -$0.35 - If No: P&L = $0.65 - $0.00 = +$0.65

This is exactly the mirror image of buying Yes. On most prediction market platforms, you do not explicitly "short" — you simply buy the other side. But the economic equivalence is important to understand.

4.1.5 The Pricing Relationship

In a frictionless market, the fundamental pricing identity holds:

$$P(\text{Yes}) + P(\text{No}) = 1$$

This must be true by no-arbitrage. If Yes costs $0.60 and No costs $0.30, the total is $0.90. A trader could buy both for $0.90 and guarantee receiving $1.00 (since one of them must pay out), locking in a risk-free profit of $0.10. Arbitrageurs would quickly exploit this, pushing prices back to sum to $1.

Conversely, if the sum exceeded $1, say Yes at $0.60 and No at $0.50, then selling both sides would guarantee a profit (receive $1.10, pay out $1.00). Again, arbitrage forces equilibrium.

4.1.6 P&L Diagrams for Binary Contracts

A P&L diagram for a binary contract is not a continuous curve like options on equities — it has exactly two points. But it is still useful to visualize:

Long Yes at $0.65:

  P&L
  +$0.35 |              *  (Yes outcome)
         |
   $0.00 |----------------------------
         |
  -$0.65 |    *  (No outcome)
         |
         +----+----------+----> Outcome
              No         Yes

Long No at $0.35:

  P&L
  +$0.65 |    *  (No outcome)
         |
   $0.00 |----------------------------
         |
  -$0.35 |              *  (Yes outcome)
         |
         +----+----------+----> Outcome
              No         Yes

4.1.7 Python: Binary Contract Class

from dataclasses import dataclass, field
from enum import Enum
from typing import Optional
from datetime import datetime


class ContractSide(Enum):
    YES = "yes"
    NO = "no"


class ContractStatus(Enum):
    OPEN = "open"
    CLOSED = "closed"
    RESOLVED_YES = "resolved_yes"
    RESOLVED_NO = "resolved_no"
    VOIDED = "voided"


@dataclass
class BinaryContract:
    """Represents a binary (Yes/No) prediction market contract.

    Attributes:
        question: The question the contract resolves on.
        resolution_date: When the contract resolves.
        resolution_criteria: Precise rules for determining the outcome.
        status: Current status of the contract.
        yes_price: Current Yes price (0 to 1).
        no_price: Current No price (0 to 1).
    """
    question: str
    resolution_date: datetime
    resolution_criteria: str
    status: ContractStatus = ContractStatus.OPEN
    yes_price: float = 0.50
    no_price: float = 0.50

    def __post_init__(self):
        if not 0 <= self.yes_price <= 1:
            raise ValueError(f"Yes price must be between 0 and 1, got {self.yes_price}")
        if not 0 <= self.no_price <= 1:
            raise ValueError(f"No price must be between 0 and 1, got {self.no_price}")

    @property
    def overround(self) -> float:
        """The overround (vig) — how much prices exceed 1.0 in total."""
        return self.yes_price + self.no_price - 1.0

    @property
    def implied_probability_yes(self) -> float:
        """Implied probability of Yes, adjusted for overround."""
        total = self.yes_price + self.no_price
        return self.yes_price / total if total > 0 else 0.5

    def payoff(self, side: ContractSide, outcome_is_yes: bool) -> float:
        """Calculate the payoff for holding one contract.

        Args:
            side: Whether holding Yes or No.
            outcome_is_yes: Whether the event occurred.

        Returns:
            Payoff amount (0.0 or 1.0).
        """
        if side == ContractSide.YES:
            return 1.0 if outcome_is_yes else 0.0
        else:
            return 0.0 if outcome_is_yes else 1.0

    def profit_loss(self, side: ContractSide, purchase_price: float,
                    outcome_is_yes: bool) -> float:
        """Calculate profit or loss for a position.

        Args:
            side: Whether holding Yes or No.
            purchase_price: Price paid per contract.
            outcome_is_yes: Whether the event occurred.

        Returns:
            Profit (positive) or loss (negative) per contract.
        """
        return self.payoff(side, outcome_is_yes) - purchase_price

    def resolve(self, outcome_is_yes: bool) -> None:
        """Resolve the contract."""
        if self.status != ContractStatus.OPEN:
            raise ValueError(f"Cannot resolve contract with status {self.status}")
        self.status = (ContractStatus.RESOLVED_YES if outcome_is_yes
                       else ContractStatus.RESOLVED_NO)


# Example usage
if __name__ == "__main__":
    contract = BinaryContract(
        question="Will the Fed raise rates in March 2026?",
        resolution_date=datetime(2026, 3, 20),
        resolution_criteria="Federal Reserve announces rate increase at March FOMC meeting.",
        yes_price=0.65,
        no_price=0.37,  # Slight overround
    )

    print(f"Question: {contract.question}")
    print(f"Yes price: ${contract.yes_price:.2f}")
    print(f"No price: ${contract.no_price:.2f}")
    print(f"Overround: {contract.overround:.1%}")
    print(f"Implied P(Yes): {contract.implied_probability_yes:.1%}")
    print()

    # Calculate P&L for buying Yes at 0.65
    for outcome in [True, False]:
        pnl = contract.profit_loss(ContractSide.YES, 0.65, outcome)
        print(f"Buy Yes at $0.65, outcome={'Yes' if outcome else 'No'}: P&L = ${pnl:+.2f}")

4.2 Multi-Outcome Contracts

4.2.1 Definition

A multi-outcome contract (also called a categorical contract or multiple-choice market) extends the binary model to questions with three or more mutually exclusive outcomes. Instead of Yes/No, the market lists a set of named outcomes, and exactly one of them will be declared the winner.

Examples:

  • "Who will win the 2028 U.S. presidential election?" — Outcomes: Candidate A, Candidate B, Candidate C, Candidate D, Other.
  • "Which team will win the 2026 World Cup?" — Outcomes: Brazil, France, Germany, Argentina, England, Other.
  • "Which company will first release a consumer brain-computer interface?" — Outcomes: Neuralink, Meta, Apple, Other.

4.2.2 Structure and Payoffs

A multi-outcome market with $n$ outcomes creates $n$ separate contracts, one for each outcome. Each contract pays $1 if its outcome wins and $0 otherwise.

For a market with outcomes $\{O_1, O_2, \ldots, O_n\}$:

$$\text{Payoff}(O_i) = \begin{cases} 1 & \text{if outcome } O_i \text{ wins} \\ 0 & \text{otherwise} \end{cases}$$

If you buy one share of $O_3$ at price $p_3 = 0.20$: - If $O_3$ wins: P&L = $1.00 - $0.20 = +$0.80 - If any other outcome wins: P&L = $0.00 - $0.20 = -$0.20

4.2.3 The Complete Market Constraint

In theory, since the outcomes are mutually exclusive and exhaustive, buying one share of every outcome guarantees a $1 payoff. Therefore:

$$\sum_{i=1}^{n} p_i = 1$$

This is the completeness constraint. In practice, the sum often exceeds 1 due to the bid-ask spread and the platform's margin (overround):

$$\sum_{i=1}^{n} p_i = 1 + \text{overround}$$

4.2.4 Overround

The overround (also called the vig, juice, or margin) is the amount by which contract prices sum to more than $1. It represents the implicit cost of trading and the platform's built-in edge.

Example — a five-candidate election market:

Candidate Price
Alice $0.42
Bob $0.28
Carol $0.15
Dave $0.10
Other $0.08
Total $1.03

The overround is $0.03, or 3%. To extract the true implied probabilities, we normalize:

$$\hat{p}_i = \frac{p_i}{\sum_{j} p_j}$$

For Alice: $\hat{p}_{\text{Alice}} = 0.42 / 1.03 \approx 0.408 = 40.8\%$

4.2.5 Arrow-Debreu Securities

Multi-outcome prediction market contracts are essentially Arrow-Debreu securities — a concept from general equilibrium theory introduced by Kenneth Arrow and Gerard Debreu in the 1950s. An Arrow-Debreu security pays $1 in exactly one "state of the world" and $0 in all others.

The theoretical power of Arrow-Debreu securities is that any complex payoff can be replicated by combining them. If you want a contract that pays $3 when Alice wins and $1 when Bob wins, you can buy 3 Alice contracts and 1 Bob contract. This spanning property makes a complete set of Arrow-Debreu securities sufficient to create any desired risk profile.

In prediction markets, each outcome contract is one Arrow-Debreu security, and the market as a whole forms a complete set for that particular event.

4.2.6 Trading Strategies in Multi-Outcome Markets

Several strategies are unique to multi-outcome markets:

  1. Single outcome bet: Buy one outcome you believe is underpriced.
  2. Field bet: Buy multiple outcomes to cover a group (e.g., "any Democrat" by buying all Democrat candidates).
  3. Spread trade: Buy one outcome and sell another, betting on the relative likelihood.
  4. Arbitrage: If prices sum to less than $1, buy all outcomes for a guaranteed profit.

4.2.7 Python: Multi-Outcome Contract

from dataclasses import dataclass, field
from typing import Dict, List, Optional
from datetime import datetime


@dataclass
class MultiOutcomeContract:
    """Represents a multi-outcome prediction market contract.

    Attributes:
        question: The question the contract resolves on.
        outcomes: List of possible outcome names.
        resolution_date: When the contract resolves.
        resolution_criteria: Precise rules for determining the outcome.
        prices: Current prices for each outcome.
    """
    question: str
    outcomes: List[str]
    resolution_date: datetime
    resolution_criteria: str
    prices: Dict[str, float] = field(default_factory=dict)
    resolved_outcome: Optional[str] = None

    def __post_init__(self):
        if len(self.outcomes) < 2:
            raise ValueError("Multi-outcome contract must have at least 2 outcomes.")
        # Initialize equal prices if not provided
        if not self.prices:
            equal_price = 1.0 / len(self.outcomes)
            self.prices = {outcome: round(equal_price, 4)
                          for outcome in self.outcomes}
        # Validate all outcomes have prices
        for outcome in self.outcomes:
            if outcome not in self.prices:
                raise ValueError(f"Missing price for outcome: {outcome}")

    @property
    def price_sum(self) -> float:
        """Sum of all outcome prices."""
        return sum(self.prices.values())

    @property
    def overround(self) -> float:
        """Overround (amount by which prices exceed 1.0)."""
        return self.price_sum - 1.0

    @property
    def overround_percentage(self) -> float:
        """Overround as a percentage."""
        return self.overround * 100

    def implied_probabilities(self) -> Dict[str, float]:
        """Normalized implied probabilities (removing overround)."""
        total = self.price_sum
        return {outcome: price / total
                for outcome, price in self.prices.items()}

    def payoff(self, held_outcome: str, winning_outcome: str) -> float:
        """Calculate payoff for holding one contract of a specific outcome.

        Args:
            held_outcome: The outcome the trader holds a contract for.
            winning_outcome: The outcome that actually wins.

        Returns:
            1.0 if held_outcome matches winning_outcome, else 0.0.
        """
        if held_outcome not in self.outcomes:
            raise ValueError(f"Unknown outcome: {held_outcome}")
        if winning_outcome not in self.outcomes:
            raise ValueError(f"Unknown outcome: {winning_outcome}")
        return 1.0 if held_outcome == winning_outcome else 0.0

    def profit_loss(self, held_outcome: str, purchase_price: float,
                    winning_outcome: str) -> float:
        """Calculate P&L for a position in one outcome.

        Args:
            held_outcome: The outcome the trader holds.
            purchase_price: Price paid per contract.
            winning_outcome: The outcome that wins.

        Returns:
            Profit or loss per contract.
        """
        return self.payoff(held_outcome, winning_outcome) - purchase_price

    def portfolio_payoff(self, holdings: Dict[str, int],
                         costs: Dict[str, float],
                         winning_outcome: str) -> Dict[str, float]:
        """Calculate payoff for a portfolio of positions.

        Args:
            holdings: Number of contracts held per outcome.
            costs: Average cost per contract per outcome.
            winning_outcome: The outcome that wins.

        Returns:
            Dictionary with total_payoff, total_cost, and net_pnl.
        """
        total_payoff = 0.0
        total_cost = 0.0
        for outcome, qty in holdings.items():
            payout = self.payoff(outcome, winning_outcome) * qty
            cost = costs.get(outcome, self.prices[outcome]) * qty
            total_payoff += payout
            total_cost += cost

        return {
            "total_payoff": total_payoff,
            "total_cost": total_cost,
            "net_pnl": total_payoff - total_cost,
        }

    def resolve(self, winning_outcome: str) -> None:
        """Resolve the contract with a winning outcome."""
        if winning_outcome not in self.outcomes:
            raise ValueError(f"Unknown outcome: {winning_outcome}")
        self.resolved_outcome = winning_outcome


# Example usage
if __name__ == "__main__":
    election = MultiOutcomeContract(
        question="Who will win the 2028 presidential election?",
        outcomes=["Alice", "Bob", "Carol", "Dave", "Other"],
        resolution_date=datetime(2028, 11, 5),
        resolution_criteria="Winner of the U.S. Electoral College.",
        prices={"Alice": 0.42, "Bob": 0.28, "Carol": 0.15,
                "Dave": 0.10, "Other": 0.08},
    )

    print(f"Question: {election.question}")
    print(f"Price sum: ${election.price_sum:.2f}")
    print(f"Overround: {election.overround_percentage:.1f}%")
    print()

    probs = election.implied_probabilities()
    print("Implied probabilities:")
    for outcome, prob in probs.items():
        print(f"  {outcome}: {prob:.1%} (price: ${election.prices[outcome]:.2f})")
    print()

    # P&L for buying Alice at $0.42
    for winner in election.outcomes:
        pnl = election.profit_loss("Alice", 0.42, winner)
        print(f"Hold Alice at $0.42, winner={winner}: P&L = ${pnl:+.2f}")

4.3 Scalar (Range) Contracts

4.3.1 Definition

A scalar contract (also called a range contract, numeric contract, or index contract) allows traders to speculate on continuous numeric outcomes rather than discrete categories. The underlying question asks "What will the value of X be?" where X is a number.

Examples:

  • "What will U.S. GDP growth be in Q3 2026?"
  • "How many seats will Party A win in the election?"
  • "What will the closing price of Bitcoin be on December 31, 2026?"
  • "What will the global average temperature anomaly be for 2026?"

4.3.2 Bracket Contracts

The most common implementation of scalar markets uses bracket contracts — a set of binary contracts that partition the numeric range into intervals (brackets).

Example: "What will U.S. GDP growth be in Q3 2026?"

Bracket Contract Pays $1 If...
Below 0% GDP growth < 0%
0% to 1% 0% ≤ growth < 1%
1% to 2% 1% ≤ growth < 2%
2% to 3% 2% ≤ growth < 3%
3% to 4% 3% ≤ growth < 4%
4% and above growth ≥ 4%

Each bracket is a binary contract: it pays $1 if the final value falls within that bracket, $0 otherwise. Collectively, the brackets form a multi-outcome market with the completeness constraint.

4.3.3 Linear (Continuous) Payoff Contracts

Some platforms offer contracts with linear payoffs — the payout scales proportionally with the outcome value.

A linear scalar contract pays:

$$\text{Payoff} = \frac{\text{Actual Value} - \text{Floor}}{\text{Ceiling} - \text{Floor}}$$

capped at $[0, 1]$.

For a GDP growth contract with floor = 0% and ceiling = 5%:

Actual GDP Growth Payoff Calculation Payoff
-1% Below floor $0.00
0% (0 - 0) / (5 - 0) $0.00
2.5% (2.5 - 0) / (5 - 0) $0.50
4% (4 - 0) / (5 - 0) $0.80
5% or above At or above ceiling $1.00

The price of this contract represents the market's expected value for the outcome (normalized to the contract range).

4.3.4 Capped vs. Uncapped Payoffs

  • Capped payoffs limit the maximum payout to $1 (as in the example above). The floor and ceiling define the range.
  • Uncapped payoffs let the payout grow linearly without bound. These are rare in prediction markets (they create unlimited liability for short sellers) but exist in some financial derivatives.

Most prediction markets use capped payoffs because they are easier to margin and settle.

4.3.5 Recovering the Implied Distribution

A set of bracket contracts reveals the market's implied probability distribution over the numeric outcome. If the "2% to 3%" bracket is priced at $0.30, the market assigns a 30% probability to GDP growth falling in that range.

By collecting all bracket prices, you can construct a probability mass function (for brackets) or approximate a probability density function (by dividing each bracket's probability by its width):

$$f(x) \approx \frac{p_i}{\Delta x_i}$$

where $p_i$ is the price of bracket $i$ and $\Delta x_i$ is its width.

4.3.6 Python: Scalar Contract Classes

from dataclasses import dataclass, field
from typing import List, Tuple, Optional, Dict
from datetime import datetime
import math


@dataclass
class Bracket:
    """A single bracket in a scalar contract."""
    label: str
    lower_bound: float  # Inclusive
    upper_bound: float  # Exclusive (except for the last bracket)
    inclusive_upper: bool = False  # True for the last bracket
    price: float = 0.0

    def contains(self, value: float) -> bool:
        """Check if a value falls within this bracket."""
        if self.inclusive_upper:
            return self.lower_bound <= value <= self.upper_bound
        return self.lower_bound <= value < self.upper_bound

    @property
    def width(self) -> float:
        return self.upper_bound - self.lower_bound

    @property
    def implied_density(self) -> float:
        """Implied probability density (probability / width)."""
        if self.width == 0:
            return 0.0
        return self.price / self.width


@dataclass
class BracketScalarContract:
    """A scalar contract implemented as a set of brackets.

    Each bracket pays $1 if the outcome falls within it, $0 otherwise.
    """
    question: str
    brackets: List[Bracket]
    resolution_date: datetime
    resolution_criteria: str
    unit: str = ""  # e.g., "%", "seats", "$"

    @property
    def price_sum(self) -> float:
        return sum(b.price for b in self.brackets)

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

    def implied_probabilities(self) -> Dict[str, float]:
        """Normalized implied probabilities for each bracket."""
        total = self.price_sum
        return {b.label: b.price / total for b in self.brackets}

    def expected_value(self) -> float:
        """Calculate the implied expected value (midpoint-weighted)."""
        total = self.price_sum
        ev = 0.0
        for b in self.brackets:
            midpoint = (b.lower_bound + b.upper_bound) / 2
            prob = b.price / total
            ev += midpoint * prob
        return ev

    def payoff(self, bracket_label: str, actual_value: float) -> float:
        """Calculate payoff for holding a bracket contract."""
        for b in self.brackets:
            if b.label == bracket_label:
                return 1.0 if b.contains(actual_value) else 0.0
        raise ValueError(f"Unknown bracket: {bracket_label}")

    def winning_bracket(self, actual_value: float) -> Optional[str]:
        """Find which bracket an actual value falls into."""
        for b in self.brackets:
            if b.contains(actual_value):
                return b.label
        return None


@dataclass
class LinearScalarContract:
    """A scalar contract with linear (capped) payoff.

    Payoff = clamp((actual - floor) / (ceiling - floor), 0, 1)
    """
    question: str
    floor: float
    ceiling: float
    resolution_date: datetime
    resolution_criteria: str
    price: float = 0.50
    unit: str = ""

    def __post_init__(self):
        if self.ceiling <= self.floor:
            raise ValueError("Ceiling must be greater than floor.")

    def payoff(self, actual_value: float) -> float:
        """Calculate the payoff given an actual outcome value.

        Args:
            actual_value: The realized numeric outcome.

        Returns:
            Payoff between 0.0 and 1.0.
        """
        raw = (actual_value - self.floor) / (self.ceiling - self.floor)
        return max(0.0, min(1.0, raw))

    def profit_loss(self, purchase_price: float, actual_value: float,
                    is_long: bool = True) -> float:
        """Calculate P&L for a position.

        Args:
            purchase_price: Price paid (for long) or received (for short).
            actual_value: The realized outcome.
            is_long: True for long position, False for short.

        Returns:
            Profit or loss per contract.
        """
        payout = self.payoff(actual_value)
        if is_long:
            return payout - purchase_price
        else:
            return purchase_price - payout

    @property
    def implied_expected_value(self) -> float:
        """The implied expected value based on the current price.

        Since price = E[(actual - floor) / (ceiling - floor)],
        we can recover E[actual] = price * (ceiling - floor) + floor.
        """
        return self.price * (self.ceiling - self.floor) + self.floor


# Example usage
if __name__ == "__main__":
    # Bracket scalar contract
    gdp_brackets = BracketScalarContract(
        question="What will U.S. GDP growth be in Q3 2026?",
        brackets=[
            Bracket("Below 0%", -float('inf'), 0.0, price=0.05),
            Bracket("0% to 1%", 0.0, 1.0, price=0.10),
            Bracket("1% to 2%", 1.0, 2.0, price=0.25),
            Bracket("2% to 3%", 2.0, 3.0, price=0.35),
            Bracket("3% to 4%", 3.0, 4.0, price=0.15),
            Bracket("4% and above", 4.0, float('inf'), inclusive_upper=True, price=0.08),
        ],
        resolution_date=datetime(2026, 10, 30),
        resolution_criteria="Bureau of Economic Analysis advance GDP estimate.",
        unit="%",
    )

    print(f"GDP Market: {gdp_brackets.question}")
    print(f"Price sum: ${gdp_brackets.price_sum:.2f}")
    print(f"Overround: {gdp_brackets.overround:.1%}")
    print(f"Implied E[GDP growth]: {gdp_brackets.expected_value():.2f}%")
    print()

    # Linear scalar contract
    linear = LinearScalarContract(
        question="What will GDP growth be?",
        floor=0.0,
        ceiling=5.0,
        resolution_date=datetime(2026, 10, 30),
        resolution_criteria="BEA advance estimate.",
        price=0.48,
        unit="%",
    )

    print(f"Linear contract price: ${linear.price:.2f}")
    print(f"Implied E[GDP growth]: {linear.implied_expected_value:.2f}%")
    for val in [-1.0, 0.0, 1.5, 2.5, 4.0, 6.0]:
        payout = linear.payoff(val)
        pnl = linear.profit_loss(0.48, val, is_long=True)
        print(f"  GDP = {val:+.1f}%: payoff = ${payout:.2f}, P&L = ${pnl:+.3f}")

4.4 The Trade Lifecycle

4.4.1 Overview

Every trade in a prediction market follows a lifecycle with distinct stages. Understanding this lifecycle is essential for knowing what happens to your money at each point and what your obligations are.

The stages are:

Research → Order Placement → Order Matching → Position Held →
    Price Monitoring → [Optional: Exit Trade] → Event Resolution → Settlement

Let us walk through each stage.

4.4.2 Stage 1: Research and Idea Generation

Before placing a trade, you form a view on the probability of an event. This involves:

  1. Reading the contract specification: What exactly is being asked? What are the resolution criteria? What is the resolution source?
  2. Estimating your probability: Based on your analysis, what do you believe the true probability is?
  3. Comparing to market price: Is there a gap between your estimate and the market's price?
  4. Evaluating edge: Is the gap large enough to overcome fees and uncertainty?

For example, you believe the probability of a Fed rate hike is 55%, but the market Yes price is $0.42. You see a 13-cent edge.

4.4.3 Stage 2: Order Placement

You submit an order to the exchange specifying:

  • Contract: Which market and which side (Yes/No, or which outcome).
  • Direction: Buy or sell.
  • Quantity: How many contracts.
  • Order type: Market, limit, or other (see Section 4.5).
  • Price: For limit orders, the maximum (buy) or minimum (sell) price.

4.4.4 Stage 3: Order Matching

The exchange's matching engine tries to match your order against existing orders on the opposite side. There are two main scenarios:

  • Immediate match: Your order matches against a resting order in the book. The trade executes, and both parties acquire their positions.
  • No match: Your order rests in the order book, waiting for someone to trade against it. It stays there until filled, cancelled, or expired.

Partial matches are also possible: your order for 100 contracts might match against 40 contracts immediately, with the remaining 60 resting in the book.

4.4.5 Stage 4: Position Held

Once matched, you hold a position. During this phase:

  • You can monitor the market price and your unrealized P&L.
  • You can place additional orders to increase or decrease your position.
  • You can exit your position entirely by selling (if you are long) or buying back (if you are short).

4.4.6 Stage 5: Event Resolution

At the resolution date (or when the resolution condition is met), the platform determines the outcome:

  1. The resolution source (e.g., an official government report, a news event, a measurable outcome) is consulted.
  2. The outcome is matched against the resolution criteria in the contract specification.
  3. The contract is marked as resolved.

4.4.7 Stage 6: Settlement

After resolution, the platform distributes payouts:

  • Holders of winning contracts receive $1 per contract.
  • Holders of losing contracts receive $0.
  • Net P&L is calculated and credited or debited.
  • Funds become available for withdrawal.

4.4.8 Python: Trade Lifecycle Simulation

from dataclasses import dataclass, field
from enum import Enum
from typing import List, Optional
from datetime import datetime
import uuid


class OrderSide(Enum):
    BUY = "buy"
    SELL = "sell"


class OrderStatus(Enum):
    PENDING = "pending"
    FILLED = "filled"
    PARTIALLY_FILLED = "partially_filled"
    CANCELLED = "cancelled"


class TradePhase(Enum):
    RESEARCH = "research"
    ORDER_PLACED = "order_placed"
    ORDER_MATCHED = "order_matched"
    POSITION_HELD = "position_held"
    RESOLVED = "resolved"
    SETTLED = "settled"


@dataclass
class Order:
    """Represents an order in the prediction market."""
    order_id: str
    contract_question: str
    side: OrderSide        # Buy or sell
    outcome: str           # "Yes", "No", or outcome name
    quantity: int
    price: float           # Limit price
    status: OrderStatus = OrderStatus.PENDING
    filled_quantity: int = 0
    fill_price: float = 0.0
    timestamp: datetime = field(default_factory=datetime.now)


@dataclass
class Position:
    """Represents a held position in a contract."""
    contract_question: str
    outcome: str
    quantity: int
    average_cost: float
    current_price: float = 0.0

    @property
    def unrealized_pnl(self) -> float:
        return (self.current_price - self.average_cost) * self.quantity

    @property
    def total_cost(self) -> float:
        return self.average_cost * self.quantity


@dataclass
class Settlement:
    """Represents the settlement of a resolved contract."""
    contract_question: str
    outcome: str
    quantity: int
    average_cost: float
    payout_per_contract: float
    total_payout: float
    total_cost: float
    net_pnl: float


class TradeLifecycleSimulator:
    """Simulates the complete trade lifecycle."""

    def __init__(self):
        self.phase = TradePhase.RESEARCH
        self.orders: List[Order] = []
        self.positions: List[Position] = []
        self.settlements: List[Settlement] = []
        self.log: List[str] = []

    def _log(self, message: str) -> None:
        timestamp = datetime.now().strftime("%H:%M:%S")
        entry = f"[{timestamp}] [{self.phase.value.upper()}] {message}"
        self.log.append(entry)
        print(entry)

    def research(self, contract_question: str, your_probability: float,
                 market_price: float) -> float:
        """Stage 1: Research and identify edge."""
        self.phase = TradePhase.RESEARCH
        edge = your_probability - market_price
        self._log(f"Analyzing: {contract_question}")
        self._log(f"Your estimate: {your_probability:.0%}, "
                  f"Market price: ${market_price:.2f}")
        self._log(f"Perceived edge: {edge:+.0%}")
        if edge > 0:
            self._log("Edge is positive -> consider buying Yes")
        elif edge < 0:
            self._log("Edge is negative -> consider buying No")
        else:
            self._log("No edge detected -> no trade")
        return edge

    def place_order(self, contract_question: str, outcome: str,
                    side: OrderSide, quantity: int, price: float) -> Order:
        """Stage 2: Place an order."""
        self.phase = TradePhase.ORDER_PLACED
        order = Order(
            order_id=str(uuid.uuid4())[:8],
            contract_question=contract_question,
            side=side,
            outcome=outcome,
            quantity=quantity,
            price=price,
        )
        self.orders.append(order)
        self._log(f"Order placed: {side.value.upper()} {quantity}x "
                  f"{outcome} @ ${price:.2f} (ID: {order.order_id})")
        return order

    def match_order(self, order: Order,
                    fill_price: Optional[float] = None) -> None:
        """Stage 3: Simulate order matching."""
        self.phase = TradePhase.ORDER_MATCHED
        actual_price = fill_price or order.price
        order.status = OrderStatus.FILLED
        order.filled_quantity = order.quantity
        order.fill_price = actual_price
        self._log(f"Order {order.order_id} FILLED: {order.quantity}x "
                  f"{order.outcome} @ ${actual_price:.2f}")

        # Create or update position
        position = Position(
            contract_question=order.contract_question,
            outcome=order.outcome,
            quantity=order.quantity,
            average_cost=actual_price,
            current_price=actual_price,
        )
        self.positions.append(position)
        self.phase = TradePhase.POSITION_HELD
        self._log(f"Position opened: {position.quantity}x {position.outcome} "
                  f"@ ${position.average_cost:.2f}")

    def update_price(self, outcome: str, new_price: float) -> None:
        """Update market price and show unrealized P&L."""
        for pos in self.positions:
            if pos.outcome == outcome:
                pos.current_price = new_price
                self._log(f"Price update: {outcome} now ${new_price:.2f}, "
                          f"Unrealized P&L: ${pos.unrealized_pnl:+.2f}")

    def resolve_and_settle(self, winning_outcome: str) -> List[Settlement]:
        """Stages 5-6: Resolution and settlement."""
        self.phase = TradePhase.RESOLVED
        self._log(f"Contract resolved: winner is '{winning_outcome}'")

        self.phase = TradePhase.SETTLED
        settlements = []
        for pos in self.positions:
            payout = 1.0 if pos.outcome == winning_outcome else 0.0
            total_payout = payout * pos.quantity
            total_cost = pos.average_cost * pos.quantity
            net = total_payout - total_cost

            settlement = Settlement(
                contract_question=pos.contract_question,
                outcome=pos.outcome,
                quantity=pos.quantity,
                average_cost=pos.average_cost,
                payout_per_contract=payout,
                total_payout=total_payout,
                total_cost=total_cost,
                net_pnl=net,
            )
            settlements.append(settlement)
            self.settlements.append(settlement)
            self._log(f"Settlement: {pos.quantity}x {pos.outcome} -> "
                      f"payout ${total_payout:.2f}, cost ${total_cost:.2f}, "
                      f"net P&L ${net:+.2f}")

        return settlements


# Run the full lifecycle
if __name__ == "__main__":
    sim = TradeLifecycleSimulator()

    contract_q = "Will the Fed raise rates in March 2026?"
    print("=" * 60)
    print("TRADE LIFECYCLE SIMULATION")
    print("=" * 60)

    # Stage 1: Research
    edge = sim.research(contract_q, your_probability=0.55, market_price=0.42)
    print()

    # Stage 2: Place order
    order = sim.place_order(contract_q, "Yes", OrderSide.BUY, 100, 0.42)
    print()

    # Stage 3: Match
    sim.match_order(order, fill_price=0.43)
    print()

    # Stage 4: Position held, price moves
    sim.update_price("Yes", 0.50)
    sim.update_price("Yes", 0.55)
    sim.update_price("Yes", 0.48)
    print()

    # Stages 5-6: Resolution and settlement
    settlements = sim.resolve_and_settle("Yes")
    print()

    print("=" * 60)
    print("FINAL SUMMARY")
    for s in settlements:
        print(f"  {s.quantity}x {s.outcome}: Net P&L = ${s.net_pnl:+.2f}")

4.5 Order Types and Execution

4.5.1 Market Orders

A market order is the simplest order type: buy or sell at whatever price is currently available in the order book. Market orders prioritize speed of execution over price.

Advantages: - Guaranteed execution (if there is liquidity). - Simple to use.

Disadvantages: - No price control — you may pay more (or sell for less) than expected. - In thin markets, you may experience significant slippage (the difference between the expected price and the actual fill price).

4.5.2 Limit Orders

A limit order specifies the maximum price you are willing to pay (for buys) or the minimum price you are willing to accept (for sells).

  • **Buy limit at $0.42**: You will buy contracts only at $0.42 or less.
  • **Sell limit at $0.60**: You will sell contracts only at $0.60 or more.

If the market does not reach your price, the order sits in the order book unfilled. Limit orders give you price control but do not guarantee execution.

4.5.3 Stop Orders

A stop order (or stop-loss order) becomes active only when the market price reaches a specified trigger price. It is used to limit losses or protect profits.

  • **Stop-loss sell at $0.35**: If you bought Yes at $0.50, a stop at $0.35 automatically sells your position if the price drops to $0.35, limiting your loss.

Note: Not all prediction market platforms support stop orders. Many traders simulate stop orders by monitoring prices programmatically and placing market or limit orders when triggered.

4.5.4 Time-in-Force Modifiers

Orders can have time-in-force instructions that determine how long they remain active:

Modifier Meaning
Good-Til-Cancelled (GTC) Order stays open until filled or manually cancelled.
Day Order Order expires at the end of the trading day.
Fill-or-Kill (FOK) Order must be filled entirely and immediately, or it is cancelled.
Immediate-or-Cancel (IOC) Fill as much as possible immediately; cancel any unfilled portion.

4.5.5 Partial Fills

When your order is larger than the available liquidity at your price, you get a partial fill. For example, you place a buy limit for 100 contracts at $0.42, but only 60 are available. You receive 60 contracts, and the remaining order for 40 contracts stays in the book (for GTC orders) or is cancelled (for FOK orders).

4.5.6 The Order Book

The order book is the record of all outstanding buy and sell orders for a contract. It is organized by price level:

--- YES Order Book ---
SELL (Asks):
  $0.48  x  50 contracts
  $0.47  x  30 contracts
  $0.46  x  80 contracts    <-- Best ask (lowest sell price)

BUY (Bids):
  $0.43  x  100 contracts   <-- Best bid (highest buy price)
  $0.42  x  200 contracts
  $0.40  x  150 contracts

Spread: $0.46 - $0.43 = $0.03

The bid-ask spread is the difference between the best bid and the best ask. It represents the cost of immediacy — a market buy would fill at $0.46, while a market sell would fill at $0.43.

4.5.7 Python: Order Types and Execution

from dataclasses import dataclass, field
from enum import Enum
from typing import List, Optional, Tuple
from datetime import datetime
import uuid


class OrderType(Enum):
    MARKET = "market"
    LIMIT = "limit"
    STOP = "stop"
    STOP_LIMIT = "stop_limit"


class TimeInForce(Enum):
    GTC = "good_til_cancelled"
    DAY = "day"
    FOK = "fill_or_kill"
    IOC = "immediate_or_cancel"


class Side(Enum):
    BUY = "buy"
    SELL = "sell"


@dataclass
class OrderBookEntry:
    """A single price level in the order book."""
    price: float
    quantity: int
    order_id: str = field(default_factory=lambda: str(uuid.uuid4())[:8])


@dataclass
class ExecutionReport:
    """Report of an order execution."""
    order_id: str
    fill_price: float
    fill_quantity: int
    remaining_quantity: int
    status: str  # "filled", "partially_filled", "cancelled", "resting"


class OrderBook:
    """Simple order book for a binary contract's Yes side."""

    def __init__(self):
        self.bids: List[OrderBookEntry] = []  # Buy orders, sorted high to low
        self.asks: List[OrderBookEntry] = []  # Sell orders, sorted low to high

    def add_bid(self, price: float, quantity: int) -> str:
        entry = OrderBookEntry(price=price, quantity=quantity)
        self.bids.append(entry)
        self.bids.sort(key=lambda x: -x.price)
        return entry.order_id

    def add_ask(self, price: float, quantity: int) -> str:
        entry = OrderBookEntry(price=price, quantity=quantity)
        self.asks.append(entry)
        self.asks.sort(key=lambda x: x.price)
        return entry.order_id

    @property
    def best_bid(self) -> Optional[float]:
        return self.bids[0].price if self.bids else None

    @property
    def best_ask(self) -> Optional[float]:
        return self.asks[0].price if self.asks else None

    @property
    def spread(self) -> Optional[float]:
        if self.best_bid is not None and self.best_ask is not None:
            return self.best_ask - self.best_bid
        return None

    @property
    def midpoint(self) -> Optional[float]:
        if self.best_bid is not None and self.best_ask is not None:
            return (self.best_bid + self.best_ask) / 2
        return None

    def execute_market_buy(self, quantity: int) -> List[ExecutionReport]:
        """Execute a market buy order against resting asks."""
        reports = []
        remaining = quantity

        while remaining > 0 and self.asks:
            best = self.asks[0]
            fill_qty = min(remaining, best.quantity)
            best.quantity -= fill_qty
            remaining -= fill_qty

            status = "filled" if remaining == 0 else "partially_filled"
            reports.append(ExecutionReport(
                order_id=best.order_id,
                fill_price=best.price,
                fill_quantity=fill_qty,
                remaining_quantity=remaining,
                status=status,
            ))

            if best.quantity == 0:
                self.asks.pop(0)

        return reports

    def execute_market_sell(self, quantity: int) -> List[ExecutionReport]:
        """Execute a market sell order against resting bids."""
        reports = []
        remaining = quantity

        while remaining > 0 and self.bids:
            best = self.bids[0]
            fill_qty = min(remaining, best.quantity)
            best.quantity -= fill_qty
            remaining -= fill_qty

            status = "filled" if remaining == 0 else "partially_filled"
            reports.append(ExecutionReport(
                order_id=best.order_id,
                fill_price=best.price,
                fill_quantity=fill_qty,
                remaining_quantity=remaining,
                status=status,
            ))

            if best.quantity == 0:
                self.bids.pop(0)

        return reports

    def execute_limit_buy(self, price: float, quantity: int,
                          tif: TimeInForce = TimeInForce.GTC
                          ) -> Tuple[List[ExecutionReport], Optional[str]]:
        """Execute a limit buy: match against asks <= price, then rest."""
        reports = []
        remaining = quantity

        while remaining > 0 and self.asks and self.asks[0].price <= price:
            best = self.asks[0]
            fill_qty = min(remaining, best.quantity)
            best.quantity -= fill_qty
            remaining -= fill_qty

            status = "filled" if remaining == 0 else "partially_filled"
            reports.append(ExecutionReport(
                order_id=best.order_id,
                fill_price=best.price,
                fill_quantity=fill_qty,
                remaining_quantity=remaining,
                status=status,
            ))

            if best.quantity == 0:
                self.asks.pop(0)

        resting_id = None
        if remaining > 0:
            if tif == TimeInForce.GTC:
                resting_id = self.add_bid(price, remaining)
            elif tif == TimeInForce.FOK:
                # FOK: if not fully filled, cancel everything
                reports = []
                return reports, None
            # IOC: filled what we could, cancel rest

        return reports, resting_id

    def display(self) -> str:
        """Display the order book."""
        lines = ["--- Order Book ---", "ASKS (Sells):"]
        for ask in reversed(self.asks):
            lines.append(f"  ${ask.price:.2f}  x  {ask.quantity}")
        lines.append("  --- spread ---")
        lines.append("BIDS (Buys):")
        for bid in self.bids:
            lines.append(f"  ${bid.price:.2f}  x  {bid.quantity}")
        if self.spread is not None:
            lines.append(f"\nSpread: ${self.spread:.2f} | "
                        f"Midpoint: ${self.midpoint:.2f}")
        return "\n".join(lines)


# Demonstration
if __name__ == "__main__":
    book = OrderBook()

    # Seed the book with resting orders
    book.add_ask(0.48, 50)
    book.add_ask(0.47, 30)
    book.add_ask(0.46, 80)
    book.add_bid(0.43, 100)
    book.add_bid(0.42, 200)
    book.add_bid(0.40, 150)

    print(book.display())
    print()

    # Market buy 100 contracts
    print("=== Market Buy 100 contracts ===")
    reports = book.execute_market_buy(100)
    total_cost = sum(r.fill_price * r.fill_quantity for r in reports)
    total_qty = sum(r.fill_quantity for r in reports)
    avg_price = total_cost / total_qty if total_qty > 0 else 0
    for r in reports:
        print(f"  Filled {r.fill_quantity} @ ${r.fill_price:.2f}")
    print(f"  Average fill: ${avg_price:.4f} for {total_qty} contracts")
    print(f"  Total cost: ${total_cost:.2f}")
    print()

    print(book.display())
    print()

    # Limit buy 50 contracts at $0.45
    print("=== Limit Buy 50 @ $0.45 ===")
    reports, resting = book.execute_limit_buy(0.45, 50)
    for r in reports:
        print(f"  Filled {r.fill_quantity} @ ${r.fill_price:.2f}")
    if resting:
        print(f"  Remaining 50 resting in book at $0.45 (ID: {resting})")
    else:
        total_filled = sum(r.fill_quantity for r in reports)
        if total_filled < 50:
            print(f"  Only {total_filled} filled, no resting order")
    print()
    print(book.display())

4.6 Position Management

4.6.1 Tracking Positions

A position is your net holding in a particular contract. Key attributes include:

  • Quantity: How many contracts you hold (positive = long, negative = short).
  • Average cost basis: The weighted average price at which you accumulated your position.
  • Current market price: What the contract is trading at now.
  • Side: Which outcome you hold (Yes, No, or a specific named outcome).

4.6.2 Cost Basis Calculation

When you accumulate a position over multiple trades at different prices, you need to calculate your average cost basis:

$$\text{Avg Cost} = \frac{\sum_{i} p_i \times q_i}{\sum_{i} q_i}$$

Example: - Buy 50 contracts at $0.42 - Buy 30 contracts at $0.45 - Buy 20 contracts at $0.48

$$\text{Avg Cost} = \frac{50 \times 0.42 + 30 \times 0.45 + 20 \times 0.48}{50 + 30 + 20} = \frac{21.00 + 13.50 + 9.60}{100} = \frac{44.10}{100} = 0.441$$

Your average cost is $0.441 per contract.

4.6.3 Unrealized vs. Realized P&L

  • Unrealized P&L (also called paper profit/loss or mark-to-market P&L): The profit or loss you would have if you closed your position at the current market price.

$$\text{Unrealized P\&L} = (\text{Current Price} - \text{Avg Cost}) \times \text{Quantity}$$

  • Realized P&L: The actual profit or loss from trades you have closed.

$$\text{Realized P\&L} = (\text{Sell Price} - \text{Avg Cost}) \times \text{Quantity Sold}$$

For the example above (100 contracts at avg cost $0.441): - If current price is $0.55: Unrealized P&L = ($0.55 - $0.441) x 100 = +$10.90 - If you sell 40 at $0.55: Realized P&L = ($0.55 - $0.441) x 40 = +$4.36

4.6.4 Margin Requirements

Some prediction market platforms require you to post margin — capital held as collateral to cover potential losses.

For binary contracts, the margin requirement is typically:

  • Long position: You pay the full purchase price upfront. Margin = purchase price (since max loss = purchase price).
  • Short position: You must post the maximum possible loss. For shorting Yes at $0.60, max loss is $1.00 - $0.60 = $0.40, so margin = $0.40.

In either case, the combined margin for a matched Yes/No pair is always $1.00:

$$\text{Margin}_{\text{Yes buyer}} + \text{Margin}_{\text{No buyer}} = p_{\text{Yes}} + (1 - p_{\text{Yes}}) = 1.00$$

This is why prediction markets are sometimes described as fully collateralized — the total capital posted by both sides always equals the maximum payout.

4.6.5 Position Sizing Basics

How much of your bankroll should you risk on a single prediction? We will cover this in depth in later chapters (especially the Kelly Criterion in the strategy section), but the basic principle is:

$$\text{Position Size} = f \times \text{Bankroll}$$

where $f$ is a fraction that depends on your edge and the odds. A simple rule of thumb for beginners: never risk more than 5% of your bankroll on a single contract.

4.6.6 Python: Portfolio Class

from dataclasses import dataclass, field
from typing import Dict, List, Optional
from datetime import datetime


@dataclass
class Trade:
    """Record of a single trade."""
    timestamp: datetime
    contract: str
    outcome: str
    side: str       # "buy" or "sell"
    quantity: int
    price: float

    @property
    def notional(self) -> float:
        return self.price * self.quantity


@dataclass
class PositionInfo:
    """Detailed position information."""
    contract: str
    outcome: str
    quantity: int
    average_cost: float
    current_price: float

    @property
    def market_value(self) -> float:
        return self.current_price * self.quantity

    @property
    def total_cost(self) -> float:
        return self.average_cost * self.quantity

    @property
    def unrealized_pnl(self) -> float:
        return (self.current_price - self.average_cost) * self.quantity

    @property
    def unrealized_pnl_percent(self) -> float:
        if self.total_cost == 0:
            return 0.0
        return (self.unrealized_pnl / self.total_cost) * 100

    @property
    def max_profit(self) -> float:
        """Maximum profit if contract resolves in your favor."""
        return (1.0 - self.average_cost) * self.quantity

    @property
    def max_loss(self) -> float:
        """Maximum loss if contract resolves against you."""
        return self.average_cost * self.quantity


class Portfolio:
    """Manages a portfolio of prediction market positions."""

    def __init__(self, initial_balance: float):
        self.cash_balance: float = initial_balance
        self.initial_balance: float = initial_balance
        self.positions: Dict[str, PositionInfo] = {}  # key = "contract|outcome"
        self.trade_history: List[Trade] = []
        self.realized_pnl: float = 0.0

    def _position_key(self, contract: str, outcome: str) -> str:
        return f"{contract}|{outcome}"

    def buy(self, contract: str, outcome: str, quantity: int,
            price: float) -> str:
        """Buy contracts (open or add to a long position).

        Returns:
            Status message.
        """
        cost = price * quantity
        if cost > self.cash_balance:
            return (f"Insufficient funds: need ${cost:.2f}, "
                    f"have ${self.cash_balance:.2f}")

        self.cash_balance -= cost
        key = self._position_key(contract, outcome)

        trade = Trade(
            timestamp=datetime.now(),
            contract=contract,
            outcome=outcome,
            side="buy",
            quantity=quantity,
            price=price,
        )
        self.trade_history.append(trade)

        if key in self.positions:
            pos = self.positions[key]
            total_qty = pos.quantity + quantity
            pos.average_cost = ((pos.average_cost * pos.quantity +
                                 price * quantity) / total_qty)
            pos.quantity = total_qty
        else:
            self.positions[key] = PositionInfo(
                contract=contract,
                outcome=outcome,
                quantity=quantity,
                average_cost=price,
                current_price=price,
            )

        return (f"Bought {quantity}x {outcome} @ ${price:.2f} "
                f"(cost: ${cost:.2f})")

    def sell(self, contract: str, outcome: str, quantity: int,
             price: float) -> str:
        """Sell contracts (reduce or close a long position).

        Returns:
            Status message.
        """
        key = self._position_key(contract, outcome)
        if key not in self.positions:
            return f"No position in {outcome} for '{contract}'"

        pos = self.positions[key]
        if quantity > pos.quantity:
            return (f"Cannot sell {quantity}, only hold {pos.quantity}")

        proceeds = price * quantity
        pnl = (price - pos.average_cost) * quantity
        self.realized_pnl += pnl
        self.cash_balance += proceeds

        trade = Trade(
            timestamp=datetime.now(),
            contract=contract,
            outcome=outcome,
            side="sell",
            quantity=quantity,
            price=price,
        )
        self.trade_history.append(trade)

        pos.quantity -= quantity
        if pos.quantity == 0:
            del self.positions[key]

        return (f"Sold {quantity}x {outcome} @ ${price:.2f} "
                f"(proceeds: ${proceeds:.2f}, P&L: ${pnl:+.2f})")

    def settle(self, contract: str, winning_outcome: str) -> str:
        """Settle all positions in a resolved contract.

        Returns:
            Summary of settlement.
        """
        messages = []
        keys_to_remove = []

        for key, pos in self.positions.items():
            if pos.contract == contract:
                payout = 1.0 if pos.outcome == winning_outcome else 0.0
                total_payout = payout * pos.quantity
                pnl = total_payout - pos.total_cost

                self.cash_balance += total_payout
                self.realized_pnl += pnl
                keys_to_remove.append(key)

                messages.append(
                    f"  {pos.quantity}x {pos.outcome}: "
                    f"payout ${total_payout:.2f}, P&L ${pnl:+.2f}"
                )

        for key in keys_to_remove:
            del self.positions[key]

        header = f"Settlement for '{contract}' (winner: {winning_outcome}):"
        return "\n".join([header] + messages)

    def update_price(self, contract: str, outcome: str,
                     new_price: float) -> None:
        """Update the current market price for a position."""
        key = self._position_key(contract, outcome)
        if key in self.positions:
            self.positions[key].current_price = new_price

    @property
    def total_unrealized_pnl(self) -> float:
        return sum(pos.unrealized_pnl for pos in self.positions.values())

    @property
    def total_market_value(self) -> float:
        return sum(pos.market_value for pos in self.positions.values())

    @property
    def total_equity(self) -> float:
        """Cash + market value of positions."""
        return self.cash_balance + self.total_market_value

    @property
    def total_return(self) -> float:
        """Total return as a percentage."""
        return ((self.total_equity - self.initial_balance)
                / self.initial_balance * 100)

    def summary(self) -> str:
        """Generate a portfolio summary."""
        lines = [
            "=" * 50,
            "PORTFOLIO SUMMARY",
            "=" * 50,
            f"Cash balance:      ${self.cash_balance:.2f}",
            f"Position value:    ${self.total_market_value:.2f}",
            f"Total equity:      ${self.total_equity:.2f}",
            f"Realized P&L:      ${self.realized_pnl:+.2f}",
            f"Unrealized P&L:    ${self.total_unrealized_pnl:+.2f}",
            f"Total return:      {self.total_return:+.1f}%",
            "-" * 50,
            "POSITIONS:",
        ]

        if not self.positions:
            lines.append("  (no open positions)")
        else:
            for pos in self.positions.values():
                lines.append(
                    f"  {pos.quantity}x {pos.outcome} "
                    f"(avg: ${pos.average_cost:.3f}, "
                    f"mkt: ${pos.current_price:.2f}, "
                    f"P&L: ${pos.unrealized_pnl:+.2f})"
                )
                lines.append(
                    f"    Max profit: ${pos.max_profit:.2f} | "
                    f"Max loss: ${pos.max_loss:.2f}"
                )

        lines.append("=" * 50)
        return "\n".join(lines)


# Demonstration
if __name__ == "__main__":
    portfolio = Portfolio(initial_balance=1000.00)

    # Make some trades
    print(portfolio.buy("Fed rate hike", "Yes", 100, 0.42))
    print(portfolio.buy("Fed rate hike", "Yes", 50, 0.45))
    print(portfolio.buy("Election winner", "Alice", 200, 0.38))
    print()

    # Update prices
    portfolio.update_price("Fed rate hike", "Yes", 0.55)
    portfolio.update_price("Election winner", "Alice", 0.42)

    print(portfolio.summary())
    print()

    # Sell some contracts
    print(portfolio.sell("Fed rate hike", "Yes", 50, 0.55))
    print()

    # Settle the election
    print(portfolio.settle("Election winner", "Alice"))
    print()

    print(portfolio.summary())

4.7 Resolution and Settlement

4.7.1 Resolution Criteria

The resolution criteria are the precise rules that determine the outcome of a contract. Clear, unambiguous resolution criteria are the foundation of a well-functioning prediction market. They must specify:

  1. What is being measured or observed.
  2. When the measurement is taken.
  3. Who or what is the authoritative source.
  4. How edge cases are handled.

Example of good resolution criteria:

"This contract resolves Yes if the U.S. Bureau of Labor Statistics (BLS) reports a seasonally adjusted Consumer Price Index (CPI) year-over-year change of 3.0% or higher for March 2026, as published in the CPI Summary report. The first release (not any subsequent revisions) will be used."

Example of poor resolution criteria:

"This contract resolves Yes if inflation is high."

The second example is ambiguous: What measure of inflation? What counts as "high"? Over what time period?

4.7.2 Resolution Sources

Common resolution sources include:

Source Type Examples Reliability
Government agencies BLS, BEA, Census Bureau Very high
Official election results Secretary of State, Electoral Commission Very high
Sporting bodies FIFA, IOC, league officials High
News organizations AP, Reuters (for event confirmation) High
Financial data providers Bloomberg, FRED, exchange data High
Platform-specific oracles UMA, Chainlink (for crypto markets) Medium-high
Social media / self-report Twitter announcements Medium

4.7.3 Disputed Resolutions

Sometimes the resolution is contested. Common reasons for disputes:

  1. Ambiguous criteria: The contract language does not clearly cover the actual outcome.
  2. Source disagreement: Different sources report different results.
  3. Timing issues: The resolution source publishes data at an unexpected time or revises it.
  4. Unforeseen circumstances: The event is cancelled, postponed, or fundamentally altered.

Platforms handle disputes through various mechanisms: - Admin resolution: Platform staff make the final call. - Community vote: Token holders or designated resolvers vote on the outcome. - Escalation process: Multi-stage process with increasing scrutiny (used by Augur, Polymarket's UMA oracle). - Arbitration: External arbitrators make a binding decision.

4.7.4 Special Resolution Types

Early resolution: Some contracts can resolve before the stated resolution date if the outcome becomes certain. For example, if a candidate drops out of a two-person race, the contract on the other candidate winning can resolve Yes immediately.

N/A (void) resolution: If the event becomes meaningless or impossible to adjudicate, the contract may be voided. All contracts are refunded at their purchase price. Examples: - The event is cancelled entirely. - The resolution source ceases to exist. - Force majeure events make resolution impossible.

4.7.5 Settlement Mechanics

Settlement is the process of distributing payouts after resolution:

  1. Calculation: For each position, calculate the payout based on the resolution.
  2. Netting: Offset winning and losing positions within the same account.
  3. Distribution: Credit winning accounts, debit losing accounts.
  4. Availability: Funds become available for withdrawal (some platforms have a delay).

In most prediction markets, settlement is automatic and nearly instantaneous after resolution. On blockchain-based platforms, settlement may require an on-chain transaction and gas fees.

4.7.6 Python: Resolution Handler

from dataclasses import dataclass, field
from enum import Enum
from typing import Dict, List, Optional, Callable
from datetime import datetime


class ResolutionType(Enum):
    STANDARD = "standard"       # Normal resolution at expiry
    EARLY = "early"             # Resolved before expiry date
    VOIDED = "voided"           # Contract voided, refunds issued
    DISPUTED = "disputed"       # Resolution under dispute


class ResolutionStatus(Enum):
    PENDING = "pending"
    PROPOSED = "proposed"
    CHALLENGED = "challenged"
    FINALIZED = "finalized"


@dataclass
class ResolutionSource:
    """Defines the authoritative source for resolution."""
    name: str
    url: Optional[str] = None
    description: str = ""
    reliability: str = "high"  # "very_high", "high", "medium", "low"


@dataclass
class ResolutionProposal:
    """A proposed resolution for a contract."""
    proposed_outcome: str
    proposed_by: str
    timestamp: datetime
    evidence: str
    source: ResolutionSource
    status: ResolutionStatus = ResolutionStatus.PROPOSED
    challenges: List[str] = field(default_factory=list)


@dataclass
class SettlementRecord:
    """Record of a single settlement."""
    account_id: str
    contract_id: str
    outcome_held: str
    quantity: int
    purchase_price: float
    payout_per_contract: float
    total_payout: float
    total_cost: float
    net_pnl: float
    settlement_type: str  # "win", "loss", "refund"


class ResolutionHandler:
    """Handles the resolution and settlement process for contracts."""

    def __init__(self):
        self.proposals: Dict[str, ResolutionProposal] = {}
        self.settlements: Dict[str, List[SettlementRecord]] = {}
        self.challenge_period_hours: int = 24
        self.log: List[str] = []

    def _log(self, msg: str) -> None:
        entry = f"[{datetime.now().strftime('%Y-%m-%d %H:%M')}] {msg}"
        self.log.append(entry)
        print(entry)

    def propose_resolution(self, contract_id: str, outcome: str,
                           proposer: str, evidence: str,
                           source: ResolutionSource) -> ResolutionProposal:
        """Propose a resolution for a contract.

        Starts the challenge period during which the resolution can be disputed.
        """
        proposal = ResolutionProposal(
            proposed_outcome=outcome,
            proposed_by=proposer,
            timestamp=datetime.now(),
            evidence=evidence,
            source=source,
        )
        self.proposals[contract_id] = proposal
        self._log(f"Resolution proposed for {contract_id}: "
                  f"'{outcome}' by {proposer}")
        self._log(f"  Evidence: {evidence}")
        self._log(f"  Source: {source.name} ({source.reliability})")
        self._log(f"  Challenge period: {self.challenge_period_hours} hours")
        return proposal

    def challenge_resolution(self, contract_id: str, challenger: str,
                             reason: str) -> bool:
        """Challenge a proposed resolution.

        Returns True if the challenge is accepted for review.
        """
        if contract_id not in self.proposals:
            self._log(f"No proposal to challenge for {contract_id}")
            return False

        proposal = self.proposals[contract_id]
        if proposal.status == ResolutionStatus.FINALIZED:
            self._log(f"Cannot challenge finalized resolution for {contract_id}")
            return False

        proposal.status = ResolutionStatus.CHALLENGED
        proposal.challenges.append(f"{challenger}: {reason}")
        self._log(f"Resolution for {contract_id} CHALLENGED by {challenger}")
        self._log(f"  Reason: {reason}")
        return True

    def finalize_resolution(self, contract_id: str) -> Optional[str]:
        """Finalize a proposed resolution (after challenge period).

        Returns the winning outcome, or None if resolution cannot be finalized.
        """
        if contract_id not in self.proposals:
            self._log(f"No proposal to finalize for {contract_id}")
            return None

        proposal = self.proposals[contract_id]
        if proposal.status == ResolutionStatus.CHALLENGED:
            self._log(f"Cannot finalize {contract_id}: under challenge")
            return None

        proposal.status = ResolutionStatus.FINALIZED
        self._log(f"Resolution FINALIZED for {contract_id}: "
                  f"'{proposal.proposed_outcome}'")
        return proposal.proposed_outcome

    def void_contract(self, contract_id: str, reason: str) -> None:
        """Void a contract (N/A resolution)."""
        proposal = ResolutionProposal(
            proposed_outcome="VOID",
            proposed_by="SYSTEM",
            timestamp=datetime.now(),
            evidence=reason,
            source=ResolutionSource(name="System", reliability="very_high"),
            status=ResolutionStatus.FINALIZED,
        )
        self.proposals[contract_id] = proposal
        self._log(f"Contract {contract_id} VOIDED: {reason}")

    def settle_positions(self, contract_id: str,
                         positions: List[Dict],
                         winning_outcome: Optional[str] = None
                         ) -> List[SettlementRecord]:
        """Settle all positions for a resolved contract.

        Args:
            contract_id: The contract identifier.
            positions: List of dicts with keys: account_id, outcome,
                       quantity, purchase_price.
            winning_outcome: The winning outcome (None for voided contracts).

        Returns:
            List of settlement records.
        """
        is_void = (self.proposals.get(contract_id) and
                   self.proposals[contract_id].proposed_outcome == "VOID")

        records = []
        for pos in positions:
            if is_void:
                # Refund: return purchase price
                payout = pos["purchase_price"]
                settlement_type = "refund"
            elif pos["outcome"] == winning_outcome:
                payout = 1.0
                settlement_type = "win"
            else:
                payout = 0.0
                settlement_type = "loss"

            total_payout = payout * pos["quantity"]
            total_cost = pos["purchase_price"] * pos["quantity"]
            net_pnl = total_payout - total_cost

            record = SettlementRecord(
                account_id=pos["account_id"],
                contract_id=contract_id,
                outcome_held=pos["outcome"],
                quantity=pos["quantity"],
                purchase_price=pos["purchase_price"],
                payout_per_contract=payout,
                total_payout=total_payout,
                total_cost=total_cost,
                net_pnl=net_pnl,
                settlement_type=settlement_type,
            )
            records.append(record)
            self._log(f"  Settled: {pos['account_id']} "
                      f"{pos['quantity']}x {pos['outcome']} -> "
                      f"${total_payout:.2f} ({settlement_type}, "
                      f"P&L: ${net_pnl:+.2f})")

        self.settlements[contract_id] = records
        return records


# Demonstration
if __name__ == "__main__":
    handler = ResolutionHandler()

    # Normal resolution flow
    contract_id = "fed-rate-hike-march-2026"
    source = ResolutionSource(
        name="Federal Reserve Press Release",
        url="https://www.federalreserve.gov/newsevents.htm",
        description="Official FOMC statement",
        reliability="very_high",
    )

    print("=== Normal Resolution ===")
    handler.propose_resolution(
        contract_id, "Yes", "oracle_bot",
        "FOMC raised rates by 25bps per official statement", source
    )
    print()

    result = handler.finalize_resolution(contract_id)
    print()

    # Settle positions
    positions = [
        {"account_id": "alice", "outcome": "Yes",
         "quantity": 100, "purchase_price": 0.42},
        {"account_id": "bob", "outcome": "No",
         "quantity": 150, "purchase_price": 0.55},
        {"account_id": "carol", "outcome": "Yes",
         "quantity": 50, "purchase_price": 0.65},
    ]

    records = handler.settle_positions(contract_id, positions, result)
    print()

    # Disputed resolution
    print("=== Disputed Resolution ===")
    contract_id2 = "gdp-growth-q3-2026"
    handler.propose_resolution(
        contract_id2, "2% to 3%", "oracle_bot",
        "BEA advance estimate shows 2.8% growth",
        ResolutionSource(name="BEA", reliability="very_high"),
    )
    handler.challenge_resolution(
        contract_id2, "trader_dave",
        "BEA revised estimate to 1.9% two days later"
    )
    # Cannot finalize while challenged
    result2 = handler.finalize_resolution(contract_id2)
    print()

    # Voided contract
    print("=== Voided Contract ===")
    contract_id3 = "cancelled-event-2026"
    handler.void_contract(contract_id3, "Event was cancelled due to force majeure")
    positions3 = [
        {"account_id": "eve", "outcome": "Yes",
         "quantity": 200, "purchase_price": 0.50},
    ]
    handler.settle_positions(contract_id3, positions3)

4.8 Payoff Diagrams and Risk Profiles

4.8.1 Why Payoff Diagrams Matter

A payoff diagram visually represents the profit or loss of a position across all possible outcomes. In traditional options markets, payoff diagrams are continuous curves because the underlying asset price is continuous. In prediction markets, payoff diagrams are typically discrete (for binary and multi-outcome contracts) or piecewise linear (for scalar contracts).

Even though prediction market payoff diagrams are simpler than their options-market counterparts, they are still invaluable for:

  1. Understanding risk: Seeing the maximum profit and maximum loss at a glance.
  2. Comparing strategies: Overlaying different positions to see which is better under which outcomes.
  3. Portfolio analysis: Combining multiple positions to see the aggregate payoff.

4.8.2 Long Binary — Yes

When you buy Yes at price $p$:

Outcome Payoff P&L
Yes $1.00 | $1 - p$
No $0.00 | $-p$
  • Max profit: $1 - p$ (occurs when event happens)
  • Max loss: $p$ (occurs when event does not happen)
  • Break-even: Event must occur

4.8.3 Short Binary — Yes (or Long No)

When you sell Yes (or buy No) at price $p$:

Outcome Payoff P&L
Yes $0.00 | $-(1-p)$
No $1.00 | $p$

Wait — let us be more precise. If you sell Yes at price $p$, you receive $p$ and must pay $1$ if Yes wins:

Outcome Revenue Cost P&L
Yes $p$ $1$ $p - 1$
No $p$ $0$ $p$

Equivalently, buying No at $1-p$ gives the same payoff profile.

4.8.4 Multi-Outcome Portfolio

Consider a portfolio where you buy multiple outcomes in the same market:

  • 100 contracts of Alice at $0.42
  • 50 contracts of Bob at $0.28
Winning Outcome Alice Payout Bob Payout Total Revenue Total Cost Net P&L
Alice $100 | $0 $100 | $56 +$44
Bob $0 | $50 $50 | $56 -$6
Carol $0 | $0 $0 | $56 -$56
Dave $0 | $0 $0 | $56 -$56
Other $0 | $0 $0 | $56 -$56

This shows that the portfolio is profitable only if Alice wins, slightly unprofitable if Bob wins, and fully losing for any other outcome.

4.8.5 Scalar Contract Payoff Profiles

For a linear scalar contract (floor=0, ceiling=5, bought at $0.48):

The payoff is a line from (0%, $0) to (5%, $1), clamped at both ends:

P&L
+$0.52 |                              ___________  (capped)
        |                         /
+$0.00 |--------------------/------
        |               /
-$0.48 |__________/
        |
        +---+---+---+---+---+---+---> GDP Growth %
            0   1   2   3   4   5

The break-even point is where payoff equals purchase price:

$$\frac{x - \text{floor}}{\text{ceiling} - \text{floor}} = p \implies x = p \times (\text{ceiling} - \text{floor}) + \text{floor}$$

For $p = 0.48$: break-even = $0.48 \times 5 + 0 = 2.4\%$.

4.8.6 Python: Payoff Diagram Generator

"""
Payoff diagram generator for prediction market contracts.

Requires matplotlib: pip install matplotlib
"""
from typing import List, Dict, Tuple, Optional


def binary_payoff_data(purchase_price: float, is_long_yes: bool = True
                       ) -> Dict[str, float]:
    """Calculate binary contract payoff data.

    Returns dict with outcomes as keys and P&L as values.
    """
    if is_long_yes:
        return {
            "Yes": 1.0 - purchase_price,
            "No": -purchase_price,
        }
    else:
        return {
            "Yes": -(1.0 - purchase_price),
            "No": purchase_price,
        }


def multi_outcome_payoff_data(
    holdings: Dict[str, Tuple[int, float]],
    outcomes: List[str],
) -> Dict[str, float]:
    """Calculate multi-outcome portfolio P&L for each possible winner.

    Args:
        holdings: {outcome: (quantity, avg_cost)} for each held outcome.
        outcomes: All possible outcomes.

    Returns:
        {winner: net_pnl} for each possible winning outcome.
    """
    total_cost = sum(qty * cost for qty, cost in holdings.values())
    result = {}
    for winner in outcomes:
        revenue = 0.0
        if winner in holdings:
            qty, _ = holdings[winner]
            revenue = qty * 1.0
        result[winner] = revenue - total_cost
    return result


def scalar_payoff_data(
    floor: float, ceiling: float, purchase_price: float,
    is_long: bool = True, num_points: int = 200,
) -> Tuple[List[float], List[float]]:
    """Calculate linear scalar contract P&L across outcome values.

    Returns (values, pnl_list) for plotting.
    """
    margin = (ceiling - floor) * 0.2
    low = floor - margin
    high = ceiling + margin
    step = (high - low) / num_points

    values = []
    pnls = []
    for i in range(num_points + 1):
        v = low + i * step
        raw_payoff = (v - floor) / (ceiling - floor)
        payoff = max(0.0, min(1.0, raw_payoff))
        if is_long:
            pnl = payoff - purchase_price
        else:
            pnl = purchase_price - payoff
        values.append(v)
        pnls.append(pnl)

    return values, pnls


def print_payoff_table(title: str, data: Dict[str, float]) -> None:
    """Print a text-based payoff table."""
    print(f"\n{'=' * 40}")
    print(f"  {title}")
    print(f"{'=' * 40}")
    print(f"  {'Outcome':<15} {'P&L':>10}")
    print(f"  {'-' * 25}")
    for outcome, pnl in data.items():
        bar_len = int(abs(pnl) * 20)
        bar = ("+" * bar_len if pnl >= 0 else "-" * bar_len)
        print(f"  {outcome:<15} ${pnl:>+8.2f}  {bar}")
    print()


def plot_payoff_diagrams():
    """Generate matplotlib payoff diagrams.

    This function is separated so the module can be imported
    without requiring matplotlib.
    """
    try:
        import matplotlib.pyplot as plt
        import matplotlib.ticker as ticker
    except ImportError:
        print("matplotlib is required for graphical plots.")
        print("Install with: pip install matplotlib")
        return

    fig, axes = plt.subplots(2, 2, figsize=(14, 10))
    fig.suptitle("Prediction Market Payoff Diagrams", fontsize=16,
                 fontweight="bold")

    # 1. Long Yes at various prices
    ax = axes[0, 0]
    prices = [0.30, 0.50, 0.70]
    outcomes = ["No", "Yes"]
    x_pos = [0, 1]
    for p in prices:
        pnl = [-p, 1.0 - p]
        ax.plot(x_pos, pnl, "o-", markersize=10, linewidth=2,
                label=f"Buy Yes @ ${p:.2f}")
    ax.axhline(y=0, color="black", linewidth=0.5, linestyle="--")
    ax.set_xticks(x_pos)
    ax.set_xticklabels(outcomes)
    ax.set_ylabel("Profit / Loss ($)")
    ax.set_title("Long Yes at Different Prices")
    ax.legend()
    ax.grid(True, alpha=0.3)

    # 2. Long Yes vs Long No
    ax = axes[0, 1]
    p = 0.60
    long_yes = [-p, 1.0 - p]
    long_no = [p, -(1.0 - p)]
    ax.plot(x_pos, long_yes, "o-", markersize=10, linewidth=2,
            color="green", label=f"Long Yes @ ${p:.2f}")
    ax.plot(x_pos, long_no, "o-", markersize=10, linewidth=2,
            color="red", label=f"Long No @ ${1 - p:.2f}")
    ax.axhline(y=0, color="black", linewidth=0.5, linestyle="--")
    ax.set_xticks(x_pos)
    ax.set_xticklabels(outcomes)
    ax.set_ylabel("Profit / Loss ($)")
    ax.set_title("Long Yes vs. Long No (Mirror Image)")
    ax.legend()
    ax.grid(True, alpha=0.3)

    # 3. Multi-outcome portfolio
    ax = axes[1, 0]
    outcomes_multi = ["Alice", "Bob", "Carol", "Dave", "Other"]
    holdings = {"Alice": (100, 0.42), "Bob": (50, 0.28)}
    payoffs = multi_outcome_payoff_data(holdings, outcomes_multi)
    x_pos_multi = list(range(len(outcomes_multi)))
    pnl_values = [payoffs[o] for o in outcomes_multi]
    colors = ["green" if v >= 0 else "red" for v in pnl_values]
    ax.bar(x_pos_multi, pnl_values, color=colors, alpha=0.7, edgecolor="black")
    ax.axhline(y=0, color="black", linewidth=0.5, linestyle="--")
    ax.set_xticks(x_pos_multi)
    ax.set_xticklabels(outcomes_multi, rotation=45)
    ax.set_ylabel("Net P&L ($)")
    ax.set_title("Multi-Outcome Portfolio P&L")
    ax.grid(True, alpha=0.3, axis="y")

    # 4. Scalar contract
    ax = axes[1, 1]
    floor, ceiling = 0.0, 5.0
    for p, style in [(0.30, "--"), (0.50, "-"), (0.70, "-.")]:
        vals, pnls = scalar_payoff_data(floor, ceiling, p, is_long=True)
        ax.plot(vals, pnls, style, linewidth=2,
                label=f"Long @ ${p:.2f}")
    ax.axhline(y=0, color="black", linewidth=0.5, linestyle="--")
    ax.axvline(x=floor, color="gray", linewidth=0.5, linestyle=":")
    ax.axvline(x=ceiling, color="gray", linewidth=0.5, linestyle=":")
    ax.set_xlabel("Outcome Value (%)")
    ax.set_ylabel("Profit / Loss ($)")
    ax.set_title("Linear Scalar Contract P&L")
    ax.legend()
    ax.grid(True, alpha=0.3)

    plt.tight_layout()
    plt.savefig("payoff_diagrams.png", dpi=150, bbox_inches="tight")
    plt.show()
    print("Payoff diagrams saved to payoff_diagrams.png")


# Demonstration (text-based, no matplotlib needed)
if __name__ == "__main__":
    # Binary payoffs
    for is_long in [True, False]:
        side = "Long Yes" if is_long else "Short Yes (Long No)"
        data = binary_payoff_data(0.60, is_long)
        print_payoff_table(f"Binary: {side} @ $0.60", data)

    # Multi-outcome portfolio
    outcomes = ["Alice", "Bob", "Carol", "Dave", "Other"]
    holdings = {"Alice": (100, 0.42), "Bob": (50, 0.28)}
    data = multi_outcome_payoff_data(holdings, outcomes)
    print_payoff_table("Multi-Outcome Portfolio", data)

    # Scalar payoff (text summary)
    floor, ceiling, price = 0.0, 5.0, 0.48
    breakeven = price * (ceiling - floor) + floor
    print(f"Scalar contract: floor={floor}%, ceiling={ceiling}%, "
          f"price=${price:.2f}")
    print(f"Break-even outcome: {breakeven:.1f}%")
    print(f"Max profit (long): ${1.0 - price:.2f}")
    print(f"Max loss (long): ${price:.2f}")

    # Attempt graphical plots
    print("\nAttempting to generate graphical payoff diagrams...")
    plot_payoff_diagrams()

4.9 Fees and Friction

4.9.1 Types of Fees

Real prediction market platforms charge various fees that affect your profitability:

  1. Trading fees: Charged per trade, typically as a percentage of the notional value or as a fixed amount per contract. - Example: 2% of the trade value, or $0.02 per contract.

  2. Settlement fees (winner's fee): Charged only on winning positions at settlement. - Example: Polymarket historically charged no fees; Kalshi charges fees on winning trades.

  3. Withdrawal fees: Charged when moving funds off the platform. - Especially relevant for crypto-based platforms (gas fees).

  4. Platform rake: Some platforms take a percentage of every dollar that flows through the system.

4.9.2 Impact on Break-Even Probability

Fees shift the break-even point. Without fees, if you buy Yes at $p$, you need the event probability to be above $p$ to have positive expected value. With fees, the threshold increases.

Let $f_{\text{trade}}$ be the trading fee (as a fraction of trade value) and $f_{\text{win}}$ be the fee on winning payouts. The fee-adjusted expected value of buying Yes at price $p$ when you believe the true probability is $q$:

$$\text{EV} = q \times (1 - f_{\text{win}} - p \times (1 + f_{\text{trade}})) + (1 - q) \times (- p \times (1 + f_{\text{trade}}))$$

Simplifying:

$$\text{EV} = q \times (1 - f_{\text{win}}) - p \times (1 + f_{\text{trade}})$$

Setting EV = 0 and solving for the break-even probability:

$$q_{\text{break-even}} = \frac{p \times (1 + f_{\text{trade}})}{1 - f_{\text{win}}}$$

Example: Buy Yes at $p = 0.50$, trading fee = 2%, winner's fee = 5%:

$$q_{\text{break-even}} = \frac{0.50 \times 1.02}{1 - 0.05} = \frac{0.51}{0.95} \approx 0.537$$

Without fees, break-even is 50%. With fees, it is 53.7%. Fees consume 3.7 percentage points of edge.

4.9.3 Fee Comparison Across Platforms

Platform Trading Fee Winner Fee Withdrawal Fee Notes
Polymarket 0% 0% Gas fees Decentralized, USDC
Kalshi 0% entry Up to 7% on profit $0 (bank) Regulated U.S. exchange
Metaculus N/A N/A N/A Reputation-based, no money
PredictIt 5% per trade 10% on profit 5% withdrawal High friction
Betfair Exchange 2-5% commission on net winnings Included Varies Sports-focused

(Note: Fee structures change frequently. Always verify current fees on each platform.)

4.9.4 The Hidden Cost of the Spread

Beyond explicit fees, the bid-ask spread is an implicit cost. If Yes is bid $0.43 and offered at $0.46, buying and immediately selling costs you $0.03 per contract — a 3-cent round-trip cost even with zero explicit fees.

The effective spread cost for a round-trip trade is:

$$\text{Spread cost} = \text{Ask} - \text{Bid}$$

For a single entry (buy or sell), the effective cost is half the spread if you assume the "fair" price is the midpoint:

$$\text{Half-spread cost} = \frac{\text{Ask} - \text{Bid}}{2}$$

4.9.5 Fee-Aware Position Sizing

When calculating whether a trade is worth making, always include fees:

def fee_adjusted_ev(
    probability: float,
    purchase_price: float,
    trade_fee_rate: float = 0.0,
    winner_fee_rate: float = 0.0,
) -> float:
    """Calculate fee-adjusted expected value of buying Yes.

    Args:
        probability: Your estimated true probability of Yes.
        purchase_price: Price per Yes contract.
        trade_fee_rate: Trading fee as a fraction (e.g., 0.02 for 2%).
        winner_fee_rate: Fee on winning payouts (e.g., 0.05 for 5%).

    Returns:
        Expected value per contract (positive means profitable).
    """
    cost = purchase_price * (1 + trade_fee_rate)
    win_payout = 1.0 * (1 - winner_fee_rate)
    ev = probability * win_payout - cost
    return ev


def break_even_probability(
    purchase_price: float,
    trade_fee_rate: float = 0.0,
    winner_fee_rate: float = 0.0,
) -> float:
    """Calculate the minimum probability needed to break even.

    Args:
        purchase_price: Price per Yes contract.
        trade_fee_rate: Trading fee as a fraction.
        winner_fee_rate: Fee on winning payouts.

    Returns:
        Break-even probability.
    """
    cost = purchase_price * (1 + trade_fee_rate)
    win_payout = 1.0 * (1 - winner_fee_rate)
    return cost / win_payout


# Example
if __name__ == "__main__":
    price = 0.50
    true_prob = 0.55

    scenarios = [
        ("No fees", 0.00, 0.00),
        ("2% trade fee", 0.02, 0.00),
        ("5% winner fee", 0.00, 0.05),
        ("2% trade + 5% winner", 0.02, 0.05),
        ("PredictIt-like (5% + 10%)", 0.05, 0.10),
    ]

    print(f"Purchase price: ${price:.2f}, True probability: {true_prob:.0%}")
    print(f"{'Scenario':<30} {'EV':>8} {'Break-even':>12}")
    print("-" * 52)
    for name, tf, wf in scenarios:
        ev = fee_adjusted_ev(true_prob, price, tf, wf)
        be = break_even_probability(price, tf, wf)
        print(f"{name:<30} ${ev:>+7.4f} {be:>11.1%}")

4.10 Practical Considerations

4.10.1 Liquidity Impact

Liquidity is the ease with which you can buy or sell contracts without significantly moving the price. In prediction markets, liquidity varies enormously:

  • High-profile markets (presidential elections, major sports): Deep order books, tight spreads, easy to trade.
  • Niche markets (obscure policy questions, long-dated events): Thin order books, wide spreads, hard to trade without significant price impact.

The practical implications of low liquidity:

  1. Large orders move the price: Buying 1,000 contracts in a thin market might push the price from $0.40 to $0.55, giving you a much worse average fill.
  2. Exiting is hard: Getting out of a position in a thin market may require accepting a poor price.
  3. Mark-to-market is unreliable: The "current price" in a thin market may not reflect what you could actually trade at.

4.10.2 Slippage

Slippage is the difference between the expected price of a trade and the actual execution price. It occurs because:

  1. Market impact: Your order consumes available liquidity at the best price and "walks" up the order book.
  2. Latency: The price may move between when you decide to trade and when your order reaches the exchange.
  3. Partial fills: You may only get filled on part of your order at the desired price.

To estimate slippage, examine the order book depth:

If you want to buy 200 contracts and the book shows:
  $0.46 x 80
  $0.47 x 30
  $0.48 x 50
  $0.49 x 100

You would fill:
  80 @ $0.46 = $36.80
  30 @ $0.47 = $14.10
  50 @ $0.48 = $24.00
  40 @ $0.49 = $19.60

Total cost: $94.50 for 200 contracts
Average: $0.4725 per contract
Slippage vs best ask: $0.4725 - $0.46 = $0.0125 per contract

4.10.3 Contract Specification Pitfalls

Poorly written contract specifications are a constant source of headaches in prediction markets. Common pitfalls:

  1. Ambiguous timing: "Will X happen in 2026?" — does this mean by December 31, 2026 at 11:59 PM? In which time zone?

  2. Unclear metrics: "Will unemployment drop?" — according to which measure? U-3? U-6? Seasonally adjusted?

  3. Missing edge cases: "Will the president sign the bill?" — what if the president uses a pocket veto? What if the bill is signed but with a signing statement that nullifies key provisions?

  4. Revision risk: "Will GDP growth exceed 3%?" — the advance estimate might say 3.1%, but the final revised figure comes in at 2.9%.

  5. Subjective resolution: "Will there be a recession in 2026?" — who decides? NBER does not typically declare recessions until well after they begin.

Best practices for reading contract specifications: - Read the full spec before trading, not just the headline question. - Look for the resolution source and timing. - Consider edge cases and how they would be resolved. - Check if the platform has a history of disputed resolutions on similar contracts.

4.10.4 Platform Risk

Beyond market risk, prediction market traders face platform risk:

  • Regulatory risk: The platform may be shut down by regulators (as happened with Intrade in 2013).
  • Counterparty risk: The platform may become insolvent or be unable to pay out.
  • Smart contract risk: For decentralized platforms, bugs in smart contracts could lead to loss of funds.
  • Custody risk: Funds held on the platform are exposed to hacking or mismanagement.

Mitigation strategies: - Do not keep more funds on any single platform than you can afford to lose. - Prefer regulated platforms when available. - Withdraw profits regularly. - Diversify across platforms.


4.11 Chapter Summary

This chapter covered the fundamental building blocks of prediction market trading:

  1. Binary contracts are the simplest instrument — pay $1 if an event happens, $0 otherwise. The price reflects the market's implied probability, and the relationship $P(\text{Yes}) + P(\text{No}) = 1$ is enforced by arbitrage.

  2. Multi-outcome contracts extend the binary model to multiple mutually exclusive outcomes. The completeness constraint requires prices to sum to approximately 1 (plus any overround). These contracts are the prediction market equivalent of Arrow-Debreu securities.

  3. Scalar contracts handle continuous numeric outcomes, either through bracket contracts (a set of binary contracts covering ranges) or linear payoff contracts (where payout scales with the outcome value).

  4. The trade lifecycle moves through research, order placement, matching, position management, resolution, and settlement. Understanding each stage helps you manage risk at every point.

  5. Order types (market, limit, stop) and time-in-force modifiers (GTC, FOK, IOC) give you control over execution. The order book shows available liquidity and the bid-ask spread.

  6. Position management requires tracking cost basis, unrealized and realized P&L, and margin requirements. The Portfolio class demonstrated how to manage multiple positions across contracts.

  7. Resolution and settlement depend on clear criteria, reliable sources, and fair dispute mechanisms. Special cases include early resolution and voided contracts.

  8. Payoff diagrams provide visual risk profiles for any position or portfolio. Binary payoffs are discrete; scalar payoffs are piecewise linear.

  9. Fees and friction — trading fees, winner's fees, and the bid-ask spread — erode your edge. The fee-adjusted EV formula shows the true break-even probability for any fee structure.

  10. Practical considerations — liquidity, slippage, contract specification pitfalls, and platform risk — separate paper profits from real-world profits.


What's Next

In Chapter 5: Probability and Pricing, we will dive deeper into the connection between prices and probabilities. You will learn how to extract calibrated probability estimates from market prices, how to compare your beliefs to the market's beliefs quantitatively, and how to identify mispricings that represent genuine trading opportunities. The contract mechanics from this chapter provide the foundation — now we will build the analytical framework on top of them.


Chapter 4 code examples are available in the code/ directory: - example-01-contract-types.py — Contract classes for binary, multi-outcome, and scalar contracts - example-02-trade-lifecycle.py — Full trade lifecycle simulation - example-03-payoff-diagrams.py — Payoff diagram generation - exercise-solutions.py — Solutions to chapter exercises - case-study-code.py — Code for both case studies