31 min read

> "Probability is not a mere computation of odds on the dice or more complicated variants; it is the acceptance of the lack of certainty in our knowledge and the development of methods for dealing with our ignorance."

Learning Objectives

  • Define sample spaces, events, and outcomes for prediction market scenarios
  • Apply the three Kolmogorov axioms and derive fundamental probability rules
  • Calculate conditional probabilities and interpret them in market contexts
  • Derive and apply Bayes' theorem to update beliefs given new evidence
  • Distinguish between independent and dependent events and evaluate their impact on portfolio construction
  • Identify and implement key probability distributions relevant to prediction markets
  • Compute expected value and variance for binary contracts and trading strategies
  • Explain the Law of Large Numbers and Central Limit Theorem and demonstrate them via Monte Carlo simulation
  • Construct a Bayesian updating system that mirrors how prediction market prices adjust to new information
  • Evaluate numerical precision issues when working with extreme probabilities

Chapter 3: Probability Fundamentals

"Probability is not a mere computation of odds on the dice or more complicated variants; it is the acceptance of the lack of certainty in our knowledge and the development of methods for dealing with our ignorance." --- Nassim Nicholas Taleb, Fooled by Randomness


Chapter Overview

In Chapter 1, you learned that a prediction market price of $0.65 on a contract means the market collectively believes the event has a 65% chance of happening. In Chapter 2, you explored how these markets function mechanically --- how orders are placed, how contracts settle, how money changes hands. But we glossed over the most important question: what does "65% chance" actually mean, and how should you reason about it?

This chapter answers that question. Probability theory is the mathematical language that underpins every prediction market. When you buy a contract at $0.40 because you believe the true probability is 0.55, you are making a probabilistic claim. When news breaks and the price swings from $0.30 to $0.70, the market is performing Bayesian updating in real time. When you evaluate whether your trading strategy is profitable, you need expected value. When you assess how much your bankroll might swing along the way, you need variance.

Probability is not just background knowledge for prediction markets --- it is the market. Every price is a probability. Every trade is a disagreement about probability. Every profit or loss is the resolution of probabilistic uncertainty.

What you will build in this chapter:

  1. A ProbabilityCalculator class that computes unions, intersections, complements, and conditional probabilities
  2. A BayesianUpdater that takes a prior belief and sequentially incorporates evidence to produce updated posteriors
  3. A Monte Carlo simulation engine that demonstrates the Law of Large Numbers and Central Limit Theorem with prediction market examples
  4. An expected value calculator for evaluating whether binary contracts are mispriced

By the end of this chapter, you will have the probabilistic toolkit needed to analyze any prediction market contract and make mathematically grounded trading decisions.


3.1 Sample Spaces and Events

The Universe of Possibilities

Every probabilistic situation begins with a sample space --- the complete set of all possible outcomes. We denote it with the Greek letter omega: S (or sometimes $\Omega$).

Definition. A sample space S is the set of all possible outcomes of a random experiment or uncertain situation.

Consider a prediction market on the 2024 U.S. presidential election. The sample space might be:

$$S = \{\text{Democratic candidate wins}, \text{Republican candidate wins}\}$$

This is a simplified model. A more detailed sample space could break this down by electoral vote margin, popular vote percentage, or individual state outcomes. The choice of sample space depends on what level of detail matters for your analysis.

Intuition Callout: Think of the sample space as the "menu" of everything that could happen. Before you can assign probabilities, you need to know what is on the menu. A prediction market contract is essentially a bet on one item from this menu.

Here are sample spaces for common prediction market scenarios:

Market Sample Space Size
Binary election outcome {Dem wins, Rep wins} 2
Fed interest rate decision {Cut, Hold, Raise} 3
Movie box office opening weekend {$0-$50M, $50M-$100M, $100M-$150M, $150M+} 4
Exact Super Bowl score All possible (x, y) score pairs Very large
Temperature on a given day All real numbers (continuous) Infinite

Events

An event is any subset of the sample space --- any collection of outcomes that we might care about.

Definition. An event A is a subset of the sample space S. We say event A occurs if the actual outcome is an element of A.

In our election sample space: - Event A = {Democratic candidate wins} is a simple event (single outcome) - Event B = {Republican candidate wins} is another simple event - Event S = {Democratic candidate wins, Republican candidate wins} is the certain event (it always occurs) - Event {} = the empty set is the impossible event (it never occurs)

For a more complex sample space like the Fed interest rate decision S = {Cut, Hold, Raise}: - Event A = {Cut, Hold} could represent "the Fed does not raise rates" - Event B = {Cut} could represent "the Fed cuts rates" - Event A contains Event B; we say B is a subset of A

Set Operations on Events

Events can be combined using set operations, and each operation has a natural prediction market interpretation:

Complement. The complement of event A, written $A^c$ or $\bar{A}$, is the event "A does not occur." It contains all outcomes in S that are not in A.

  • If A = {Dem wins}, then $A^c$ = {Rep wins}
  • In prediction markets, if a YES contract is priced at $0.65, the implied price of the NO contract is $1.00 - $0.65 = $0.35. This is the complement rule in action.

Union. The union $A \cup B$ is the event "A occurs or B occurs (or both)."

  • If A = {Cut} and B = {Hold}, then $A \cup B$ = {Cut, Hold} = "the Fed does not raise rates"

Intersection. The intersection $A \cap B$ is the event "both A and B occur."

  • If A = "inflation falls below 3%" and B = "unemployment stays below 4%", then $A \cap B$ = "inflation falls below 3% AND unemployment stays below 4%." This intersection is the kind of compound event you might see in a conditional prediction market.

A Note on Sigma Algebras

Technically, probability theory requires that events form a structure called a sigma algebra ($\sigma$-algebra) --- a collection of subsets of S that is closed under complements and countable unions, and that contains S itself. For finite sample spaces (which covers most prediction market scenarios), the set of all subsets (the power set) automatically satisfies this. You can safely ignore sigma algebras for practical prediction market work, but knowing the term helps when reading academic literature.

"""
Sample spaces and events for prediction markets.
"""
from typing import Set, FrozenSet, Any

# Define a sample space for a prediction market
election_sample_space: Set[str] = {"Dem wins", "Rep wins"}

# Events are subsets of the sample space
event_dem_wins: Set[str] = {"Dem wins"}
event_rep_wins: Set[str] = {"Rep wins"}

# Set operations
complement_dem = election_sample_space - event_dem_wins
print(f"Complement of 'Dem wins': {complement_dem}")  # {'Rep wins'}

# Union and intersection with a richer sample space
fed_sample_space: Set[str] = {"Cut", "Hold", "Raise"}
event_not_raise: Set[str] = {"Cut", "Hold"}
event_cut: Set[str] = {"Cut"}

union_example = event_cut | event_not_raise  # Union
print(f"Union: {union_example}")  # {'Cut', 'Hold'}

intersection_example = event_cut & event_not_raise  # Intersection
print(f"Intersection: {intersection_example}")  # {'Cut'}

3.2 Probability Axioms and Rules

The Three Axioms of Probability

In the 1930s, mathematician Andrey Kolmogorov formalized probability theory with three simple axioms. Every rule of probability you will ever use derives from these three foundations.

Axiom 1 (Non-negativity). For any event A, $P(A) \geq 0$.

Probabilities cannot be negative. A prediction market price cannot go below $0.00.

Axiom 2 (Normalization). $P(S) = 1$.

Something must happen. The probability of the entire sample space is 1. In a prediction market, if you hold YES and NO contracts for the same event, one of them must pay out.

Axiom 3 (Additivity). For mutually exclusive events $A_1, A_2, A_3, \ldots$ (events that cannot all occur simultaneously):

$$P(A_1 \cup A_2 \cup A_3 \cup \cdots) = P(A_1) + P(A_2) + P(A_3) + \cdots$$

If events cannot happen at the same time, the probability of any one of them happening is the sum of their individual probabilities.

Intuition Callout: These axioms match our intuition about prediction markets perfectly. A contract price is between $0 and $1 (Axioms 1 and 2). If a market offers contracts on mutually exclusive outcomes (e.g., which party wins), the prices should sum to $1 (Axiom 3). When they don't sum to exactly $1, the difference represents the market's vigorish or transaction costs.

