> "The only free lunch in finance is diversification." -- Harry Markowitz
In This Chapter
- 17.1 Why Portfolio Thinking Matters
- 17.2 Correlation in Prediction Markets
- 17.3 Portfolio Kelly Criterion
- 17.4 Position Sizing Rules
- 17.5 Diversification Strategies
- 17.6 Risk Metrics for Prediction Portfolios
- 17.7 Monte Carlo Simulation
- 17.8 Drawdown Management
- 17.9 Bankroll Management
- 17.10 Rebalancing and Portfolio Maintenance
- 17.11 Stress Testing
- 17.12 Chapter Summary
- What's Next
Chapter 17: Portfolio Construction and Risk Management
"The only free lunch in finance is diversification." -- Harry Markowitz
In previous chapters, we studied how to identify edge in individual prediction markets and how to size individual bets using the Kelly Criterion. But real prediction market trading rarely involves placing a single wager and waiting for its resolution. In practice, serious traders maintain portfolios of dozens or even hundreds of simultaneous positions across different markets, platforms, and event types. This chapter bridges the gap between single-trade analysis and the management of an entire prediction market portfolio.
We will cover the mathematics of portfolio diversification adapted to binary outcomes, extend the Kelly Criterion to simultaneous bets, build Monte Carlo simulations to stress-test our portfolios, and develop practical systems for managing drawdowns and bankroll. By the end, you will have the tools to construct, monitor, and protect a prediction market portfolio that balances growth with survival.
17.1 Why Portfolio Thinking Matters
The Limitations of Single-Trade Thinking
Imagine you have identified what you believe is a mispriced prediction market contract. The event has a true probability of 60%, but the market offers a price of $0.45 (implying 45%). You know from Chapter 14 that the Kelly Criterion suggests a bet size of approximately 27% of your bankroll on such an opportunity:
$$f^* = \frac{bp - q}{b} = \frac{\frac{0.55}{0.45} \times 0.60 - 0.40}{\frac{0.55}{0.45}} \approx 0.27$$
You place the bet and wait. The event either resolves Yes or No. If Yes, you earn a healthy profit. If No, you lose 27% of your bankroll in a single trade.
This is single-trade thinking: analyzing one opportunity in isolation, sizing it according to its individual merit, and living with the result. While not wrong in principle, this approach ignores critical realities:
-
Variance is enormous on single binary outcomes. Unlike continuous-return assets where you might lose 2-5% on a bad day, a binary outcome can wipe out your entire stake instantly.
-
Opportunities arise concurrently. While waiting for one election to resolve, you might see mispriced contracts on sporting events, economic indicators, and policy decisions.
-
Correlations exist between markets. If you hold positions in five different U.S. Senate races, those outcomes are not independent. A national political wave can cause all five to move against you simultaneously.
-
Bankroll is finite. If you Kelly-size each bet independently, you can easily end up wanting to wager more than 100% of your bankroll across simultaneous positions.
The Portfolio Approach
Portfolio thinking treats your collection of prediction market positions as a unified whole. Instead of asking "How much should I bet on this single market?" you ask "How should I allocate my total bankroll across all available opportunities, given their individual edges, their correlations, and my risk tolerance?"
This shift in perspective unlocks several advantages:
Reduced variance through diversification. If you hold 50 uncorrelated positions, each with positive expected value, the chance that a majority resolve against you is far smaller than the chance of losing any individual bet. The law of large numbers begins to work in your favor.
Better capital utilization. Instead of having most of your bankroll sitting idle while one large bet resolves, you can deploy capital across many opportunities simultaneously, increasing expected returns per unit of time.
Systematic risk management. With a portfolio view, you can measure and control aggregate risk metrics like maximum drawdown, Value at Risk, and risk of ruin -- concepts that are meaningless for a single binary bet.
Emotional stability. A diversified portfolio produces smoother returns over time, reducing the psychological toll of inevitable losses on individual positions.
Unique Challenges in Prediction Markets
Portfolio construction for prediction markets differs from traditional finance in several important ways:
Binary outcomes. Most prediction market contracts resolve to either $0 or $1, creating a discrete return distribution rather than the continuous returns seen in stock and bond portfolios. This fundamentally changes how we measure correlation and compute optimal allocations.
Resolution timing. Prediction market contracts have definite resolution dates, unlike equities that can be held indefinitely. This creates natural portfolio turnover and means positions "decay" as events resolve.
Illiquidity. Many prediction markets have limited depth, meaning large positions can move prices significantly. This constrains maximum position sizes and makes rebalancing costly.
Platform risk. Capital spread across multiple prediction market platforms faces counterparty risk. A platform failure can cause losses regardless of whether your predictions were correct.
Event clustering. Certain periods (elections, end of fiscal years, major sporting events) create clusters of simultaneous opportunities that may be highly correlated with each other.
These challenges make prediction market portfolio management both more difficult and more rewarding than simply applying textbook portfolio theory. We need adapted tools, and that is what this chapter provides.
17.2 Correlation in Prediction Markets
Why Correlation Matters
Correlation is the cornerstone of portfolio construction. Two prediction market positions that are perfectly positively correlated provide zero diversification benefit -- if one loses, the other loses too. Conversely, uncorrelated or negatively correlated positions can dramatically reduce portfolio variance while maintaining expected returns.
In traditional finance, correlation is measured between continuous return streams. In prediction markets, we need to think about correlation between binary events: the degree to which knowing the outcome of Event A tells us something about the outcome of Event B.
Measuring Correlation Between Binary Events
For two binary random variables $X$ and $Y$ with probabilities $p_X = P(X=1)$ and $p_Y = P(Y=1)$, the Pearson correlation coefficient is:
$$\rho_{XY} = \frac{P(X=1, Y=1) - p_X \cdot p_Y}{\sqrt{p_X(1-p_X) \cdot p_Y(1-p_Y)}}$$
This requires knowing the joint probability $P(X=1, Y=1)$. In practice, estimating joint probabilities is the hard part of correlation analysis in prediction markets.
The correlation is bounded by the marginal probabilities. For two binary events with probabilities $p_X$ and $p_Y$, the maximum possible correlation is:
$$\rho_{max} = \sqrt{\frac{\min(p_X, p_Y) \cdot (1 - \max(p_X, p_Y))}{\max(p_X, p_Y) \cdot (1 - \min(p_X, p_Y))}}$$
And the minimum possible correlation is:
$$\rho_{min} = -\sqrt{\frac{\min(p_X, 1-p_Y) \cdot \min(1-p_X, p_Y)}{\max(p_X, 1-p_Y) \cdot \max(1-p_X, p_Y)}}$$
These bounds are more restrictive than the usual $[-1, 1]$ range for continuous variables. For example, if $p_X = 0.9$ and $p_Y = 0.1$, the maximum positive correlation is only about 0.33, not 1.0.
Sources of Correlation in Prediction Markets
Political correlations. Senate races in the same election cycle share common factors: national mood, presidential coattails, economic conditions. A strong night for one party usually means a strong night across multiple races. In the 2020 U.S. elections, prediction markets learned this the hard way when correlated positions in multiple swing states all moved in the same direction.
Conditional markets. Some markets are explicitly linked. "Will X win the primary?" and "Will X win the general election?" are correlated because winning the general is conditional on winning the primary.
Macroeconomic linkages. "Will GDP growth exceed 3%?" and "Will unemployment fall below 4%?" are correlated through the underlying economic cycle.
Sporting correlations. In a tournament bracket, if Team A upsets Team B in Round 1, it changes the probabilities of all downstream matches involving those slots.
Weather and seasonal correlations. "Will it snow in New York on Christmas?" and "Will December temperatures be below average?" share a common weather driver.
Building a Correlation Matrix
For a portfolio of $n$ prediction market contracts, we need an $n \times n$ correlation matrix $\Sigma$. Each entry $\rho_{ij}$ captures the correlation between events $i$ and $j$.
In practice, you often cannot estimate correlations from historical data because prediction market events are typically one-time occurrences. Instead, you must rely on:
-
Structural analysis. Identify common causal factors. Two Senate races in the same state share more common factors than races in different states.
-
Conditional probability estimation. Estimate $P(Y=1 | X=1)$ and $P(Y=1 | X=0)$ through careful reasoning, then back out the joint probability and correlation.
-
Market-implied correlations. If a platform offers both individual and joint contracts (e.g., "Party wins both Senate and Presidency"), you can back out the implied correlation.
-
Historical analogues. While specific events do not repeat, you can study correlations between similar past event types (e.g., how correlated were Senate races in previous election cycles?).
-
Expert judgment. Sometimes your best estimate comes from domain expertise about the causal structure linking events.
Python Correlation Estimator
import numpy as np
from itertools import combinations
def binary_correlation(p_x, p_y, p_joint):
"""
Calculate Pearson correlation between two binary events.
Parameters:
p_x: P(X=1), marginal probability of event X
p_y: P(Y=1), marginal probability of event Y
p_joint: P(X=1, Y=1), joint probability of both events
Returns:
Pearson correlation coefficient
"""
numerator = p_joint - p_x * p_y
denominator = np.sqrt(p_x * (1 - p_x) * p_y * (1 - p_y))
if denominator == 0:
return 0.0
return numerator / denominator
def correlation_bounds(p_x, p_y):
"""
Compute the feasible range of correlations for two binary events.
"""
# Joint probability bounds (Frechet bounds)
p_joint_max = min(p_x, p_y)
p_joint_min = max(0, p_x + p_y - 1)
denom = np.sqrt(p_x * (1 - p_x) * p_y * (1 - p_y))
if denom == 0:
return 0.0, 0.0
rho_max = (p_joint_max - p_x * p_y) / denom
rho_min = (p_joint_min - p_x * p_y) / denom
return rho_min, rho_max
def build_correlation_matrix(probabilities, conditional_probs=None):
"""
Build a correlation matrix for a set of binary events.
Parameters:
probabilities: list of marginal probabilities for each event
conditional_probs: dict mapping (i, j) -> P(j=1 | i=1)
If None, assume independence.
Returns:
Correlation matrix as numpy array
"""
n = len(probabilities)
corr_matrix = np.eye(n)
for i, j in combinations(range(n), 2):
p_i = probabilities[i]
p_j = probabilities[j]
if conditional_probs and (i, j) in conditional_probs:
# P(j=1 | i=1) is given
p_j_given_i = conditional_probs[(i, j)]
p_joint = p_j_given_i * p_i
elif conditional_probs and (j, i) in conditional_probs:
p_i_given_j = conditional_probs[(j, i)]
p_joint = p_i_given_j * p_j
else:
# Assume independence
p_joint = p_i * p_j
rho = binary_correlation(p_i, p_j, p_joint)
corr_matrix[i, j] = rho
corr_matrix[j, i] = rho
return corr_matrix
# Example: Three Senate races with shared national factors
probs = [0.55, 0.60, 0.45] # Marginal probabilities
# Conditional: if Dem wins race 0, 70% chance they win race 1
cond = {(0, 1): 0.70, (0, 2): 0.35, (1, 2): 0.35}
corr = build_correlation_matrix(probs, cond)
print("Correlation matrix:")
print(np.round(corr, 3))
This code lets you build the correlation structures that feed into portfolio optimization, which we turn to next.
17.3 Portfolio Kelly Criterion
From Single Kelly to Portfolio Kelly
In Chapter 14, we derived the Kelly Criterion for a single bet:
$$f^* = \frac{p \cdot b - q}{b}$$
where $p$ is the true probability, $q = 1 - p$, and $b$ is the odds received. This maximizes the expected logarithmic growth rate of your bankroll.
When you have multiple simultaneous betting opportunities, you cannot simply apply single-bet Kelly to each one independently. The reasons are:
-
Budget constraint. The sum of all positions cannot exceed your total bankroll (and arguably should remain well below it).
-
Correlation effects. Correlated bets compound risk. If two bets are perfectly correlated, holding both is equivalent to doubling down on one, which would over-bet relative to Kelly.
-
Interaction effects. The optimal size for Bet A depends on how much you have allocated to Bet B, because your effective bankroll for any remaining bet shrinks as you allocate to other bets.
The Simultaneous Kelly Problem
For $n$ simultaneous binary bets, we want to find the vector of bet fractions $\mathbf{f} = (f_1, f_2, \ldots, f_n)$ that maximizes the expected log growth of the bankroll:
$$\max_{\mathbf{f}} \; E\left[\log\left(1 + \sum_{i=1}^{n} f_i \cdot R_i\right)\right]$$
where $R_i$ is the random return on bet $i$: - $R_i = b_i$ if event $i$ occurs (with probability $p_i$) - $R_i = -1$ if event $i$ does not occur (with probability $q_i = 1-p_i$)
Subject to: - $f_i \geq 0$ for all $i$ (no short selling, or allow it if the platform permits) - $\sum_{i=1}^{n} f_i \leq 1$ (budget constraint)
For $n$ binary events, there are $2^n$ possible joint outcomes. The expected log growth is:
$$G(\mathbf{f}) = \sum_{s=1}^{2^n} P(s) \cdot \log\left(1 + \sum_{i=1}^{n} f_i \cdot R_i(s)\right)$$
where $P(s)$ is the probability of joint outcome state $s$, and $R_i(s)$ is the return on bet $i$ in state $s$.
This is a concave optimization problem (the log of a linear function is concave), so any local maximum is the global maximum. Standard convex optimization solvers can handle it efficiently.
Accounting for Correlations
The joint probabilities $P(s)$ encode all correlation information. For two bets with marginal probabilities $p_1, p_2$ and correlation $\rho$:
$$P(X_1=1, X_2=1) = p_1 p_2 + \rho \sqrt{p_1 q_1 p_2 q_2}$$ $$P(X_1=1, X_2=0) = p_1 q_2 - \rho \sqrt{p_1 q_1 p_2 q_2}$$ $$P(X_1=0, X_2=1) = q_1 p_2 - \rho \sqrt{p_1 q_1 p_2 q_2}$$ $$P(X_1=0, X_2=0) = q_1 q_2 + \rho \sqrt{p_1 q_1 p_2 q_2}$$
For larger portfolios, generating the full $2^n$ joint distribution from a correlation matrix requires multivariate modeling. A common approach is to use a Gaussian copula:
- Generate $n$ correlated standard normal variables $Z_1, \ldots, Z_n$ using the correlation matrix.
- Convert each $Z_i$ to a binary outcome: $X_i = 1$ if $Z_i < \Phi^{-1}(p_i)$, else $X_i = 0$.
This preserves the marginal probabilities and approximately matches the specified correlations.
Numerical Optimization Approach
For portfolios beyond a few positions, the $2^n$ enumeration becomes impractical. Instead, we use sampling-based optimization:
- Generate $M$ samples from the joint distribution of outcomes (e.g., $M = 10{,}000$).
- For each candidate allocation $\mathbf{f}$, compute the sample average of $\log(1 + \sum f_i R_i)$.
- Use a numerical optimizer to find the $\mathbf{f}$ that maximizes this sample average.
import numpy as np
from scipy.optimize import minimize
from scipy.stats import norm
def generate_correlated_binary(probabilities, corr_matrix, n_samples=10000):
"""
Generate correlated binary outcomes using a Gaussian copula.
Parameters:
probabilities: array of marginal probabilities
corr_matrix: correlation matrix for the Gaussian copula
n_samples: number of samples to generate
Returns:
Binary outcome matrix of shape (n_samples, n_events)
"""
n_events = len(probabilities)
# Generate correlated normals
L = np.linalg.cholesky(corr_matrix)
Z = np.random.randn(n_samples, n_events) @ L.T
# Convert to binary using inverse CDF thresholds
thresholds = norm.ppf(probabilities)
outcomes = (Z < thresholds).astype(float)
return outcomes
def portfolio_kelly(probabilities, market_prices, corr_matrix,
n_samples=50000, fractional_kelly=0.5):
"""
Compute optimal portfolio Kelly fractions for correlated binary bets.
Parameters:
probabilities: array of true probabilities for each event
market_prices: array of market prices (cost to buy Yes)
corr_matrix: correlation matrix between events
n_samples: Monte Carlo samples for optimization
fractional_kelly: fraction of full Kelly to use (0.5 = half Kelly)
Returns:
Optimal bet fractions for each market
"""
n = len(probabilities)
# Generate correlated outcome scenarios
outcomes = generate_correlated_binary(probabilities, corr_matrix, n_samples)
# Returns matrix: profit per dollar wagered
# If event occurs: win (1 - price) / price per unit bet fraction
# If event doesn't: lose 1.0 per unit bet fraction
# Bet fraction f_i means we spend f_i * bankroll to buy at price_i
# We get f_i * (1/price_i) shares, each paying $1 if Yes
# Profit if Yes: f_i * (1 - price_i) / price_i
# Loss if No: -f_i
returns = np.zeros_like(outcomes)
for i in range(n):
price = market_prices[i]
win_return = (1 - price) / price # Return per unit risked
returns[:, i] = np.where(outcomes[:, i] == 1, win_return, -1.0)
def neg_expected_log_growth(fractions):
"""Negative expected log growth (to minimize)."""
portfolio_returns = 1.0 + returns @ fractions
# Avoid log of non-positive numbers
portfolio_returns = np.maximum(portfolio_returns, 1e-10)
return -np.mean(np.log(portfolio_returns))
# Constraints: fractions >= 0, sum of fractions <= 1
bounds = [(0, 0.25) for _ in range(n)] # Cap individual positions at 25%
constraints = [{'type': 'ineq', 'fun': lambda f: 1.0 - np.sum(f)}]
# Initial guess: equal weight scaled down
x0 = np.full(n, min(0.02, 0.8 / n))
result = minimize(neg_expected_log_growth, x0,
method='SLSQP', bounds=bounds,
constraints=constraints,
options={'maxiter': 1000, 'ftol': 1e-12})
# Apply fractional Kelly
optimal_fractions = result.x * fractional_kelly
return optimal_fractions
# Example usage
probs = np.array([0.60, 0.55, 0.70, 0.45])
prices = np.array([0.45, 0.48, 0.60, 0.35])
corr = np.array([
[1.0, 0.3, 0.1, 0.0],
[0.3, 1.0, 0.1, 0.0],
[0.1, 0.1, 1.0, 0.2],
[0.0, 0.0, 0.2, 1.0]
])
fractions = portfolio_kelly(probs, prices, corr, fractional_kelly=0.5)
print("Optimal half-Kelly fractions:")
for i, f in enumerate(fractions):
edge = probs[i] - prices[i]
print(f" Market {i+1}: {f:.4f} (edge: {edge:+.2f})")
print(f" Total allocation: {sum(fractions):.4f}")
Key Insights from Portfolio Kelly
Several important patterns emerge from portfolio Kelly optimization:
Correlated bets get smaller allocations. When two bets share positive correlation, the optimizer reduces the size of both to avoid concentrated risk. If two bets with identical edge are perfectly correlated, the optimal allocation is roughly the same as for a single bet, split between them.
Uncorrelated bets are additive. With zero correlation, you can allocate close to the individual Kelly fraction for each bet, limited mainly by the budget constraint. This is the diversification benefit in action.
Negative correlation is a gift. If two positive-edge bets are negatively correlated, the optimizer may allocate more than the individual Kelly fraction to each, because their combined variance is lower than either alone.
Fractional Kelly is even more important in portfolios. Estimation errors in probabilities and correlations compound across a portfolio. Using half-Kelly or even quarter-Kelly provides a crucial buffer against model misspecification.
17.4 Position Sizing Rules
Overview of Position Sizing Approaches
While Kelly is theoretically optimal, practical position sizing requires additional rules and constraints. Here we survey the major approaches and their application to prediction market portfolios.
Fixed Fraction
The simplest approach: allocate the same fraction of your bankroll to every bet.
$$f_i = \frac{c}{n}$$
where $c$ is the total allocation (e.g., 0.5 for investing 50% of your bankroll) and $n$ is the number of positions.
Advantages: Simple, avoids overconcentration, no estimation required.
Disadvantages: Ignores edge size, treats all bets equally, wastes capital on marginal opportunities.
Kelly-Based Sizing
Use the full or fractional Kelly Criterion, adjusted for portfolio effects as described in Section 17.3.
Advantages: Theoretically optimal for long-run growth, allocates more to higher-edge bets.
Disadvantages: Requires accurate probability estimates, can produce aggressive allocations, sensitive to estimation errors.
Volatility Targeting
Set position sizes so that each position contributes equal volatility (risk) to the portfolio. For a binary bet at price $p$ with position size $f$:
$$\text{Volatility contribution} = f \cdot \sqrt{p(1-p)} \cdot \frac{1}{p}$$
Scale each $f_i$ so that volatility contributions are equalized:
$$f_i \propto \frac{1}{\sqrt{p_i(1-p_i)} / p_i}$$
Advantages: Balances risk across positions, prevents extreme-odds bets from dominating.
Disadvantages: Ignores edge, may undersize high-edge opportunities.
Maximum Position Limits
Regardless of the sizing method used, impose hard caps on individual and aggregate positions:
- Individual position cap: No single bet exceeds X% of bankroll (common: 2-5%).
- Aggregate allocation cap: Total invested capital does not exceed Y% of bankroll (common: 50-80%).
- Correlated group cap: Total allocation to a correlated group does not exceed Z% (common: 10-20%).
- Platform cap: No more than W% of capital on any single platform (common: 25-40%).
Practical Position Sizing Framework
In practice, the best approach combines multiple methods:
- Start with Kelly-based sizing for each opportunity.
- Apply fractional Kelly (typically 25-50% of full Kelly).
- Impose individual position caps.
- Check correlated group limits and scale down proportionally if exceeded.
- Verify the aggregate allocation stays within budget.
- Apply platform diversification constraints.
import numpy as np
class PositionSizer:
"""
Practical position sizing for prediction market portfolios.
Combines Kelly-based sizing with risk limits.
"""
def __init__(self, bankroll, max_individual=0.05, max_aggregate=0.70,
max_correlated_group=0.15, max_platform=0.35,
kelly_fraction=0.5):
self.bankroll = bankroll
self.max_individual = max_individual
self.max_aggregate = max_aggregate
self.max_correlated_group = max_correlated_group
self.max_platform = max_platform
self.kelly_fraction = kelly_fraction
def single_kelly(self, true_prob, market_price):
"""Compute single-bet Kelly fraction."""
if true_prob <= market_price:
return 0.0 # No edge
b = (1 - market_price) / market_price
q = 1 - true_prob
kelly = (b * true_prob - q) / b
return max(0, kelly * self.kelly_fraction)
def size_portfolio(self, opportunities):
"""
Size a portfolio of opportunities.
Parameters:
opportunities: list of dicts with keys:
'name', 'true_prob', 'market_price',
'correlation_group', 'platform'
Returns:
List of dicts with added 'fraction' and 'dollar_amount' keys
"""
# Step 1: Compute Kelly-based sizes
for opp in opportunities:
opp['raw_kelly'] = self.single_kelly(
opp['true_prob'], opp['market_price']
)
# Step 2: Apply individual caps
for opp in opportunities:
opp['fraction'] = min(opp['raw_kelly'], self.max_individual)
# Step 3: Apply correlated group caps
groups = {}
for opp in opportunities:
g = opp.get('correlation_group', 'default')
groups.setdefault(g, []).append(opp)
for group_name, group_opps in groups.items():
total = sum(o['fraction'] for o in group_opps)
if total > self.max_correlated_group:
scale = self.max_correlated_group / total
for o in group_opps:
o['fraction'] *= scale
# Step 4: Apply platform caps
platforms = {}
for opp in opportunities:
p = opp.get('platform', 'default')
platforms.setdefault(p, []).append(opp)
for plat_name, plat_opps in platforms.items():
total = sum(o['fraction'] for o in plat_opps)
if total > self.max_platform:
scale = self.max_platform / total
for o in plat_opps:
o['fraction'] *= scale
# Step 5: Apply aggregate cap
total = sum(o['fraction'] for o in opportunities)
if total > self.max_aggregate:
scale = self.max_aggregate / total
for o in opportunities:
o['fraction'] *= scale
# Step 6: Calculate dollar amounts
for opp in opportunities:
opp['dollar_amount'] = opp['fraction'] * self.bankroll
return opportunities
def summary(self, opportunities):
"""Print a portfolio sizing summary."""
total_frac = sum(o['fraction'] for o in opportunities)
total_dollars = sum(o['dollar_amount'] for o in opportunities)
print(f"{'Market':<25} {'Edge':>6} {'Kelly':>7} {'Final':>7} {'Amount':>10}")
print("-" * 60)
for o in opportunities:
edge = o['true_prob'] - o['market_price']
print(f"{o['name']:<25} {edge:>+.3f} {o['raw_kelly']:>7.4f} "
f"{o['fraction']:>7.4f} ${o['dollar_amount']:>9.2f}")
print("-" * 60)
print(f"{'Total':<25} {'':>6} {'':>7} {total_frac:>7.4f} ${total_dollars:>9.2f}")
print(f"Bankroll utilization: {total_frac:.1%}")
# Example
sizer = PositionSizer(bankroll=10000, kelly_fraction=0.5)
opps = [
{'name': 'Senate Race A', 'true_prob': 0.60, 'market_price': 0.50,
'correlation_group': 'elections', 'platform': 'Polymarket'},
{'name': 'Senate Race B', 'true_prob': 0.55, 'market_price': 0.48,
'correlation_group': 'elections', 'platform': 'Polymarket'},
{'name': 'GDP > 3%', 'true_prob': 0.40, 'market_price': 0.30,
'correlation_group': 'economy', 'platform': 'Kalshi'},
{'name': 'Oscar Best Picture', 'true_prob': 0.35, 'market_price': 0.25,
'correlation_group': 'entertainment', 'platform': 'Polymarket'},
{'name': 'Fed Rate Cut', 'true_prob': 0.65, 'market_price': 0.55,
'correlation_group': 'economy', 'platform': 'Kalshi'},
]
sized = sizer.size_portfolio(opps)
sizer.summary(sized)
17.5 Diversification Strategies
The Goal of Diversification
Diversification in prediction markets aims to reduce portfolio variance without sacrificing expected returns. Since binary outcomes have inherently high variance (each position either pays off fully or goes to zero), diversification is even more important here than in traditional investing.
The variance of a portfolio of $n$ equally-weighted uncorrelated binary bets, each with probability $p$ of success, is:
$$\text{Var}(R_p) = \frac{p(1-p)}{n} \cdot \left(\frac{1-p}{p}\right)^2$$
This decreases as $1/n$, the classic diversification effect. But if bets are correlated with average pairwise correlation $\bar{\rho}$, the portfolio variance converges to:
$$\text{Var}(R_p) \to \bar{\rho} \cdot p(1-p) \cdot \left(\frac{1-p}{p}\right)^2$$
as $n \to \infty$. This irreducible variance due to correlation is systematic risk that cannot be diversified away.
Diversification Across Event Types
The most powerful diversification comes from holding positions in fundamentally unrelated event types:
| Event Type | Examples | Typical Correlations |
|---|---|---|
| Politics | Elections, legislation, appointments | High within type |
| Economics | GDP, inflation, employment | Moderate within type |
| Sports | Game outcomes, championships | Low to moderate |
| Entertainment | Awards, box office | Very low |
| Science/Tech | Discoveries, launches | Very low |
| Weather | Temperature records, storms | Moderate (seasonal) |
| Geopolitics | Conflicts, treaties | Low to moderate |
A portfolio mixing positions across all these categories achieves far better diversification than one concentrated in a single domain.
Diversification Across Time Horizons
Holding positions with different resolution dates smooths returns over time:
- Short-term (days to weeks): Higher turnover, faster feedback, smaller edge per trade.
- Medium-term (weeks to months): Moderate turnover, reasonable edge opportunities.
- Long-term (months to years): Lower turnover, potentially larger edges due to illiquidity premium, but capital is tied up longer.
A healthy portfolio typically has a mix: 30-40% in short-term positions for income and feedback, 40-50% in medium-term for core returns, and 10-20% in long-term for strategic alpha.
Diversification Across Platforms
Spreading capital across multiple prediction market platforms provides two benefits:
- Access to more markets. Different platforms specialize in different event types.
- Reduced platform risk. If one platform faces regulatory, technical, or solvency issues, only a fraction of your capital is affected.
However, multi-platform diversification comes with costs: multiple accounts to manage, different interfaces, currency conversion, and potentially different fee structures.
Diversification Across Strategies
Use different analytical approaches simultaneously:
- Fundamental analysis: Deep research into event probabilities.
- Market-making: Providing liquidity and capturing bid-ask spreads.
- Arbitrage: Exploiting price differences across platforms.
- Statistical models: Quantitative models for forecasting.
- Sentiment/contrarian: Betting against market overreactions.
Measuring Diversification Benefit
The diversification ratio measures how much benefit you get from combining positions:
$$\text{DR} = \frac{\sum_{i=1}^{n} w_i \sigma_i}{\sigma_p}$$
where $w_i$ are portfolio weights, $\sigma_i$ are individual position volatilities, and $\sigma_p$ is portfolio volatility. A DR of 1.0 means no diversification benefit (perfect correlation). Higher values indicate greater benefit.
import numpy as np
class DiversificationAnalyzer:
"""Analyze diversification benefits in a prediction market portfolio."""
def __init__(self, positions, corr_matrix):
"""
Parameters:
positions: list of dicts with 'name', 'weight', 'prob', 'price'
corr_matrix: correlation matrix between events
"""
self.positions = positions
self.corr_matrix = np.array(corr_matrix)
self.n = len(positions)
self.weights = np.array([p['weight'] for p in positions])
self.probs = np.array([p['prob'] for p in positions])
self.prices = np.array([p['price'] for p in positions])
# Individual volatilities (of returns)
self.vols = np.sqrt(self.probs * (1 - self.probs)) / self.prices
def portfolio_volatility(self):
"""Calculate portfolio volatility considering correlations."""
# Covariance matrix
cov = np.outer(self.vols, self.vols) * self.corr_matrix
port_var = self.weights @ cov @ self.weights
return np.sqrt(port_var)
def diversification_ratio(self):
"""Calculate the diversification ratio."""
weighted_vol_sum = np.sum(self.weights * self.vols)
port_vol = self.portfolio_volatility()
if port_vol == 0:
return float('inf')
return weighted_vol_sum / port_vol
def marginal_contribution_to_risk(self):
"""
Calculate each position's marginal contribution to portfolio risk.
Useful for identifying which positions add the most risk.
"""
cov = np.outer(self.vols, self.vols) * self.corr_matrix
port_vol = self.portfolio_volatility()
# Marginal contribution = (Cov @ w) * w / port_vol
mcr = (cov @ self.weights) * self.weights / port_vol
return mcr
def concentration_metrics(self):
"""Calculate portfolio concentration metrics."""
# Herfindahl-Hirschman Index
hhi = np.sum(self.weights ** 2)
# Effective number of bets
eff_n = 1.0 / hhi if hhi > 0 else self.n
# Maximum weight
max_w = np.max(self.weights)
return {
'hhi': hhi,
'effective_n_bets': eff_n,
'max_weight': max_w,
'top_5_concentration': np.sum(np.sort(self.weights)[-5:])
}
def category_concentration(self):
"""Analyze diversification across event categories."""
categories = {}
for i, pos in enumerate(self.positions):
cat = pos.get('category', 'Unknown')
categories.setdefault(cat, 0)
categories[cat] += self.weights[i]
return categories
def report(self):
"""Print a comprehensive diversification report."""
print("=" * 60)
print("DIVERSIFICATION ANALYSIS REPORT")
print("=" * 60)
print(f"\nNumber of positions: {self.n}")
print(f"Portfolio volatility: {self.portfolio_volatility():.4f}")
print(f"Diversification ratio: {self.diversification_ratio():.2f}")
conc = self.concentration_metrics()
print(f"\nConcentration Metrics:")
print(f" HHI: {conc['hhi']:.4f}")
print(f" Effective # of bets: {conc['effective_n_bets']:.1f}")
print(f" Max single weight: {conc['max_weight']:.2%}")
print(f" Top 5 concentration: {conc['top_5_concentration']:.2%}")
cats = self.category_concentration()
print(f"\nCategory Allocation:")
for cat, weight in sorted(cats.items(), key=lambda x: -x[1]):
print(f" {cat}: {weight:.2%}")
mcr = self.marginal_contribution_to_risk()
print(f"\nTop Risk Contributors:")
sorted_idx = np.argsort(mcr)[::-1]
for idx in sorted_idx[:5]:
print(f" {self.positions[idx]['name']}: {mcr[idx]:.4f} "
f"({mcr[idx]/np.sum(mcr):.1%} of total risk)")
17.6 Risk Metrics for Prediction Portfolios
Why Standard Risk Metrics Need Adaptation
Traditional risk metrics like Value at Risk (VaR) and Sharpe Ratio were designed for portfolios with approximately continuous return distributions. Prediction markets produce discrete, binary outcomes, which requires adaptation of these metrics.
Maximum Drawdown
Maximum drawdown (MDD) measures the largest peak-to-trough decline in portfolio value:
$$\text{MDD} = \max_{t} \left( \frac{\max_{s \leq t} V_s - V_t}{\max_{s \leq t} V_s} \right)$$
where $V_t$ is the portfolio value at time $t$.
In prediction markets, drawdowns can occur both from positions resolving against you and from mark-to-market price movements before resolution. Both types matter:
- Realized drawdowns come from resolved bets that lost.
- Unrealized drawdowns come from price movements on open positions.
Value at Risk (VaR) for Binary Outcomes
For a portfolio of binary bets, VaR at confidence level $\alpha$ is the loss that will not be exceeded with probability $\alpha$. Because outcomes are discrete, VaR is computed by:
- Enumerating or simulating all possible outcome combinations.
- Computing the portfolio return for each combination.
- Finding the $\alpha$-percentile of the loss distribution.
$$\text{VaR}_\alpha = -\text{Quantile}_{1-\alpha}(R_p)$$
For example, 95% VaR tells you the loss that is exceeded only 5% of the time.
Expected Shortfall (Conditional VaR)
Expected Shortfall (ES) is the average loss in the worst $(1-\alpha)$% of scenarios:
$$\text{ES}_\alpha = -E[R_p \mid R_p \leq -\text{VaR}_\alpha]$$
ES is considered a better risk measure than VaR because it accounts for the severity of tail losses, not just their threshold.
Risk of Ruin
Risk of ruin is the probability that your bankroll drops below a critical level (e.g., 10% of starting capital) at any point during a sequence of bets. For a single bet repeated many times:
$$P(\text{ruin}) = \left(\frac{q}{p}\right)^{B/u}$$
where $B$ is the starting bankroll, $u$ is the bet size, $p$ is the win probability, and $q = 1-p$, assuming $p > q$.
For a portfolio of heterogeneous bets, risk of ruin is best estimated through Monte Carlo simulation (Section 17.7).
Sharpe Ratio for Prediction Markets
The Sharpe Ratio measures risk-adjusted returns:
$$\text{SR} = \frac{E[R_p] - R_f}{\sigma_p}$$
For prediction markets, the risk-free rate $R_f$ is the return on uninvested capital (often near zero for crypto-based platforms, or the savings rate for fiat platforms). The expected return $E[R_p]$ and standard deviation $\sigma_p$ should be computed from the binary outcome distribution, not from price time series.
An adjusted Sharpe ratio for prediction markets might use the semi-deviation (downside risk) instead of standard deviation:
$$\text{Sortino} = \frac{E[R_p] - R_f}{\sigma_{\text{down}}}$$
where $\sigma_{\text{down}} = \sqrt{E[\min(R_p - R_f, 0)^2]}$.
Python Risk Metrics Suite
import numpy as np
from scipy.stats import norm
class RiskMetrics:
"""Comprehensive risk metrics for prediction market portfolios."""
def __init__(self, portfolio_returns):
"""
Parameters:
portfolio_returns: array of simulated or historical portfolio returns
"""
self.returns = np.array(portfolio_returns)
self.n = len(self.returns)
def expected_return(self):
"""Expected portfolio return."""
return np.mean(self.returns)
def volatility(self):
"""Portfolio return standard deviation."""
return np.std(self.returns, ddof=1)
def var(self, alpha=0.95):
"""Value at Risk at confidence level alpha."""
return -np.percentile(self.returns, (1 - alpha) * 100)
def expected_shortfall(self, alpha=0.95):
"""Expected Shortfall (CVaR) at confidence level alpha."""
var = self.var(alpha)
tail_losses = self.returns[self.returns <= -var]
if len(tail_losses) == 0:
return var
return -np.mean(tail_losses)
def maximum_drawdown(self):
"""
Maximum drawdown from a sequence of returns.
Assumes returns are sequential (not i.i.d. samples).
"""
cumulative = np.cumprod(1 + self.returns)
running_max = np.maximum.accumulate(cumulative)
drawdowns = (running_max - cumulative) / running_max
return np.max(drawdowns)
def sharpe_ratio(self, risk_free_rate=0.0):
"""Sharpe ratio."""
excess = self.expected_return() - risk_free_rate
vol = self.volatility()
if vol == 0:
return float('inf') if excess > 0 else 0.0
return excess / vol
def sortino_ratio(self, risk_free_rate=0.0):
"""Sortino ratio (using downside deviation)."""
excess = self.expected_return() - risk_free_rate
downside = self.returns[self.returns < risk_free_rate]
if len(downside) == 0:
return float('inf') if excess > 0 else 0.0
downside_dev = np.sqrt(np.mean((downside - risk_free_rate) ** 2))
if downside_dev == 0:
return float('inf') if excess > 0 else 0.0
return excess / downside_dev
def calmar_ratio(self):
"""Calmar ratio: annualized return / max drawdown."""
mdd = self.maximum_drawdown()
if mdd == 0:
return float('inf')
return self.expected_return() / mdd
def risk_of_ruin(self, ruin_threshold=0.1):
"""
Estimate probability that bankroll drops below ruin_threshold
of starting value. Uses the simulated return sequence.
"""
n_paths = 1000
path_length = len(self.returns)
ruin_count = 0
for _ in range(n_paths):
# Random permutation of returns (bootstrap)
shuffled = np.random.choice(self.returns, size=path_length,
replace=True)
cumulative = np.cumprod(1 + shuffled)
if np.any(cumulative < ruin_threshold):
ruin_count += 1
return ruin_count / n_paths
def win_rate(self):
"""Fraction of positive returns."""
return np.mean(self.returns > 0)
def profit_factor(self):
"""Gross profits / gross losses."""
gains = self.returns[self.returns > 0].sum()
losses = abs(self.returns[self.returns < 0].sum())
if losses == 0:
return float('inf')
return gains / losses
def report(self):
"""Print comprehensive risk report."""
print("=" * 50)
print("RISK METRICS REPORT")
print("=" * 50)
print(f"Number of observations: {self.n}")
print(f"\nReturn Metrics:")
print(f" Expected return: {self.expected_return():>10.4f}")
print(f" Volatility: {self.volatility():>10.4f}")
print(f" Win rate: {self.win_rate():>10.2%}")
print(f" Profit factor: {self.profit_factor():>10.2f}")
print(f"\nRisk Metrics:")
print(f" VaR (95%): {self.var(0.95):>10.4f}")
print(f" VaR (99%): {self.var(0.99):>10.4f}")
print(f" Expected Shortfall:{self.expected_shortfall(0.95):>10.4f}")
print(f" Max Drawdown: {self.maximum_drawdown():>10.4f}")
print(f"\nRisk-Adjusted Metrics:")
print(f" Sharpe Ratio: {self.sharpe_ratio():>10.4f}")
print(f" Sortino Ratio: {self.sortino_ratio():>10.4f}")
print(f" Calmar Ratio: {self.calmar_ratio():>10.4f}")
print(f"\nTail Risk:")
print(f" Risk of Ruin (10%):{self.risk_of_ruin(0.10):>10.4f}")
17.7 Monte Carlo Simulation
Why Monte Carlo?
Analytical formulas for portfolio risk become intractable when dealing with many correlated binary outcomes. Monte Carlo simulation lets us estimate any portfolio statistic by:
- Generating thousands of possible future scenarios.
- Computing the portfolio outcome under each scenario.
- Analyzing the distribution of outcomes.
For prediction market portfolios, Monte Carlo is particularly powerful because:
- Binary outcomes do not follow normal distributions.
- Correlations between events create complex joint distributions.
- Path-dependent statistics like maximum drawdown require sequential simulation.
- Non-linear position sizing rules (caps, Kelly adjustments) make analytical solutions impossible.
Simulating Correlated Binary Events
The Gaussian copula approach from Section 17.3 extends naturally to Monte Carlo simulation:
import numpy as np
from scipy.stats import norm
import matplotlib.pyplot as plt
class MonteCarloEngine:
"""
Monte Carlo simulation engine for prediction market portfolios.
Handles correlated binary outcomes.
"""
def __init__(self, seed=42):
self.rng = np.random.default_rng(seed)
def simulate_outcomes(self, probabilities, corr_matrix, n_simulations):
"""
Simulate correlated binary outcomes.
Parameters:
probabilities: array of true probabilities
corr_matrix: correlation matrix
n_simulations: number of scenarios to generate
Returns:
Binary outcome matrix (n_simulations x n_events)
"""
n_events = len(probabilities)
# Cholesky decomposition for correlated normals
L = np.linalg.cholesky(corr_matrix)
Z = self.rng.standard_normal((n_simulations, n_events)) @ L.T
# Convert to binary
thresholds = norm.ppf(probabilities)
outcomes = (Z < thresholds).astype(float)
return outcomes
def simulate_portfolio(self, positions, corr_matrix, n_simulations=10000):
"""
Simulate portfolio returns across many scenarios.
Parameters:
positions: list of dicts with:
'prob': true probability
'price': market price
'weight': fraction of bankroll allocated
corr_matrix: correlation matrix
n_simulations: number of scenarios
Returns:
Array of portfolio returns (one per simulation)
"""
probs = np.array([p['prob'] for p in positions])
prices = np.array([p['price'] for p in positions])
weights = np.array([p['weight'] for p in positions])
outcomes = self.simulate_outcomes(probs, corr_matrix, n_simulations)
# Compute returns per position per scenario
# Win: gain (1 - price) for each unit of price paid
# Loss: lose the price paid
returns_if_win = (1 - prices) / prices
returns_if_lose = -1.0
position_returns = np.where(
outcomes == 1,
returns_if_win,
returns_if_lose
)
# Portfolio return = weighted sum of position returns
portfolio_returns = position_returns @ weights
return portfolio_returns
def simulate_sequential(self, positions, corr_matrix,
n_simulations=5000, n_rounds=20):
"""
Simulate sequential portfolio outcomes over multiple rounds
of betting. Useful for drawdown and path analysis.
Each round, all positions resolve simultaneously,
bankroll is updated, and positions are re-entered.
Returns:
Bankroll paths: array of shape (n_simulations, n_rounds + 1)
"""
probs = np.array([p['prob'] for p in positions])
prices = np.array([p['price'] for p in positions])
weights = np.array([p['weight'] for p in positions])
paths = np.ones((n_simulations, n_rounds + 1))
for t in range(n_rounds):
outcomes = self.simulate_outcomes(probs, corr_matrix, n_simulations)
returns_if_win = (1 - prices) / prices
position_returns = np.where(outcomes == 1, returns_if_win, -1.0)
portfolio_returns = position_returns @ weights
paths[:, t + 1] = paths[:, t] * (1 + portfolio_returns)
return paths
def analyze_results(self, portfolio_returns):
"""Compute summary statistics from simulation results."""
results = {
'mean_return': np.mean(portfolio_returns),
'median_return': np.median(portfolio_returns),
'std_return': np.std(portfolio_returns),
'min_return': np.min(portfolio_returns),
'max_return': np.max(portfolio_returns),
'prob_positive': np.mean(portfolio_returns > 0),
'prob_loss_gt_10pct': np.mean(portfolio_returns < -0.10),
'prob_loss_gt_25pct': np.mean(portfolio_returns < -0.25),
'var_95': -np.percentile(portfolio_returns, 5),
'var_99': -np.percentile(portfolio_returns, 1),
'cvar_95': -np.mean(portfolio_returns[
portfolio_returns <= np.percentile(portfolio_returns, 5)
]),
'skewness': float(
np.mean(((portfolio_returns - np.mean(portfolio_returns))
/ np.std(portfolio_returns)) ** 3)
),
}
return results
def analyze_paths(self, paths):
"""Analyze sequential simulation paths."""
# Maximum drawdown for each path
max_drawdowns = np.zeros(paths.shape[0])
for i in range(paths.shape[0]):
running_max = np.maximum.accumulate(paths[i])
drawdowns = (running_max - paths[i]) / running_max
max_drawdowns[i] = np.max(drawdowns)
# Final bankroll statistics
final = paths[:, -1]
results = {
'median_final_bankroll': np.median(final),
'mean_final_bankroll': np.mean(final),
'prob_profit': np.mean(final > 1.0),
'prob_double': np.mean(final > 2.0),
'prob_ruin_50pct': np.mean(np.min(paths, axis=1) < 0.50),
'prob_ruin_25pct': np.mean(np.min(paths, axis=1) < 0.25),
'prob_ruin_10pct': np.mean(np.min(paths, axis=1) < 0.10),
'mean_max_drawdown': np.mean(max_drawdowns),
'median_max_drawdown': np.median(max_drawdowns),
'p95_max_drawdown': np.percentile(max_drawdowns, 95),
'worst_max_drawdown': np.max(max_drawdowns),
}
return results
def print_report(self, positions, corr_matrix, n_simulations=10000):
"""Generate and print a full simulation report."""
returns = self.simulate_portfolio(
positions, corr_matrix, n_simulations
)
stats = self.analyze_results(returns)
print("=" * 55)
print("MONTE CARLO SIMULATION REPORT")
print(f"({n_simulations:,} simulations)")
print("=" * 55)
print(f"\nReturn Distribution:")
print(f" Mean return: {stats['mean_return']:>+10.4f}")
print(f" Median return: {stats['median_return']:>+10.4f}")
print(f" Std deviation: {stats['std_return']:>10.4f}")
print(f" Min return: {stats['min_return']:>+10.4f}")
print(f" Max return: {stats['max_return']:>+10.4f}")
print(f" Skewness: {stats['skewness']:>10.4f}")
print(f"\nProbabilities:")
print(f" P(profit): {stats['prob_positive']:>10.2%}")
print(f" P(loss > 10%): {stats['prob_loss_gt_10pct']:>10.2%}")
print(f" P(loss > 25%): {stats['prob_loss_gt_25pct']:>10.2%}")
print(f"\nRisk Metrics:")
print(f" VaR (95%): {stats['var_95']:>10.4f}")
print(f" VaR (99%): {stats['var_99']:>10.4f}")
print(f" CVaR (95%): {stats['cvar_95']:>10.4f}")
return stats
Confidence Intervals on Returns
Monte Carlo simulation naturally produces confidence intervals. For an expected portfolio return estimate $\hat{\mu}$ from $N$ simulations with sample standard deviation $s$:
$$\hat{\mu} \pm z_{\alpha/2} \cdot \frac{s}{\sqrt{N}}$$
With $N = 10{,}000$ simulations, the standard error of the mean is $s / 100$, giving precise estimates. However, tail statistics like 99% VaR require more simulations for stability. A general guideline:
| Statistic | Minimum Simulations |
|---|---|
| Mean return | 1,000 |
| Standard deviation | 5,000 |
| 95% VaR | 10,000 |
| 99% VaR | 50,000 |
| Risk of ruin | 100,000 |
Interpreting Simulation Results
When reviewing Monte Carlo output, focus on these questions:
- Is the expected return positive? If not, your portfolio lacks edge.
- What is the probability of loss? Even positive-edge portfolios can lose. Ensure this probability is acceptable.
- How bad can it get? Look at VaR and CVaR to understand tail risk.
- How long to recover? Use sequential simulation to estimate recovery time from drawdowns.
- Is the result sensitive to correlation assumptions? Run simulations with higher correlations to see how results degrade.
17.8 Drawdown Management
What Is a Drawdown?
A drawdown is any decline from a peak portfolio value. If your portfolio was worth $12,000 at its peak and is currently worth $10,000, you are in a 16.7% drawdown:
$$\text{Drawdown} = \frac{12{,}000 - 10{,}000}{12{,}000} = 16.7\%$$
Drawdowns are inevitable, even for skilled traders with genuine edge. The question is not whether you will experience drawdowns, but how deep they will be, how long they will last, and whether you can survive them.
Defining Acceptable Drawdowns
Before you begin trading, establish drawdown limits:
| Drawdown Level | Classification | Action |
|---|---|---|
| 0-10% | Normal | No action needed |
| 10-20% | Elevated | Review positions, reduce sizing |
| 20-30% | Serious | Halve position sizes, close marginal bets |
| 30-40% | Critical | Reduce to 25% of normal sizing |
| 40%+ | Emergency | Close all positions, reassess strategy |
These thresholds should be set in advance when you are thinking rationally, not in the heat of a losing streak.
Circuit Breakers
Implement automated rules that trigger when drawdowns reach specified levels:
class DrawdownMonitor:
"""
Monitor portfolio drawdowns and enforce risk limits.
"""
def __init__(self, initial_bankroll,
warning_level=0.10,
reduce_level=0.15,
halt_level=0.25,
emergency_level=0.40):
self.initial_bankroll = initial_bankroll
self.peak_bankroll = initial_bankroll
self.current_bankroll = initial_bankroll
self.history = [initial_bankroll]
self.warning_level = warning_level
self.reduce_level = reduce_level
self.halt_level = halt_level
self.emergency_level = emergency_level
self.position_scale = 1.0 # Current position sizing multiplier
self.is_halted = False
self.drawdown_log = []
def update(self, new_bankroll):
"""Update bankroll and check drawdown levels."""
self.current_bankroll = new_bankroll
self.history.append(new_bankroll)
# Update peak
if new_bankroll > self.peak_bankroll:
self.peak_bankroll = new_bankroll
# Reset scaling when we make new highs
self.position_scale = 1.0
self.is_halted = False
# Calculate drawdown
dd = self.current_drawdown()
# Determine action
status = self._evaluate_drawdown(dd)
self.drawdown_log.append({
'bankroll': new_bankroll,
'peak': self.peak_bankroll,
'drawdown': dd,
'status': status,
'position_scale': self.position_scale
})
return status
def current_drawdown(self):
"""Calculate current drawdown from peak."""
if self.peak_bankroll == 0:
return 0.0
return (self.peak_bankroll - self.current_bankroll) / self.peak_bankroll
def _evaluate_drawdown(self, dd):
"""Evaluate drawdown and set position scaling."""
if dd >= self.emergency_level:
self.position_scale = 0.0
self.is_halted = True
return 'EMERGENCY - ALL POSITIONS CLOSED'
elif dd >= self.halt_level:
self.position_scale = 0.25
return 'CRITICAL - Sizing at 25%'
elif dd >= self.reduce_level:
self.position_scale = 0.50
return 'SERIOUS - Sizing at 50%'
elif dd >= self.warning_level:
self.position_scale = 0.75
return 'WARNING - Sizing at 75%'
else:
self.position_scale = 1.0
return 'NORMAL'
def adjusted_position_size(self, base_size):
"""Return position size adjusted for current drawdown state."""
return base_size * self.position_scale
def max_drawdown(self):
"""Calculate maximum drawdown from history."""
peak = self.history[0]
max_dd = 0
for val in self.history:
if val > peak:
peak = val
dd = (peak - val) / peak
if dd > max_dd:
max_dd = dd
return max_dd
def recovery_estimate(self, expected_return_per_round=0.02):
"""
Estimate rounds needed to recover from current drawdown.
Assumes expected return per round with reduced sizing.
"""
if self.current_drawdown() == 0:
return 0
target = self.peak_bankroll
current = self.current_bankroll
scale = self.position_scale
# Adjusted expected return
adj_return = expected_return_per_round * scale
if adj_return <= 0:
return float('inf')
# Rounds to recover: target/current = (1 + adj_return)^n
# n = log(target/current) / log(1 + adj_return)
n = np.log(target / current) / np.log(1 + adj_return)
return int(np.ceil(n))
def report(self):
"""Print drawdown status report."""
dd = self.current_drawdown()
max_dd = self.max_drawdown()
print("=" * 50)
print("DRAWDOWN MONITOR REPORT")
print("=" * 50)
print(f" Initial bankroll: ${self.initial_bankroll:>10,.2f}")
print(f" Peak bankroll: ${self.peak_bankroll:>10,.2f}")
print(f" Current bankroll: ${self.current_bankroll:>10,.2f}")
print(f" Current drawdown: {dd:>10.2%}")
print(f" Maximum drawdown: {max_dd:>10.2%}")
print(f" Position scaling: {self.position_scale:>10.0%}")
print(f" Status: {self._evaluate_drawdown(dd)}")
recovery = self.recovery_estimate()
if recovery < float('inf'):
print(f" Est. rounds to recover: {recovery}")
else:
print(f" Recovery: HALTED (no new positions)")
Reducing Position Sizes During Drawdowns
The logic behind reducing sizes during drawdowns is twofold:
-
Mathematical. If your edge estimates were wrong (which the drawdown might be evidence of), smaller sizing protects against further losses.
-
Psychological. Large drawdowns create emotional pressure that leads to poor decisions. Smaller positions reduce the emotional stakes, helping you think clearly.
A graduated approach (as in the code above) is better than a binary on/off switch. Gradually reducing sizing as drawdowns deepen preserves some upside while limiting downside.
Psychological Aspects of Drawdowns
Even with perfect mathematical understanding, drawdowns are psychologically painful. Common reactions include:
- Revenge trading: Increasing bet sizes to "win it back quickly." This is the opposite of what you should do.
- Strategy abandonment: Discarding a sound strategy because of a losing streak that is well within expected variance.
- Paralysis: Becoming afraid to place any bets, even clearly profitable ones.
- Denial: Ignoring the drawdown and continuing to trade at full size.
Countermeasures:
- Pre-commit to rules. Write down your drawdown management rules before you start trading.
- Automate where possible. Use code (like the DrawdownMonitor) to enforce rules mechanically.
- Keep a trading journal. Record your reasoning for each trade to review during drawdowns. Are you making sound decisions, or has emotion taken over?
- Take breaks. After a significant drawdown, step away for a day or two. Markets will still be there when you return.
- Review base rates. Remind yourself what normal drawdowns look like for your strategy. A 15% drawdown might feel catastrophic but be perfectly normal for a strategy with 30% expected annual return.
Recovery Time Estimation
The time to recover from a drawdown depends on your edge and position sizing. If your expected return per round is $\mu$ and you are in a drawdown of fraction $d$:
$$\text{Recovery rounds} = \frac{\log(1/(1-d))}{\log(1 + \mu)}$$
For example, recovering from a 20% drawdown with a 2% expected return per round requires approximately:
$$\frac{\log(1/0.8)}{\log(1.02)} \approx \frac{0.223}{0.020} \approx 11 \text{ rounds}$$
But this is with full-size positions. If you have scaled down to 50% during the drawdown, the effective return is only 1%, doubling the recovery time to about 22 rounds.
17.9 Bankroll Management
The Foundation of Survival
Bankroll management is the practice of structuring your capital to ensure you can survive adverse outcomes and continue trading. It sits at the intersection of mathematics and discipline.
Separating Trading Capital from Reserves
Never put your entire available capital into prediction market trading. Structure your capital into tiers:
| Tier | Purpose | Percentage | Access |
|---|---|---|---|
| Trading Capital | Active positions | 40-60% of total | Deployed on platforms |
| Reserve Capital | Replenish losses | 20-30% of total | Accessible but not deployed |
| Emergency Fund | Personal safety net | 10-20% of total | Separate account |
| Risk Capital | Only amount you can lose | Sum of above | Total prediction market capital |
The Reserve Capital tier is particularly important. It provides a buffer that lets you: - Replenish trading capital after drawdowns. - Add capital when new opportunities arise. - Avoid desperate trading to "save" a depleted bankroll.
Growth vs Safety Tradeoff
Aggressive bankroll management (deploying more capital, using full Kelly) maximizes expected growth but increases ruin risk. Conservative management (deploying less, using fractional Kelly) sacrifices some growth for safety.
The right balance depends on:
- Replaceability of capital. Can you earn more trading capital from other income? If so, you can afford to be more aggressive.
- Time horizon. Longer time horizons favor more conservative approaches, as they increase the probability of encountering extreme drawdowns.
- Skill certainty. How confident are you in your edge? Less certainty should mean more conservative bankroll management.
- Opportunity cost. What else could you do with the capital? Higher opportunity costs favor more conservative deployment.
When to Add Capital
Adding capital to your trading bankroll should be systematic, not emotional:
Good reasons to add capital: - Your strategy has been profitable over a meaningful sample size. - New opportunities have arisen that you cannot fund from current capital. - You have reached a new all-time high and want to scale up proportionally.
Bad reasons to add capital: - You want to recover from a drawdown faster. - You feel pressure to "make up for lost time." - A single trade looks "too good to miss."
When to Withdraw Capital
Withdrawing profits regularly serves several purposes:
- Reduces risk. Capital withdrawn cannot be lost.
- Provides feedback. If you are withdrawing profits regularly, your strategy is working.
- Maintains discipline. It prevents the temptation to over-trade with a growing bankroll.
A simple rule: withdraw 50% of profits above your target bankroll each month. If your target bankroll is $10,000 and you end the month with $11,200, withdraw $600 (50% of the $1,200 profit).
Python Bankroll Tracker
import numpy as np
from datetime import datetime, timedelta
class BankrollTracker:
"""Track bankroll, deposits, withdrawals, and performance over time."""
def __init__(self, initial_deposit, target_bankroll=None):
self.transactions = [
{'date': datetime.now(), 'type': 'deposit',
'amount': initial_deposit, 'note': 'Initial deposit'}
]
self.daily_values = [
{'date': datetime.now(), 'value': initial_deposit}
]
self.target_bankroll = target_bankroll or initial_deposit
self.total_deposited = initial_deposit
self.total_withdrawn = 0
def deposit(self, amount, note=""):
"""Record a deposit."""
self.transactions.append({
'date': datetime.now(), 'type': 'deposit',
'amount': amount, 'note': note
})
self.total_deposited += amount
def withdraw(self, amount, note=""):
"""Record a withdrawal."""
self.transactions.append({
'date': datetime.now(), 'type': 'withdrawal',
'amount': amount, 'note': note
})
self.total_withdrawn += amount
def record_value(self, value, date=None):
"""Record the current portfolio value."""
self.daily_values.append({
'date': date or datetime.now(),
'value': value
})
def suggested_withdrawal(self, current_value, profit_withdrawal_pct=0.5):
"""
Suggest a withdrawal amount based on profits above target.
"""
profit_above_target = current_value - self.target_bankroll
if profit_above_target <= 0:
return 0
return profit_above_target * profit_withdrawal_pct
def net_profit(self, current_value):
"""Calculate net profit accounting for deposits and withdrawals."""
return current_value + self.total_withdrawn - self.total_deposited
def roi(self, current_value):
"""Return on investment."""
if self.total_deposited == 0:
return 0
return self.net_profit(current_value) / self.total_deposited
def report(self, current_value):
"""Print bankroll report."""
print("=" * 50)
print("BANKROLL TRACKER REPORT")
print("=" * 50)
print(f" Total deposited: ${self.total_deposited:>10,.2f}")
print(f" Total withdrawn: ${self.total_withdrawn:>10,.2f}")
print(f" Current value: ${current_value:>10,.2f}")
print(f" Net profit: ${self.net_profit(current_value):>+10,.2f}")
print(f" ROI: {self.roi(current_value):>+10.2%}")
print(f" Target bankroll: ${self.target_bankroll:>10,.2f}")
suggested = self.suggested_withdrawal(current_value)
if suggested > 0:
print(f"\n Suggested withdrawal: ${suggested:>10,.2f}")
if len(self.daily_values) > 1:
values = [d['value'] for d in self.daily_values]
peak = max(values)
dd = (peak - current_value) / peak if peak > 0 else 0
print(f"\n Peak value: ${peak:>10,.2f}")
print(f" Current drawdown: {dd:>10.2%}")
17.10 Rebalancing and Portfolio Maintenance
When to Rebalance
Prediction market portfolios require rebalancing for several reasons:
- Position resolution. As events resolve, capital is freed up (or lost) and must be redeployed.
- Price movements. As market prices change, the edge on each position changes, potentially requiring size adjustments.
- New information. Your probability estimates may update, changing the optimal allocation.
- New opportunities. New markets open that may be more attractive than current holdings.
Unlike traditional portfolios where rebalancing is typically calendar-based (monthly, quarterly), prediction market rebalancing should be event-driven:
- Rebalance when a position resolves. Redeploy the freed capital.
- Rebalance when edge changes significantly. If a position's edge drops below your minimum threshold, close it.
- Rebalance when new high-edge opportunities appear. Reallocate from lower-edge to higher-edge positions.
Cost of Rebalancing
Rebalancing in prediction markets incurs several costs:
- Bid-ask spread. Closing and opening positions crosses the spread, which can be wide in illiquid markets.
- Price impact. Large orders can move prices against you.
- Transaction fees. Some platforms charge per-trade fees.
- Opportunity cost of attention. Time spent rebalancing could be spent on research.
As a rule of thumb, only rebalance when the expected improvement exceeds twice the transaction cost. If closing and re-opening a position costs 2% in spreads and fees, only do it if the expected improvement is at least 4%.
New Opportunity Integration
When a new opportunity appears, evaluate it in the context of your existing portfolio:
- Does it add diversification? A new opportunity in an uncorrelated event type is more valuable than one correlated with existing positions.
- Does it have sufficient edge? The minimum edge should cover transaction costs plus a hurdle rate for your time.
- Where does the capital come from? You must either deploy reserve capital or close an existing position. Only close an existing position if the new opportunity has clearly higher risk-adjusted edge.
Portfolio Decay Management
Prediction market portfolios naturally "decay" as events resolve. Without active management:
- Resolved positions free capital that earns no return.
- The portfolio becomes increasingly concentrated in the few remaining unresolved positions.
- Time-sensitive opportunities may be missed.
To combat decay:
- Maintain a pipeline of opportunities. Always have a watchlist of potential new positions.
- Set capital deployment targets. Aim to keep 50-70% of your trading capital actively deployed.
- Review weekly. Assess whether your current deployment matches your target allocation.
17.11 Stress Testing
Why Stress Test?
Normal Monte Carlo simulation reveals the range of likely outcomes. Stress testing reveals what happens under extreme conditions that may not be captured by your base assumptions. In prediction markets, stress scenarios include:
- Correlation spikes. During a crisis or major event, normally uncorrelated markets become correlated.
- Probability estimation errors. What if your edge is half of what you think?
- Liquidity evaporation. What if you cannot close positions when you need to?
- Platform failure. What if a major platform goes down?
- Systematic model failure. What if your entire forecasting approach has a blind spot?
Scenario Analysis
Define specific scenarios and compute their impact on your portfolio:
import numpy as np
class StressTester:
"""Stress testing framework for prediction market portfolios."""
def __init__(self, positions, base_corr_matrix):
"""
Parameters:
positions: list of dicts with 'name', 'prob', 'price', 'weight',
'category'
base_corr_matrix: normal-conditions correlation matrix
"""
self.positions = positions
self.base_corr = np.array(base_corr_matrix)
self.n = len(positions)
def scenario_edge_reduction(self, edge_multiplier=0.5):
"""
Scenario: Your edges are smaller than estimated.
Recompute expected returns with reduced edge.
"""
results = []
for pos in self.positions:
original_edge = pos['prob'] - pos['price']
reduced_prob = pos['price'] + original_edge * edge_multiplier
reduced_prob = max(0.01, min(0.99, reduced_prob))
expected_return = (
reduced_prob * (1 - pos['price']) / pos['price']
+ (1 - reduced_prob) * (-1)
) * pos['weight']
results.append({
'name': pos['name'],
'original_edge': original_edge,
'reduced_edge': reduced_prob - pos['price'],
'expected_return': expected_return
})
total = sum(r['expected_return'] for r in results)
return results, total
def scenario_correlation_spike(self, spike_level=0.7):
"""
Scenario: All correlations jump to a high level.
Common during crises when "everything is correlated."
"""
stressed_corr = np.full_like(self.base_corr, spike_level)
np.fill_diagonal(stressed_corr, 1.0)
return stressed_corr
def scenario_category_wipeout(self, category):
"""
Scenario: All positions in a given category lose.
Returns the portfolio impact.
"""
total_loss = 0
affected = []
for pos in self.positions:
if pos.get('category') == category:
loss = pos['weight'] # Lose entire position
total_loss += loss
affected.append(pos['name'])
return {
'category': category,
'affected_positions': affected,
'total_loss': total_loss,
'remaining_bankroll_pct': 1 - total_loss
}
def scenario_platform_failure(self, platform):
"""
Scenario: A platform fails, losing all capital on it.
"""
total_loss = 0
affected = []
for pos in self.positions:
if pos.get('platform') == platform:
total_loss += pos['weight']
affected.append(pos['name'])
return {
'platform': platform,
'affected_positions': affected,
'total_loss': total_loss,
'remaining_bankroll_pct': 1 - total_loss
}
def worst_case_analysis(self, n_worst=5):
"""
Find the N positions whose simultaneous loss would be worst.
"""
weights = np.array([p['weight'] for p in self.positions])
sorted_idx = np.argsort(weights)[::-1]
worst_positions = []
total_loss = 0
for i in sorted_idx[:n_worst]:
worst_positions.append(self.positions[i]['name'])
total_loss += weights[i]
return {
'positions': worst_positions,
'total_loss': total_loss,
'remaining_bankroll_pct': 1 - total_loss
}
def run_all_scenarios(self):
"""Run all stress scenarios and print results."""
print("=" * 60)
print("STRESS TEST REPORT")
print("=" * 60)
# Edge reduction
print("\n--- Scenario: 50% Edge Reduction ---")
results, total = self.scenario_edge_reduction(0.5)
print(f" Expected portfolio return: {total:+.4f}")
still_positive = sum(1 for r in results if r['expected_return'] > 0)
print(f" Positions still profitable: {still_positive}/{len(results)}")
# Edge elimination
print("\n--- Scenario: Zero Edge (no skill) ---")
results, total = self.scenario_edge_reduction(0.0)
print(f" Expected portfolio return: {total:+.4f}")
print(f" (This represents the cost of trading without edge)")
# Category wipeouts
categories = set(p.get('category', 'Unknown') for p in self.positions)
print("\n--- Scenario: Category Wipeout ---")
for cat in categories:
result = self.scenario_category_wipeout(cat)
if result['total_loss'] > 0:
print(f" {cat}: Loss = {result['total_loss']:.2%}, "
f"Remaining = {result['remaining_bankroll_pct']:.2%}")
# Platform failure
platforms = set(p.get('platform', 'Unknown') for p in self.positions)
print("\n--- Scenario: Platform Failure ---")
for plat in platforms:
result = self.scenario_platform_failure(plat)
if result['total_loss'] > 0:
print(f" {plat}: Loss = {result['total_loss']:.2%}, "
f"Remaining = {result['remaining_bankroll_pct']:.2%}")
# Worst case
print("\n--- Scenario: 5 Largest Positions All Lose ---")
worst = self.worst_case_analysis(5)
print(f" Total loss: {worst['total_loss']:.2%}")
print(f" Remaining: {worst['remaining_bankroll_pct']:.2%}")
for name in worst['positions']:
print(f" - {name}")
Historical Stress Scenarios
Study past events that caused prediction market turmoil:
- 2016 U.S. Presidential Election: Markets priced Clinton at 85-90% on election day. Traders holding large Clinton positions experienced catastrophic losses, especially those concentrated in correlated political markets.
- COVID-19 onset (March 2020): Disrupted nearly every type of prediction market -- sports were cancelled, economic indicators plunged, political calendars shifted.
- Brexit referendum (2016): Markets priced Remain at ~85% on the day of the vote. The shock affected markets well beyond UK politics.
These events share a common pattern: correlations spiked dramatically, turning "diversified" portfolios into concentrated bets on a single narrative.
Tail Risk Assessment
For each stress scenario, assess:
- Probability. How likely is this scenario? (Be honest -- investors systematically underestimate tail risk.)
- Impact. What percentage of your bankroll would you lose?
- Recovery. How long would recovery take?
- Survivability. Would you still be solvent? Could you continue trading?
If any plausible scenario threatens your ability to continue trading, you are taking too much risk.
17.12 Chapter Summary
This chapter has covered the essential tools and concepts for managing a prediction market portfolio as a unified whole, rather than a collection of isolated bets.
Key concepts reviewed:
-
Portfolio thinking shifts the focus from individual bets to aggregate risk and return. It unlocks diversification benefits and enables systematic risk management.
-
Correlation between prediction market events is the critical input to portfolio construction. Binary events have bounded correlations that depend on marginal probabilities, and correlations must often be estimated from structural analysis rather than historical data.
-
Portfolio Kelly extends the Kelly Criterion to simultaneous bets, accounting for correlations through numerical optimization. Fractional Kelly (25-50% of full) provides crucial protection against estimation errors.
-
Position sizing combines Kelly-based calculations with practical constraints: individual position caps, correlated group limits, platform diversification, and aggregate deployment limits.
-
Diversification across event types, time horizons, platforms, and strategies is the primary tool for reducing portfolio variance. The diversification ratio quantifies this benefit.
-
Risk metrics including VaR, Expected Shortfall, maximum drawdown, and risk of ruin must be adapted to the binary outcome structure of prediction markets. Monte Carlo simulation is the primary computational tool.
-
Drawdown management requires pre-committed rules for reducing position sizes during adverse periods, automated circuit breakers, and psychological preparedness for inevitable losing streaks.
-
Bankroll management separates trading capital from reserves and emergency funds, systematizes deposits and withdrawals, and ensures survival through adverse periods.
-
Stress testing reveals vulnerabilities that normal-condition analysis misses: correlation spikes, edge deterioration, platform failures, and worst-case scenarios.
The unifying theme is that survival is the prerequisite for success. The best edge in the world is worthless if a drawdown wipes you out before it can compound. Portfolio construction and risk management ensure that you stay in the game long enough for your skill to produce results.
What's Next
In Chapter 18: Market Making and Liquidity Provision, we will explore the other side of prediction markets: instead of taking directional positions based on forecasting edge, we will study how to profit by providing liquidity. Market makers earn the bid-ask spread in exchange for taking on inventory risk, and we will learn how to set quotes, manage inventory, and integrate market-making with the portfolio management framework developed in this chapter.
Chapter 17 is part of Learning Prediction Markets — From Concepts to Strategies, Part III: Trading Strategies & Edge.
Related Reading
Explore this topic in other books
Sports Betting Simulation & Monte Carlo Methods NFL Analytics Game Simulation