SPB Git forge

spb/wp8_uqo

Public
3commits 1branches 1releases
4.2 MBsize
maindefault branch
5 h agolast push
Python 56.2% TeX 43.7%
7.3 KB · 235 lines python
Raw Blame History
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