Rules Derived from the Axioms

Complement Rule:

$$P(A^c) = 1 - P(A)$$

If a YES contract is priced at $0.65, the NO contract should be priced at $0.35. This is arguably the most used rule in prediction market trading.

Addition Rule (General):

$$P(A \cup B) = P(A) + P(B) - P(A \cap B)$$

We subtract $P(A \cap B)$ to avoid double-counting outcomes that belong to both events. For mutually exclusive events, $P(A \cap B) = 0$, and this simplifies to Axiom 3.

Inclusion-Exclusion (Three Events):

$$P(A \cup B \cup C) = P(A) + P(B) + P(C) - P(A \cap B) - P(A \cap C) - P(B \cap C) + P(A \cap B \cap C)$$

This generalizes the addition rule. It matters when you are analyzing multiple correlated prediction markets simultaneously.

Worked Example: Overlapping Prediction Markets

Suppose you are tracking three prediction markets for economic events by year-end: - P(Recession) = 0.25 - P(Rate Cut) = 0.60 - P(Market Crash) = 0.15 - P(Recession AND Rate Cut) = 0.22 - P(Recession AND Market Crash) = 0.12 - P(Rate Cut AND Market Crash) = 0.10 - P(All three) = 0.08

What is the probability that at least one of these events occurs?

$$P(R \cup C \cup M) = 0.25 + 0.60 + 0.15 - 0.22 - 0.12 - 0.10 + 0.08 = 0.64$$

There is a 64% probability that at least one of these economic events occurs.

Python Implementation: Probability Calculator

"""
Probability calculator implementing fundamental probability rules.
"""
from typing import Optional


class ProbabilityCalculator:
    """Calculator for fundamental probability operations.

    All probabilities must be between 0 and 1 inclusive.
    """

    @staticmethod
    def validate_probability(p: float, name: str = "Probability") -> None:
        """Validate that a value is a valid probability."""
        if not 0.0 <= p <= 1.0:
            raise ValueError(f"{name} must be between 0 and 1, got {p}")

    @staticmethod
    def complement(p_a: float) -> float:
        """Calculate P(A^c) = 1 - P(A).

        Args:
            p_a: Probability of event A.

        Returns:
            Probability of the complement of A.
        """
        ProbabilityCalculator.validate_probability(p_a, "P(A)")
        return 1.0 - p_a

    @staticmethod
    def union(p_a: float, p_b: float,
              p_a_and_b: float = 0.0) -> float:
        """Calculate P(A ∪ B) = P(A) + P(B) - P(A ∩ B).

        Args:
            p_a: Probability of event A.
            p_b: Probability of event B.
            p_a_and_b: Probability of A and B both occurring.
                        Defaults to 0 (mutually exclusive events).

        Returns:
            Probability of A or B occurring.
        """
        ProbabilityCalculator.validate_probability(p_a, "P(A)")
        ProbabilityCalculator.validate_probability(p_b, "P(B)")
        ProbabilityCalculator.validate_probability(p_a_and_b, "P(A∩B)")

        result = p_a + p_b - p_a_and_b
        if result > 1.0 + 1e-10:
            raise ValueError(
                f"Result {result} > 1. Check your inputs for consistency."
            )
        return min(result, 1.0)  # Clamp for floating point

    @staticmethod
    def intersection_if_independent(p_a: float, p_b: float) -> float:
        """Calculate P(A ∩ B) = P(A) * P(B) assuming independence.

        Args:
            p_a: Probability of event A.
            p_b: Probability of event B.

        Returns:
            Probability of both A and B occurring (if independent).
        """
        ProbabilityCalculator.validate_probability(p_a, "P(A)")
        ProbabilityCalculator.validate_probability(p_b, "P(B)")
        return p_a * p_b

    @staticmethod
    def conditional(p_a_and_b: float, p_b: float) -> float:
        """Calculate P(A|B) = P(A ∩ B) / P(B).

        Args:
            p_a_and_b: Probability of both A and B.
            p_b: Probability of B (must be > 0).

        Returns:
            Conditional probability of A given B.
        """
        if p_b == 0:
            raise ValueError("Cannot condition on an event with probability 0")
        ProbabilityCalculator.validate_probability(p_a_and_b, "P(A∩B)")
        ProbabilityCalculator.validate_probability(p_b, "P(B)")
        return p_a_and_b / p_b


# Demonstration
calc = ProbabilityCalculator()

# Complement: YES contract at $0.65
yes_price = 0.65
no_price = calc.complement(yes_price)
print(f"YES price: ${yes_price:.2f}, NO price: ${no_price:.2f}")

# Union: probability of recession OR rate cut
p_recession = 0.25
p_rate_cut = 0.60
p_both = 0.22
p_either = calc.union(p_recession, p_rate_cut, p_both)
print(f"P(Recession OR Rate Cut) = {p_either:.2f}")

# Independence check
p_independent = calc.intersection_if_independent(p_recession, p_rate_cut)
print(f"If independent, P(both) would be {p_independent:.2f}")
print(f"Actual P(both) = {p_both:.2f}")
print(f"Events are {'independent' if abs(p_independent - p_both) < 0.01 else 'dependent'}")

3.3 Conditional Probability

Updating Probabilities with New Information

Conditional probability answers the question: "Given that I know event B has occurred, what is the probability of event A?"

Definition. The conditional probability of A given B is:

$$P(A|B) = \frac{P(A \cap B)}{P(B)}, \quad \text{provided } P(B) > 0$$

The vertical bar "|" is read as "given." $P(A|B)$ is "the probability of A given B."

Intuition Callout: Conditional probability is like zooming in. Instead of looking at all possible outcomes (the full sample space S), you restrict your attention to only those outcomes where B happened. Then you ask: among those outcomes, how many also have A happening?

Prediction Market Interpretation

Conditional probability is the mathematical formalization of what happens every minute in a prediction market. Consider this scenario:

  • Market: "Will Company X be acquired this year?"
  • Current price: $0.30 (market thinks 30% chance)
  • News breaks: "Company X is in acquisition talks with Company Y"
  • New price: $0.72

The market just computed a conditional probability. Before the news, P(Acquisition) = 0.30. After learning that talks are happening, the market updated to P(Acquisition | Talks revealed) = 0.72.

Worked Example: Multi-Stage Event

Suppose a prediction market tracks whether a bill passes through Congress. You know: - P(Passes House) = 0.70 - P(Passes Senate AND Passes House) = 0.45 - P(Passes Senate | Passes House) = ?

Applying the formula:

$$P(\text{Passes Senate} | \text{Passes House}) = \frac{P(\text{Passes Senate} \cap \text{Passes House})}{P(\text{Passes House})} = \frac{0.45}{0.70} = 0.643$$

Given that the bill passes the House, there is a 64.3% chance it also passes the Senate.

The Multiplication Rule

Rearranging the conditional probability formula gives us the multiplication rule:

$$P(A \cap B) = P(A|B) \cdot P(B) = P(B|A) \cdot P(A)$$

This is useful when you know conditional probabilities but need the joint probability. For example, if you know: - P(Fed raises rates) = 0.40 - P(Market drops 5%+ | Fed raises rates) = 0.30

Then: P(Fed raises AND Market drops 5%+) = 0.30 x 0.40 = 0.12

The Law of Total Probability

If events $B_1, B_2, \ldots, B_n$ are mutually exclusive and exhaustive (they partition the sample space), then for any event A:

$$P(A) = \sum_{i=1}^{n} P(A|B_i) \cdot P(B_i)$$

This is enormously useful for breaking complex probability calculations into simpler conditional pieces.

Example: What is the probability a prediction market contract pays out?

  • Scenario 1: Economy enters recession (P = 0.25). If recession, P(contract pays) = 0.80.
  • Scenario 2: Economy does not enter recession (P = 0.75). If no recession, P(contract pays) = 0.20.

$$P(\text{pays}) = P(\text{pays}|\text{recession}) \cdot P(\text{recession}) + P(\text{pays}|\text{no recession}) \cdot P(\text{no recession})$$ $$= 0.80 \times 0.25 + 0.20 \times 0.75 = 0.20 + 0.15 = 0.35$$

