Python 56.2%
TeX 43.7%
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