SPB Git forge

spb/wp8_uqo

Public
3commits 1branches 1releases
4.2 MBsize
maindefault branch
2 h agolast push
Python 56.2% TeX 43.7%
11.9 KB · 301 lines python
Raw Blame History
1"""Publication-quality figures for the Polymarket Arrow-Debreu paper."""23from __future__ import annotations45from datetime import datetime67import matplotlib.pyplot as plt8import matplotlib.dates as mdates9import numpy as np10import pandas as pd1112from src.config import ELECTION_KEY_DATES, JOF_COLORS13from src.plots.style import COLORS, save_figure, set_jof_style141516def fig_state_prices(state_prices: pd.DataFrame, title: str = "") -> plt.Figure:17    """Figure: Arrow-Debreu state prices over time (Fed rate decisions)."""18    set_jof_style()19    fig, ax = plt.subplots(figsize=(7, 4.5))20    for i, col in enumerate(state_prices.columns):21        ax.plot(state_prices.index, state_prices[col], label=col,22                color=COLORS[i % len(COLORS)], linewidth=1.3)23    ax.set_xlabel("Date")24    ax.set_ylabel("State Price")25    ax.set_title(title or "Arrow--Debreu State Prices: Fed Rate Decision")26    ax.legend(loc="best", framealpha=0.9)27    ax.xaxis.set_major_formatter(mdates.DateFormatter("%b %Y"))28    ax.xaxis.set_major_locator(mdates.MonthLocator(interval=2))29    fig.autofmt_xdate(rotation=45)30    fig.tight_layout()31    return fig323334def fig_completeness_test(daily_sums: pd.Series) -> plt.Figure:35    """Figure: Sum of state prices over time (should ≈ 1.0)."""36    set_jof_style()37    fig, ax = plt.subplots(figsize=(7, 3.5))38    ax.plot(daily_sums.index, daily_sums.values, color=JOF_COLORS["primary"], linewidth=1.2)39    ax.axhline(y=1.0, color=JOF_COLORS["secondary"], linestyle="--", linewidth=0.8, label="Complete market (1.0)")40    ax.fill_between(daily_sums.index, 0.98, 1.02, alpha=0.1, color=JOF_COLORS["gray"])41    ax.set_xlabel("Date")42    ax.set_ylabel("Sum of State Prices")43    ax.set_title("Arrow--Debreu Completeness Test")44    ax.legend(loc="best")45    ax.xaxis.set_major_formatter(mdates.DateFormatter("%b %Y"))46    fig.autofmt_xdate(rotation=45)47    fig.tight_layout()48    return fig495051def fig_risk_neutral_density(52    rn_density: pd.DataFrame,53    state_values: list[float],54    snapshot_dates: list[str] | None = None,55) -> plt.Figure:56    """Figure: Risk-neutral density snapshots at key dates."""57    set_jof_style()58    fig, ax = plt.subplots(figsize=(7, 4.5))59    if snapshot_dates is None:60        idx = rn_density.index61        n = len(idx)62        positions = [0, n // 4, n // 2, 3 * n // 4, n - 1]63        snapshot_dates = [idx[p] for p in positions if p < n]6465    for i, d in enumerate(snapshot_dates):66        if d in rn_density.index:67            row = rn_density.loc[d]68        else:69            nearest = rn_density.index[rn_density.index.get_indexer([d], method="nearest")]70            row = rn_density.loc[nearest[0]]71            d = nearest[0]72        label = d.strftime("%b %d, %Y") if hasattr(d, "strftime") else str(d)73        ax.plot(state_values, row.values, "o-", color=COLORS[i % len(COLORS)],74                label=label, linewidth=1.3, markersize=5)7576    ax.set_xlabel("Rate Change (basis points)")77    ax.set_ylabel("Risk-Neutral Probability")78    ax.set_title("Risk-Neutral Density: Fed Rate Decision")79    ax.legend(loc="best")80    fig.tight_layout()81    return fig828384def fig_event_study_cars(85    cars_by_type: dict[str, pd.DataFrame],86    window: tuple[int, int] = (-5, 5),87) -> plt.Figure:88    """Figure: Cumulative abnormal returns around events, paneled by event type."""89    set_jof_style()90    n_types = len(cars_by_type)91    fig, axes = plt.subplots(1, n_types, figsize=(4 * n_types, 4), sharey=True)92    if n_types == 1:93        axes = [axes]9495    for ax, (event_type, cars) in zip(axes, cars_by_type.items()):96        days = range(window[0], window[1] + 1)97        if "mean_CAR" in cars.columns:98            mean_car = cars["mean_CAR"].values99            if "std_CAR" in cars.columns:100                std_car = cars["std_CAR"].values101                ax.fill_between(days[:len(mean_car)],102                                mean_car - 1.96 * std_car,103                                mean_car + 1.96 * std_car,104                                alpha=0.2, color=JOF_COLORS["primary"])105            ax.plot(days[:len(mean_car)], mean_car, color=JOF_COLORS["primary"], linewidth=1.5)106        ax.axhline(0, color="black", linewidth=0.5)107        ax.axvline(0, color=JOF_COLORS["secondary"], linewidth=0.8, linestyle="--")108        ax.set_xlabel("Days Relative to Event")109        ax.set_title(event_type)110111    axes[0].set_ylabel("Cumulative Abnormal Return")112    fig.suptitle("Event Study: Cumulative Abnormal Returns", y=1.02)113    fig.tight_layout()114    return fig115116117def fig_granger_heatmap(granger_pvalues: pd.DataFrame) -> plt.Figure:118    """Figure: Heatmap of Granger causality p-values."""119    set_jof_style()120    fig, ax = plt.subplots(figsize=(6, 5))121    im = ax.imshow(granger_pvalues.values, cmap="RdYlGn_r", vmin=0, vmax=0.10, aspect="auto")122    ax.set_xticks(range(len(granger_pvalues.columns)))123    ax.set_xticklabels(granger_pvalues.columns, rotation=45, ha="right")124    ax.set_yticks(range(len(granger_pvalues.index)))125    ax.set_yticklabels(granger_pvalues.index)126    ax.set_xlabel("Causing Variable")127    ax.set_ylabel("Caused Variable")128    ax.set_title("Granger Causality Test ($p$-values)")129    for i in range(len(granger_pvalues.index)):130        for j in range(len(granger_pvalues.columns)):131            val = granger_pvalues.iloc[i, j]132            color = "white" if val < 0.05 else "black"133            ax.text(j, i, f"{val:.3f}", ha="center", va="center", fontsize=8, color=color)134    fig.colorbar(im, ax=ax, label="$p$-value")135    fig.tight_layout()136    return fig137138139def fig_impulse_response(irf_data: dict, periods: int = 20) -> plt.Figure:140    """Figure: VAR impulse response functions."""141    set_jof_style()142    shock_vars = list({k[0] for k in irf_data.keys()})143    response_vars = list({k[1] for k in irf_data.keys()})144    n_shocks = len(shock_vars)145    n_responses = len(response_vars)146    fig, axes = plt.subplots(n_shocks, n_responses, figsize=(4 * n_responses, 3 * n_shocks),147                             sharex=True)148    if n_shocks == 1 and n_responses == 1:149        axes = np.array([[axes]])150    elif n_shocks == 1:151        axes = axes.reshape(1, -1)152    elif n_responses == 1:153        axes = axes.reshape(-1, 1)154155    for i, shock in enumerate(shock_vars):156        for j, resp in enumerate(response_vars):157            ax = axes[i, j]158            key = (shock, resp)159            if key in irf_data:160                response = irf_data[key][:periods]161                ax.plot(range(len(response)), response, color=JOF_COLORS["primary"], linewidth=1.2)162                ax.axhline(0, color="black", linewidth=0.5)163            if i == 0:164                ax.set_title(f"Response: {resp}", fontsize=9)165            if j == 0:166                ax.set_ylabel(f"Shock: {shock}", fontsize=9)167            if i == n_shocks - 1:168                ax.set_xlabel("Periods")169170    fig.suptitle("Impulse Response Functions", y=1.02)171    fig.tight_layout()172    return fig173174175def fig_volatility(176    rv: pd.Series,177    cond_vol: pd.Series | None = None,178    fitted_har: pd.Series | None = None,179) -> plt.Figure:180    """Figure: Volatility analysis panels."""181    set_jof_style()182    n_panels = 1 + (cond_vol is not None) + (fitted_har is not None)183    fig, axes = plt.subplots(n_panels, 1, figsize=(7, 3 * n_panels), sharex=True)184    if n_panels == 1:185        axes = [axes]186187    panel = 0188    axes[panel].plot(rv.index, rv.values, color=JOF_COLORS["primary"], linewidth=0.8)189    axes[panel].set_ylabel("Realized Volatility")190    axes[panel].set_title("Panel A: Realized Volatility")191    panel += 1192193    if cond_vol is not None:194        axes[panel].plot(cond_vol.index, cond_vol.values, color=JOF_COLORS["secondary"], linewidth=0.8)195        axes[panel].set_ylabel("Conditional Volatility")196        axes[panel].set_title("Panel B: GARCH Conditional Volatility")197        panel += 1198199    if fitted_har is not None:200        axes[panel].plot(rv.index, rv.values, color=JOF_COLORS["gray"], linewidth=0.6, alpha=0.5, label="Actual")201        axes[panel].plot(fitted_har.index, fitted_har.values, color=JOF_COLORS["tertiary"], linewidth=0.8, label="HAR-RV Fit")202        axes[panel].set_ylabel("Realized Volatility")203        axes[panel].set_title("Panel C: HAR-RV Model Fit")204        axes[panel].legend()205206    axes[-1].set_xlabel("Date")207    axes[-1].xaxis.set_major_formatter(mdates.DateFormatter("%b %Y"))208    fig.autofmt_xdate(rotation=45)209    fig.tight_layout()210    return fig211212213def fig_liquidity(volume: pd.Series, spreads: pd.Series | None = None) -> plt.Figure:214    """Figure: Liquidity dynamics."""215    set_jof_style()216    n_panels = 1 + (spreads is not None)217    fig, axes = plt.subplots(n_panels, 1, figsize=(7, 3 * n_panels), sharex=True)218    if n_panels == 1:219        axes = [axes]220221    axes[0].bar(volume.index, volume.values, color=JOF_COLORS["primary"], alpha=0.6, width=1)222    axes[0].set_ylabel("Trading Volume (\\$)")223    axes[0].set_title("Panel A: Daily Trading Volume")224225    if spreads is not None:226        axes[1].plot(spreads.index, spreads.values, color=JOF_COLORS["secondary"], linewidth=0.8)227        axes[1].set_ylabel("Estimated Spread")228        axes[1].set_title("Panel B: Bid--Ask Spread (Roll Estimator)")229230    axes[-1].set_xlabel("Date")231    axes[-1].xaxis.set_major_formatter(mdates.DateFormatter("%b %Y"))232    fig.autofmt_xdate(rotation=45)233    fig.tight_layout()234    return fig235236237def fig_election_case_study(238    trump_prices: pd.Series,239    spy_returns: pd.Series | None = None,240) -> plt.Figure:241    """Figure: 2024 Presidential Election case study with event annotations."""242    set_jof_style()243    has_spy = spy_returns is not None244    fig, ax1 = plt.subplots(figsize=(8, 4.5))245246    ax1.plot(trump_prices.index, trump_prices.values, color=JOF_COLORS["primary"],247             linewidth=1.2, label="Trump Win Probability")248    ax1.set_xlabel("Date")249    ax1.set_ylabel("Probability", color=JOF_COLORS["primary"])250251    if has_spy:252        ax2 = ax1.twinx()253        cum_ret = (1 + spy_returns).cumprod() - 1254        common_idx = cum_ret.index.intersection(trump_prices.index)255        ax2.plot(common_idx, cum_ret.loc[common_idx].values, color=JOF_COLORS["secondary"],256                 linewidth=0.8, alpha=0.7, label="SPY Cumulative Return")257        ax2.set_ylabel("SPY Cumulative Return", color=JOF_COLORS["secondary"])258259    for label, d in ELECTION_KEY_DATES.items():260        dt = pd.Timestamp(d)261        if dt in trump_prices.index or (trump_prices.index.min() <= dt <= trump_prices.index.max()):262            ax1.axvline(dt, color=JOF_COLORS["gray"], linewidth=0.8, linestyle=":")263            ypos = ax1.get_ylim()[1] * 0.95264            ax1.annotate(label, xy=(dt, ypos), fontsize=7, rotation=90,265                         ha="right", va="top")266267    ax1.set_title("2024 US Presidential Election: Price Discovery on Polymarket")268    ax1.legend(loc="upper left")269    if has_spy:270        ax2.legend(loc="upper right")271    ax1.xaxis.set_major_formatter(mdates.DateFormatter("%b %Y"))272    fig.autofmt_xdate(rotation=45)273    fig.tight_layout()274    return fig275276277def fig_arbitrage_violations(violations: pd.DataFrame) -> plt.Figure:278    """Figure: Arbitrage violations over time."""279    set_jof_style()280    fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(10, 4))281282    if "violation_size" in violations.columns:283        ax1.plot(violations.index, violations["violation_size"], color=JOF_COLORS["primary"],284                 linewidth=0.8)285        ax1.axhline(0, color="black", linewidth=0.5)286        ax1.set_xlabel("Date")287        ax1.set_ylabel("State Price Sum $-$ 1.0")288        ax1.set_title("Panel A: Completeness Deviation Over Time")289        ax1.xaxis.set_major_formatter(mdates.DateFormatter("%b %Y"))290291        ax2.hist(violations["violation_size"].dropna(), bins=30,292                 color=JOF_COLORS["primary"], alpha=0.7, edgecolor="white")293        ax2.axvline(0, color=JOF_COLORS["secondary"], linewidth=0.8, linestyle="--")294        ax2.set_xlabel("Deviation from Completeness")295        ax2.set_ylabel("Frequency")296        ax2.set_title("Panel B: Distribution of Violations")297298    fig.autofmt_xdate(rotation=45)299    fig.tight_layout()300    return fig301