The contract should be priced at $0.35.

"""
Conditional probability examples in prediction market contexts.
"""

def conditional_probability(p_a_and_b: float, p_b: float) -> float:
    """Calculate P(A|B) = P(A ∩ B) / P(B)."""
    if p_b <= 0:
        raise ValueError("P(B) must be positive")
    return p_a_and_b / p_b


def total_probability(
    conditionals: list[float],
    scenario_probs: list[float]
) -> float:
    """Calculate P(A) using the law of total probability.

    Args:
        conditionals: List of P(A|B_i) for each scenario.
        scenario_probs: List of P(B_i) for each scenario.

    Returns:
        Total probability P(A).
    """
    if len(conditionals) != len(scenario_probs):
        raise ValueError("Lists must have the same length")
    if abs(sum(scenario_probs) - 1.0) > 1e-10:
        raise ValueError(
            f"Scenario probabilities must sum to 1, got {sum(scenario_probs)}"
        )
    return sum(c * s for c, s in zip(conditionals, scenario_probs))


# Example: Bill passing Congress
p_passes_house = 0.70
p_both_pass = 0.45
p_senate_given_house = conditional_probability(p_both_pass, p_passes_house)
print(f"P(Senate passes | House passes) = {p_senate_given_house:.3f}")

# Example: Contract payout using total probability
p_payout = total_probability(
    conditionals=[0.80, 0.20],    # P(pays|recession), P(pays|no recession)
    scenario_probs=[0.25, 0.75]   # P(recession), P(no recession)
)
print(f"Fair contract price: ${p_payout:.2f}")

3.4 Bayes' Theorem

The Most Important Formula in Prediction Markets

Bayes' theorem is arguably the single most important mathematical tool for prediction market participants. It tells you exactly how to update your beliefs when you encounter new evidence.

Derivation

Starting from the definition of conditional probability, we know:

$$P(A|B) = \frac{P(A \cap B)}{P(B)} \quad \text{and} \quad P(B|A) = \frac{P(A \cap B)}{P(A)}$$

From the second equation: $P(A \cap B) = P(B|A) \cdot P(A)$

Substituting into the first equation:

$$\boxed{P(A|B) = \frac{P(B|A) \cdot P(A)}{P(B)}}$$

This is Bayes' theorem.

Understanding Each Term

Each term in Bayes' theorem has a specific name and interpretation:

Term Name Meaning
$P(A \mid B)$ Posterior Your updated belief about A after seeing evidence B
$P(A)$ Prior Your belief about A before seeing evidence B
$P(B \mid A)$ Likelihood How probable the evidence B is, assuming A is true
$P(B)$ Evidence (or marginal likelihood) The total probability of seeing evidence B under any hypothesis

Intuition Callout: Bayes' theorem is a "belief updating machine." You feed in what you believed before (the prior), how surprised you would be to see this evidence if your belief were true (the likelihood), and you get back a rationally updated belief (the posterior). This is exactly what happens when a prediction market price moves in response to news.

The Evidence Term via Total Probability

The denominator $P(B)$ is often calculated using the law of total probability:

$$P(B) = P(B|A) \cdot P(A) + P(B|A^c) \cdot P(A^c)$$

This makes the full Bayes' theorem formula:

$$P(A|B) = \frac{P(B|A) \cdot P(A)}{P(B|A) \cdot P(A) + P(B|A^c) \cdot P(A^c)}$$

Worked Example: Prediction Market for a Policy Announcement

Setup: A prediction market contract pays $1 if a major trade policy is announced this month. The current market price is $0.30, which we interpret as a 30% prior probability.

A credible news outlet reports that "senior officials are drafting the policy document." You need to figure out: given this news, what should the new probability be?

Step 1: Define your terms. - A = "Policy is announced this month" - B = "News reports officials are drafting the document" - P(A) = 0.30 (prior, from the market price) - P(A^c) = 0.70

Step 2: Estimate the likelihoods. - P(B|A): If the policy IS going to be announced, how likely is it that drafting news would leak? Let's say 0.85. - P(B|A^c): If the policy is NOT going to be announced, how likely is it that such news would appear? Maybe 0.15 (false reports, speculative journalism, drafting that goes nowhere).

Step 3: Calculate the evidence.

$$P(B) = P(B|A) \cdot P(A) + P(B|A^c) \cdot P(A^c) = 0.85 \times 0.30 + 0.15 \times 0.70 = 0.255 + 0.105 = 0.36$$

Step 4: Apply Bayes' theorem.

$$P(A|B) = \frac{P(B|A) \cdot P(A)}{P(B)} = \frac{0.85 \times 0.30}{0.36} = \frac{0.255}{0.36} = 0.708$$

The posterior probability is approximately 70.8%. The market price should move from $0.30 to about $0.71.

Common Pitfall: A frequent mistake is to confuse P(A|B) with P(B|A). The fact that evidence would be very likely if your hypothesis is true does NOT mean your hypothesis is very likely given the evidence. This confusion is called the prosecutor's fallacy or the base rate fallacy. Always remember: the prior P(A) matters enormously.

Odds Form of Bayes' Theorem

An elegant alternative formulation uses odds. The odds of event A are:

$$O(A) = \frac{P(A)}{P(A^c)} = \frac{P(A)}{1 - P(A)}$$

Bayes' theorem in odds form:

$$\underbrace{\frac{P(A|B)}{P(A^c|B)}}_{\text{posterior odds}} = \underbrace{\frac{P(B|A)}{P(B|A^c)}}_{\text{likelihood ratio}} \times \underbrace{\frac{P(A)}{P(A^c)}}_{\text{prior odds}}$$

The likelihood ratio (also called the Bayes factor) tells you how much the evidence should shift your odds. A likelihood ratio of 5 means the evidence is 5 times more likely under the hypothesis than under the alternative.

From our example: Prior odds = 0.30/0.70 = 3/7. Likelihood ratio = 0.85/0.15 = 17/3. Posterior odds = (17/3) x (3/7) = 17/7 = 2.43. Converting back to probability: 17/(17+7) = 17/24 = 0.708. Same answer.

Python Bayesian Updater

"""
Bayesian updater for prediction market analysis.
"""
from dataclasses import dataclass, field


@dataclass
class Evidence:
    """A piece of evidence for Bayesian updating.

    Attributes:
        name: Description of the evidence.
        likelihood_if_true: P(evidence | hypothesis is true).
        likelihood_if_false: P(evidence | hypothesis is false).
    """
    name: str
    likelihood_if_true: float
    likelihood_if_false: float

    @property
    def likelihood_ratio(self) -> float:
        """Bayes factor: how much this evidence shifts the odds."""
        if self.likelihood_if_false == 0:
            return float('inf')
        return self.likelihood_if_true / self.likelihood_if_false


@dataclass
class BayesianUpdater:
    """Sequential Bayesian updater for binary hypotheses.

    Attributes:
        prior: Initial probability of the hypothesis.
        hypothesis_name: Description of the hypothesis.
        history: List of (evidence_name, posterior) tuples.
    """
    prior: float
    hypothesis_name: str = "Hypothesis"
    history: list[tuple[str, float]] = field(default_factory=list)

    def __post_init__(self) -> None:
        self.current_probability = self.prior
        self.history.append(("Prior", self.prior))

    def update(self, evidence: Evidence) -> float:
        """Update the probability given new evidence.

        Args:
            evidence: The new evidence to incorporate.

        Returns:
            The posterior probability.
        """
        p = self.current_probability

        # Bayes' theorem
        numerator = evidence.likelihood_if_true * p
        denominator = (
            evidence.likelihood_if_true * p
            + evidence.likelihood_if_false * (1 - p)
        )

        if denominator == 0:
            raise ValueError("Denominator is zero --- check your likelihoods")

        posterior = numerator / denominator
        self.current_probability = posterior
        self.history.append((evidence.name, posterior))

        return posterior

    def print_history(self) -> None:
        """Print the full update history."""
        print(f"\nBayesian Update History: {self.hypothesis_name}")
        print("-" * 55)
        for name, prob in self.history:
            odds = prob / (1 - prob) if prob < 1 else float('inf')
            print(f"  {name:35s} -> P = {prob:.4f} (odds {odds:.2f}:1)")
        print("-" * 55)


