Python 56.2%
TeX 43.7%
1"""Arrow-Debreu state-contingent securities analysis.23Core theoretical contribution: tests for market completeness,4risk-neutral density extraction, moment computation, and arbitrage5violation detection using Polymarket state-price data.6"""78from __future__ import annotations910import pandas as pd11import numpy as np12from dataclasses import dataclass131415# ---------------------------------------------------------------------------16# Data classes17# ---------------------------------------------------------------------------1819@dataclass20class CompletenessTestResult:21 """Result container for the Arrow-Debreu completeness test."""22 daily_sums: pd.Series # sum of state prices each day23 mean_sum: float # should be close to 1.024 std_sum: float25 mean_deviation: float # mean(sum - 1.0)26 max_deviation: float27 all_positive: bool # no negative state prices28 discount_factor: float # implied 1/(1+r) from mean sum293031# ---------------------------------------------------------------------------32# Completeness33# ---------------------------------------------------------------------------3435def test_market_completeness(state_prices: pd.DataFrame) -> CompletenessTestResult:36 """Test Arrow-Debreu completeness conditions.3738 Parameters39 ----------40 state_prices : pd.DataFrame41 DataFrame where columns = states, values = prices.42 In a complete market the sum of state prices equals 1/(1+r),43 which is approximately 1.0 for short horizons.4445 Returns46 -------47 CompletenessTestResult48 """49 daily_sums = state_prices.sum(axis=1)50 deviations = daily_sums - 1.05152 return CompletenessTestResult(53 daily_sums=daily_sums,54 mean_sum=float(daily_sums.mean()),55 std_sum=float(daily_sums.std()),56 mean_deviation=float(deviations.abs().mean()),57 max_deviation=float(deviations.abs().max()),58 all_positive=bool((state_prices >= 0).all().all()),59 discount_factor=float(daily_sums.mean()), # 1/(1+r) ≈ mean sum60 )616263# ---------------------------------------------------------------------------64# Risk-neutral density65# ---------------------------------------------------------------------------6667def extract_risk_neutral_density(68 state_prices: pd.DataFrame,69 state_values: list[float],70) -> pd.DataFrame:71 """Extract risk-neutral probability density from state prices.7273 Normalises each row so prices sum to 1.0; the resulting values are74 interpreted as risk-neutral probabilities.7576 Parameters77 ----------78 state_prices : pd.DataFrame79 Columns = states, values = prices.80 state_values : list[float]81 Numerical value associated with each state (same order as columns).8283 Returns84 -------85 pd.DataFrame86 Same shape as *state_prices* with values summing to 1.0 per row.87 """88 if len(state_values) != state_prices.shape[1]:89 raise ValueError(90 f"state_values length ({len(state_values)}) must match "91 f"number of columns ({state_prices.shape[1]})"92 )9394 row_sums = state_prices.sum(axis=1)95 # Avoid division by zero96 row_sums = row_sums.replace(0, np.nan)97 rn_density = state_prices.div(row_sums, axis=0)98 return rn_density99100101# ---------------------------------------------------------------------------102# Risk-neutral moments103# ---------------------------------------------------------------------------104105def compute_risk_neutral_moments(106 rn_density: pd.DataFrame,107 state_values: list[float],108) -> pd.DataFrame:109 """Compute time series of risk-neutral moments.110111 Parameters112 ----------113 rn_density : pd.DataFrame114 Risk-neutral probabilities (rows sum to 1).115 state_values : list[float]116 Numerical values for each state (e.g., basis-point changes117 [-50, -25, 0, 25, 50]).118119 Returns120 -------121 pd.DataFrame122 Columns ``[mean, variance, skewness, kurtosis]`` indexed by date.123 """124 vals = np.array(state_values, dtype=float)125 probs = rn_density.values # (T, K)126127 # Mean128 mu = probs @ vals # (T,)129130 # Central moments131 deviations = vals[np.newaxis, :] - mu[:, np.newaxis] # (T, K)132 var = (probs * deviations ** 2).sum(axis=1)133 sigma = np.sqrt(np.maximum(var, 0))134135 # Protect against zero variance136 sigma_safe = np.where(sigma > 0, sigma, np.nan)137 skew = (probs * deviations ** 3).sum(axis=1) / sigma_safe ** 3138 kurt = (probs * deviations ** 4).sum(axis=1) / sigma_safe ** 4139140 return pd.DataFrame(141 {"mean": mu, "variance": var, "skewness": skew, "kurtosis": kurt},142 index=rn_density.index,143 )144145146# ---------------------------------------------------------------------------147# Arbitrage violations148# ---------------------------------------------------------------------------149150def detect_arbitrage_violations(151 prices: pd.DataFrame,152 market_type: str = "binary",153) -> pd.DataFrame:154 """Detect no-arbitrage violations.155156 For binary markets: p(Yes) + p(No) should equal 1.157 For state-space groups: sum of state prices should be approximately 1.158159 Parameters160 ----------161 prices : pd.DataFrame162 Price data (columns = states or Yes/No).163 market_type : str164 ``"binary"`` or ``"state_space"``.165166 Returns167 -------168 pd.DataFrame169 Columns ``[date, violation_size, violation_pct]``.170 """171 row_sums = prices.sum(axis=1)172 deviation = (row_sums - 1.0).abs()173174 result = pd.DataFrame({175 "date": prices.index,176 "violation_size": deviation.values,177 "violation_pct": (deviation.values * 100),178 })179 result = result.set_index("date")180 return result181182183# ---------------------------------------------------------------------------184# State-price dynamics185# ---------------------------------------------------------------------------186187def state_price_dynamics(state_prices: pd.DataFrame) -> dict:188 """Analyse dynamics of state prices over time.189190 Returns191 -------192 dict193 ``correlation_matrix`` : cross-correlations between states.194 ``persistence`` : AR(1) coefficients for each state price.195 ``mean_reversion_speed``: half-life of deviations from mean.196 """197 # Cross-correlation matrix198 corr_matrix = state_prices.corr()199200 # AR(1) coefficients: regress p_t on p_{t-1} for each column201 persistence = {}202 half_lives = {}203 for col in state_prices.columns:204 series = state_prices[col].dropna()205 if len(series) < 3:206 persistence[col] = np.nan207 half_lives[col] = np.nan208 continue209210 y = series.iloc[1:].values211 x = series.iloc[:-1].values212 # OLS: phi = cov(y,x) / var(x)213 x_dm = x - x.mean()214 y_dm = y - y.mean()215 var_x = (x_dm ** 2).sum()216 if var_x == 0:217 persistence[col] = np.nan218 half_lives[col] = np.nan219 continue220221 phi = (x_dm * y_dm).sum() / var_x222 persistence[col] = float(phi)223224 # Half-life: t_{1/2} = -ln(2) / ln(|phi|), defined when |phi| < 1225 if 0 < abs(phi) < 1:226 half_lives[col] = float(-np.log(2) / np.log(abs(phi)))227 else:228 half_lives[col] = np.nan229230 return {231 "correlation_matrix": corr_matrix,232 "persistence": pd.Series(persistence),233 "mean_reversion_speed": pd.Series(half_lives),234 }235