# Example: Policy announcement prediction market
updater = BayesianUpdater(
    prior=0.30,
    hypothesis_name="Policy announced this month"
)

# Evidence 1: Officials drafting document
e1 = Evidence(
    name="Officials drafting document",
    likelihood_if_true=0.85,
    likelihood_if_false=0.15
)

# Evidence 2: President cancels unrelated trip (to stay for announcement?)
e2 = Evidence(
    name="President cancels trip",
    likelihood_if_true=0.70,
    likelihood_if_false=0.30
)

# Evidence 3: Opposition leader preemptively criticizes the policy
e3 = Evidence(
    name="Opposition preemptive criticism",
    likelihood_if_true=0.75,
    likelihood_if_false=0.10
)

updater.update(e1)
updater.update(e2)
updater.update(e3)
updater.print_history()

Output:

Bayesian Update History: Policy announced this month
-------------------------------------------------------
  Prior                               -> P = 0.3000 (odds 0.43:1)
  Officials drafting document         -> P = 0.7083 (odds 2.43:1)
  President cancels trip              -> P = 0.8500 (odds 5.67:1)
  Opposition preemptive criticism     -> P = 0.9769 (odds 42.50:1)
-------------------------------------------------------

After three pieces of evidence, the probability has moved from 30% to nearly 98%. Each piece of evidence multiplied the odds by its likelihood ratio.


3.5 Independence and Dependence

When Events Don't Affect Each Other

Two events A and B are independent if knowing that one occurred gives you no information about whether the other occurred.

Definition. Events A and B are independent if and only if:

$$P(A \cap B) = P(A) \cdot P(B)$$

Equivalently, $P(A|B) = P(A)$ --- learning that B happened does not change the probability of A.

Example: Consider two prediction markets: - Market 1: "Will it rain in Tokyo on March 15?" - Market 2: "Will the Chicago Cubs win their next game?"

These events are plausibly independent. Knowing it rained in Tokyo tells you nothing about Cubs baseball. If P(Rain in Tokyo) = 0.40 and P(Cubs win) = 0.55, then P(Both) = 0.40 x 0.55 = 0.22.

When Events Are Dependent

Most interesting prediction market events are dependent --- knowing one outcome changes your assessment of others.

Example: - Market 1: "Will GDP growth exceed 3%?" - Market 2: "Will the stock market rise 10%+ this year?"

These are dependent. Strong GDP growth makes a stock market rise more likely. If P(GDP > 3%) = 0.35 and P(Stocks up 10%+) = 0.30, the independence prediction would be P(Both) = 0.105. But the actual P(Both) might be 0.20 because these events tend to happen together.

Testing for Independence

Given data, you can test whether events are independent by checking whether $P(A \cap B) \approx P(A) \cdot P(B)$:

"""
Testing for independence between prediction market events.
"""

def test_independence(
    p_a: float,
    p_b: float,
    p_a_and_b: float,
    tolerance: float = 0.01
) -> dict:
    """Test whether two events are approximately independent.

    Args:
        p_a: P(A).
        p_b: P(B).
        p_a_and_b: Observed P(A ∩ B).
        tolerance: Acceptable deviation from independence.

    Returns:
        Dictionary with test results.
    """
    expected_if_independent = p_a * p_b
    deviation = p_a_and_b - expected_if_independent

    return {
        "P(A)": p_a,
        "P(B)": p_b,
        "P(A∩B) observed": p_a_and_b,
        "P(A∩B) if independent": expected_if_independent,
        "deviation": deviation,
        "independent": abs(deviation) <= tolerance,
        "relationship": (
            "positive dependence" if deviation > tolerance
            else "negative dependence" if deviation < -tolerance
            else "approximately independent"
        )
    }


# Example: GDP growth and stock market
result = test_independence(
    p_a=0.35,     # P(GDP > 3%)
    p_b=0.30,     # P(Stocks up 10%+)
    p_a_and_b=0.20  # P(Both)
)
for key, value in result.items():
    print(f"  {key}: {value}")

Why Independence Matters for Portfolio Construction

If you are trading multiple prediction market contracts, independence (or the lack of it) dramatically affects your portfolio risk:

  • Independent contracts: Total portfolio variance is the sum of individual variances. Diversification works as expected.
  • Positively dependent contracts: Portfolio variance is higher than the sum of individual variances. You have hidden concentration risk.
  • Negatively dependent contracts: Portfolio variance is lower than the sum. You have natural hedging.

Common Pitfall: Traders often assume prediction market contracts are independent when they are not. For instance, "Will Candidate A win State X?" and "Will Candidate A win State Y?" are heavily correlated. Treating them as independent leads to severe underestimation of portfolio risk. We will explore this in depth in later chapters on portfolio construction.


3.6 Random Variables and Distributions

From Events to Numbers

A random variable is a function that assigns a numerical value to each outcome in the sample space. It transforms qualitative outcomes into numbers we can do math with.

Definition. A random variable X is a function from the sample space S to the real numbers: $X: S \rightarrow \mathbb{R}$.

For a prediction market binary contract:

$$X = \begin{cases} 1 & \text{if the event occurs (contract pays out)} \\ 0 & \text{if the event does not occur} \end{cases}$$

If you bought the contract at price $p$, your profit is:

$$\text{Profit} = X - p = \begin{cases} 1 - p & \text{with probability } q \\ -p & \text{with probability } 1 - q \end{cases}$$

where $q$ is the true probability of the event.

Discrete Random Variables and PMFs

A discrete random variable takes on countably many values. Its probability distribution is described by a probability mass function (PMF).

$$\text{PMF: } p(x) = P(X = x)$$

Properties of a PMF: 1. $p(x) \geq 0$ for all x 2. $\sum_x p(x) = 1$

Continuous Random Variables and PDFs

A continuous random variable takes on uncountably many values (e.g., any real number in an interval). Its distribution is described by a probability density function (PDF) $f(x)$.

$$P(a \leq X \leq b) = \int_a^b f(x) \, dx$$

Note: For continuous random variables, $P(X = x) = 0$ for any specific value x. Only intervals have nonzero probability.

The Cumulative Distribution Function (CDF)

The CDF works for both discrete and continuous random variables:

$$F(x) = P(X \leq x)$$

The CDF is a non-decreasing function from 0 to 1. It tells you the probability of the random variable being at most x.

Key Distributions for Prediction Markets

1. Bernoulli Distribution

The simplest prediction market distribution. A single binary contract.

$$X \sim \text{Bernoulli}(p)$$ $$P(X = 1) = p, \quad P(X = 0) = 1 - p$$ $$E[X] = p, \quad \text{Var}(X) = p(1 - p)$$

A contract priced at $0.65 is modeled as Bernoulli(0.65).

2. Binomial Distribution

The number of successes in n independent Bernoulli trials.

$$X \sim \text{Binomial}(n, p)$$ $$P(X = k) = \binom{n}{k} p^k (1-p)^{n-k}$$ $$E[X] = np, \quad \text{Var}(X) = np(1-p)$$

If you make 100 independent trades, each with a 55% win probability, your number of wins follows Binomial(100, 0.55).

3. Beta Distribution

The conjugate prior for Bernoulli/Binomial likelihoods. It represents uncertainty about a probability itself.

$$X \sim \text{Beta}(\alpha, \beta)$$ $$E[X] = \frac{\alpha}{\alpha + \beta}, \quad \text{Var}(X) = \frac{\alpha\beta}{(\alpha + \beta)^2(\alpha + \beta + 1)}$$

The Beta distribution is the workhorse of Bayesian analysis for prediction markets. If you believe a contract's true probability is "around 0.60 but you are not sure," the Beta distribution lets you formalize that uncertainty.

4. Normal (Gaussian) Distribution

$$X \sim N(\mu, \sigma^2)$$ $$f(x) = \frac{1}{\sigma\sqrt{2\pi}} e^{-\frac{(x-\mu)^2}{2\sigma^2}}$$ $$E[X] = \mu, \quad \text{Var}(X) = \sigma^2$$

The normal distribution appears in prediction markets via the Central Limit Theorem: the average of many trades converges to a normal distribution. It is also used in pricing models.

"""
Key probability distributions for prediction markets.
"""
from scipy import stats
import numpy as np


def demonstrate_distributions() -> None:
    """Show key distributions with prediction market parameters."""

    # 1. Bernoulli: single binary contract
    contract_price = 0.65
    bernoulli = stats.bernoulli(p=contract_price)
    print("=== Bernoulli (Single Binary Contract) ===")
    print(f"  Contract price: ${contract_price:.2f}")
    print(f"  E[payout] = {bernoulli.mean():.2f}")
    print(f"  Var(payout) = {bernoulli.var():.4f}")
    print(f"  Std(payout) = {bernoulli.std():.4f}")

    # 2. Binomial: number of wins in 100 trades
    n_trades = 100
    win_prob = 0.55
    binomial = stats.binom(n=n_trades, p=win_prob)
    print(f"\n=== Binomial ({n_trades} trades, {win_prob} win rate) ===")
    print(f"  E[wins] = {binomial.mean():.1f}")
    print(f"  Std(wins) = {binomial.std():.2f}")
    print(f"  P(wins >= 60) = {1 - binomial.cdf(59):.4f}")
    print(f"  P(wins < 50) = {binomial.cdf(49):.4f}")

    # 3. Beta: uncertainty about true probability
    # "I think probability is around 0.6, but not very sure"
    alpha, beta_param = 6, 4  # mean = 6/(6+4) = 0.6
    beta_dist = stats.beta(alpha, beta_param)
    print(f"\n=== Beta({alpha}, {beta_param}): Belief about true probability ===")
    print(f"  E[p] = {beta_dist.mean():.2f}")
    print(f"  Std(p) = {beta_dist.std():.4f}")
    print(f"  90% credible interval: [{beta_dist.ppf(0.05):.3f}, {beta_dist.ppf(0.95):.3f}]")

    # 4. Normal: P&L distribution over many trades (via CLT)
    # If each trade profits $0.10 on average with std $0.50
    mean_profit = 0.10
    std_profit = 0.50
    n_trades_big = 1000
    # Average profit distribution via CLT
    normal = stats.norm(
        loc=mean_profit,
        scale=std_profit / np.sqrt(n_trades_big)
    )
    print(f"\n=== Normal (Average profit over {n_trades_big} trades) ===")
    print(f"  E[avg profit] = ${normal.mean():.4f}")
    print(f"  Std(avg profit) = ${normal.std():.4f}")
    print(f"  P(avg profit < 0) = {normal.cdf(0):.6f}")


demonstrate_distributions()

3.7 Expected Value

The Long-Run Average

Expected value (EV) is the probability-weighted average of all possible outcomes. It answers the question: "If I could repeat this situation infinitely many times, what would my average outcome be?"

Definition (Discrete). For a discrete random variable X:

$$E[X] = \sum_x x \cdot P(X = x)$$

Definition (Continuous). For a continuous random variable X with PDF $f(x)$:

$$E[X] = \int_{-\infty}^{\infty} x \cdot f(x) \, dx$$

Expected Value of a Binary Contract Trade

This is the most important EV calculation in prediction markets. Suppose you buy a binary contract at price $p$ and believe the true probability of the event is $q$.

Outcome Probability Profit
Event occurs q $1 - p$
Event does not occur 1 - q $-p$

$$EV = q \cdot (1 - p) + (1 - q) \cdot (-p) = q - qp - p + qp = q - p$$

$$\boxed{EV = q - p}$$

This is a beautifully simple result: the expected value of buying a binary contract equals the difference between the true probability and the price you pay.

  • If q > p (you think the event is more likely than the market does), EV > 0: the trade has positive expected value.
  • If q < p (you think the event is less likely than the market does), EV < 0: you should not buy (or you should sell/short).
  • If q = p (you agree with the market), EV = 0: there is no edge.

Intuition Callout: Expected value is the prediction market trader's "edge." If you consistently find contracts where your estimated probability q exceeds the market price p, you have a positive expected value strategy. The challenge is that estimating q correctly is extremely difficult --- the market is aggregating the estimates of all other participants.

Linearity of Expected Value

A remarkable property of expected value is linearity: the EV of a sum is the sum of the EVs, regardless of whether the random variables are independent or not.

$$E[X + Y] = E[X] + E[Y]$$ $$E[aX + b] = aE[X] + b$$

This means if you hold multiple contracts, your total expected profit is the sum of the individual expected profits. If Contract A has EV = $0.05 and Contract B has EV = $0.03, your portfolio EV is $0.08, regardless of whether A and B are correlated.

Worked Example: Should You Take This Trade?

A prediction market offers a contract: "Will inflation exceed 4% next quarter?" at a price of $0.35.

After your analysis, you estimate the true probability at 0.45. Should you buy?

$$EV = q - p = 0.45 - 0.35 = 0.10$$

For every dollar risked, you expect a return of $0.10. On a $100 position, your expected profit is $10.

But EV is not the whole story. You also need to consider how much you might lose in any single instance (risk), which brings us to variance.

Python EV Calculator

"""
Expected value calculator for prediction market trades.
"""
from dataclasses import dataclass


@dataclass
class BinaryTrade:
    """Represents a binary contract trade.

    Attributes:
        contract_name: Name of the contract.
        market_price: Price you buy at (between 0 and 1).
        estimated_prob: Your estimated true probability.
        position_size: Dollar amount of your position.
    """
    contract_name: str
    market_price: float
    estimated_prob: float
    position_size: float = 1.0

    @property
    def edge(self) -> float:
        """The difference between estimated probability and market price."""
        return self.estimated_prob - self.market_price

    @property
    def expected_value(self) -> float:
        """Expected profit on this trade."""
        return self.edge * self.position_size

    @property
    def max_profit(self) -> float:
        """Maximum possible profit."""
        return (1.0 - self.market_price) * self.position_size

    @property
    def max_loss(self) -> float:
        """Maximum possible loss (expressed as negative number)."""
        return -self.market_price * self.position_size

    def describe(self) -> None:
        """Print a full analysis of this trade."""
        print(f"\n--- Trade Analysis: {self.contract_name} ---")
        print(f"  Market price:      ${self.market_price:.2f}")
        print(f"  Your probability:   {self.estimated_prob:.2%}")
        print(f"  Edge:               {self.edge:+.2%}")
        print(f"  Position size:     ${self.position_size:.2f}")
        print(f"  Expected value:    ${self.expected_value:+.2f}")
        print(f"  Max profit:        ${self.max_profit:+.2f}")
        print(f"  Max loss:          ${self.max_loss:+.2f}")
        print(f"  Recommendation:     {'BUY' if self.edge > 0 else 'PASS'}")


# Analyze several potential trades
trades = [
    BinaryTrade("Inflation > 4%", 0.35, 0.45, 100),
    BinaryTrade("Fed rate cut", 0.60, 0.55, 100),
    BinaryTrade("GDP > 3%", 0.20, 0.30, 100),
    BinaryTrade("Unemployment < 4%", 0.70, 0.70, 100),  # No edge
]

total_ev = 0.0
for trade in trades:
    trade.describe()
    total_ev += trade.expected_value

print(f"\n=== Portfolio Expected Value: ${total_ev:+.2f} ===")

3.8 Variance, Standard Deviation, and Risk

Measuring Uncertainty

Expected value tells you the center of the distribution, but it says nothing about how spread out outcomes are. Variance measures the average squared deviation from the mean.

Definition. For a random variable X with mean $\mu = E[X]$:

$$\text{Var}(X) = E[(X - \mu)^2] = E[X^2] - (E[X])^2$$

Standard deviation is the square root of variance:

$$\sigma = \sqrt{\text{Var}(X)}$$

Standard deviation is in the same units as X, making it more interpretable than variance.

Variance of a Binary Contract

For a Bernoulli random variable with parameter p:

$$\text{Var}(X) = p(1 - p)$$

This is maximized when p = 0.5 (maximum uncertainty) and minimized when p is near 0 or 1 (near certainty).

Contract Price Variance Std Dev
0.10 0.090 0.300
0.25 0.188 0.433
0.50 0.250 0.500
0.75 0.188 0.433
0.90 0.090 0.300

Intuition Callout: Contracts priced near $0.50 are the riskiest in terms of variance. Contracts near $0.00 or $1.00 have low variance but also low potential profit. This creates a fundamental tradeoff: high-variance contracts offer larger potential gains but also larger potential losses.

Risk in Prediction Market Portfolios

For independent trades, the portfolio variance is additive:

$$\text{Var}(X_1 + X_2 + \cdots + X_n) = \text{Var}(X_1) + \text{Var}(X_2) + \cdots + \text{Var}(X_n)$$

For dependent trades, covariance terms appear:

$$\text{Var}(X_1 + X_2) = \text{Var}(X_1) + \text{Var}(X_2) + 2\text{Cov}(X_1, X_2)$$

Positive covariance increases portfolio risk. Negative covariance decreases it.

Python Implementation

"""
Variance and risk calculations for prediction market portfolios.
"""
import math
from dataclasses import dataclass


@dataclass
class ContractRisk:
    """Risk analysis for a binary contract position.

    Attributes:
        name: Contract name.
        price: Contract price (probability).
        position_size: Number of contracts held.
    """
    name: str
    price: float
    position_size: float = 1.0

    @property
    def variance(self) -> float:
        """Variance of payout for a single contract."""
        return self.price * (1 - self.price)

    @property
    def position_variance(self) -> float:
        """Variance of total position payout."""
        return self.variance * (self.position_size ** 2)

    @property
    def std_dev(self) -> float:
        """Standard deviation of position payout."""
        return math.sqrt(self.position_variance)

    def value_at_risk(self, confidence: float = 0.95) -> float:
        """Simplified VaR: maximum loss at given confidence.

        For binary contracts, this is straightforward:
        the worst case is losing the entire position cost.
        """
        return self.price * self.position_size


def portfolio_std_dev_independent(contracts: list[ContractRisk]) -> float:
    """Calculate portfolio standard deviation assuming independence.

    Args:
        contracts: List of ContractRisk objects.

    Returns:
        Portfolio standard deviation.
    """
    total_variance = sum(c.position_variance for c in contracts)
    return math.sqrt(total_variance)


# Example: Portfolio of 3 independent contracts
contracts = [
    ContractRisk("Election", 0.55, position_size=50),
    ContractRisk("Rate Cut", 0.40, position_size=30),
    ContractRisk("GDP > 3%", 0.25, position_size=20),
]

print("=== Portfolio Risk Analysis (Independent) ===")
for c in contracts:
    print(f"  {c.name}: price={c.price:.2f}, "
          f"var={c.position_variance:.2f}, "
          f"std=${c.std_dev:.2f}")

port_std = portfolio_std_dev_independent(contracts)
print(f"\n  Portfolio Std Dev: ${port_std:.2f}")
print(f"  Total position cost: "
      f"${sum(c.price * c.position_size for c in contracts):.2f}")

3.9 The Law of Large Numbers and Central Limit Theorem

The Law of Large Numbers (LLN)

The Law of Large Numbers states that as you increase the number of independent trials, the sample average converges to the expected value.

Formal Statement (Weak LLN). Let $X_1, X_2, \ldots, X_n$ be independent random variables with the same distribution, each with mean $\mu$. Then for any $\epsilon > 0$:

$$P\left(\left|\frac{X_1 + X_2 + \cdots + X_n}{n} - \mu\right| > \epsilon\right) \rightarrow 0 \quad \text{as } n \rightarrow \infty$$

In plain English: the more times you repeat an experiment, the closer the average result gets to the true expected value.

LLN and Prediction Markets

The LLN is the mathematical reason why expected value matters for prediction market traders:

  • Short term: Anything can happen. You can make good bets and still lose money.
  • Long term: If you consistently make positive EV trades, your average profit per trade will converge to your true edge.

This is why professional traders focus on EV rather than individual outcomes. A single bet at $0.40 with a true probability of 0.55 might lose (45% chance). But after 1,000 such bets, your average profit will be very close to $0.15 per contract.

Common Pitfall: The LLN does NOT say that losses will be "made up" by future wins. It does not say that after a losing streak, a winning streak is "due." That is the gambler's fallacy. The LLN says that as n grows, the impact of early deviations becomes diluted by the growing number of trials.

The Central Limit Theorem (CLT)

The Central Limit Theorem states that the sum (or average) of many independent random variables is approximately normally distributed, regardless of the underlying distribution.

Formal Statement. Let $X_1, X_2, \ldots, X_n$ be independent, identically distributed random variables with mean $\mu$ and variance $\sigma^2$. Then:

$$\frac{\bar{X}_n - \mu}{\sigma / \sqrt{n}} \xrightarrow{d} N(0, 1) \quad \text{as } n \rightarrow \infty$$

where $\bar{X}_n = \frac{1}{n}\sum_{i=1}^n X_i$.

In plain English: the average of many random outcomes follows a bell curve, centered at the true mean, with a spread that shrinks as $1/\sqrt{n}$.

CLT and Prediction Markets

The CLT tells prediction market traders:

  1. Profit distribution: After many trades, your total profit will be approximately normally distributed. This lets you calculate confidence intervals for your returns.

  2. The $\sqrt{n}$ rule: Standard deviation of your average profit shrinks as $1/\sqrt{n}$. To cut your uncertainty in half, you need 4 times as many trades. To cut it by a factor of 10, you need 100 times as many.

  3. Confidence intervals: After n trades with average profit $\bar{X}$ and sample standard deviation s, a 95% confidence interval for your true edge is approximately:

$$\bar{X} \pm 1.96 \cdot \frac{s}{\sqrt{n}}$$

Monte Carlo Simulation

Monte Carlo simulation lets us see the LLN and CLT in action:

"""
Monte Carlo simulation demonstrating LLN and CLT.
"""
import numpy as np
from typing import Tuple


def simulate_trades(
    true_prob: float,
    market_price: float,
    n_trades: int,
    n_simulations: int = 10000,
    seed: int = 42
) -> dict:
    """Simulate a prediction market trading strategy.

    Args:
        true_prob: True probability of event occurring.
        market_price: Price paid for each contract.
        n_trades: Number of trades in each simulation.
        n_simulations: Number of simulation runs.
        seed: Random seed for reproducibility.

    Returns:
        Dictionary with simulation results.
    """
    rng = np.random.default_rng(seed)

    # Expected profit per trade
    ev_per_trade = true_prob - market_price

    # Simulate outcomes: 1 if event occurs, 0 if not
    outcomes = rng.random((n_simulations, n_trades)) < true_prob

    # Profit per trade: (1 - price) if win, (-price) if lose
    profits = np.where(outcomes, 1 - market_price, -market_price)

    # Cumulative profit for each simulation
    cumulative_profits = np.cumsum(profits, axis=1)

    # Average profit per trade at each step
    trade_numbers = np.arange(1, n_trades + 1)
    average_profits = cumulative_profits / trade_numbers

    # Final results
    final_profits = cumulative_profits[:, -1]
    final_averages = average_profits[:, -1]

    return {
        "ev_per_trade": ev_per_trade,
        "total_ev": ev_per_trade * n_trades,
        "mean_final_profit": float(np.mean(final_profits)),
        "std_final_profit": float(np.std(final_profits)),
        "mean_avg_profit": float(np.mean(final_averages)),
        "std_avg_profit": float(np.std(final_averages)),
        "pct_profitable": float(np.mean(final_profits > 0) * 100),
        "worst_case": float(np.min(final_profits)),
        "best_case": float(np.max(final_profits)),
        "cumulative_profits": cumulative_profits,
        "average_profits": average_profits,
    }


# Simulate: true prob = 0.55, buy at 0.45 (10% edge)
results = simulate_trades(
    true_prob=0.55,
    market_price=0.45,
    n_trades=500,
    n_simulations=10000
)

print("=== Monte Carlo Simulation: 500 Trades with 10% Edge ===")
print(f"  EV per trade:        ${results['ev_per_trade']:.4f}")
print(f"  Total EV (500):      ${results['total_ev']:.2f}")
print(f"  Mean final profit:   ${results['mean_final_profit']:.2f}")
print(f"  Std of final profit: ${results['std_final_profit']:.2f}")
print(f"  % simulations profitable: {results['pct_profitable']:.1f}%")
print(f"  Worst simulation:    ${results['worst_case']:.2f}")
print(f"  Best simulation:     ${results['best_case']:.2f}")

# LLN demonstration: average profit converges to EV
print(f"\n  Mean avg profit per trade: ${results['mean_avg_profit']:.4f}")
print(f"  Std avg profit per trade:  ${results['std_avg_profit']:.4f}")
print(f"  True EV per trade:         ${results['ev_per_trade']:.4f}")

# CLT: predicted std of average should be sigma/sqrt(n)
sigma = np.sqrt(0.55 * 0.45)  # std of single Bernoulli outcome
predicted_std = sigma / np.sqrt(500)
print(f"\n  CLT predicted std of average: ${predicted_std:.4f}")
print(f"  Observed std of average:      ${results['std_avg_profit']:.4f}")

The simulation confirms both laws: - LLN: The mean average profit per trade converges to the true EV of $0.10. - CLT: The standard deviation of the average profit matches the CLT prediction of $\sigma / \sqrt{n}$.


3.10 Bayesian Updating in Practice

Sequential Updating

In real prediction markets, information arrives sequentially. A price at 8 AM is updated by news at 9 AM, then again by a report at 11 AM, then by a speech at 2 PM. Each update is a Bayesian update where the previous posterior becomes the new prior.

$$P(H|E_1, E_2, E_3) = P(H|E_1) \xrightarrow{E_2} P(H|E_1, E_2) \xrightarrow{E_3} P(H|E_1, E_2, E_3)$$

The posterior after one piece of evidence becomes the prior for the next. This is called sequential Bayesian updating, and it is mathematically equivalent to updating on all evidence simultaneously (provided the evidence is conditionally independent given the hypothesis).

Conjugate Priors: The Beta-Binomial Model

A conjugate prior is a prior distribution that, when combined with a particular likelihood, produces a posterior of the same distributional family. The most important conjugate pair for prediction markets is the Beta-Binomial model.

Setup: - Prior: $p \sim \text{Beta}(\alpha, \beta)$ --- your belief about the true probability p - Likelihood: $X \sim \text{Binomial}(n, p)$ --- you observe X successes in n trials - Posterior: $p | X \sim \text{Beta}(\alpha + X, \beta + n - X)$

The update rule is astonishingly simple: add the number of successes to $\alpha$, and the number of failures to $\beta$.

Example: You are uncertain about the true probability of a recurring prediction market event (e.g., "Will the monthly jobs report beat expectations?").

  • Your prior belief: Beta(2, 3) --- you think the probability is around 2/(2+3) = 0.40, but you're not very sure.
  • You observe 7 months of data: 5 beats, 2 misses.
  • Posterior: Beta(2+5, 3+2) = Beta(7, 5), with mean 7/(7+5) = 0.583.

The data pulled your belief from 0.40 toward the observed frequency of 5/7 = 0.714. The posterior (0.583) is a compromise between prior and data, weighted by how much data you have versus how strong your prior was.

"""
Beta-Binomial conjugate updating for prediction markets.
"""
from scipy import stats
import numpy as np


class BetaBinomialUpdater:
    """Bayesian updater using Beta-Binomial conjugate model.

    Models uncertainty about the true probability of a binary event.

    Attributes:
        alpha: Alpha parameter of the Beta prior.
        beta: Beta parameter of the Beta prior.
        history: List of (description, alpha, beta) tuples.
    """

    def __init__(self, alpha: float = 1.0, beta: float = 1.0,
                 name: str = "Event") -> None:
        """Initialize with a Beta prior.

        Args:
            alpha: Prior successes + 1. Default 1 (uniform prior).
            beta: Prior failures + 1. Default 1 (uniform prior).
            name: Description of the event.
        """
        self.alpha = alpha
        self.beta = beta
        self.name = name
        self.history: list[tuple[str, float, float]] = [
            ("Prior", alpha, beta)
        ]

    @property
    def mean(self) -> float:
        """Posterior mean (point estimate of probability)."""
        return self.alpha / (self.alpha + self.beta)

    @property
    def mode(self) -> float:
        """Posterior mode (most likely probability value)."""
        if self.alpha > 1 and self.beta > 1:
            return (self.alpha - 1) / (self.alpha + self.beta - 2)
        return self.mean  # Fallback for edge cases

    @property
    def variance(self) -> float:
        """Posterior variance (uncertainty about probability)."""
        a, b = self.alpha, self.beta
        return (a * b) / ((a + b) ** 2 * (a + b + 1))

    @property
    def std(self) -> float:
        """Posterior standard deviation."""
        return np.sqrt(self.variance)

    def credible_interval(self, confidence: float = 0.90) -> tuple[float, float]:
        """Calculate a credible interval for the probability.

        Args:
            confidence: Confidence level (e.g., 0.90 for 90%).

        Returns:
            Tuple of (lower, upper) bounds.
        """
        tail = (1 - confidence) / 2
        dist = stats.beta(self.alpha, self.beta)
        return (float(dist.ppf(tail)), float(dist.ppf(1 - tail)))

    def update(self, successes: int, failures: int,
               description: str = "Observation") -> None:
        """Update the posterior with new observations.

        Args:
            successes: Number of successes observed.
            failures: Number of failures observed.
            description: Label for this update.
        """
        self.alpha += successes
        self.beta += failures
        self.history.append((description, self.alpha, self.beta))

    def print_summary(self) -> None:
        """Print current posterior summary."""
        ci = self.credible_interval(0.90)
        print(f"\n=== {self.name} ===")
        print(f"  Posterior: Beta({self.alpha:.1f}, {self.beta:.1f})")
        print(f"  Mean:     {self.mean:.4f}")
        print(f"  Std:      {self.std:.4f}")
        print(f"  90% CI:   [{ci[0]:.4f}, {ci[1]:.4f}]")

    def print_history(self) -> None:
        """Print full update history."""
        print(f"\n--- Update History: {self.name} ---")
        for desc, a, b in self.history:
            mean = a / (a + b)
            print(f"  {desc:30s}  Beta({a:5.1f}, {b:5.1f})  "
                  f"Mean={mean:.4f}")


# Example: Jobs report beating expectations
updater = BetaBinomialUpdater(
    alpha=2, beta=3,
    name="Jobs Report Beats Expectations"
)

# Observe monthly data sequentially
monthly_results = [
    ("January: Beat", 1, 0),
    ("February: Miss", 0, 1),
    ("March: Beat", 1, 0),
    ("April: Beat", 1, 0),
    ("May: Miss", 0, 1),
    ("June: Beat", 1, 0),
    ("July: Beat", 1, 0),
]

for desc, s, f in monthly_results:
    updater.update(s, f, desc)

updater.print_history()
updater.print_summary()

How Prediction Market Prices Implicitly Perform Bayesian Updating

Prediction market prices naturally perform Bayesian updating, even though no single participant may be explicitly computing Bayes' theorem. Here is how it works:

  1. The price is the prior. At any moment, the market price reflects the consensus probability, aggregating all available information and all participants' beliefs.

  2. News arrives (evidence). When new information becomes available, traders who process it faster or better update their personal beliefs.

  3. Trading is the updating mechanism. Traders who believe the new information raises the probability buy contracts, pushing the price up. Traders who believe it lowers the probability sell, pushing the price down.

  4. The new price is the posterior. After trading activity settles, the new price reflects the market's updated consensus probability, incorporating the new evidence.

This process is sometimes called the market as a Bayesian computer. The remarkable finding from decades of research is that prediction markets often update more efficiently than any individual participant, because they aggregate diverse information from many sources.

Common Pitfall: Markets are not perfectly Bayesian. They can overreact to dramatic news (recency bias), underreact to abstract statistical evidence, and exhibit herding behavior. Understanding Bayes' theorem gives you a normative benchmark --- you can evaluate whether the market's update was too large or too small relative to what the evidence warrants, and trade accordingly.


3.11 Practical Considerations

Numerical Precision

When working with probabilities in code, floating-point arithmetic can cause problems:

"""
Numerical precision issues with probabilities.
"""

# Problem: floating point arithmetic
p1, p2, p3 = 0.1, 0.2, 0.3
print(f"0.1 + 0.2 == 0.3? {p1 + p2 == p3}")  # False!
print(f"0.1 + 0.2 = {p1 + p2}")  # 0.30000000000000004

# Solution: use tolerance-based comparison
import math
print(f"math.isclose(0.1 + 0.2, 0.3)? {math.isclose(p1 + p2, p3)}")  # True

# Solution for accumulation: use Decimal for financial calculations
from decimal import Decimal, getcontext
getcontext().prec = 10
d1, d2, d3 = Decimal("0.1"), Decimal("0.2"), Decimal("0.3")
print(f"Decimal: 0.1 + 0.2 == 0.3? {d1 + d2 == d3}")  # True

Log-Probabilities

When multiplying many small probabilities (as in sequential Bayesian updating with many pieces of evidence), the product can underflow to zero. The solution is to work in log space:

$$\log P(A \cap B) = \log P(A) + \log P(B|A)$$

Addition in log space equals multiplication in probability space. This avoids underflow.

"""
Log-probabilities for numerical stability.
"""
import math

# Problem: multiplying many small probabilities
probs = [0.01] * 100  # 100 events each with probability 0.01
product = 1.0
for p in probs:
    product *= p
print(f"Direct multiplication: {product}")  # 0.0 (underflow!)

# Solution: log-probabilities
log_sum = sum(math.log(p) for p in probs)
print(f"Log-probability sum: {log_sum:.2f}")  # -460.52
print(f"Recovered probability: {math.exp(log_sum):.2e}")  # Still 0 in float

# For comparison and ranking, log-probabilities are sufficient
# and avoid the underflow problem entirely.

# Log-sum-exp trick for combining log-probabilities
def log_sum_exp(log_probs: list[float]) -> float:
    """Compute log(sum(exp(log_probs))) numerically stably.

    This is used when you need to add probabilities that are
    stored in log space (e.g., for the evidence term in Bayes).
    """
    max_log = max(log_probs)
    return max_log + math.log(
        sum(math.exp(lp - max_log) for lp in log_probs)
    )

# Example: log P(A) = -2, log P(B) = -3
# We want log(P(A) + P(B))
result = log_sum_exp([-2.0, -3.0])
print(f"\nlog(e^-2 + e^-3) = {result:.4f}")
print(f"Direct: {math.log(math.exp(-2) + math.exp(-3)):.4f}")

Handling Extreme Probabilities

Prediction markets sometimes have contracts priced very near 0 or 1 (e.g., $0.01 or $0.99). Special care is needed:

  1. Do not use probability = 0 or 1. In Bayesian analysis, assigning probability 0 or 1 to any hypothesis means no amount of evidence can change your mind. This is called Cromwell's rule (after Oliver Cromwell's plea to the Church of Scotland: "I beseech you, in the bowels of Christ, think it possible that you may be mistaken").

  2. Clamp probabilities to a range like [0.001, 0.999] for numerical stability.

  3. Be aware of the overconfidence trap. A contract at $0.99 means one participant is willing to risk $99 to win $1. This is only rational if the true probability really is above 99%. Small calibration errors at extreme probabilities lead to large expected losses.

"""
Safe probability handling for extreme values.
"""

def safe_probability(p: float,
                     min_p: float = 0.001,
                     max_p: float = 0.999) -> float:
    """Clamp probability to avoid numerical issues.

    Args:
        p: Raw probability value.
        min_p: Minimum allowed probability.
        max_p: Maximum allowed probability.

    Returns:
        Clamped probability value.
    """
    return max(min_p, min(max_p, p))


def safe_log_odds(p: float) -> float:
    """Convert probability to log-odds safely.

    Log-odds (logit) transform: log(p / (1-p))
    Useful for Bayesian updating in log space.
    """
    p = safe_probability(p)
    return math.log(p / (1 - p))


def logit_to_probability(logit: float) -> float:
    """Convert log-odds back to probability.

    Inverse logit (sigmoid): 1 / (1 + exp(-logit))
    """
    return 1.0 / (1.0 + math.exp(-logit))


# Example: updating extreme probabilities
p = 0.99
log_odds = safe_log_odds(p)
print(f"P = {p}, log-odds = {log_odds:.4f}")

# Update with evidence (likelihood ratio = 0.5, evidence against)
lr = 0.5
new_log_odds = log_odds + math.log(lr)
new_p = logit_to_probability(new_log_odds)
print(f"After evidence (LR={lr}): P = {new_p:.4f}")

3.12 Chapter Summary

Key Concepts

This chapter covered the mathematical foundations that make prediction markets possible:

  1. Sample spaces and events define the universe of possibilities and the subsets we assign probabilities to.
  2. Kolmogorov's axioms (non-negativity, normalization, additivity) are the foundation from which all probability rules derive.
  3. Conditional probability captures how probabilities change when we learn new information --- the mathematical core of every market price movement.
  4. Bayes' theorem provides the exact recipe for updating beliefs given evidence: posterior = (likelihood x prior) / evidence.
  5. Independence determines whether knowing one event informs you about another, which is critical for portfolio risk assessment.
  6. Random variables and distributions (Bernoulli, Binomial, Beta, Normal) give us the mathematical machinery to model prediction market outcomes.
  7. Expected value tells you whether a trade is worth making: EV = q - p for a binary contract.
  8. Variance tells you how much risk you are taking: Var = p(1-p) for a Bernoulli outcome.
  9. The LLN and CLT explain why EV strategies work in the long run and why profit distributions become normal.
  10. Bayesian updating --- especially via the Beta-Binomial model --- is how rational agents (and efficient markets) process sequential information.

Key Formulas

Formula Expression Use
Complement $P(A^c) = 1 - P(A)$ NO contract pricing
Addition $P(A \cup B) = P(A) + P(B) - P(A \cap B)$ Overlapping market events
Conditional $P(A \mid B) = P(A \cap B) / P(B)$ Price reaction to news
Bayes' theorem $P(A \mid B) = P(B \mid A) P(A) / P(B)$ Belief updating
Independence $P(A \cap B) = P(A) P(B)$ Portfolio diversification
Expected value $EV = q - p$ Trade evaluation
Bernoulli variance $\text{Var} = p(1-p)$ Risk assessment
Beta update $\text{Beta}(\alpha + s, \beta + f)$ Sequential learning

Key Code Patterns

  1. ProbabilityCalculator: Use static methods with validation for fundamental operations.
  2. BayesianUpdater: Maintain state (current probability) and history; update with Evidence objects.
  3. BetaBinomialUpdater: Track alpha and beta; update by adding successes and failures.
  4. Monte Carlo simulation: Use NumPy vectorization; run thousands of simulations to validate analytical results.
  5. Numerical safety: Clamp extreme probabilities; use log-probabilities for products of many small numbers.

Decision Framework

When analyzing a prediction market contract, follow this sequence:

  1. Define the sample space. What are all the possible outcomes?
  2. Assess the probability. What is your estimate of the true probability?
  3. Compare to market price. Is there an edge (q - p)?
  4. Consider the evidence. What information supports your estimate? Use Bayes' theorem to justify your probability.
  5. Evaluate the risk. What is the variance? Can you afford the maximum loss?
  6. Check independence. If you hold other positions, how does this trade correlate with them?
  7. Think long-term. If you make many trades like this, does the LLN guarantee convergence to a positive outcome?

What's Next

In Chapter 4: Statistics for Market Analysis, we will move from probability theory to statistical practice. You will learn how to estimate probabilities from historical data, test whether your trading strategy is statistically significant, build confidence intervals for your edge, and assess the calibration of prediction market prices. Where this chapter gave you the mathematical toolkit, the next chapter will teach you how to wield it with real data.


Glossary of Notation

Symbol Meaning
S, $\Omega$ Sample space
A, B Events (subsets of sample space)
$A^c$ Complement of A
$A \cup B$ Union of A and B
$A \cap B$ Intersection of A and B
$P(A)$ Probability of A
$P(A \mid B)$ Conditional probability of A given B
$E[X]$ Expected value of random variable X
$\text{Var}(X)$ Variance of X
$\sigma$ Standard deviation
$\alpha, \beta$ Parameters of the Beta distribution
$\mu$ Mean
$\sim$ "is distributed as"