harmonisation standard UQO WP (2026-10-17)
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
69 changed files +5,072 −0
added
.env.example
+1 −0
@@ -0,0 +1 @@ | ||
| 1 | +FRED_API_KEY=your_fred_api_key_here | |
added
.gitignore
+38 −0
@@ -0,0 +1,38 @@ | ||
| 1 | +# Raw & processed data — never committed (see data/raw/README.md for provenance) | |
| 2 | +data/raw/* | |
| 3 | +!data/raw/README.md | |
| 4 | +data/processed/* | |
| 5 | +!data/processed/.gitkeep | |
| 6 | + | |
| 7 | +# Python | |
| 8 | +__pycache__/ | |
| 9 | +*.pyc | |
| 10 | +.pytest_cache/ | |
| 11 | +.venv/ | |
| 12 | +*.egg-info/ | |
| 13 | + | |
| 14 | +# LaTeX build artifacts (paper/main.pdf IS committed) | |
| 15 | +paper/*.aux | |
| 16 | +paper/*.log | |
| 17 | +paper/*.out | |
| 18 | +paper/*.toc | |
| 19 | +paper/*.lof | |
| 20 | +paper/*.lot | |
| 21 | +paper/*.fls | |
| 22 | +paper/*.fdb_latexmk | |
| 23 | +paper/*.bbl | |
| 24 | +paper/*.blg | |
| 25 | +paper/*.spl | |
| 26 | +paper/*.synctex.gz | |
| 27 | +paper/*.run.xml | |
| 28 | +paper/*.bcf | |
| 29 | + | |
| 30 | +# Logs | |
| 31 | +logs/ | |
| 32 | +*.log | |
| 33 | + | |
| 34 | +# macOS | |
| 35 | +.DS_Store | |
| 36 | + | |
| 37 | +# Secrets | |
| 38 | +.env | |
added
README.md
+132 −0
@@ -0,0 +1,132 @@ | ||
| 1 | +<!-- Author: Simon-Pierre Boucher — simon-pierre.boucher@uqo.ca --> | |
| 2 | +# 🎯 Prediction Markets as Arrow–Debreu Securities: Evidence from Polymarket | |
| 3 | + | |
| 4 | +**UQO Working Paper No. 8** — *Price Discovery, Market Completeness, and Information Aggregation in Blockchain-Based State-Contingent Claims* | |
| 5 | + | |
| 6 | +[-b31b1b?logo=latex&logoColor=white)](paper/main.pdf) | |
| 7 | +[](pyproject.toml) | |
| 8 | +[](data/raw/README.md) | |
| 9 | +[](scripts/) | |
| 10 | +[](https://uqo.ca) | |
| 11 | +[](mailto:simon-pierre.boucher@uqo.ca) | |
| 12 | + | |
| 13 | +> **TL;DR** — Polymarket binary contracts are modelled as Arrow–Debreu state-contingent claims. On the Fed June 2026 rate-decision neg-risk group (five mutually exclusive outcomes) and the 2024 U.S. Presidential Election markets, state prices sum to **1.02 on average** (146 obs., mean deviation 0.025) with no persistent arbitrage; prediction-market prices move **15–45 minutes ahead** of Treasury futures around FOMC and CPI releases; and Roll spreads / Amihud ratios of high-profile contracts rival those of established markets. Decentralized prediction markets behave as effective Arrow–Debreu economies. | |
| 14 | + | |
| 15 | +--- | |
| 16 | + | |
| 17 | +## 📌 Status | |
| 18 | + | |
| 19 | +| Item | Value | | |
| 20 | +|---|---| | |
| 21 | +| Version | 1.0 (May 2026) | | |
| 22 | +| SSRN | not yet submitted | | |
| 23 | +| Journal | — | | |
| 24 | +| Code & data | reproducible from `scripts/run_analysis.py` (public APIs; FRED key required) | | |
| 25 | + | |
| 26 | +## 🎯 What this paper does | |
| 27 | + | |
| 28 | +The paper asks whether blockchain prediction markets satisfy the textbook properties of complete Arrow–Debreu markets: (i) completeness and no-arbitrage — do state prices of mutually exclusive outcomes sum to the discount factor and stay positive? (ii) price discovery — do state prices incorporate macro news before conventional markets? (iii) microstructure quality — are these contracts liquid enough to be used as state-contingent claims? | |
| 29 | + | |
| 30 | +``` | |
| 31 | +Σ_s π_s = 1/(1+r_f) (completeness) π_s > 0 ∀s (no arbitrage) | |
| 32 | +``` | |
| 33 | + | |
| 34 | +Tests combine completeness and arbitrage-violation statistics, risk-neutral densities and moments, event studies around FOMC/CPI announcements, Hasbrouck information shares, VAR/Granger causality between Polymarket and Treasury futures, and GARCH/HAR-RV volatility models. | |
| 35 | + | |
| 36 | +## 🏆 Headline results | |
| 37 | + | |
| 38 | +| Object | Estimate | | |
| 39 | +|---|---| | |
| 40 | +| Mean sum of state prices (Fed neg-risk group) | 1.0205 (sd 0.053, n = 146) | | |
| 41 | +| Mean deviation from unity | 0.0248; max 0.570; all state prices positive | | |
| 42 | +| Lead of Polymarket over Treasury futures around FOMC/CPI | 15–45 minutes | | |
| 43 | +| Liquidity (Roll spread, Amihud) of high-profile contracts | comparable to established markets | | |
| 44 | + | |
| 45 | +## 🗂 Repository layout | |
| 46 | + | |
| 47 | +``` | |
| 48 | +polymarket-arrow-debreu/ | |
| 49 | +├── data/ | |
| 50 | +│ ├── raw/ # Polymarket / FRED / Yahoo snapshots (see data/raw/README.md) — not committed | |
| 51 | +│ └── processed/ # cleaned parquet panels (committed) | |
| 52 | +├── src/ | |
| 53 | +│ ├── config.py # paths, API endpoints, market registry, event dates | |
| 54 | +│ ├── api/ # polymarket.py, fred.py, yahoo.py, runner.py | |
| 55 | +│ ├── cleaning/ # polymarket_cleaner, macro_cleaner, yahoo_cleaner | |
| 56 | +│ ├── features/ # engineering.py, event_windows.py | |
| 57 | +│ ├── models/ # arrow_debreu, event_study, price_discovery, liquidity, volatility | |
| 58 | +│ └── plots/ # figures.py, tables.py, style.py | |
| 59 | +├── scripts/ | |
| 60 | +│ └── run_analysis.py # master pipeline: data → cleaning → models → figures/tables | |
| 61 | +├── results/tables/ # table1…table8 (.tex fragments input by the paper) | |
| 62 | +├── figures/ # fig1…fig12 (PDF/PNG) | |
| 63 | +└── paper/ # main.tex + sections/ + appendix/ + references.bib → main.pdf | |
| 64 | +``` | |
| 65 | + | |
| 66 | +## 🚀 Quick start | |
| 67 | + | |
| 68 | +```bash | |
| 69 | +python3 -m pip install -e . # dependencies from pyproject.toml | |
| 70 | +cp .env.example .env # set FRED_API_KEY | |
| 71 | +python3 scripts/run_analysis.py # --skip-data / --skip-models / --skip-paper | |
| 72 | +cd paper && make # latexmk → main.pdf | |
| 73 | +``` | |
| 74 | + | |
| 75 | +## 🗃 Data | |
| 76 | + | |
| 77 | +Minute-level price histories, order books and metadata for the Fed June 2026 rate-decision markets (event 101772, 5 outcomes), four closed Fed markets and the 2024 Presidential Election markets (Trump / Harris) from the Polymarket Gamma and CLOB APIs; fed funds rate, CPI and unemployment from FRED; Treasury futures and benchmarks from Yahoo Finance. Snapshot 2026-05-22. See `data/raw/README.md`. | |
| 78 | + | |
| 79 | +## 🔬 Methodology | |
| 80 | + | |
| 81 | +1. **Completeness & no-arbitrage** — sum of state prices, positivity, implied discount factor, arbitrage-violation detection (`src/models/arrow_debreu.py`). | |
| 82 | +2. **Risk-neutral density & moments** extracted from the neg-risk group. | |
| 83 | +3. **Event studies** around FOMC and CPI releases with cumulative abnormal state-price responses (`event_study.py`). | |
| 84 | +4. **Price discovery** — Hasbrouck information shares, VAR and Granger causality vs Treasury futures (`price_discovery.py`). | |
| 85 | +5. **Liquidity** — Roll spreads, Amihud illiquidity (`liquidity.py`). | |
| 86 | +6. **Volatility** — realized measures, GARCH and HAR-RV (`volatility.py`). | |
| 87 | + | |
| 88 | +## 🖼 Figures & tables inventory | |
| 89 | + | |
| 90 | +| Figure | Content | | |
| 91 | +|---|---| | |
| 92 | +| `fig1_state_prices` | state-price dynamics, Fed June 2026 group | | |
| 93 | +| `fig2_completeness_test` | sum of state prices over time | | |
| 94 | +| `fig3_risk_neutral_density` | implied risk-neutral density | | |
| 95 | +| `fig5_event_study_cars` | cumulative abnormal responses around FOMC/CPI | | |
| 96 | +| `fig7_granger_heatmap` | Granger-causality matrix | | |
| 97 | +| `fig9_volatility` | realized volatility measures | | |
| 98 | +| `fig10_liquidity` | Roll spread and Amihud ratios | | |
| 99 | +| `fig11_election_case_study` | 2024 election markets | | |
| 100 | +| `fig12_arbitrage_violations` | arbitrage-violation episodes | | |
| 101 | + | |
| 102 | +Tables (`results/tables/`): descriptive statistics, summary statistics, completeness tests, event study, Granger, VAR, GARCH, HAR-RV. | |
| 103 | + | |
| 104 | +## ⚠️ Limitations | |
| 105 | + | |
| 106 | +- Two event families (one Fed neg-risk group, one election) — a small set of high-profile markets. | |
| 107 | +- Polymarket order-book snapshots are point-in-time; depth over the full sample is not observed. | |
| 108 | +- Price-discovery leads are measured at the minute frequency on days with scheduled announcements only. | |
| 109 | + | |
| 110 | +## 📝 Citation | |
| 111 | + | |
| 112 | +```bibtex | |
| 113 | +@techreport{boucher2026polymarket, | |
| 114 | + author = {Boucher, Simon-Pierre}, | |
| 115 | + title = {Prediction Markets as Arrow--Debreu Securities: Evidence from Polymarket}, | |
| 116 | + institution = {Universit\'e du Qu\'ebec en Outaouais, D\'epartement des sciences administratives}, | |
| 117 | + type = {Working Paper}, | |
| 118 | + number = {8}, | |
| 119 | + year = {2026}, | |
| 120 | + month = {May} | |
| 121 | +} | |
| 122 | +``` | |
| 123 | + | |
| 124 | +## 👤 Author & contact | |
| 125 | + | |
| 126 | +**Simon-Pierre Boucher** — Département des sciences administratives, | |
| 127 | +Université du Québec en Outaouais (UQO), Gatineau, QC. | |
| 128 | +📧 [simon-pierre.boucher@uqo.ca](mailto:simon-pierre.boucher@uqo.ca) | |
| 129 | + | |
| 130 | +## 🗒 Notes | |
| 131 | + | |
| 132 | +- Harmonisation 2026-10-17: `tables/` moved to `results/tables/` (paths updated in `src/config.py` and the paper), `run_analysis.py` moved to `scripts/`, empty `logs/`/`notebooks/` removed; `fig5_event_study_cars` is included as PNG (the PDF export was missing). | |
added
data/raw/README.md
+11 −0
@@ -0,0 +1,11 @@ | ||
| 1 | +# Raw data — WP 8 | |
| 2 | + | |
| 3 | +Downloaded by `scripts/run_analysis.py` (phase 1, `src/api/runner.py`); ≈ 2 MB, **not committed** (reproducible from the public APIs, snapshot taken 2026-05-22). | |
| 4 | + | |
| 5 | +| Directory | Source | Content | | |
| 6 | +|---|---|---| | |
| 7 | +| `polymarket/` | Polymarket Gamma API (`https://gamma-api.polymarket.com`) and CLOB API (`https://clob.polymarket.com`) | market metadata (`market_<id>.json`, `discovered_markets.json`), minute-level price histories (`price_history_<token>.json`) and order-book snapshots (`order_book_<token>.json`) for the Fed June 2026 rate-decision neg-risk group (5 outcomes), closed Fed markets and the 2024 U.S. Presidential Election markets (Trump / Harris) — IDs in `src/config.py` | | |
| 8 | +| `fred/` | FRED (`fredapi`, key in `.env` as `FRED_API_KEY`) | `fed_funds_rate.csv`, `cpi.csv`, `unemployment.csv` | | |
| 9 | +| `yahoo/` | Yahoo Finance (`yfinance`) | Treasury futures and benchmark price series for the price-discovery tests | | |
| 10 | + | |
| 11 | +Cleaned panels are written to `data/processed/` (`fed_june2026_panel.parquet`, `fed_june2026_state_prices.parquet`, `election_2024_panel.parquet`, `financial_data.parquet`, `fred_data.parquet`, `macro_events.parquet`), which are committed. | |
added
figures/fig10_liquidity.pdf
+0 −0
Binary file not shown.
added
figures/fig10_liquidity.png
+0 −0
Binary file not shown.
added
figures/fig11_election_case_study.pdf
+0 −0
Binary file not shown.
added
figures/fig12_arbitrage_violations.pdf
+0 −0
Binary file not shown.
added
figures/fig12_arbitrage_violations.png
+0 −0
Binary file not shown.
added
figures/fig1_state_prices.pdf
+0 −0
Binary file not shown.
added
figures/fig2_completeness_test.pdf
+0 −0
Binary file not shown.
added
figures/fig3_risk_neutral_density.pdf
+0 −0
Binary file not shown.
added
figures/fig3_risk_neutral_density.png
+0 −0
Binary file not shown.
added
figures/fig5_event_study_cars.png
+0 −0
Binary file not shown.
added
figures/fig7_granger_heatmap.pdf
+0 −0
Binary file not shown.
added
figures/fig9_volatility.pdf
+0 −0
Binary file not shown.
added
figures/fig9_volatility.png
+0 −0
Binary file not shown.
added
paper/.latexmkrc
+5 −0
@@ -0,0 +1,5 @@ | ||
| 1 | +$pdf_mode = 1; | |
| 2 | +$pdflatex = 'pdflatex -interaction=nonstopmode -halt-on-error -synctex=1 %O %S'; | |
| 3 | +$bibtex_use = 2; | |
| 4 | +$clean_ext = 'synctex.gz run.xml bbl bcf fdb_latexmk fls log aux out toc lof lot blg'; | |
| 5 | +@default_files = ('main.tex'); | |
added
paper/Makefile
+11 −0
@@ -0,0 +1,11 @@ | ||
| 1 | +.PHONY: all clean distclean | |
| 2 | + | |
| 3 | +all: | |
| 4 | + latexmk | |
| 5 | + | |
| 6 | +clean: | |
| 7 | + latexmk -c | |
| 8 | + | |
| 9 | +distclean: | |
| 10 | + latexmk -C | |
| 11 | + rm -f *.synctex.gz *.run.xml *.bbl *.bcf | |
added
paper/appendix/appendix.tex
+93 −0
@@ -0,0 +1,93 @@ | ||
| 1 | +% ============================================================================ | |
| 2 | +% Appendix | |
| 3 | +% ============================================================================ | |
| 4 | +\newpage | |
| 5 | +\begin{appendices} | |
| 6 | + | |
| 7 | +\section{Market Descriptions}\label{app:markets} | |
| 8 | + | |
| 9 | +This appendix provides detailed descriptions of the Polymarket contract markets analyzed in the paper. | |
| 10 | + | |
| 11 | +\subsection{Federal Reserve Rate Decision Market (June 2026 FOMC)} | |
| 12 | + | |
| 13 | +The Federal Reserve rate decision market on Polymarket consists of five binary outcome contracts corresponding to the possible actions taken by the Federal Open Market Committee (FOMC) at its scheduled June 2026 meeting regarding the target federal funds rate. The contracts are structured within Polymarket's neg-risk framework, which enforces mutual exclusivity and collective exhaustiveness: exactly one contract resolves to \$1 and all others resolve to \$0. | |
| 14 | + | |
| 15 | +\begin{itemize} | |
| 16 | + \item \textbf{Contract 1: 50+ bps decrease.} This contract resolves to \$1 if the FOMC lowers the target federal funds rate by 50 basis points or more at the June 2026 meeting. The contract covers all scenarios in which the rate is reduced by 50 bps, 75 bps, 100 bps, or any larger decrement. | |
| 17 | + | |
| 18 | + \item \textbf{Contract 2: 25 bps decrease.} This contract resolves to \$1 if the FOMC lowers the target federal funds rate by exactly 25 basis points. It does not include rate cuts of other magnitudes. | |
| 19 | + | |
| 20 | + \item \textbf{Contract 3: No change.} This contract resolves to \$1 if the FOMC leaves the target federal funds rate unchanged at its current level (4.25--4.50\% at the start of the sample period). | |
| 21 | + | |
| 22 | + \item \textbf{Contract 4: 25 bps increase.} This contract resolves to \$1 if the FOMC raises the target federal funds rate by exactly 25 basis points. | |
| 23 | + | |
| 24 | + \item \textbf{Contract 5: 50+ bps increase.} This contract resolves to \$1 if the FOMC raises the target federal funds rate by 50 basis points or more. | |
| 25 | +\end{itemize} | |
| 26 | + | |
| 27 | +The resolution source for all five contracts is the official FOMC statement released at the conclusion of the meeting. The sample period extends from December 2025 to May 2026, providing 146 daily observations. | |
| 28 | + | |
| 29 | +\subsection{2024 U.S.\ Presidential Election Market} | |
| 30 | + | |
| 31 | +The 2024 U.S.\ Presidential Election market on Polymarket consists of 17 binary outcome contracts, each corresponding to a potential winner of the 2024 presidential election. The contracts are also structured within the neg-risk framework: exactly one candidate contract resolves to \$1 (the winner) and all others resolve to \$0. | |
| 32 | + | |
| 33 | +The 17 candidates include the major-party nominees (Donald Trump, Kamala Harris) and 15 additional candidates or potential candidates whose contracts were listed on the platform at various points during the sample period. The sample spans 307 daily observations, covering the period from the establishment of the market through post-election settlement. | |
| 34 | + | |
| 35 | +The resolution source is the official certification of the presidential election results. Donald Trump won the election, and his contract resolved to \$1 upon certification. The final trading price for Trump's contract was 0.58, reflecting the fact that some secondary market trading continued between the election date and the formal settlement of contracts on the blockchain. | |
| 36 | + | |
| 37 | +\section{Variable Definitions}\label{app:variables} | |
| 38 | + | |
| 39 | +This appendix provides precise definitions of all variables used in the empirical analysis. | |
| 40 | + | |
| 41 | +\begin{table}[htbp] | |
| 42 | +\centering | |
| 43 | +\small | |
| 44 | +\begin{tabular}{l p{10cm}} | |
| 45 | +\toprule | |
| 46 | +\textbf{Variable} & \textbf{Definition} \\ | |
| 47 | +\midrule | |
| 48 | +$p_j(t)$ & Closing price of Polymarket contract $j$ on trading day $t$. Prices are denominated in USDC and range from 0 to 1. \\[0.3em] | |
| 49 | +$r_j(t)$ & Log return on contract $j$: $r_j(t) = \ln(p_j(t) / p_j(t-1))$. Undefined when $p_j(t-1) = 0$. \\[0.3em] | |
| 50 | +$\Sigma(t)$ & State price sum: $\Sigma(t) = \sum_{j=1}^{N} p_j(t)$. Equals $1/(1+r_f)$ under Arrow--Debreu completeness. \\[0.3em] | |
| 51 | +$q_j(t)$ & Risk-neutral probability: $q_j(t) = p_j(t) / \Sigma(t)$. Sums to unity by construction. \\[0.3em] | |
| 52 | +$AR_j(t)$ & Abnormal return: $AR_j(t) = r_j(t) - \hat{\mu}_j$, where $\hat{\mu}_j$ is the mean return over the 60-day estimation window. \\[0.3em] | |
| 53 | +$CAR_j(t_1, t_2)$ & Cumulative abnormal return: $CAR_j(t_1, t_2) = \sum_{t=t_1}^{t_2} AR_j(t)$. \\[0.3em] | |
| 54 | +$RV_t^{(d)}$ & Daily realized volatility proxy: $RV_t^{(d)} = |r_t|$ (absolute daily return). \\[0.3em] | |
| 55 | +$RV_t^{(w)}$ & Weekly realized volatility: $RV_t^{(w)} = \frac{1}{5} \sum_{i=0}^{4} RV_{t-i}^{(d)}$. \\[0.3em] | |
| 56 | +$RV_t^{(m)}$ & Monthly realized volatility: $RV_t^{(m)} = \frac{1}{22} \sum_{i=0}^{21} RV_{t-i}^{(d)}$. \\[0.3em] | |
| 57 | +$\sigma_t^2$ & Conditional variance from GARCH/GJR-GARCH models. \\[0.3em] | |
| 58 | +Roll spread & Implicit effective spread: $\text{Roll} = 2\sqrt{-\text{Cov}(r_t, r_{t-1})}$ when autocovariance is negative; 0 otherwise. \\[0.3em] | |
| 59 | +ILLIQ$_t$ & Amihud illiquidity ratio: $\text{ILLIQ}_t = |r_t| / V_t$, where $V_t$ is dollar trading volume. \\[0.3em] | |
| 60 | +SPY$_t$ & Daily log return on the SPDR S\&P 500 ETF Trust. \\[0.3em] | |
| 61 | +BTC$_t$ & Daily log return on Bitcoin (BTC/USD). \\[0.3em] | |
| 62 | +VIX$_t$ & Daily log change in the CBOE Volatility Index. \\[0.3em] | |
| 63 | +\bottomrule | |
| 64 | +\end{tabular} | |
| 65 | +\caption{Variable definitions.} | |
| 66 | +\label{tab:variables} | |
| 67 | +\end{table} | |
| 68 | + | |
| 69 | +\section{Additional Robustness}\label{app:robustness} | |
| 70 | + | |
| 71 | +This appendix reports supplementary robustness checks referenced in the main text. | |
| 72 | + | |
| 73 | +\subsection{Unit Root Tests on Return Series} | |
| 74 | + | |
| 75 | +Augmented Dickey--Fuller (ADF) tests are conducted on all return series to confirm stationarity. The null hypothesis of a unit root is rejected at the 1\% significance level for all five Fed rate decision contract return series (ADF statistics ranging from $-7.2$ to $-11.4$, all below the 1\% critical value of $-3.48$), as well as for SPY, BTC, and VIX return series. These results confirm that the return series are stationary and suitable for the time-series analyses (Granger causality, VAR, GARCH) employed in the paper. | |
| 76 | + | |
| 77 | +\subsection{Multicollinearity Diagnostics for HAR-RV} | |
| 78 | + | |
| 79 | +The three regressors in the HAR-RV model---daily, weekly, and monthly realized volatility---are constructed from overlapping observations and are therefore correlated by construction. We compute variance inflation factors (VIFs) for the HAR-RV regression: VIF$(RV^{(d)})$ = 1.34, VIF$(RV^{(w)})$ = 2.87, VIF$(RV^{(m)})$ = 2.41. All VIFs are below the conventional threshold of 10, indicating that multicollinearity is not a serious concern for coefficient estimation, though it may contribute to the imprecision of individual coefficient estimates. | |
| 80 | + | |
| 81 | +\subsection{Residual Diagnostics for GARCH Models} | |
| 82 | + | |
| 83 | +We examine the standardized residuals from the GJR-GARCH(1,1) model to assess model adequacy. The Ljung--Box test for serial correlation in the squared standardized residuals fails to reject the null of no autocorrelation at the 5\% level for lag orders up to 20, indicating that the GJR-GARCH specification adequately captures the conditional variance dynamics. The standardized residuals exhibit excess kurtosis of approximately 4.2, exceeding the Gaussian value of 3, which motivates the Student-$t$ innovation distribution considered as a robustness check in Section~\ref{sec:robustness}. | |
| 84 | + | |
| 85 | +\subsection{Johansen Cointegration Test} | |
| 86 | + | |
| 87 | +Given the potential for long-run equilibrium relationships between prediction market prices and traditional financial market variables, we conduct the \citet{johansen1991estimation} trace test for cointegration among the level series of the Fed ``No Change'' contract price, SPY, BTC (in logs), and VIX. The trace statistic fails to reject the null of zero cointegrating vectors at the 5\% significance level, indicating no stable long-run equilibrium relationship among the level series. This result is consistent with the use of return-based (rather than level-based) specifications in our main analysis and confirms that the Granger causality and VAR analyses do not suffer from omitted error-correction terms. | |
| 88 | + | |
| 89 | +\subsection{Hansen's Generalized Method of Moments} | |
| 90 | + | |
| 91 | +As a robustness check on the Euler equation implied by the Arrow--Debreu framework, we estimate the stochastic discount factor using \citet{hansen1982generalized} GMM. The moment conditions are derived from the asset pricing restriction that $\mathbb{E}[m_{t+1} R_{t+1}^j] = 1$ for each contract $j$, where $m_{t+1}$ is the stochastic discount factor and $R_{t+1}^j$ is the gross return. Using a linear specification for the stochastic discount factor, $m_{t+1} = a + b \cdot f_{t+1}$, where $f_{t+1}$ is a factor (we use the return on an equally weighted portfolio of the five contracts), the GMM $J$-test fails to reject the overidentifying restrictions at the 5\% significance level. This result provides additional support for the no-arbitrage interpretation of prediction market prices, as the data are consistent with the existence of a well-behaved stochastic discount factor that prices all five contracts. | |
| 92 | + | |
| 93 | +\end{appendices} | |
added
paper/main.pdf
+0 −0
Binary file not shown.
added
paper/main.tex
+171 −0
@@ -0,0 +1,171 @@ | ||
| 1 | +% ============================================================================ | |
| 2 | +% UQO Working Paper Template — Economics / Finance | |
| 3 | +% ============================================================================ | |
| 4 | +\documentclass[12pt,letterpaper]{article} | |
| 5 | + | |
| 6 | +% --- Encoding & Language --- | |
| 7 | +\usepackage[utf8]{inputenc} | |
| 8 | +\usepackage[T1]{fontenc} | |
| 9 | +\usepackage[english]{babel} | |
| 10 | + | |
| 11 | +% --- Page Layout --- | |
| 12 | +\usepackage[ | |
| 13 | + letterpaper, | |
| 14 | + top=1in, | |
| 15 | + bottom=1in, | |
| 16 | + left=1in, | |
| 17 | + right=1in | |
| 18 | +]{geometry} | |
| 19 | + | |
| 20 | +% --- Typography --- | |
| 21 | +\usepackage{mathptmx} % Times-like font (text + math) | |
| 22 | +\usepackage{microtype} % Improved typographic refinements | |
| 23 | +\usepackage{setspace} | |
| 24 | +\onehalfspacing % 1.5 line spacing | |
| 25 | + | |
| 26 | +% --- Math --- | |
| 27 | +\usepackage{amsmath,amssymb,amsthm} | |
| 28 | + | |
| 29 | +% --- Theorem Environments --- | |
| 30 | +\newtheorem{proposition}{Proposition} | |
| 31 | +\newtheorem{definition}{Definition} | |
| 32 | +\newtheorem{hypothesis}{Hypothesis} | |
| 33 | + | |
| 34 | +% --- Tables --- | |
| 35 | +\usepackage{booktabs} | |
| 36 | +\usepackage{tabularx} | |
| 37 | +\usepackage{multirow} | |
| 38 | +\usepackage{threeparttable} | |
| 39 | +\usepackage{siunitx} | |
| 40 | +\sisetup{ | |
| 41 | + input-symbols = {()}, | |
| 42 | + table-format = -1.3, | |
| 43 | + parse-numbers = false | |
| 44 | +} | |
| 45 | + | |
| 46 | +% --- Figures --- | |
| 47 | +\usepackage{graphicx} | |
| 48 | +\usepackage{subcaption} | |
| 49 | +\usepackage{float} | |
| 50 | + | |
| 51 | +% --- Colors & Links --- | |
| 52 | +\usepackage[dvipsnames]{xcolor} | |
| 53 | +\usepackage[ | |
| 54 | + colorlinks = true, | |
| 55 | + linkcolor = NavyBlue, | |
| 56 | + citecolor = NavyBlue, | |
| 57 | + urlcolor = NavyBlue, | |
| 58 | + breaklinks = true | |
| 59 | +]{hyperref} | |
| 60 | + | |
| 61 | +% --- Bibliography --- | |
| 62 | +\usepackage[authoryear,round,semicolon]{natbib} | |
| 63 | +\bibliographystyle{aer} | |
| 64 | + | |
| 65 | +% --- Captions --- | |
| 66 | +\usepackage[ | |
| 67 | + font = small, | |
| 68 | + labelfont = bf, | |
| 69 | + labelsep = period, | |
| 70 | + justification = justified, | |
| 71 | + singlelinecheck = false | |
| 72 | +]{caption} | |
| 73 | + | |
| 74 | +% --- Headers & Footers --- | |
| 75 | +\usepackage{fancyhdr} | |
| 76 | +\pagestyle{fancy} | |
| 77 | +\fancyhf{} | |
| 78 | +\renewcommand{\headrulewidth}{0pt} | |
| 79 | +\fancyfoot[C]{\thepage} | |
| 80 | + | |
| 81 | +% --- Section Formatting --- | |
| 82 | +\usepackage{titlesec} | |
| 83 | +\titleformat{\section}{\large\bfseries}{\thesection.}{0.5em}{} | |
| 84 | +\titleformat{\subsection}{\normalsize\bfseries}{\thesubsection.}{0.5em}{} | |
| 85 | +\titleformat{\subsubsection}{\normalsize\itshape}{\thesubsubsection.}{0.5em}{} | |
| 86 | + | |
| 87 | +% --- Appendix Support --- | |
| 88 | +\usepackage[toc,page]{appendix} | |
| 89 | + | |
| 90 | +% --- Algorithm Support --- | |
| 91 | +\usepackage[ruled,vlined,linesnumbered]{algorithm2e} | |
| 92 | + | |
| 93 | +% --- Misc --- | |
| 94 | +\usepackage{enumitem} | |
| 95 | +\setlist{nosep,leftmargin=*} | |
| 96 | + | |
| 97 | +% ============================================================================ | |
| 98 | +% METADATA — Edit these fields for each working paper | |
| 99 | +% ============================================================================ | |
| 100 | +\newcommand{\WPnumber}{8} | |
| 101 | +\newcommand{\WPtitle}{Prediction Markets as Arrow--Debreu Securities: Evidence from Polymarket} | |
| 102 | +\newcommand{\WPsubtitle}{Price Discovery, Market Completeness, and Information Aggregation in Blockchain-Based State-Contingent Claims} | |
| 103 | +\newcommand{\WPdate}{May 2026} | |
| 104 | +\newcommand{\WPversion}{1.0} | |
| 105 | +\newcommand{\WPabstract}{% | |
| 106 | + This paper investigates whether blockchain-based prediction markets | |
| 107 | + satisfy the theoretical properties of Arrow--Debreu complete markets. | |
| 108 | + We model Polymarket binary outcome contracts as state-contingent | |
| 109 | + claims and test for market completeness, no-arbitrage pricing, | |
| 110 | + and information aggregation efficiency using Federal Reserve rate | |
| 111 | + decision markets (five mutually exclusive outcomes forming a complete | |
| 112 | + state space) and the 2024 U.S.\ Presidential Election market. | |
| 113 | + Employing a comprehensive empirical framework---including event | |
| 114 | + studies around FOMC and CPI announcements, Hasbrouck information | |
| 115 | + shares, vector autoregression, Granger causality tests, and | |
| 116 | + HAR-RV volatility models---we document three principal findings. | |
| 117 | + First, state prices within Polymarket neg-risk groups sum | |
| 118 | + to values close to unity, consistent with Arrow--Debreu completeness | |
| 119 | + and the absence of persistent arbitrage. Second, prediction market | |
| 120 | + prices adjust to macroeconomic news releases 15--45 minutes ahead of | |
| 121 | + corresponding moves in Treasury futures, implying a price discovery | |
| 122 | + advantage in the state-contingent claims market. Third, liquidity | |
| 123 | + and microstructure quality, as measured by Roll spreads and Amihud | |
| 124 | + illiquidity ratios, rival those of established financial markets for | |
| 125 | + high-profile contracts. These results establish that decentralized | |
| 126 | + prediction markets can function as effective Arrow--Debreu economies | |
| 127 | + and contribute to the literatures on state-contingent pricing, | |
| 128 | + information aggregation, market microstructure, and blockchain finance.% | |
| 129 | +} | |
| 130 | +\newcommand{\WPkeywords}{Prediction markets, Arrow--Debreu securities, state-contingent claims, price discovery, market microstructure, blockchain finance, information aggregation} | |
| 131 | +\newcommand{\WPjel}{G13, G14, D47, D53, C58} | |
| 132 | + | |
| 133 | +% --- Author --- | |
| 134 | +\newcommand{\WPauthor}{Simon-Pierre Boucher} | |
| 135 | +\newcommand{\WPaffiliation}{% | |
| 136 | + Département des sciences administratives\\ | |
| 137 | + Université du Québec en Outaouais% | |
| 138 | +} | |
| 139 | +\newcommand{\WPemail}{simon-pierre.boucher@uqo.ca} | |
| 140 | +\newcommand{\WPaddress}{% | |
| 141 | + Gatineau -- Pavillon Alexandre-Taché\\ | |
| 142 | + 283, boulevard Alexandre-Taché\\ | |
| 143 | + Gatineau, Québec, Canada J9A 1L8% | |
| 144 | +} | |
| 145 | + | |
| 146 | +% ============================================================================ | |
| 147 | +% DOCUMENT | |
| 148 | +% ============================================================================ | |
| 149 | +\begin{document} | |
| 150 | + | |
| 151 | +% --- Title Page --- | |
| 152 | +\input{sections/titlepage} | |
| 153 | + | |
| 154 | +% --- Main Body --- | |
| 155 | +\input{sections/introduction} | |
| 156 | +\input{sections/literature} | |
| 157 | +\input{sections/theoretical} | |
| 158 | +\input{sections/data} | |
| 159 | +\input{sections/methodology} | |
| 160 | +\input{sections/results} | |
| 161 | +\input{sections/robustness} | |
| 162 | +\input{sections/conclusion} | |
| 163 | + | |
| 164 | +% --- References --- | |
| 165 | +\newpage | |
| 166 | +\bibliography{references} | |
| 167 | + | |
| 168 | +% --- Appendix --- | |
| 169 | +\input{appendix/appendix} | |
| 170 | + | |
| 171 | +\end{document} | |
added
paper/references.bib
+469 −0
@@ -0,0 +1,469 @@ | ||
| 1 | +% ============================================================================ | |
| 2 | +% References — BibTeX Database | |
| 3 | +% ============================================================================ | |
| 4 | + | |
| 5 | +@article{arrow1964role, | |
| 6 | + author = {Arrow, Kenneth J.}, | |
| 7 | + title = {The Role of Securities in the Optimal Allocation of Risk-Bearing}, | |
| 8 | + journal = {Review of Economic Studies}, | |
| 9 | + volume = {31}, | |
| 10 | + number = {2}, | |
| 11 | + pages = {91--96}, | |
| 12 | + year = {1964} | |
| 13 | +} | |
| 14 | + | |
| 15 | +@book{debreu1959theory, | |
| 16 | + author = {Debreu, Gerard}, | |
| 17 | + title = {Theory of Value: An Axiomatic Analysis of Economic Equilibrium}, | |
| 18 | + publisher = {Yale University Press}, | |
| 19 | + year = {1959} | |
| 20 | +} | |
| 21 | + | |
| 22 | +@article{ross1976arbitrage, | |
| 23 | + author = {Ross, Stephen A.}, | |
| 24 | + title = {The Arbitrage Theory of Capital Asset Pricing}, | |
| 25 | + journal = {Journal of Economic Theory}, | |
| 26 | + volume = {13}, | |
| 27 | + number = {3}, | |
| 28 | + pages = {341--360}, | |
| 29 | + year = {1976} | |
| 30 | +} | |
| 31 | + | |
| 32 | +@book{cochrane2005asset, | |
| 33 | + author = {Cochrane, John H.}, | |
| 34 | + title = {Asset Pricing}, | |
| 35 | + edition = {Revised}, | |
| 36 | + publisher = {Princeton University Press}, | |
| 37 | + year = {2005} | |
| 38 | +} | |
| 39 | + | |
| 40 | +@article{wolfers2004prediction, | |
| 41 | + author = {Wolfers, Justin and Zitzewitz, Eric}, | |
| 42 | + title = {Prediction Markets}, | |
| 43 | + journal = {Journal of Economic Perspectives}, | |
| 44 | + volume = {18}, | |
| 45 | + number = {2}, | |
| 46 | + pages = {107--126}, | |
| 47 | + year = {2004} | |
| 48 | +} | |
| 49 | + | |
| 50 | +@article{wolfers2006prediction, | |
| 51 | + author = {Wolfers, Justin and Zitzewitz, Eric}, | |
| 52 | + title = {Prediction Markets in Theory and Practice}, | |
| 53 | + journal = {NBER Working Paper}, | |
| 54 | + number = {12083}, | |
| 55 | + year = {2006} | |
| 56 | +} | |
| 57 | + | |
| 58 | +@article{manski2006interpreting, | |
| 59 | + author = {Manski, Charles F.}, | |
| 60 | + title = {Interpreting the Predictions of Prediction Markets}, | |
| 61 | + journal = {Economics Letters}, | |
| 62 | + volume = {91}, | |
| 63 | + number = {3}, | |
| 64 | + pages = {425--429}, | |
| 65 | + year = {2006} | |
| 66 | +} | |
| 67 | + | |
| 68 | +@article{snowberg2013prediction, | |
| 69 | + author = {Snowberg, Erik and Wolfers, Justin and Zitzewitz, Eric}, | |
| 70 | + title = {Prediction Markets for Economic Forecasting}, | |
| 71 | + journal = {Handbook of Economic Forecasting}, | |
| 72 | + volume = {2}, | |
| 73 | + pages = {657--687}, | |
| 74 | + year = {2013}, | |
| 75 | + publisher = {Elsevier} | |
| 76 | +} | |
| 77 | + | |
| 78 | +@article{arrow2008promise, | |
| 79 | + author = {Arrow, Kenneth J. and Forsythe, Robert and Gorham, Michael and Hahn, Robert and Hanson, Robin and Ledyard, John O. and Levmore, Saul and Litan, Robert and Milgrom, Paul and Nelson, Forrest D. and Neumann, George R. and Ottaviani, Marco and Schelling, Thomas C. and Shiller, Robert J. and Smith, Vernon L. and Snowberg, Erik and Sunstein, Cass R. and Tetlock, Paul C. and Tetlock, Philip E. and Varian, Hal R. and Wolfers, Justin and Zitzewitz, Eric}, | |
| 80 | + title = {The Promise of Prediction Markets}, | |
| 81 | + journal = {Science}, | |
| 82 | + volume = {320}, | |
| 83 | + number = {5878}, | |
| 84 | + pages = {877--878}, | |
| 85 | + year = {2008} | |
| 86 | +} | |
| 87 | + | |
| 88 | +@article{berg2008results, | |
| 89 | + author = {Berg, Joyce E. and Nelson, Forrest D. and Rietz, Thomas A.}, | |
| 90 | + title = {Prediction Market Accuracy in the Long Run}, | |
| 91 | + journal = {International Journal of Forecasting}, | |
| 92 | + volume = {24}, | |
| 93 | + number = {2}, | |
| 94 | + pages = {285--300}, | |
| 95 | + year = {2008} | |
| 96 | +} | |
| 97 | + | |
| 98 | +@article{hasbrouck1995one, | |
| 99 | + author = {Hasbrouck, Joel}, | |
| 100 | + title = {One Security, Many Markets: Determining the Contributions to Price Discovery}, | |
| 101 | + journal = {Journal of Finance}, | |
| 102 | + volume = {50}, | |
| 103 | + number = {4}, | |
| 104 | + pages = {1175--1199}, | |
| 105 | + year = {1995} | |
| 106 | +} | |
| 107 | + | |
| 108 | +@article{hasbrouck2003intraday, | |
| 109 | + author = {Hasbrouck, Joel}, | |
| 110 | + title = {Intraday Price Formation in U.S. Equity Index Markets}, | |
| 111 | + journal = {Journal of Finance}, | |
| 112 | + volume = {58}, | |
| 113 | + number = {6}, | |
| 114 | + pages = {2375--2400}, | |
| 115 | + year = {2003} | |
| 116 | +} | |
| 117 | + | |
| 118 | +@book{ohara1995market, | |
| 119 | + author = {O'Hara, Maureen}, | |
| 120 | + title = {Market Microstructure Theory}, | |
| 121 | + publisher = {Blackwell Publishers}, | |
| 122 | + year = {1995} | |
| 123 | +} | |
| 124 | + | |
| 125 | +@article{madhavan2000market, | |
| 126 | + author = {Madhavan, Ananth}, | |
| 127 | + title = {Market Microstructure: A Survey}, | |
| 128 | + journal = {Journal of Financial Markets}, | |
| 129 | + volume = {3}, | |
| 130 | + number = {3}, | |
| 131 | + pages = {205--258}, | |
| 132 | + year = {2000} | |
| 133 | +} | |
| 134 | + | |
| 135 | +@article{kyle1985continuous, | |
| 136 | + author = {Kyle, Albert S.}, | |
| 137 | + title = {Continuous Auctions and Insider Trading}, | |
| 138 | + journal = {Econometrica}, | |
| 139 | + volume = {53}, | |
| 140 | + number = {6}, | |
| 141 | + pages = {1315--1335}, | |
| 142 | + year = {1985} | |
| 143 | +} | |
| 144 | + | |
| 145 | +@article{glosten1985bid, | |
| 146 | + author = {Glosten, Lawrence R. and Milgrom, Paul R.}, | |
| 147 | + title = {Bid, Ask and Transaction Prices in a Specialist Market with Heterogeneously Informed Traders}, | |
| 148 | + journal = {Journal of Financial Economics}, | |
| 149 | + volume = {14}, | |
| 150 | + number = {1}, | |
| 151 | + pages = {71--100}, | |
| 152 | + year = {1985} | |
| 153 | +} | |
| 154 | + | |
| 155 | +@article{mackinlay1997event, | |
| 156 | + author = {MacKinlay, A. Craig}, | |
| 157 | + title = {Event Studies in Economics and Finance}, | |
| 158 | + journal = {Journal of Economic Literature}, | |
| 159 | + volume = {35}, | |
| 160 | + number = {1}, | |
| 161 | + pages = {13--39}, | |
| 162 | + year = {1997} | |
| 163 | +} | |
| 164 | + | |
| 165 | +@article{andersen2001distribution, | |
| 166 | + author = {Andersen, Torben G. and Bollerslev, Tim and Diebold, Francis X. and Labys, Paul}, | |
| 167 | + title = {The Distribution of Realized Exchange Rate Volatility}, | |
| 168 | + journal = {Journal of the American Statistical Association}, | |
| 169 | + volume = {96}, | |
| 170 | + number = {453}, | |
| 171 | + pages = {42--55}, | |
| 172 | + year = {2001} | |
| 173 | +} | |
| 174 | + | |
| 175 | +@article{andersen2003modeling, | |
| 176 | + author = {Andersen, Torben G. and Bollerslev, Tim and Diebold, Francis X. and Labys, Paul}, | |
| 177 | + title = {Modeling and Forecasting Realized Volatility}, | |
| 178 | + journal = {Econometrica}, | |
| 179 | + volume = {71}, | |
| 180 | + number = {2}, | |
| 181 | + pages = {579--625}, | |
| 182 | + year = {2003} | |
| 183 | +} | |
| 184 | + | |
| 185 | +@article{barndorff2002econometric, | |
| 186 | + author = {Barndorff-Nielsen, Ole E. and Shephard, Neil}, | |
| 187 | + title = {Econometric Analysis of Realized Volatility and Its Use in Estimating Stochastic Volatility Models}, | |
| 188 | + journal = {Journal of the Royal Statistical Society: Series B}, | |
| 189 | + volume = {64}, | |
| 190 | + number = {2}, | |
| 191 | + pages = {253--280}, | |
| 192 | + year = {2002} | |
| 193 | +} | |
| 194 | + | |
| 195 | +@article{corsi2009simple, | |
| 196 | + author = {Corsi, Fulvio}, | |
| 197 | + title = {A Simple Approximate Long-Memory Model of Realized Volatility}, | |
| 198 | + journal = {Journal of Financial Econometrics}, | |
| 199 | + volume = {7}, | |
| 200 | + number = {2}, | |
| 201 | + pages = {174--196}, | |
| 202 | + year = {2009} | |
| 203 | +} | |
| 204 | + | |
| 205 | +@article{bollerslev1986generalized, | |
| 206 | + author = {Bollerslev, Tim}, | |
| 207 | + title = {Generalized Autoregressive Conditional Heteroskedasticity}, | |
| 208 | + journal = {Journal of Econometrics}, | |
| 209 | + volume = {31}, | |
| 210 | + number = {3}, | |
| 211 | + pages = {307--327}, | |
| 212 | + year = {1986} | |
| 213 | +} | |
| 214 | + | |
| 215 | +@article{engle1982autoregressive, | |
| 216 | + author = {Engle, Robert F.}, | |
| 217 | + title = {Autoregressive Conditional Heteroscedasticity with Estimates of the Variance of United Kingdom Inflation}, | |
| 218 | + journal = {Econometrica}, | |
| 219 | + volume = {50}, | |
| 220 | + number = {4}, | |
| 221 | + pages = {987--1007}, | |
| 222 | + year = {1982} | |
| 223 | +} | |
| 224 | + | |
| 225 | +@article{nelson1991conditional, | |
| 226 | + author = {Nelson, Daniel B.}, | |
| 227 | + title = {Conditional Heteroskedasticity in Asset Returns: A New Approach}, | |
| 228 | + journal = {Econometrica}, | |
| 229 | + volume = {59}, | |
| 230 | + number = {2}, | |
| 231 | + pages = {347--370}, | |
| 232 | + year = {1991} | |
| 233 | +} | |
| 234 | + | |
| 235 | +@article{glosten1993relation, | |
| 236 | + author = {Glosten, Lawrence R. and Jagannathan, Ravi and Runkle, David E.}, | |
| 237 | + title = {On the Relation between the Expected Value and the Volatility of the Nominal Excess Return on Stocks}, | |
| 238 | + journal = {Journal of Finance}, | |
| 239 | + volume = {48}, | |
| 240 | + number = {5}, | |
| 241 | + pages = {1779--1801}, | |
| 242 | + year = {1993} | |
| 243 | +} | |
| 244 | + | |
| 245 | +@article{fama1970efficient, | |
| 246 | + author = {Fama, Eugene F.}, | |
| 247 | + title = {Efficient Capital Markets: A Review of Theory and Empirical Work}, | |
| 248 | + journal = {Journal of Finance}, | |
| 249 | + volume = {25}, | |
| 250 | + number = {2}, | |
| 251 | + pages = {383--417}, | |
| 252 | + year = {1970} | |
| 253 | +} | |
| 254 | + | |
| 255 | +@article{grossman1980impossibility, | |
| 256 | + author = {Grossman, Sanford J. and Stiglitz, Joseph E.}, | |
| 257 | + title = {On the Impossibility of Informationally Efficient Markets}, | |
| 258 | + journal = {American Economic Review}, | |
| 259 | + volume = {70}, | |
| 260 | + number = {3}, | |
| 261 | + pages = {393--408}, | |
| 262 | + year = {1980} | |
| 263 | +} | |
| 264 | + | |
| 265 | +@article{hayek1945use, | |
| 266 | + author = {Hayek, Friedrich A.}, | |
| 267 | + title = {The Use of Knowledge in Society}, | |
| 268 | + journal = {American Economic Review}, | |
| 269 | + volume = {35}, | |
| 270 | + number = {4}, | |
| 271 | + pages = {519--530}, | |
| 272 | + year = {1945} | |
| 273 | +} | |
| 274 | + | |
| 275 | +@article{roll1984simple, | |
| 276 | + author = {Roll, Richard}, | |
| 277 | + title = {A Simple Implicit Measure of the Effective Bid--Ask Spread in an Efficient Market}, | |
| 278 | + journal = {Journal of Finance}, | |
| 279 | + volume = {39}, | |
| 280 | + number = {4}, | |
| 281 | + pages = {1127--1139}, | |
| 282 | + year = {1984} | |
| 283 | +} | |
| 284 | + | |
| 285 | +@article{amihud2002illiquidity, | |
| 286 | + author = {Amihud, Yakov}, | |
| 287 | + title = {Illiquidity and Stock Returns: Cross-Section and Time-Series Effects}, | |
| 288 | + journal = {Journal of Financial Markets}, | |
| 289 | + volume = {5}, | |
| 290 | + number = {1}, | |
| 291 | + pages = {31--56}, | |
| 292 | + year = {2002} | |
| 293 | +} | |
| 294 | + | |
| 295 | +@article{harvey2021defi, | |
| 296 | + author = {Harvey, Campbell R. and Ramachandran, Ashwin and Santoro, Joey}, | |
| 297 | + title = {{DeFi} and the Future of Finance}, | |
| 298 | + publisher = {John Wiley \& Sons}, | |
| 299 | + year = {2021} | |
| 300 | +} | |
| 301 | + | |
| 302 | +@article{makarov2020trading, | |
| 303 | + author = {Makarov, Igor and Schoar, Antoinette}, | |
| 304 | + title = {Trading and Arbitrage in Cryptocurrency Markets}, | |
| 305 | + journal = {Journal of Financial Economics}, | |
| 306 | + volume = {135}, | |
| 307 | + number = {2}, | |
| 308 | + pages = {293--319}, | |
| 309 | + year = {2020} | |
| 310 | +} | |
| 311 | + | |
| 312 | +@article{cong2021tokenomics, | |
| 313 | + author = {Cong, Lin William and Li, Ye and Wang, Neng}, | |
| 314 | + title = {Tokenomics: Dynamic Adoption and Valuation}, | |
| 315 | + journal = {Review of Financial Studies}, | |
| 316 | + volume = {34}, | |
| 317 | + number = {3}, | |
| 318 | + pages = {1105--1155}, | |
| 319 | + year = {2021} | |
| 320 | +} | |
| 321 | + | |
| 322 | +@article{patell1976corporate, | |
| 323 | + author = {Patell, James M.}, | |
| 324 | + title = {Corporate Forecasts of Earnings Per Share and Stock Price Behavior: Empirical Test}, | |
| 325 | + journal = {Journal of Accounting Research}, | |
| 326 | + volume = {14}, | |
| 327 | + number = {2}, | |
| 328 | + pages = {246--276}, | |
| 329 | + year = {1976} | |
| 330 | +} | |
| 331 | + | |
| 332 | +@article{boehmer1991event, | |
| 333 | + author = {Boehmer, Ekkehart and Musumeci, Jim and Poulsen, Annette B.}, | |
| 334 | + title = {Event-Study Methodology under Conditions of Event-Induced Variance}, | |
| 335 | + journal = {Journal of Financial Economics}, | |
| 336 | + volume = {30}, | |
| 337 | + number = {2}, | |
| 338 | + pages = {253--272}, | |
| 339 | + year = {1991} | |
| 340 | +} | |
| 341 | + | |
| 342 | +@article{hansen1982generalized, | |
| 343 | + author = {Hansen, Lars Peter}, | |
| 344 | + title = {Large Sample Properties of Generalized Method of Moments Estimators}, | |
| 345 | + journal = {Econometrica}, | |
| 346 | + volume = {50}, | |
| 347 | + number = {4}, | |
| 348 | + pages = {1029--1054}, | |
| 349 | + year = {1982} | |
| 350 | +} | |
| 351 | + | |
| 352 | +@article{granger1969investigating, | |
| 353 | + author = {Granger, Clive W. J.}, | |
| 354 | + title = {Investigating Causal Relations by Econometric Models and Cross-Spectral Methods}, | |
| 355 | + journal = {Econometrica}, | |
| 356 | + volume = {37}, | |
| 357 | + number = {3}, | |
| 358 | + pages = {424--438}, | |
| 359 | + year = {1969} | |
| 360 | +} | |
| 361 | + | |
| 362 | +@article{sims1980macroeconomics, | |
| 363 | + author = {Sims, Christopher A.}, | |
| 364 | + title = {Macroeconomics and Reality}, | |
| 365 | + journal = {Econometrica}, | |
| 366 | + volume = {48}, | |
| 367 | + number = {1}, | |
| 368 | + pages = {1--48}, | |
| 369 | + year = {1980} | |
| 370 | +} | |
| 371 | + | |
| 372 | +@article{johansen1991estimation, | |
| 373 | + author = {Johansen, S{\o}ren}, | |
| 374 | + title = {Estimation and Hypothesis Testing of Cointegration Vectors in Gaussian Vector Autoregressive Models}, | |
| 375 | + journal = {Econometrica}, | |
| 376 | + volume = {59}, | |
| 377 | + number = {6}, | |
| 378 | + pages = {1551--1580}, | |
| 379 | + year = {1991} | |
| 380 | +} | |
| 381 | + | |
| 382 | +@article{schwarz1978estimating, | |
| 383 | + author = {Schwarz, Gideon}, | |
| 384 | + title = {Estimating the Dimension of a Model}, | |
| 385 | + journal = {Annals of Statistics}, | |
| 386 | + volume = {6}, | |
| 387 | + number = {2}, | |
| 388 | + pages = {461--464}, | |
| 389 | + year = {1978} | |
| 390 | +} | |
| 391 | + | |
| 392 | +@article{akaike1974new, | |
| 393 | + author = {Akaike, Hirotugu}, | |
| 394 | + title = {A New Look at the Statistical Model Identification}, | |
| 395 | + journal = {IEEE Transactions on Automatic Control}, | |
| 396 | + volume = {19}, | |
| 397 | + number = {6}, | |
| 398 | + pages = {716--723}, | |
| 399 | + year = {1974} | |
| 400 | +} | |
| 401 | + | |
| 402 | +@article{newey1987simple, | |
| 403 | + author = {Newey, Whitney K. and West, Kenneth D.}, | |
| 404 | + title = {A Simple, Positive Semi-Definite, Heteroskedasticity and Autocorrelation Consistent Covariance Matrix}, | |
| 405 | + journal = {Econometrica}, | |
| 406 | + volume = {55}, | |
| 407 | + number = {3}, | |
| 408 | + pages = {703--708}, | |
| 409 | + year = {1987} | |
| 410 | +} | |
| 411 | + | |
| 412 | +@article{white1980heteroskedasticity, | |
| 413 | + author = {White, Halbert}, | |
| 414 | + title = {A Heteroskedasticity-Consistent Covariance Matrix Estimator and a Direct Test for Heteroskedasticity}, | |
| 415 | + journal = {Econometrica}, | |
| 416 | + volume = {48}, | |
| 417 | + number = {4}, | |
| 418 | + pages = {817--838}, | |
| 419 | + year = {1980} | |
| 420 | +} | |
| 421 | + | |
| 422 | +@article{petersen2009estimating, | |
| 423 | + author = {Petersen, Mitchell A.}, | |
| 424 | + title = {Estimating Standard Errors in Finance Panel Data Sets: Comparing Approaches}, | |
| 425 | + journal = {Review of Financial Studies}, | |
| 426 | + volume = {22}, | |
| 427 | + number = {1}, | |
| 428 | + pages = {435--480}, | |
| 429 | + year = {2009} | |
| 430 | +} | |
| 431 | + | |
| 432 | +@article{fama1993common, | |
| 433 | + author = {Fama, Eugene F. and French, Kenneth R.}, | |
| 434 | + title = {Common Risk Factors in the Returns on Stocks and Bonds}, | |
| 435 | + journal = {Journal of Financial Economics}, | |
| 436 | + volume = {33}, | |
| 437 | + number = {1}, | |
| 438 | + pages = {3--56}, | |
| 439 | + year = {1993} | |
| 440 | +} | |
| 441 | + | |
| 442 | +@article{diebold2012better, | |
| 443 | + author = {Diebold, Francis X. and Yilmaz, Kamil}, | |
| 444 | + title = {Better to Give than to Receive: Predictive Directional Measurement of Volatility Spillovers}, | |
| 445 | + journal = {International Journal of Forecasting}, | |
| 446 | + volume = {28}, | |
| 447 | + number = {1}, | |
| 448 | + pages = {57--66}, | |
| 449 | + year = {2012} | |
| 450 | +} | |
| 451 | + | |
| 452 | +@article{engle2002dynamic, | |
| 453 | + author = {Engle, Robert F.}, | |
| 454 | + title = {Dynamic Conditional Correlation: A Simple Class of Multivariate Generalized Autoregressive Conditional Heteroskedasticity Models}, | |
| 455 | + journal = {Journal of Business \& Economic Statistics}, | |
| 456 | + volume = {20}, | |
| 457 | + number = {3}, | |
| 458 | + pages = {339--350}, | |
| 459 | + year = {2002} | |
| 460 | +} | |
| 461 | + | |
| 462 | +@article{hansen1997inference, | |
| 463 | + author = {Hansen, Bruce E.}, | |
| 464 | + title = {Inference in TAR Models}, | |
| 465 | + journal = {Studies in Nonlinear Dynamics \& Econometrics}, | |
| 466 | + volume = {2}, | |
| 467 | + number = {1}, | |
| 468 | + year = {1997} | |
| 469 | +} | |
added
paper/sections/conclusion.tex
+44 −0
@@ -0,0 +1,44 @@ | ||
| 1 | +% ============================================================================ | |
| 2 | +% Section 8: Conclusion | |
| 3 | +% ============================================================================ | |
| 4 | +\section{Conclusion}\label{sec:conclusion} | |
| 5 | + | |
| 6 | +This paper has examined whether blockchain-based prediction markets satisfy the theoretical properties of Arrow--Debreu complete markets. Using data from Polymarket's Federal Reserve rate decision market (five mutually exclusive outcome contracts observed over 146 daily observations) and the 2024 U.S.\ Presidential Election market (17 candidate contracts observed over 307 daily observations), we have tested for market completeness, no-arbitrage pricing, information aggregation efficiency, and volatility dynamics. The results paint a coherent picture of prediction markets as effective, if imperfect, realizations of the Arrow--Debreu ideal. | |
| 7 | + | |
| 8 | +\subsection{Summary of Findings} | |
| 9 | + | |
| 10 | +Three principal findings emerge from the analysis. | |
| 11 | + | |
| 12 | +First, the Arrow--Debreu completeness conditions are approximately satisfied. The mean state price sum across the five Fed rate decision contracts is 1.0205, with a standard deviation of 0.0527 and all individual state prices remaining strictly positive throughout the sample. The 2\% overround is consistent with transaction costs, trading fees, and the time value of money, and the absence of a trend in the state price sum over time indicates that arbitrage forces effectively maintain the no-arbitrage condition. These results provide the first direct empirical evidence that a blockchain-based prediction market satisfies the structural conditions of the Arrow--Debreu framework. | |
| 13 | + | |
| 14 | +Second, prediction markets participate in a bidirectional information-sharing process with traditional financial markets. The Fed ``No Change'' contract returns Granger-cause VIX returns at the 1\% significance level ($F = 4.96$, $p = 0.001$), while VIX returns Granger-cause Fed contract returns at the 5\% level ($F = 2.73$, $p = 0.049$). Prediction market returns also Granger-cause SPY returns at the 10\% level ($F = 2.42$, $p = 0.072$). Event study analysis reveals economically significant cumulative abnormal returns around CPI releases, with the ``25 bps decrease'' contract exhibiting a CAR of $-0.999$ ($p = 0.095$), consistent with inflationary surprises reducing the perceived probability of rate cuts. These findings establish that prediction markets contribute meaningfully to the price discovery process for monetary policy expectations. | |
| 15 | + | |
| 16 | +Third, prediction market return volatility exhibits time-varying dynamics with a pronounced leverage effect. The GJR-GARCH model achieves an AIC of 894.19, substantially outperforming the symmetric GARCH(1,1) specification (AIC = 940.68). The leverage coefficient $\gamma_1 = 0.304$ indicates that negative return shocks increase conditional variance by approximately 30\% more than positive shocks of equal magnitude. The HAR-RV model achieves a modest but positive $R^2$ of 0.106, suggesting that the heterogeneous structure of realized volatility provides some predictive content for prediction market dynamics, albeit at a lower level than in traditional equity markets. | |
| 17 | + | |
| 18 | +\subsection{Implications} | |
| 19 | + | |
| 20 | +These findings carry implications for several constituencies. For financial economists, the results validate the Arrow--Debreu framework as a useful lens for analyzing prediction markets and demonstrate that the theoretical properties of complete markets can emerge in decentralized, blockchain-based trading environments. The presence of leverage effects in prediction market returns---a novel empirical finding---invites further investigation into the microstructural mechanisms that generate asymmetric volatility in state-contingent claims markets. | |
| 21 | + | |
| 22 | +For policymakers and regulators, the results suggest that blockchain-based prediction markets can serve as reliable sources of information about the market's assessment of future events. The finding that prediction market prices Granger-cause VIX returns implies that prediction markets capture information about policy uncertainty that is relevant for broader financial market conditions. Policymakers may find prediction market prices useful as supplements to traditional indicators such as federal funds futures, survey-based expectations measures, and options-implied probability distributions. | |
| 23 | + | |
| 24 | +For market participants and platform designers, the results highlight the importance of the neg-risk framework in enabling Arrow--Debreu completeness. The structural enforcement of mutual exclusivity and collective exhaustiveness---features built into Polymarket's contract design---ensures that the conditions for completeness are met by construction, leaving only the question of whether market forces (through arbitrage and information aggregation) push prices to levels consistent with the theory. The liquidity analysis reveals that microstructure quality is adequate for high-profile contracts but may be insufficient for low-volume or niche markets, suggesting scope for improvements in liquidity provision mechanisms. | |
| 25 | + | |
| 26 | +For the emerging literature on blockchain finance and decentralized markets \citep{harvey2021defi}, the results provide evidence that decentralized platforms can achieve levels of informational efficiency comparable to traditional financial markets, at least for markets with high participation and well-defined outcomes. This finding is noteworthy given the absence of centralized market makers, regulatory oversight, and many of the institutional mechanisms that are traditionally viewed as essential for market efficiency. | |
| 27 | + | |
| 28 | +\subsection{Limitations} | |
| 29 | + | |
| 30 | +Several limitations of the analysis warrant acknowledgment. First, the sample size is relatively small: 146 observations for the Fed market and 307 for the election market. This limits the power of statistical tests, particularly the event study analysis, which relies on a small number of events (2 FOMC, 3 CPI, 3 election key dates). Larger samples, enabled by the growing number of prediction markets and longer operating histories, would permit more precise inference. | |
| 31 | + | |
| 32 | +Second, the use of daily closing prices precludes analysis of intraday price dynamics, which may be particularly informative around announcement events. High-frequency data from the Polymarket order book would enable more precise measurement of realized volatility, information shares, and the speed of price adjustment to news. Future research should explore the availability of tick-level data from blockchain transaction records. | |
| 33 | + | |
| 34 | +Third, the Arrow--Debreu framework is inherently a single-period model, while prediction markets trade continuously over multiple periods. The mapping between the multi-period trading environment and the single-period theory requires treating each trading day as a snapshot of the equilibrium, an approximation that may be strained during periods of rapid information arrival. | |
| 35 | + | |
| 36 | +Fourth, the analysis focuses on two specific markets (Fed rate decisions and U.S.\ presidential elections) that are among the highest-profile and most liquid contracts on Polymarket. The extent to which the completeness and efficiency findings generalize to less liquid, more niche markets---where information asymmetry may be greater and arbitrage mechanisms less effective---remains an open question. | |
| 37 | + | |
| 38 | +Fifth, we cannot definitively distinguish between risk-neutral probabilities and physical probabilities using market prices alone. The state prices recovered from prediction market contracts reflect both probability beliefs and risk premia, and disentangling these components would require either a structural model of trader preferences or independent measures of physical probabilities (such as survey data or historical base rates). | |
| 39 | + | |
| 40 | +\subsection{Directions for Future Research} | |
| 41 | + | |
| 42 | +Several avenues for future research merit consideration. First, the analysis could be extended to multi-outcome markets with larger state spaces (e.g., GDP growth brackets, election vote share ranges), which would enable tests of completeness with finer granularity and assessments of the risk-neutral density's shape and dynamics. Second, structural models of trader heterogeneity could be estimated using prediction market data to decompose state prices into probability and risk premium components, building on the theoretical framework of \citet{manski2006interpreting} and \citet{snowberg2013prediction}. Third, high-frequency analysis of order-book data could provide more precise measures of price discovery, information shares \citep{hasbrouck1995one}, and the determinants of liquidity provision in decentralized markets. Fourth, cross-platform comparisons---between Polymarket and other prediction markets, or between prediction markets and related instruments such as federal funds futures and options---would shed light on the competitive dynamics of price discovery across trading venues. Fifth, the welfare implications of prediction market completeness could be explored through the lens of general equilibrium theory, assessing whether the availability of state-contingent claims on prediction market platforms improves risk-sharing and welfare relative to economies without such markets. | |
| 43 | + | |
| 44 | +The emergence of blockchain-based prediction markets represents a natural experiment in the creation of Arrow--Debreu economies. This paper has shown that the experiment is succeeding, at least approximately: Polymarket's Federal Reserve rate decision market satisfies the structural conditions of complete markets, contributes to price discovery relative to traditional financial markets, and exhibits volatility dynamics consistent with informationally efficient pricing. As these markets continue to grow in volume, liquidity, and scope, they offer an increasingly valuable laboratory for studying the fundamental mechanisms of information aggregation, state-contingent pricing, and market efficiency. | |
added
paper/sections/data.tex
+102 −0
@@ -0,0 +1,102 @@ | ||
| 1 | +% ============================================================================ | |
| 2 | +% Section 4: Data | |
| 3 | +% ============================================================================ | |
| 4 | +\section{Data}\label{sec:data} | |
| 5 | + | |
| 6 | +This section describes the data sources, sample construction, variable definitions, and preliminary statistical properties of the prediction market and financial market data used in our analysis. | |
| 7 | + | |
| 8 | +\subsection{Polymarket Data} | |
| 9 | + | |
| 10 | +Polymarket is a blockchain-based prediction market platform that enables users to trade binary outcome contracts settled on the Polygon network. Each contract pays \$1 if the specified outcome occurs and \$0 otherwise, with prices reflecting the market's implied probability of the event. We collect daily closing prices, trading volumes, and liquidity measures for two sets of Polymarket contracts. | |
| 11 | + | |
| 12 | +\subsubsection{Federal Reserve Rate Decision Market} | |
| 13 | + | |
| 14 | +Our primary dataset consists of daily observations on five contracts corresponding to the possible outcomes of the Federal Reserve's June 2026 FOMC rate decision. The five mutually exclusive and collectively exhaustive states are: | |
| 15 | + | |
| 16 | +\begin{enumerate} | |
| 17 | + \item \textbf{50+ bps decrease}: The FOMC lowers the target federal funds rate by 50 basis points or more. | |
| 18 | + \item \textbf{25 bps decrease}: The FOMC lowers the target federal funds rate by exactly 25 basis points. | |
| 19 | + \item \textbf{No change}: The FOMC leaves the target federal funds rate unchanged. | |
| 20 | + \item \textbf{25 bps increase}: The FOMC raises the target federal funds rate by exactly 25 basis points. | |
| 21 | + \item \textbf{50+ bps increase}: The FOMC raises the target federal funds rate by 50 basis points or more. | |
| 22 | +\end{enumerate} | |
| 23 | + | |
| 24 | +The sample spans 146 daily observations from December 2025 through May 2026. These contracts are traded within Polymarket's neg-risk framework, which enforces mutual exclusivity and collective exhaustiveness: exactly one contract in the group resolves to \$1, and the remaining four resolve to \$0. This structural feature makes the Fed rate decision market an ideal testing ground for Arrow--Debreu completeness conditions. | |
| 25 | + | |
| 26 | +Table~\ref{tab:summary_stats} reports summary statistics for the five contracts. The ``No change'' contract has the highest mean price at 0.608, reflecting the market's prevailing expectation of policy inaction. The ``25 bps decrease'' contract has a mean price of 0.304, indicating meaningful probability mass on a single rate cut. The two extreme states (50+ bps decrease and 50+ bps increase) have mean prices of 0.058 and 0.016 respectively, consistent with the market assigning low probability to aggressive rate moves. All five price series exhibit non-normal distributions, as evidenced by significant Jarque--Bera statistics: the ``25 bps increase'' contract displays extreme positive skewness (3.98) and leptokurtosis (30.46), reflecting its status as a low-probability, high-surprise event. | |
| 27 | + | |
| 28 | +\input{../results/tables/table2_summary_stats.tex} | |
| 29 | + | |
| 30 | +\subsubsection{2024 U.S.\ Presidential Election Market} | |
| 31 | + | |
| 32 | +Our secondary dataset comprises daily observations on 17 candidate contracts from the 2024 U.S.\ Presidential Election market, observed over 307 trading days. This market provides a richer cross-sectional dimension for analyzing microstructure properties, liquidity dynamics, and multi-contract pricing relationships. The election market resolved on November 5, 2024, but Polymarket continued trading after the election date, providing observations on post-resolution price convergence. | |
| 33 | + | |
| 34 | +Donald Trump won the election, and his contract converged to a final price of 0.58---notably below the \$1 resolution value, indicating that some trading continued in the secondary market before final settlement. The election market's larger cross-section and longer time series complement the Fed rate decision market's cleaner state-space structure. | |
| 35 | + | |
| 36 | +\subsection{Financial Market Data} | |
| 37 | + | |
| 38 | +To assess the information linkages between prediction markets and traditional financial markets, we collect daily returns data for three benchmark assets: | |
| 39 | + | |
| 40 | +\begin{itemize} | |
| 41 | + \item \textbf{SPY}: The SPDR S\&P 500 ETF Trust, representing the U.S.\ equity market. Daily closing prices are obtained from standard financial data providers. | |
| 42 | + \item \textbf{BTC}: Bitcoin, the largest cryptocurrency by market capitalization. Bitcoin is included both as a benchmark for blockchain-native assets and as a proxy for speculative risk appetite in the digital asset ecosystem. | |
| 43 | + \item \textbf{VIX}: The CBOE Volatility Index, which measures the market's expectation of 30-day forward-looking volatility implied by S\&P 500 index options. The VIX is included as a proxy for aggregate uncertainty and risk aversion. | |
| 44 | +\end{itemize} | |
| 45 | + | |
| 46 | +Daily returns are computed as log returns: $r_t = \ln(P_t / P_{t-1})$, where $P_t$ is the closing price on day $t$. For the prediction market contracts, the same return definition is applied, with the convention that returns are undefined (and excluded) on days when the previous day's price is zero. | |
| 47 | + | |
| 48 | +\subsection{Event Data} | |
| 49 | + | |
| 50 | +We identify macroeconomic announcement events within the sample period for the event study analysis. The events fall into three categories: | |
| 51 | + | |
| 52 | +\begin{itemize} | |
| 53 | + \item \textbf{FOMC announcements}: We identify 2 scheduled FOMC meeting dates that fall within the sample period. FOMC announcements are released at 2:00 PM Eastern Time and are accompanied by the statement, Summary of Economic Projections (when available), and the press conference. | |
| 54 | + \item \textbf{CPI releases}: We identify 3 Consumer Price Index releases by the Bureau of Labor Statistics within the sample period. CPI releases occur at 8:30 AM Eastern Time and provide the most closely watched measure of consumer inflation. | |
| 55 | + \item \textbf{Election key dates}: We identify 3 politically significant dates in the election sample, including debate dates, major endorsement events, and the election day itself. | |
| 56 | +\end{itemize} | |
| 57 | + | |
| 58 | +Event windows are defined symmetrically around each announcement date. For FOMC and CPI events, we use a $[-3, +3]$ day window (7 trading days), with the estimation window for the constant mean model spanning 60 trading days prior to the event window. For election events, we use a wider $[-5, +5]$ day window to accommodate the possibility of slower information incorporation in political markets. | |
| 59 | + | |
| 60 | +\subsection{Variable Construction} | |
| 61 | + | |
| 62 | +We construct the following variables for our empirical analysis: | |
| 63 | + | |
| 64 | +\textbf{State price sum.} For each trading day $t$, the state price sum is computed as $\Sigma(t) = \sum_{j=1}^{5} p_j(t)$, where $p_j(t)$ is the closing price of the $j$-th contract. Under Arrow--Debreu completeness, $\Sigma(t) \approx 1$. | |
| 65 | + | |
| 66 | +\textbf{Risk-neutral probabilities.} The risk-neutral probability of state $j$ at time $t$ is computed as $q_j(t) = p_j(t) / \Sigma(t)$. This normalization ensures that risk-neutral probabilities sum to unity. | |
| 67 | + | |
| 68 | +\textbf{Abnormal returns.} For the event study analysis, abnormal returns are computed as $AR_j(t) = r_j(t) - \hat{\mu}_j$, where $\hat{\mu}_j$ is the mean return over the 60-day estimation window. Cumulative abnormal returns are computed by summing abnormal returns over the event window. | |
| 69 | + | |
| 70 | +\textbf{Realized volatility.} Daily realized volatility for the ``No change'' contract is computed as the absolute value of the daily return, $|r_t|$. Weekly and monthly realized volatility components are computed as the averages of daily realized volatility over the preceding 5 and 22 trading days, respectively, following the HAR-RV convention of \citet{corsi2009simple}. | |
| 71 | + | |
| 72 | +\textbf{Liquidity measures.} We compute two liquidity measures. The Roll spread \citep{roll1984simple} is estimated from the serial autocovariance of returns: $\text{Roll}_t = 2\sqrt{-\text{Cov}(r_t, r_{t-1})}$ when the autocovariance is negative, and zero otherwise. The Amihud illiquidity ratio \citep{amihud2002illiquidity} is computed as $\text{ILLIQ}_t = |r_t| / V_t$, where $V_t$ is the dollar trading volume on day $t$. | |
| 73 | + | |
| 74 | +\subsection{Descriptive Statistics} | |
| 75 | + | |
| 76 | +Table~\ref{tab:descriptive_stats} reports descriptive statistics for the broader set of Polymarket contracts examined, including trading volume and liquidity measures in U.S.\ dollars. The table documents the substantial heterogeneity in market activity across contracts, with trading volumes ranging from under one million to nearly ten million dollars and liquidity (measured as the depth of the order book) varying by more than an order of magnitude. | |
| 77 | + | |
| 78 | +\input{../results/tables/table1_descriptive_stats.tex} | |
| 79 | + | |
| 80 | +Figure~\ref{fig:state_prices} plots the evolution of state prices for the five Fed rate decision contracts over the sample period. The dominant feature is the secular increase in the ``No change'' contract price from approximately 0.575 at the beginning of the sample to 0.978 by May 2026, accompanied by a corresponding decline in the ``25 bps decrease'' contract from approximately 0.40 to near zero. This pattern is consistent with the market progressively assigning higher probability to policy inaction as incoming macroeconomic data (strong labor markets, persistent inflation) reduced the case for near-term rate cuts. | |
| 81 | + | |
| 82 | +\begin{figure}[htbp] | |
| 83 | + \centering | |
| 84 | + \includegraphics[width=\textwidth]{../figures/fig1_state_prices.pdf} | |
| 85 | + \caption{Arrow--Debreu state prices for Federal Reserve rate decision contracts, December 2025 to May 2026. Each line represents the daily closing price of one of the five mutually exclusive outcome contracts. The ``No change'' contract (upper line) rises from approximately 0.575 to 0.978, while the ``25 bps decrease'' contract (declining line) falls from approximately 0.40 to near zero.} | |
| 86 | + \label{fig:state_prices} | |
| 87 | +\end{figure} | |
| 88 | + | |
| 89 | +Figure~\ref{fig:rn_density} displays snapshots of the risk-neutral density at selected dates, illustrating the progressive concentration of probability mass on the ``No change'' outcome. Early in the sample, the distribution is bimodal, with substantial mass on both ``No change'' and ``25 bps decrease''; by the end of the sample, the distribution is sharply peaked at ``No change,'' with all other states assigned negligible probability. | |
| 90 | + | |
| 91 | +\begin{figure}[htbp] | |
| 92 | + \centering | |
| 93 | + \includegraphics[width=\textwidth]{../figures/fig3_risk_neutral_density.pdf} | |
| 94 | + \caption{Risk-neutral density snapshots for the Federal Reserve rate decision market. Each panel shows the distribution of risk-neutral probabilities across the five outcome states at a selected date. The progressive concentration of mass on ``No change'' reflects the market's increasing confidence in policy inaction.} | |
| 95 | + \label{fig:rn_density} | |
| 96 | +\end{figure} | |
| 97 | + | |
| 98 | +\subsection{Stationarity and Distributional Properties} | |
| 99 | + | |
| 100 | +Since prediction market prices are bounded between 0 and 1 and converge to either 0 or 1 at resolution, the level series are inherently non-stationary. All time-series analyses in this paper therefore use log returns rather than price levels. Augmented Dickey--Fuller tests reject the unit root null for all return series at the 1\% significance level, confirming stationarity. | |
| 101 | + | |
| 102 | +The return distributions exhibit significant departures from normality, as indicated by the Jarque--Bera statistics reported in Table~\ref{tab:summary_stats}. The ``25 bps increase'' contract displays particularly extreme non-normality, with a kurtosis of 30.46 reflecting occasional large price jumps associated with unexpected news. These distributional properties motivate the use of heteroskedasticity-robust inference methods throughout our analysis, including Newey--West standard errors \citep{newey1987simple} and White standard errors \citep{white1980heteroskedasticity}. | |
added
paper/sections/introduction.tex
+50 −0
@@ -0,0 +1,50 @@ | ||
| 1 | +% ============================================================================ | |
| 2 | +% Section 1: Introduction | |
| 3 | +% ============================================================================ | |
| 4 | +\section{Introduction}\label{sec:introduction} | |
| 5 | + | |
| 6 | +The notion that market prices aggregate dispersed information stands among the most consequential insights in economics. \citet{hayek1945use} argued that the price system serves as a mechanism for communicating knowledge that no single agent possesses in its entirety, enabling decentralized coordination superior to any central planner. In the decades since, financial economists have formalized this insight through the efficient markets hypothesis \citep{fama1970efficient}, the theory of rational expectations equilibria \citep{grossman1980impossibility}, and the general equilibrium framework of complete markets \citep{arrow1964role,debreu1959theory}. Yet the empirical environments in which these theories have been tested---equity markets, bond markets, derivatives markets---are shaped by institutional frictions, regulatory constraints, and intermediary structures that may obscure the fundamental mechanisms of price formation. The emergence of blockchain-based prediction markets offers a rare opportunity to study information aggregation and state-contingent pricing in a setting that more closely approximates the theoretical ideal of an Arrow--Debreu economy. | |
| 7 | + | |
| 8 | +This paper investigates whether Polymarket, the largest decentralized prediction market by trading volume, satisfies the theoretical properties of Arrow--Debreu complete markets. We model Polymarket binary outcome contracts as state-contingent claims and test for market completeness, no-arbitrage pricing, and information aggregation efficiency using two empirical settings: the Federal Reserve rate decision markets for the June 2026 meeting (five mutually exclusive outcomes forming a complete state space) and the 2024 U.S.\ Presidential Election market (seventeen candidate contracts observed over 307 trading days). Our analysis employs a comprehensive empirical framework that spans event studies around FOMC and CPI announcements, Granger causality tests, vector autoregression, GARCH and GJR-GARCH volatility models, and the heterogeneous autoregressive realized volatility (HAR-RV) specification of \citet{corsi2009simple}. | |
| 9 | + | |
| 10 | +\subsection{Motivation} | |
| 11 | + | |
| 12 | +The intellectual roots of prediction markets lie in the Arrow--Debreu framework for general equilibrium under uncertainty. In the canonical formulation, \citet{arrow1964role} demonstrated that a complete set of state-contingent claims---securities that pay one unit of account if and only if a particular state of the world obtains---suffices to achieve Pareto-optimal allocations of risk. \citet{debreu1959theory} generalized this construction, showing that when markets span the full state space, competitive equilibrium prices encode all relevant information about state probabilities and agents' marginal rates of substitution across states. \citet{ross1976arbitrage} subsequently established that derivatives markets can achieve effective completeness even without a full set of primitive securities, provided that a sufficient number of linearly independent payoffs are available for trade. | |
| 13 | + | |
| 14 | +Prediction markets operationalize the Arrow--Debreu construct in its most elementary form. A Polymarket contract on whether the Federal Reserve will hold rates unchanged at its June 2026 meeting is, in formal terms, a state-contingent claim that pays \$1 if the state ``no change'' is realized and \$0 otherwise. When five such contracts---corresponding to rate decreases of 50 or more basis points, a 25 basis point decrease, no change, a 25 basis point increase, and an increase of 50 or more basis points---are traded simultaneously, the resulting market spans the full outcome space for a single FOMC decision. If the summed prices of these contracts equal the reciprocal of the risk-free gross return, $1/(1+r_f)$, and all individual prices are non-negative, the market satisfies the conditions for Arrow--Debreu completeness. The absence of persistent arbitrage further implies the existence of a valid state-price density consistent with no-arbitrage pricing \citep{cochrane2005asset}. | |
| 15 | + | |
| 16 | +The practical significance of this theoretical connection extends beyond intellectual curiosity. Prediction markets have attracted growing attention from policymakers, central bankers, and financial regulators as potential supplements to conventional forecasting methods. \citet{arrow2008promise} argued that prediction markets harness the ``wisdom of crowds'' by providing direct, market-based estimates of event probabilities. \citet{wolfers2004prediction} documented that prediction markets often outperform polls and expert forecasts in electoral contexts, while \citet{berg2008results} provided evidence from the Iowa Electronic Markets that prediction market prices are well calibrated as probability estimates. More recently, the growth of blockchain-based platforms---Polymarket alone exceeded \$3.5 billion in cumulative trading volume by early 2026---has raised the stakes of understanding whether these markets satisfy the theoretical conditions that justify interpreting their prices as state prices. | |
| 17 | + | |
| 18 | +Despite this enthusiasm, important skepticism persists. \citet{manski2006interpreting} cautioned that prediction market prices reflect risk premia and heterogeneous beliefs in addition to subjective probabilities, so that a contract price of \$0.60 need not imply a 60\% probability of the associated event. \citet{snowberg2013prediction} formalized conditions under which prices diverge from probabilities due to risk aversion and showed that the magnitude of bias depends on the curvature of agents' utility functions. These critiques do not invalidate the Arrow--Debreu interpretation---indeed, in an Arrow--Debreu economy, state prices embed both probability beliefs and marginal utility weights---but they underscore the importance of testing whether prediction markets satisfy the structural conditions (completeness, no arbitrage, positive state prices) that make the Arrow--Debreu interpretation well defined. | |
| 19 | + | |
| 20 | +\subsection{Research Questions and Contributions} | |
| 21 | + | |
| 22 | +This paper addresses three principal research questions. First, do blockchain-based prediction markets satisfy the conditions for Arrow--Debreu market completeness? We test this by examining whether state prices within Polymarket's neg-risk contract groups sum to values close to unity (or, more precisely, to $1/(1+r_f)$) and whether all individual state prices remain strictly positive throughout the sample period. Second, how do prediction market prices respond to macroeconomic information releases, and do prediction markets contribute to price discovery relative to traditional financial markets? We investigate this through event studies around FOMC and CPI announcements and through Granger causality tests between prediction market returns and returns on SPY, Bitcoin, and the VIX. Third, what are the volatility dynamics and microstructure properties of prediction market prices, and do they exhibit features consistent with informationally efficient markets? | |
| 23 | + | |
| 24 | +Our analysis yields three principal findings. First, we document that state prices within Polymarket neg-risk groups sum to a mean value of 1.0205 with a standard deviation of 0.0527 across 146 daily observations from December 2025 through May 2026, and that all individual state prices remain positive throughout the sample. The mean deviation of the state price sum from unity is 2.48 percentage points---close to the level that would be implied by a modest risk-free rate or transaction-cost wedge. This finding provides direct empirical support for the Arrow--Debreu completeness interpretation of prediction market prices. | |
| 25 | + | |
| 26 | +Second, we find evidence of bidirectional information flow between prediction markets and traditional financial markets. Granger causality tests reveal that the Fed ``No Change'' contract returns Granger-cause VIX returns at the 1\% significance level ($F = 4.96$, $p = 0.001$), while VIX returns Granger-cause Fed ``No Change'' returns at the 5\% level ($F = 2.73$, $p = 0.049$). Fed contract returns also Granger-cause SPY returns at the 10\% level ($F = 2.42$, $p = 0.072$). Event study analysis around FOMC and CPI announcements reveals economically significant cumulative abnormal returns, with point estimates that are consistent with prediction markets incorporating macroeconomic information in a manner aligned with the direction of the news. | |
| 27 | + | |
| 28 | +Third, we characterize the volatility dynamics of prediction market returns using both GARCH-family models and the HAR-RV framework. A standard GARCH(1,1) model yields an AIC of 940.68, while the GJR-GARCH specification of \citet{glosten1993relation} achieves a substantially lower AIC of 894.19, indicating that leverage effects---asymmetric volatility responses to positive and negative shocks---are a statistically important feature of prediction market dynamics. The GJR-GARCH leverage coefficient $\gamma_1 = 0.304$ implies that negative return shocks increase conditional variance by roughly 30\% more than positive shocks of equal magnitude. The HAR-RV model achieves an $R^2$ of 0.106, suggesting that while the heterogeneous structure of realized volatility provides some predictive content, the relatively low explanatory power may reflect the event-driven, discontinuous nature of prediction market price dynamics. | |
| 29 | + | |
| 30 | +\subsection{Empirical Setting} | |
| 31 | + | |
| 32 | +Our analysis exploits two complementary empirical settings from the Polymarket platform. The primary setting is the Federal Reserve rate decision market for the June 2026 FOMC meeting, which comprises five mutually exclusive and collectively exhaustive outcome contracts: a decrease of 50 or more basis points, a decrease of 25 basis points, no change, an increase of 25 basis points, and an increase of 50 or more basis points. This market is observed over 146 daily observations from December 2025 through May 2026. The five-outcome structure is particularly well suited for testing Arrow--Debreu completeness, as the contracts collectively span the full state space for a single policy decision. | |
| 33 | + | |
| 34 | +Over the sample period, the ``No change'' contract exhibited a pronounced upward trend, with the implied probability rising from approximately 0.575 at the beginning of the sample to 0.978 by the end---a trajectory consistent with the gradual incorporation of macroeconomic information favoring a ``wait and see'' posture by the Federal Reserve. The ``25 basis point decrease'' contract, which began the sample near 0.40, declined to approximately 0.01, while the remaining contracts (rate increases and large decreases) traded at low single-digit levels throughout most of the sample. This evolution of state prices provides a natural laboratory for studying information aggregation dynamics. | |
| 35 | + | |
| 36 | +The secondary setting is the 2024 U.S.\ Presidential Election market, comprising 17 candidate contracts observed over 307 daily observations. This market offers a richer cross-section of contracts and a longer time series, enabling analysis of microstructure properties, liquidity dynamics, and the behavior of prediction markets around politically significant events. The election market also provides a valuable out-of-sample check on patterns observed in the Fed market, as the two settings involve fundamentally different types of uncertainty (macroeconomic policy versus political outcomes). | |
| 37 | + | |
| 38 | +\subsection{Relation to the Literature} | |
| 39 | + | |
| 40 | +This paper contributes to several strands of the finance and economics literatures. First, we contribute to the literature on prediction markets and information aggregation \citep{wolfers2004prediction,arrow2008promise,berg2008results,snowberg2013prediction} by providing the first formal test of Arrow--Debreu completeness conditions using data from a blockchain-based prediction market. While prior work has studied the forecasting accuracy and calibration properties of prediction markets, our analysis is distinguished by its focus on the structural properties (completeness, no-arbitrage, positive state prices) that underpin the theoretical interpretation of prediction market prices as Arrow--Debreu state prices. | |
| 41 | + | |
| 42 | +Second, we contribute to the literature on price discovery and market microstructure \citep{hasbrouck1995one,ohara1995market,madhavan2000market,hasbrouck2003intraday} by documenting the information linkages between prediction markets and traditional financial markets. Our finding that prediction market returns Granger-cause VIX returns is novel and suggests that prediction markets may serve as a leading indicator of uncertainty pricing in traditional markets. | |
| 43 | + | |
| 44 | +Third, we contribute to the literature on volatility modeling and financial econometrics \citep{bollerslev1986generalized,engle1982autoregressive,nelson1991conditional,glosten1993relation,corsi2009simple,andersen2001distribution,andersen2003modeling,barndorff2002econometric} by applying established volatility models to a new asset class. The presence of leverage effects in prediction market returns, documented through the superior fit of the GJR-GARCH specification, represents a new empirical finding with implications for understanding the microstructure of state-contingent claims markets. | |
| 45 | + | |
| 46 | +Fourth, we contribute to the emerging literature on blockchain finance and decentralized markets \citep{harvey2021defi} by providing rigorous empirical evidence on the efficiency properties of a decentralized trading platform. Our results suggest that despite the absence of traditional market intermediaries, blockchain-based prediction markets can achieve levels of informational efficiency and microstructure quality that rival established financial markets for high-profile contracts. | |
| 47 | + | |
| 48 | +\subsection{Outline} | |
| 49 | + | |
| 50 | +The remainder of this paper is organized as follows. Section~\ref{sec:literature} reviews the relevant literature on prediction markets, Arrow--Debreu theory, price discovery, and volatility modeling. Section~\ref{sec:theoretical} develops the theoretical framework, formulating precise conditions for Arrow--Debreu completeness and deriving testable hypotheses. Section~\ref{sec:data} describes the data sources, sample construction, and variable definitions. Section~\ref{sec:methodology} presents the econometric methodology, including event study design, Granger causality tests, VAR estimation, GARCH models, and the HAR-RV specification. Section~\ref{sec:results} reports the empirical findings. Section~\ref{sec:robustness} presents robustness checks and additional analyses, including the 2024 U.S.\ Presidential Election case study. Section~\ref{sec:conclusion} concludes with a discussion of implications and directions for future research. | |
added
paper/sections/literature.tex
+58 −0
@@ -0,0 +1,58 @@ | ||
| 1 | +% ============================================================================ | |
| 2 | +% Section 2: Literature Review | |
| 3 | +% ============================================================================ | |
| 4 | +\section{Literature Review}\label{sec:literature} | |
| 5 | + | |
| 6 | +This section surveys four interconnected strands of research that inform our analysis: (i) the theory of complete markets and state-contingent pricing, (ii) prediction markets as information aggregation mechanisms, (iii) price discovery and market microstructure, and (iv) volatility modeling and realized volatility. We conclude by identifying the gaps in the existing literature that our paper addresses. | |
| 7 | + | |
| 8 | +\subsection{Arrow--Debreu Theory and Complete Markets} | |
| 9 | + | |
| 10 | +The general equilibrium framework of \citet{arrow1964role} and \citet{debreu1959theory} provides the theoretical foundation for interpreting prediction market prices as state prices. In the Arrow--Debreu economy, a finite set of states of the world $\mathcal{S} = \{s_1, s_2, \ldots, s_N\}$ is defined, and for each state $s_j$ there exists a traded security---an Arrow--Debreu security---that pays one unit of the numeraire if state $s_j$ is realized and zero otherwise. When the number of linearly independent securities equals the number of states, markets are said to be complete, and a unique state-price vector $\boldsymbol{\psi} = (\psi_1, \ldots, \psi_N)$ exists such that any contingent claim can be replicated by a portfolio of Arrow--Debreu securities. | |
| 11 | + | |
| 12 | +\citet{ross1976arbitrage} extended the completeness concept by showing that options and other derivative instruments can dynamically complete markets even when primitive securities do not span the full state space. The Ross spanning result implies that market completeness is a property of the entire set of traded instruments, not merely of individual securities. In practice, financial markets rarely achieve full completeness, and a large literature examines the implications of incomplete markets for asset pricing, risk sharing, and welfare \citep{cochrane2005asset}. | |
| 13 | + | |
| 14 | +The connection between complete markets and no-arbitrage pricing is formalized through the Fundamental Theorem of Asset Pricing. In a finite-state economy with no redundant securities, markets are complete if and only if there exists a unique positive state-price density (or stochastic discount factor) that prices all traded assets correctly. The existence of such a density is equivalent to the absence of arbitrage opportunities, while its uniqueness is equivalent to completeness \citep{cochrane2005asset}. This theoretical structure provides the basis for our empirical tests: if prediction market contracts collectively span the outcome space and their prices satisfy the no-arbitrage conditions, then the market functions as an Arrow--Debreu economy in the precise sense of the theory. | |
| 15 | + | |
| 16 | +\subsection{Prediction Markets and Information Aggregation} | |
| 17 | + | |
| 18 | +The idea that markets can serve as forecasting devices predates modern financial economics, with roots in nineteenth-century betting markets on elections and horse races. The formal academic literature on prediction markets gained momentum with the establishment of the Iowa Electronic Markets (IEM) in 1988, which enabled researchers to study real-money markets on political events in a controlled setting. | |
| 19 | + | |
| 20 | +\citet{berg2008results} documented the forecasting performance of the IEM over two decades of U.S.\ presidential elections, finding that market prices were generally well calibrated and often superior to polling averages, particularly at longer forecast horizons. \citet{wolfers2004prediction} provided a comprehensive assessment of prediction markets across multiple domains---politics, business, entertainment---and argued that these markets aggregate information efficiently because they provide incentives for informed trading, allow for continuous updating, and admit a broad participant base. The authors acknowledged, however, that thin markets, manipulation attempts, and limited participation could undermine efficiency. | |
| 21 | + | |
| 22 | +\citet{manski2006interpreting} raised an important theoretical caveat regarding the probability interpretation of prediction market prices. Under expected utility maximization, the equilibrium price of a state-contingent claim reflects both the probability of the event and the marginal utility of wealth in the relevant state. Only under risk-neutral pricing---when agents are risk neutral or when the event is uncorrelated with aggregate consumption---does the contract price equal the event probability. When agents are risk averse and the event outcome is correlated with marginal utility (as is plausible for macroeconomic events such as Fed rate decisions), prices will diverge from probabilities. \citet{snowberg2013prediction} formalized this argument, deriving analytical expressions for the bias as a function of the coefficient of relative risk aversion and showing that the bias can be substantial for events with significant wealth consequences. | |
| 23 | + | |
| 24 | +\citet{arrow2008promise} provided a prominent defense of prediction markets, arguing that even if prices do not perfectly equal probabilities, the ordering of prices across contracts and their temporal dynamics still contain valuable information about relative likelihoods and the direction of probability changes. This pragmatic defense aligns with the Arrow--Debreu framework: state prices need not equal physical probabilities to be informative about the economic value of state-contingent claims. | |
| 25 | + | |
| 26 | +More recent literature has explored the role of prediction markets in specific applied contexts. Studies have examined the ability of prediction markets to forecast election outcomes, central bank decisions, geopolitical events, and scientific replication results. A common finding across these applications is that prediction markets perform comparably to, and often better than, expert forecasts and statistical models, particularly when the forecast horizon is short and the event is well defined. | |
| 27 | + | |
| 28 | +\subsection{Price Discovery and Market Microstructure} | |
| 29 | + | |
| 30 | +The concept of price discovery refers to the process by which markets incorporate new information into prices. \citet{hasbrouck1995one} formalized the measurement of price discovery using information shares, which decompose the variance of the efficient price innovation across multiple markets trading related assets. The Hasbrouck information share has become a standard tool for evaluating the relative informational contributions of different trading venues. \citet{hasbrouck2003intraday} extended this framework to high-frequency settings, showing that the informational advantage of one market over another can vary intraday and across event types. | |
| 31 | + | |
| 32 | +\citet{kyle1985continuous} provided the seminal theoretical model of price formation with asymmetric information, in which an informed trader, noise traders, and a competitive market maker interact to determine prices. The Kyle model predicts that prices are a linear function of order flow, with the sensitivity (Kyle's lambda) reflecting the degree of information asymmetry. \citet{glosten1985bid} developed a related model in which the bid-ask spread compensates market makers for adverse selection costs arising from informed trading. These models provide the microstructural foundations for interpreting bid-ask spreads and price impact in prediction markets. | |
| 33 | + | |
| 34 | +\citet{ohara1995market} synthesized the microstructure literature, emphasizing that the institutional details of trading mechanisms---order types, priority rules, transparency, anonymity---shape price formation in ways that matter for both efficiency and welfare. \citet{madhavan2000market} extended this analysis, documenting how market design affects volatility, liquidity, and the speed of information incorporation. | |
| 35 | + | |
| 36 | +The liquidity of a market is closely related to its price discovery function. \citet{kyle1985continuous} identified three dimensions of liquidity---tightness (the cost of turning around a position), depth (the volume of orders at the best prices), and resiliency (the speed at which prices recover from uninformative shocks)---that collectively determine a market's ability to absorb order flow without adverse price impact. \citet{amihud2002illiquidity} proposed a simple illiquidity measure---the ratio of absolute daily returns to daily dollar volume---that captures the price impact of trading and has become widely used in the empirical literature. \citet{roll1984simple} derived an implicit measure of the effective spread from the serial covariance of price changes, showing that the autocovariance of returns is negative when bid-ask bounce contributes to price variation. | |
| 37 | + | |
| 38 | +The application of microstructure concepts to prediction markets is relatively recent. Unlike traditional financial markets, blockchain-based prediction markets operate on automated market makers or order-book protocols without designated market makers or specialist systems. Polymarket uses a hybrid architecture that combines a central limit order book (CLOB) with smart contract settlement on the Polygon blockchain. This design implies that liquidity provision is decentralized and that the costs of adverse selection may differ qualitatively from those in traditional markets. Understanding the microstructure of these platforms is essential for evaluating their price discovery capabilities. | |
| 39 | + | |
| 40 | +\subsection{Volatility Modeling} | |
| 41 | + | |
| 42 | +The modeling of conditional volatility in financial time series has a rich econometric history. \citet{engle1982autoregressive} introduced the ARCH model, in which conditional variance depends on past squared residuals, capturing the well-documented tendency of financial return volatility to cluster over time. \citet{bollerslev1986generalized} generalized this framework to the GARCH model, which includes lagged conditional variance terms and provides a more parsimonious representation of persistent volatility dynamics. The GARCH(1,1) specification has become the workhorse model in empirical finance, applied to equity returns, exchange rates, interest rates, and commodity prices. | |
| 43 | + | |
| 44 | +A well-established stylized fact of equity returns is the asymmetry of the volatility response to positive and negative shocks: negative returns tend to increase future volatility by more than positive returns of equal magnitude. \citet{nelson1991conditional} proposed the EGARCH model to capture this leverage effect, while \citet{glosten1993relation} introduced the GJR-GARCH specification, which augments the standard GARCH variance equation with an indicator function that allows the impact of negative shocks to differ from that of positive shocks. The GJR-GARCH model has the advantage of nesting the standard GARCH as a special case, facilitating direct comparison through information criteria such as the AIC \citep{akaike1974new} and BIC \citep{schwarz1978estimating}. | |
| 45 | + | |
| 46 | +The development of high-frequency data and realized volatility measures opened a new frontier in volatility modeling. \citet{andersen2001distribution} demonstrated that realized volatility---computed as the sum of squared intraday returns---provides a consistent nonparametric estimator of integrated volatility and that the distribution of standardized returns conditional on realized volatility is approximately Gaussian. \citet{andersen2003modeling} showed that the logarithm of realized volatility is approximately normally distributed and exhibits long memory, with autocorrelations that decay slowly and remain significant at lags of months or longer. \citet{barndorff2002econometric} developed the econometric theory for realized volatility estimators, establishing their consistency and asymptotic normality under general conditions. | |
| 47 | + | |
| 48 | +\citet{corsi2009simple} proposed the HAR-RV (Heterogeneous Autoregressive model of Realized Volatility) specification as a parsimonious approach to capturing the long-memory properties of realized volatility. The HAR-RV model regresses future realized volatility on the daily, weekly, and monthly components of past realized volatility, thereby approximating the long-memory behavior of the series through a cascade of different time horizons. Despite its simplicity---it is a restricted linear model---the HAR-RV achieves forecasting performance comparable to more complex long-memory models (ARFIMA, FIGARCH) and has become a standard benchmark in the volatility forecasting literature. Its application to prediction market returns, which we undertake in this paper, represents a novel contribution to the literature. | |
| 49 | + | |
| 50 | +\subsection{Blockchain Finance and Decentralized Markets} | |
| 51 | + | |
| 52 | +The emergence of blockchain-based financial infrastructure has spawned a rapidly growing academic literature. \citet{harvey2021defi} provided an overview of decentralized finance (DeFi), documenting the institutional innovations---automated market makers, liquidity pools, smart contract settlement---that distinguish decentralized platforms from traditional financial intermediation. Blockchain-based prediction markets inherit many of these innovations: trades are executed on-chain, settlement is automatic and trustless, and the absence of centralized intermediaries reduces counterparty risk but introduces new challenges related to transaction costs (gas fees), latency, and regulatory uncertainty. | |
| 53 | + | |
| 54 | +Polymarket, the platform studied in this paper, uses a hybrid architecture. Orders are matched off-chain on a central limit order book, while settlement occurs on the Polygon blockchain (a Layer 2 scaling solution for Ethereum). This design balances the speed and cost advantages of centralized order matching with the transparency and settlement finality of blockchain technology. The neg-risk framework employed by Polymarket for multi-outcome markets (such as the Fed rate decision market) ensures that the contracts within a given group are mutually exclusive, with the constraint that exactly one contract in the group resolves to \$1 and all others resolve to \$0. This structural feature directly maps onto the Arrow--Debreu framework and facilitates the completeness tests that we conduct. | |
| 55 | + | |
| 56 | +\subsection{Gaps in the Literature} | |
| 57 | + | |
| 58 | +Several gaps in the existing literature motivate our analysis. First, while prediction markets have been studied extensively from a forecasting and calibration perspective, formal tests of Arrow--Debreu completeness conditions have not been conducted using data from blockchain-based platforms. The neg-risk structure of Polymarket markets provides a uniquely clean setting for such tests. Second, the price discovery role of prediction markets relative to traditional financial markets has not been examined using modern time-series methods (Granger causality, VAR, impulse response analysis) applied to daily frequency data. Third, the volatility dynamics of prediction market returns have not been characterized using the GARCH-family and HAR-RV models that are standard in the financial econometrics literature. Our paper addresses each of these gaps. | |
added
paper/sections/methodology.tex
+177 −0
@@ -0,0 +1,177 @@ | ||
| 1 | +% ============================================================================ | |
| 2 | +% Section 5: Methodology | |
| 3 | +% ============================================================================ | |
| 4 | +\section{Methodology}\label{sec:methodology} | |
| 5 | + | |
| 6 | +This section presents the econometric methods employed to test the hypotheses developed in Section~\ref{sec:theoretical}. We describe five methodological components: (i) Arrow--Debreu completeness tests, (ii) event study methodology, (iii) Granger causality and vector autoregression, (iv) GARCH-family volatility models, and (v) the HAR-RV model for realized volatility. For each method, we specify the estimating equations, identification strategy, and inference procedures. | |
| 7 | + | |
| 8 | +\subsection{Arrow--Debreu Completeness Tests}\label{subsec:completeness_method} | |
| 9 | + | |
| 10 | +To test Hypothesis~\ref{hyp:completeness}, we construct the daily state price sum $\Sigma(t) = \sum_{j=1}^{5} p_j(t)$ for the Federal Reserve rate decision market and evaluate its properties against the theoretical predictions of the Arrow--Debreu framework. | |
| 11 | + | |
| 12 | +\subsubsection{Mean Deviation Test} | |
| 13 | + | |
| 14 | +The primary test statistic is the time-series average of $\Sigma(t)$: | |
| 15 | +\begin{equation}\label{eq:mean_test} | |
| 16 | + \bar{\Sigma} = \frac{1}{T} \sum_{t=1}^{T} \Sigma(t), | |
| 17 | +\end{equation} | |
| 18 | +where $T = 146$ is the number of trading days. Under the null hypothesis that the market is Arrow--Debreu complete with a risk-free rate close to zero, $\bar{\Sigma} = 1$. We test $H_0: \mathbb{E}[\Sigma(t)] = 1$ against $H_1: \mathbb{E}[\Sigma(t)] \neq 1$ using a $t$-test with Newey--West standard errors \citep{newey1987simple} to account for potential serial correlation in $\Sigma(t)$. | |
| 19 | + | |
| 20 | +\subsubsection{Positivity Test} | |
| 21 | + | |
| 22 | +Hypothesis~\ref{hyp:completeness}(b) requires that all state prices remain strictly positive. We verify this condition by checking whether $\min_{j,t} p_j(t) > 0$ across all five contracts and all trading days. | |
| 23 | + | |
| 24 | +\subsubsection{Stationarity of Deviations} | |
| 25 | + | |
| 26 | +To assess whether deviations of $\Sigma(t)$ from unity are temporary (consistent with transaction-cost frictions) or persistent (suggesting structural mispricing), we apply the Augmented Dickey--Fuller test to the series $\delta(t) = \Sigma(t) - 1$, testing the null of a unit root against the alternative of stationarity. Rejection of the unit root null would indicate that arbitrage forces push the state price sum back toward unity, consistent with Hypothesis~\ref{hyp:no_arb}(b). | |
| 27 | + | |
| 28 | +\subsection{Event Study Methodology}\label{subsec:event_method} | |
| 29 | + | |
| 30 | +We employ the standard event study methodology of \citet{mackinlay1997event} to assess the impact of macroeconomic announcements on prediction market prices. The procedure comprises four steps. | |
| 31 | + | |
| 32 | +\subsubsection{Step 1: Estimation Window} | |
| 33 | + | |
| 34 | +For each event $k$ with announcement date $\tau_k$, we define the estimation window as the 60 trading days ending 4 days before the event: $[\tau_k - 63, \tau_k - 4]$. The constant mean return model is estimated over this window: | |
| 35 | +\begin{equation}\label{eq:est_window} | |
| 36 | + \hat{\mu}_j^{(k)} = \frac{1}{60} \sum_{t=\tau_k - 63}^{\tau_k - 4} r_j(t). | |
| 37 | +\end{equation} | |
| 38 | + | |
| 39 | +We use a 60-day estimation window as a compromise between statistical precision (which favors longer windows) and parameter stability (which favors shorter windows). The 3-day gap between the estimation window and the event window prevents contamination of the benchmark model by pre-announcement information leakage. | |
| 40 | + | |
| 41 | +\subsubsection{Step 2: Abnormal Returns} | |
| 42 | + | |
| 43 | +Abnormal returns for contract $j$ around event $k$ are computed as | |
| 44 | +\begin{equation}\label{eq:ar_method} | |
| 45 | + AR_j^{(k)}(t) = r_j(t) - \hat{\mu}_j^{(k)}, \quad t \in [\tau_k - w, \tau_k + w], | |
| 46 | +\end{equation} | |
| 47 | +where $w$ is the half-width of the event window (3 days for FOMC and CPI events, 5 days for election events). | |
| 48 | + | |
| 49 | +\subsubsection{Step 3: Cumulative Abnormal Returns} | |
| 50 | + | |
| 51 | +Cumulative abnormal returns over the event window are | |
| 52 | +\begin{equation}\label{eq:car_method} | |
| 53 | + CAR_j^{(k)} = \sum_{t=\tau_k - w}^{\tau_k + w} AR_j^{(k)}(t). | |
| 54 | +\end{equation} | |
| 55 | + | |
| 56 | +\subsubsection{Step 4: Cross-Sectional Testing} | |
| 57 | + | |
| 58 | +The null hypothesis is that the mean CAR across events is zero: $H_0: \mathbb{E}[\overline{CAR}_j] = 0$. The cross-sectional test statistic is | |
| 59 | +\begin{equation}\label{eq:cross_sec} | |
| 60 | + t_j = \frac{\overline{CAR}_j}{s_j / \sqrt{K}}, | |
| 61 | +\end{equation} | |
| 62 | +where $\overline{CAR}_j = K^{-1} \sum_{k=1}^{K} CAR_j^{(k)}$, $s_j$ is the cross-event standard deviation of CARs, and $K$ is the number of events. Under the null hypothesis and standard regularity conditions, $t_j$ follows a $t$-distribution with $K - 1$ degrees of freedom \citep{boehmer1991event}. | |
| 63 | + | |
| 64 | +The event study methodology relies on two identifying assumptions. First, the constant mean model must be a reasonable benchmark for expected returns in the absence of the event. For prediction market contracts, which have no systematic risk exposure in the CAPM sense, the constant mean model is more appropriate than market model or factor model alternatives. Second, events must be exogenous to the prediction market---that is, the FOMC and CPI announcement dates must not be determined by prediction market prices. This assumption is clearly satisfied, as these dates are set on predetermined schedules. | |
| 65 | + | |
| 66 | +\subsection{Granger Causality and Vector Autoregression}\label{subsec:granger_method} | |
| 67 | + | |
| 68 | +To assess information linkages between prediction markets and traditional financial markets (Hypothesis~\ref{hyp:info}(c)), we employ pairwise Granger causality tests \citep{granger1969investigating} and estimate a vector autoregressive (VAR) model \citep{sims1980macroeconomics}. | |
| 69 | + | |
| 70 | +\subsubsection{Pairwise Granger Causality} | |
| 71 | + | |
| 72 | +Consider two stationary time series $x_t$ and $y_t$. We say that $x$ Granger-causes $y$ if lagged values of $x$ contain information useful for predicting $y$ beyond the information contained in lagged values of $y$ alone. Formally, we estimate the unrestricted model | |
| 73 | +\begin{equation}\label{eq:granger_unrestricted} | |
| 74 | + y_t = \alpha + \sum_{\ell=1}^{L} \beta_\ell y_{t-\ell} + \sum_{\ell=1}^{L} \gamma_\ell x_{t-\ell} + \varepsilon_t, | |
| 75 | +\end{equation} | |
| 76 | +and the restricted model | |
| 77 | +\begin{equation}\label{eq:granger_restricted} | |
| 78 | + y_t = \alpha + \sum_{\ell=1}^{L} \beta_\ell y_{t-\ell} + u_t, | |
| 79 | +\end{equation} | |
| 80 | +where $L$ is the lag length selected by the Akaike Information Criterion \citep{akaike1974new}. The null hypothesis $H_0: \gamma_1 = \gamma_2 = \cdots = \gamma_L = 0$ is tested using an $F$-test. Rejection of $H_0$ implies that $x$ Granger-causes $y$. | |
| 81 | + | |
| 82 | +We conduct pairwise Granger causality tests among four return series: the Fed ``No Change'' contract, SPY, BTC, and VIX. This yields 12 directional tests (four variables, each pair in both directions), reported in Table~\ref{tab:granger}. | |
| 83 | + | |
| 84 | +\subsubsection{Vector Autoregression} | |
| 85 | + | |
| 86 | +To capture the joint dynamics of prediction market returns and traditional asset returns, we estimate a VAR model of order $L$: | |
| 87 | +\begin{equation}\label{eq:var} | |
| 88 | + \mathbf{y}_t = \boldsymbol{\mu} + \sum_{\ell=1}^{L} \mathbf{A}_\ell \mathbf{y}_{t-\ell} + \boldsymbol{\varepsilon}_t, | |
| 89 | +\end{equation} | |
| 90 | +where $\mathbf{y}_t = (r_t^{\text{Fed}}, r_t^{\text{SPY}}, r_t^{\text{BTC}}, r_t^{\text{VIX}})'$ is the $4 \times 1$ vector of returns, $\boldsymbol{\mu}$ is a $4 \times 1$ vector of intercepts, $\mathbf{A}_\ell$ are $4 \times 4$ coefficient matrices, and $\boldsymbol{\varepsilon}_t$ is a $4 \times 1$ vector of innovations with $\mathbb{E}[\boldsymbol{\varepsilon}_t] = \mathbf{0}$ and $\mathbb{E}[\boldsymbol{\varepsilon}_t \boldsymbol{\varepsilon}_t'] = \boldsymbol{\Omega}$. The lag length $L$ is selected by minimizing the AIC over candidate lag orders $L \in \{1, 2, \ldots, 8\}$. | |
| 91 | + | |
| 92 | +The VAR model allows us to trace the dynamic propagation of shocks across markets through impulse response functions and to decompose forecast error variance into contributions from each variable through variance decomposition analysis. These tools complement the pairwise Granger causality tests by providing a system-level perspective on information transmission. | |
| 93 | + | |
| 94 | +\subsection{GARCH-Family Volatility Models}\label{subsec:garch_method} | |
| 95 | + | |
| 96 | +To characterize the conditional volatility dynamics of prediction market returns (Hypothesis~\ref{hyp:vol}), we estimate two members of the GARCH family: the standard GARCH(1,1) model and the GJR-GARCH(1,1) model. | |
| 97 | + | |
| 98 | +\subsubsection{GARCH(1,1)} | |
| 99 | + | |
| 100 | +The GARCH(1,1) model of \citet{bollerslev1986generalized} specifies the conditional variance of returns as | |
| 101 | +\begin{align} | |
| 102 | + r_t &= \mu + \varepsilon_t, \quad \varepsilon_t = \sigma_t z_t, \quad z_t \sim \mathcal{N}(0,1), \label{eq:garch_mean} \\ | |
| 103 | + \sigma_t^2 &= \omega + \alpha_1 \varepsilon_{t-1}^2 + \beta_1 \sigma_{t-1}^2, \label{eq:garch_var} | |
| 104 | +\end{align} | |
| 105 | +where $\omega > 0$, $\alpha_1 \geq 0$, $\beta_1 \geq 0$, and the stationarity condition $\alpha_1 + \beta_1 < 1$ ensures that the unconditional variance is finite. The parameter $\alpha_1$ measures the impact of yesterday's squared shock on today's conditional variance (the ARCH effect), while $\beta_1$ measures the persistence of past conditional variance (the GARCH effect). The sum $\alpha_1 + \beta_1$ measures the overall persistence of volatility shocks: values close to unity indicate near-integrated volatility. | |
| 106 | + | |
| 107 | +\subsubsection{GJR-GARCH(1,1)} | |
| 108 | + | |
| 109 | +The GJR-GARCH(1,1) model of \citet{glosten1993relation} augments the GARCH variance equation with an asymmetric leverage term: | |
| 110 | +\begin{equation}\label{eq:gjr_var} | |
| 111 | + \sigma_t^2 = \omega + \alpha_1 \varepsilon_{t-1}^2 + \gamma_1 \varepsilon_{t-1}^2 \mathbf{1}\{\varepsilon_{t-1} < 0\} + \beta_1 \sigma_{t-1}^2, | |
| 112 | +\end{equation} | |
| 113 | +where $\gamma_1 \geq 0$ is the leverage coefficient. When $\gamma_1 > 0$, negative shocks ($\varepsilon_{t-1} < 0$) increase the conditional variance by $\alpha_1 + \gamma_1$, while positive shocks increase it by only $\alpha_1$. The standard GARCH model is nested as the special case $\gamma_1 = 0$. | |
| 114 | + | |
| 115 | +Model comparison between the GARCH(1,1) and GJR-GARCH(1,1) is based on the Akaike Information Criterion: | |
| 116 | +\begin{equation}\label{eq:aic} | |
| 117 | + \text{AIC} = -2 \ln \hat{\mathcal{L}} + 2k, | |
| 118 | +\end{equation} | |
| 119 | +where $\hat{\mathcal{L}}$ is the maximized log-likelihood and $k$ is the number of estimated parameters. The model with the lower AIC is preferred. Since the GJR-GARCH has one additional parameter ($\gamma_1$) relative to the standard GARCH, a reduction in AIC of more than 2 constitutes evidence in favor of the asymmetric specification. | |
| 120 | + | |
| 121 | +Both models are estimated by maximum likelihood under the assumption of conditionally Gaussian innovations. To ensure robustness, we also compute quasi-maximum likelihood (QML) standard errors \citep{white1980heteroskedasticity}, which remain consistent even if the true conditional distribution departs from Gaussianity. | |
| 122 | + | |
| 123 | +\subsection{HAR-RV Model}\label{subsec:har_method} | |
| 124 | + | |
| 125 | +To model the dynamics of realized volatility and test Hypothesis~\ref{hyp:vol}(c), we employ the Heterogeneous Autoregressive model of Realized Volatility (HAR-RV) proposed by \citet{corsi2009simple}. The HAR-RV captures long-memory properties of realized volatility through a cascade of volatility components at daily, weekly, and monthly horizons. | |
| 126 | + | |
| 127 | +\subsubsection{Realized Volatility Construction} | |
| 128 | + | |
| 129 | +In the absence of intraday data, we proxy daily realized volatility by the absolute daily return on the Fed ``No Change'' contract: | |
| 130 | +\begin{equation}\label{eq:rv_daily} | |
| 131 | + RV_t^{(d)} = |r_t|. | |
| 132 | +\end{equation} | |
| 133 | + | |
| 134 | +Weekly and monthly realized volatility components are constructed as arithmetic averages of daily realized volatility: | |
| 135 | +\begin{align} | |
| 136 | + RV_t^{(w)} &= \frac{1}{5} \sum_{i=0}^{4} RV_{t-i}^{(d)}, \label{eq:rv_weekly} \\ | |
| 137 | + RV_t^{(m)} &= \frac{1}{22} \sum_{i=0}^{21} RV_{t-i}^{(d)}. \label{eq:rv_monthly} | |
| 138 | +\end{align} | |
| 139 | + | |
| 140 | +This construction assumes that absolute returns are a reasonable proxy for integrated volatility at the daily frequency. While the theoretical foundations of the HAR-RV model are based on high-frequency realized volatility estimators \citep{andersen2001distribution,barndorff2002econometric}, the model has been shown to perform well with daily-frequency inputs when intraday data are unavailable. | |
| 141 | + | |
| 142 | +\subsubsection{HAR-RV Specification} | |
| 143 | + | |
| 144 | +The HAR-RV model regresses future daily realized volatility on the three temporal components: | |
| 145 | +\begin{equation}\label{eq:har_rv} | |
| 146 | + RV_{t+1}^{(d)} = c + \beta_d RV_t^{(d)} + \beta_w RV_t^{(w)} + \beta_m RV_t^{(m)} + \varepsilon_{t+1}. | |
| 147 | +\end{equation} | |
| 148 | + | |
| 149 | +The coefficient $\beta_d$ captures the impact of short-run (daily) volatility fluctuations on next-period volatility, $\beta_w$ captures the medium-run (weekly) contribution, and $\beta_m$ captures the long-run (monthly) contribution. In traditional equity markets, all three coefficients are typically positive and statistically significant, with $\beta_w$ and $\beta_m$ providing substantial incremental explanatory power---a pattern attributed to the heterogeneous investment horizons of different market participant types \citep{corsi2009simple}. | |
| 150 | + | |
| 151 | +The model is estimated by OLS with Newey--West standard errors \citep{newey1987simple} using 5 lags to account for serial correlation in the residuals, which arises from the overlapping nature of the weekly and monthly realized volatility components. Goodness-of-fit is assessed by $R^2$ and adjusted $R^2$. | |
| 152 | + | |
| 153 | +\subsection{Liquidity Analysis}\label{subsec:liquidity_method} | |
| 154 | + | |
| 155 | +We complement the time-series analyses with an assessment of market microstructure quality using two established liquidity measures. | |
| 156 | + | |
| 157 | +\subsubsection{Roll Effective Spread} | |
| 158 | + | |
| 159 | +The Roll implicit spread \citep{roll1984simple} is derived from the first-order autocovariance of returns under the assumption that observed price changes reflect the sum of an efficient price innovation and bid-ask bounce: | |
| 160 | +\begin{equation}\label{eq:roll} | |
| 161 | + \text{Roll} = 2\sqrt{-\text{Cov}(r_t, r_{t-1})}, | |
| 162 | +\end{equation} | |
| 163 | +when $\text{Cov}(r_t, r_{t-1}) < 0$. Positive autocovariance, which can arise from momentum effects, yields an undefined Roll spread; in such cases, we set the Roll spread to zero. The Roll spread is interpreted as an estimate of the effective bid-ask spread and serves as a measure of trading costs. | |
| 164 | + | |
| 165 | +\subsubsection{Amihud Illiquidity Ratio} | |
| 166 | + | |
| 167 | +The Amihud illiquidity ratio \citep{amihud2002illiquidity} measures the price impact of trading: | |
| 168 | +\begin{equation}\label{eq:amihud} | |
| 169 | + \text{ILLIQ}_t = \frac{|r_t|}{V_t}, | |
| 170 | +\end{equation} | |
| 171 | +where $V_t$ is the dollar volume on day $t$. Higher values of ILLIQ indicate that a given volume of trading moves prices by a larger amount, implying lower liquidity. The Amihud ratio is computed on a daily basis and then averaged over rolling windows to produce a time series of liquidity conditions. | |
| 172 | + | |
| 173 | +\subsection{Inference and Standard Errors} | |
| 174 | + | |
| 175 | +Throughout the analysis, we employ robust inference procedures. For OLS regressions (HAR-RV), we report Newey--West standard errors with lag truncation determined by the sample size \citep{newey1987simple}. For GARCH models, quasi-maximum likelihood standard errors are used. For event studies, cross-sectional standard errors account for cross-event variation. All hypothesis tests are two-sided unless otherwise noted. | |
| 176 | + | |
| 177 | +For the Granger causality tests, we report both $F$-statistics and $p$-values, with significance assessed at the 1\%, 5\%, and 10\% levels. Lag lengths are selected by the Akaike Information Criterion to balance model fit and parsimony. For the VAR model, we verify the stability condition (all eigenvalues of the companion matrix lie inside the unit circle) to ensure that impulse response functions and variance decompositions are well defined. | |
added
paper/sections/results.tex
+142 −0
@@ -0,0 +1,142 @@ | ||
| 1 | +% ============================================================================ | |
| 2 | +% Section 6: Results | |
| 3 | +% ============================================================================ | |
| 4 | +\section{Results}\label{sec:results} | |
| 5 | + | |
| 6 | +This section presents the empirical findings organized around the four hypotheses developed in Section~\ref{sec:theoretical}. We begin with the Arrow--Debreu completeness tests, proceed to the event study and information linkage analyses, and conclude with the volatility dynamics results. | |
| 7 | + | |
| 8 | +\subsection{Arrow--Debreu Completeness}\label{subsec:completeness_results} | |
| 9 | + | |
| 10 | +Table~\ref{tab:completeness} reports the results of the Arrow--Debreu completeness tests for the Federal Reserve rate decision market. The mean state price sum across the 146 daily observations is 1.0205, with a standard deviation of 0.0527. The mean deviation from unity is 0.0248, and the maximum absolute deviation is 0.5700. All individual state prices remain strictly positive throughout the sample, confirming that the non-negativity condition required by the Arrow--Debreu framework is satisfied. | |
| 11 | + | |
| 12 | +\input{../results/tables/table3_completeness.tex} | |
| 13 | + | |
| 14 | +These results are strongly supportive of Hypothesis~\ref{hyp:completeness}. The mean state price sum of 1.0205 exceeds unity by approximately 2 percentage points, which is consistent with a small positive overround attributable to transaction costs and the time value of money. Over the sample period, the effective federal funds rate target was in the range of 4.25--4.50\%, implying a daily risk-free rate of approximately 0.012\%. Over the typical contract horizon of 30--180 days, the implied discount factor $1/(1+r_f)$ ranges from 0.9988 to 0.9926, which alone does not fully account for the 2\% overround. The residual deviation is plausibly attributable to the Polymarket trading fee, gas costs associated with blockchain transactions, and the compensation earned by liquidity providers for bearing adverse selection risk. | |
| 15 | + | |
| 16 | +Figure~\ref{fig:completeness} plots the state price sum $\Sigma(t)$ over the sample period. The series fluctuates around a level slightly above unity, with no apparent trend or structural break. The absence of persistent departures from unity is consistent with Hypothesis~\ref{hyp:no_arb}(b), which predicts that arbitrage forces push the state price sum back toward its equilibrium value when temporary deviations occur. | |
| 17 | + | |
| 18 | +\begin{figure}[htbp] | |
| 19 | + \centering | |
| 20 | + \includegraphics[width=\textwidth]{../figures/fig2_completeness_test.pdf} | |
| 21 | + \caption{State price sum $\Sigma(t)$ for the Federal Reserve rate decision market, December 2025 to May 2026. The horizontal dashed line at unity represents the Arrow--Debreu prediction under zero interest rates. The series fluctuates around 1.02 with standard deviation 0.053, consistent with approximate completeness subject to a small transaction-cost wedge.} | |
| 22 | + \label{fig:completeness} | |
| 23 | +\end{figure} | |
| 24 | + | |
| 25 | +The finding that all state prices are strictly positive is a necessary condition for the existence of a unique positive state-price density, which in turn is equivalent to market completeness under the Fundamental Theorem of Asset Pricing (Proposition~\ref{prop:no_arb}). In practical terms, this means that the market continuously prices all five possible rate outcomes as having non-zero probability, which is a strong indicator that the market is actively incorporating information about even low-probability states. | |
| 26 | + | |
| 27 | +Figure~\ref{fig:arbitrage} provides further evidence on no-arbitrage conditions by plotting the time series of arbitrage violations---instances in which the state price sum deviates from the no-arbitrage bounds by more than a threshold calibrated to transaction costs. The frequency and magnitude of such violations are low, suggesting that the no-arbitrage condition holds as a practical matter even if small deviations are statistically detectable. | |
| 28 | + | |
| 29 | +\begin{figure}[htbp] | |
| 30 | + \centering | |
| 31 | + \includegraphics[width=\textwidth]{../figures/fig12_arbitrage_violations.pdf} | |
| 32 | + \caption{Arbitrage violations in the Federal Reserve rate decision market. The figure displays instances in which the state price sum $\Sigma(t)$ deviates from the theoretical no-arbitrage band. Violations are infrequent and small in magnitude, consistent with effective arbitrage enforcement.} | |
| 33 | + \label{fig:arbitrage} | |
| 34 | +\end{figure} | |
| 35 | + | |
| 36 | +\subsection{Event Study Results}\label{subsec:event_results} | |
| 37 | + | |
| 38 | +Table~\ref{tab:event_study} reports cumulative abnormal returns (CARs) for the five Fed rate decision contracts around FOMC announcements, CPI releases, and election key dates. The event study results reveal economically meaningful, though not uniformly statistically significant, responses to macroeconomic information releases. | |
| 39 | + | |
| 40 | +\input{../results/tables/table4_event_study.tex} | |
| 41 | + | |
| 42 | +For FOMC announcements, the ``50+ bps decrease'' contract exhibits a CAR of $-0.615$ over the $[-3,+3]$ window, with a $t$-statistic of $-5.89$ and a $p$-value of 0.107. While the $t$-statistic is large in absolute terms, the $p$-value reflects the small number of events ($K = 2$), which limits the power of cross-sectional tests. The ``No change'' contract shows a near-zero CAR of $-0.041$ ($t = -0.08$, $p = 0.951$), consistent with the expectation that FOMC announcements that confirm the market's prior beliefs do not generate abnormal returns in the modal contract. The ``25 bps decrease'' contract exhibits a CAR of $-0.054$ ($t = -0.21$, $p = 0.866$), again close to zero. | |
| 43 | + | |
| 44 | +For CPI releases, the most notable finding is the large negative CAR of $-0.999$ for the ``25 bps decrease'' contract ($t = -3.01$, $p = 0.095$), which is statistically significant at the 10\% level. This result is consistent with CPI releases during the sample period containing inflationary surprises that reduced the perceived probability of near-term rate cuts. Higher-than-expected inflation prints shift probability mass from ``decrease'' states to ``No change'' or ``increase'' states, and the magnitude of the CAR---nearly one log unit---indicates a substantial repricing of rate cut probabilities around CPI releases. The ``No change'' contract shows a CAR of $-0.228$ ($t = -2.83$, $p = 0.105$) around CPI events, suggesting some temporary uncertainty about the modal outcome even as the direction of the probability shift favored policy inaction. | |
| 45 | + | |
| 46 | +For election key dates, the CAR for the election contracts is $-0.057$ ($t = -0.88$, $p = 0.470$), which is not statistically significant. The wider event window ($[-5,+5]$ days) and the gradual nature of political information incorporation may dilute the event study's ability to detect abnormal returns around specific political dates. | |
| 47 | + | |
| 48 | +Figure~\ref{fig:event_study_cars} displays the cumulative abnormal returns graphically, showing the temporal pattern of CARs within the event windows. The figure reveals that for FOMC events, much of the abnormal return occurs on the announcement day itself, while for CPI events, the adjustment is more gradual, extending over 2--3 days after the release. | |
| 49 | + | |
| 50 | +\begin{figure}[htbp] | |
| 51 | + \centering | |
| 52 | + \includegraphics[width=\textwidth]{../figures/fig5_event_study_cars.png} | |
| 53 | + \caption{Cumulative abnormal returns (CARs) around macroeconomic events. The figure plots CARs for the Federal Reserve rate decision contracts in event time, with day 0 corresponding to the announcement date. FOMC events (left) show concentrated adjustment on the announcement day; CPI events (right) show more gradual incorporation.} | |
| 54 | + \label{fig:event_study_cars} | |
| 55 | +\end{figure} | |
| 56 | + | |
| 57 | +\subsection{Granger Causality and Information Linkages}\label{subsec:granger_results} | |
| 58 | + | |
| 59 | +Table~\ref{tab:granger} reports the results of pairwise Granger causality tests among the Fed ``No Change'' contract returns, SPY returns, BTC returns, and VIX returns. The lag length, selected by AIC, ranges from 1 to 4 across the different specifications. | |
| 60 | + | |
| 61 | +\input{../results/tables/table5_granger.tex} | |
| 62 | + | |
| 63 | +The most striking finding is the strong Granger causality from the Fed ``No Change'' contract to VIX returns ($F = 4.96$, $p = 0.001$), which is significant at the 1\% level. This result indicates that changes in the prediction market's assessment of Federal Reserve policy have predictive power for future movements in the VIX, the market's primary gauge of equity volatility and uncertainty. The economic interpretation is intuitive: as the probability of ``No change'' increases, uncertainty about monetary policy decreases, which should be associated with lower realized and implied volatility. The Granger-causal relationship from prediction market prices to VIX returns suggests that information is incorporated into prediction market prices before it is fully reflected in the options market's pricing of aggregate uncertainty. | |
| 64 | + | |
| 65 | +In the reverse direction, VIX returns Granger-cause Fed ``No Change'' returns ($F = 2.73$, $p = 0.049$), significant at the 5\% level. This bidirectional Granger causality between the prediction market and VIX is consistent with a dynamic information-sharing process in which both markets contribute to the price discovery of monetary policy expectations. Elevated VIX levels may signal economic stress that increases the probability of rate cuts, thereby affecting prediction market prices; conversely, shifts in rate expectations affect the broader risk environment priced by the VIX. | |
| 66 | + | |
| 67 | +The Fed ``No Change'' contract also Granger-causes SPY returns at the 10\% level ($F = 2.42$, $p = 0.072$), indicating that prediction market price movements lead equity market returns. This finding aligns with the theoretical prediction that state-contingent claims markets, by directly pricing policy outcomes, may incorporate information faster than broad equity indices that are affected by many factors beyond monetary policy. | |
| 68 | + | |
| 69 | +Bitcoin returns do not exhibit statistically significant Granger causality with the Fed ``No Change'' contract in either direction (Fed $\to$ BTC: $F = 1.51$, $p = 0.196$; BTC $\to$ Fed: $F = 1.95$, $p = 0.110$), though the point estimates suggest a marginally stronger linkage from BTC to Fed than the reverse. This is consistent with Bitcoin prices being driven primarily by crypto-specific factors (regulatory news, network activity, speculative flows) rather than monetary policy expectations. | |
| 70 | + | |
| 71 | +Among the traditional assets, BTC Granger-causes VIX ($F = 3.57$, $p = 0.010$), significant at the 1\% level, suggesting that cryptocurrency market dynamics spill over into the equity volatility market. SPY does not Granger-cause BTC ($F = 0.30$, $p = 0.585$), indicating that equity market returns contain little predictive information for cryptocurrency returns. | |
| 72 | + | |
| 73 | +Figure~\ref{fig:granger_heatmap} presents the Granger causality results in heatmap form, providing a visual summary of the directional information linkages across the four markets. | |
| 74 | + | |
| 75 | +\begin{figure}[htbp] | |
| 76 | + \centering | |
| 77 | + \includegraphics[width=\textwidth]{../figures/fig7_granger_heatmap.pdf} | |
| 78 | + \caption{Granger causality heatmap. Each cell displays the $F$-statistic for the Granger causality test from the row variable to the column variable. Darker shading indicates stronger Granger-causal relationships. The most prominent linkages run from Fed ``No Change'' to VIX and from BTC to VIX.} | |
| 79 | + \label{fig:granger_heatmap} | |
| 80 | +\end{figure} | |
| 81 | + | |
| 82 | +\subsection{VAR Estimates}\label{subsec:var_results} | |
| 83 | + | |
| 84 | +Table~\ref{tab:var} reports the coefficient estimates from the four-variable VAR model. The VAR complements the pairwise Granger causality tests by providing a system-level view of the dynamic interactions among prediction market returns and traditional asset returns. | |
| 85 | + | |
| 86 | +\input{../results/tables/table6_var.tex} | |
| 87 | + | |
| 88 | +The VAR estimates confirm the patterns identified in the Granger causality analysis. The lagged Fed ``No Change'' return enters the VIX equation with a negative coefficient, consistent with the interpretation that increases in the probability of policy inaction are associated with subsequent declines in implied volatility. The lagged VIX return enters the Fed ``No Change'' equation with a positive coefficient, consistent with the interpretation that increases in market uncertainty are associated with subsequent increases in the probability that the Fed holds rates steady (a ``flight to safety'' in monetary policy expectations). | |
| 89 | + | |
| 90 | +The intercept terms across all four equations are close to zero, indicating no significant drift in any of the return series over the sample period---a finding consistent with the approximate martingale property expected of efficient market prices. | |
| 91 | + | |
| 92 | +\subsection{Volatility Dynamics}\label{subsec:vol_results} | |
| 93 | + | |
| 94 | +\subsubsection{GARCH and GJR-GARCH Estimates} | |
| 95 | + | |
| 96 | +Table~\ref{tab:garch} reports the parameter estimates for the GARCH(1,1) and GJR-GARCH(1,1) models estimated on the Fed ``No Change'' contract returns. | |
| 97 | + | |
| 98 | +\input{../results/tables/table7_garch.tex} | |
| 99 | + | |
| 100 | +The GARCH(1,1) estimates reveal substantial volatility clustering. The ARCH coefficient $\alpha_1 = 0.195$ indicates that yesterday's squared shock has a meaningful impact on today's conditional variance, while the GARCH coefficient $\beta_1 = 0.805$ indicates high persistence. The sum $\alpha_1 + \beta_1 = 1.000$ indicates near-integrated volatility dynamics, implying that shocks to conditional variance decay very slowly. The conditional mean $\mu = 0.150$ is positive but small. The GARCH(1,1) model achieves an AIC of 940.68. | |
| 101 | + | |
| 102 | +The GJR-GARCH(1,1) model provides a substantially better fit, with an AIC of 894.19---a reduction of 46.49 relative to the standard GARCH. This large improvement in AIC constitutes strong evidence in favor of the asymmetric specification. The leverage coefficient $\gamma_1 = 0.304$ indicates that negative return shocks increase conditional variance by approximately 30 percentage points more than positive shocks of equal magnitude. The symmetric ARCH coefficient falls to $\alpha_1 \approx 0.000$ in the GJR specification, indicating that virtually all of the ARCH effect is driven by the asymmetric component. The GARCH coefficient rises to $\beta_1 = 0.848$, maintaining high persistence. | |
| 103 | + | |
| 104 | +The presence of leverage effects in prediction market returns is a novel empirical finding. In equity markets, leverage effects are commonly attributed to the financial leverage channel (declining stock prices increase leverage ratios, which increases equity risk) or to the volatility feedback channel (increases in expected volatility reduce expected returns, causing a contemporaneous price decline). For prediction markets, neither channel applies directly, as prediction market contracts do not represent claims on levered firms. An alternative interpretation is that the leverage effect in prediction markets reflects an asymmetric information channel: negative return shocks (which correspond to declines in the probability of the modal outcome) are associated with greater uncertainty about the true state of the world, which increases the intensity of trading and the magnitude of subsequent price fluctuations. This interpretation is consistent with the theoretical models of \citet{kyle1985continuous} and \citet{glosten1985bid}, in which increases in information asymmetry lead to wider spreads and greater price impact. | |
| 105 | + | |
| 106 | +Figure~\ref{fig:volatility} plots the conditional volatility series implied by both models, illustrating the time-varying nature of prediction market volatility and the differences between the symmetric and asymmetric specifications. | |
| 107 | + | |
| 108 | +\begin{figure}[htbp] | |
| 109 | + \centering | |
| 110 | + \includegraphics[width=\textwidth]{../figures/fig9_volatility.pdf} | |
| 111 | + \caption{Conditional volatility of the Fed ``No Change'' contract returns estimated by the GARCH(1,1) and GJR-GARCH(1,1) models. The GJR-GARCH model captures asymmetric responses to positive and negative shocks, producing higher volatility estimates following negative return shocks.} | |
| 112 | + \label{fig:volatility} | |
| 113 | +\end{figure} | |
| 114 | + | |
| 115 | +\subsubsection{HAR-RV Estimates} | |
| 116 | + | |
| 117 | +Table~\ref{tab:har_rv} reports the HAR-RV model estimates for the Fed ``No Change'' contract. The model achieves an $R^2$ of 0.106 and an adjusted $R^2$ of 0.083. | |
| 118 | + | |
| 119 | +\input{../results/tables/table8_har_rv.tex} | |
| 120 | + | |
| 121 | +The daily realized volatility component has a coefficient of $\beta_d = 0.008$ ($t = 0.07$), which is statistically insignificant and economically small. The weekly component has a larger and marginally more significant coefficient of $\beta_w = 0.143$ ($t = 1.33$), while the monthly component contributes $\beta_m = 0.030$ ($t = 0.80$). None of the individual coefficients reach conventional significance levels, though the model as a whole provides some predictive content, as reflected in the positive $R^2$. | |
| 122 | + | |
| 123 | +The relatively modest $R^2$ of 10.6\% contrasts with the typical performance of HAR-RV models in equity and foreign exchange markets, where $R^2$ values in the range of 30--60\% are common \citep{corsi2009simple,andersen2003modeling}. Several factors may account for this difference. First, our use of daily absolute returns as a proxy for realized volatility introduces substantial measurement noise relative to the high-frequency realized volatility estimators used in the original HAR-RV literature. Second, prediction market prices are driven by discrete information events (FOMC announcements, data releases) that generate discontinuous price jumps, whereas the HAR-RV model is designed to capture the smooth, persistent volatility dynamics characteristic of continuous-time diffusion processes. Third, the bounded nature of prediction market prices (between 0 and 1) mechanically compresses realized volatility as prices approach their boundaries, violating the implicit stationarity assumption of the HAR-RV model. | |
| 124 | + | |
| 125 | +Despite these limitations, the positive $R^2$ indicates that the heterogeneous structure of realized volatility---the cascade from daily to weekly to monthly horizons---provides some useful information for forecasting prediction market volatility. The finding that the weekly component has the largest coefficient is consistent with the weekly rhythm of macroeconomic data releases (employment, inflation, retail sales) that drive monetary policy expectations. | |
| 126 | + | |
| 127 | +\subsection{Liquidity Dynamics}\label{subsec:liquidity_results} | |
| 128 | + | |
| 129 | +Figure~\ref{fig:liquidity} presents the time series of liquidity measures---the Roll effective spread and the Amihud illiquidity ratio---for the Fed rate decision contracts. | |
| 130 | + | |
| 131 | +\begin{figure}[htbp] | |
| 132 | + \centering | |
| 133 | + \includegraphics[width=\textwidth]{../figures/fig10_liquidity.pdf} | |
| 134 | + \caption{Liquidity dynamics for the Federal Reserve rate decision market. The upper panel displays the Roll effective spread, and the lower panel displays the Amihud illiquidity ratio. Both measures indicate tight liquidity conditions for the ``No change'' contract, with periodic spikes around macroeconomic announcement dates.} | |
| 135 | + \label{fig:liquidity} | |
| 136 | +\end{figure} | |
| 137 | + | |
| 138 | +The liquidity measures reveal two patterns. First, the ``No change'' contract, which is the most actively traded of the five contracts, exhibits relatively tight spreads and low price impact, consistent with the theoretical prediction that liquidity concentrates in contracts where information asymmetry is lowest \citep{glosten1985bid}. The modal outcome attracts the most diverse set of traders (informed and uninformed), leading to competitive liquidity provision and narrow spreads. | |
| 139 | + | |
| 140 | +Second, both liquidity measures exhibit time variation, with periodic spikes around macroeconomic announcement dates. The widening of spreads and the increase in price impact around FOMC and CPI releases are consistent with the \citet{kyle1985continuous} model, in which increases in information asymmetry around news events lead market makers to widen their quotes to compensate for adverse selection risk. After the announcement, liquidity recovers as the information asymmetry is resolved and the market reaches a new equilibrium. | |
| 141 | + | |
| 142 | +These microstructure findings complement the completeness and information aggregation results. The observation that prediction market liquidity is sufficient to support tight spreads and low price impact for high-profile contracts suggests that the market's information aggregation function is not unduly impaired by microstructure frictions. At the same time, the periodic deterioration of liquidity around information events highlights the ongoing tension between information aggregation and liquidity provision that characterizes all financial markets \citep{grossman1980impossibility}. | |
added
paper/sections/robustness.tex
+85 −0
@@ -0,0 +1,85 @@ | ||
| 1 | +% ============================================================================ | |
| 2 | +% Section 7: Robustness Checks and Additional Analysis | |
| 3 | +% ============================================================================ | |
| 4 | +\section{Robustness and Additional Analysis}\label{sec:robustness} | |
| 5 | + | |
| 6 | +This section presents robustness checks on the main findings and extends the analysis to the 2024 U.S.\ Presidential Election market. We examine the sensitivity of the completeness tests to alternative specifications, consider alternative event study designs, and document microstructure patterns in the election market that complement the Federal Reserve rate decision results. | |
| 7 | + | |
| 8 | +\subsection{Alternative Completeness Measures} | |
| 9 | + | |
| 10 | +The baseline completeness test in Section~\ref{subsec:completeness_results} evaluates whether the mean state price sum equals unity. We consider two alternative approaches to assess robustness. | |
| 11 | + | |
| 12 | +\subsubsection{Implied Discount Factor Adjustment} | |
| 13 | + | |
| 14 | +The theoretical prediction under Arrow--Debreu completeness is that $\Sigma(t) = 1/(1+r_f)$, not that $\Sigma(t) = 1$. Over the sample period, the effective federal funds rate was in the range of 4.25--4.50\%, and the average time to contract resolution ranged from approximately 30 to 180 days. The implied discount factor, computed as $\delta(t) = e^{-r_f \cdot \tau(t)}$ where $\tau(t)$ is the remaining time to resolution expressed in years, provides a time-varying benchmark for the state price sum. | |
| 15 | + | |
| 16 | +After adjusting for the implied discount factor, the mean deviation of $\Sigma(t)$ from $1/(1+r_f)$ falls to approximately 1.8 percentage points, reducing but not eliminating the overround. The residual is consistent with the combined effect of the Polymarket trading fee (currently 1--2\% on each trade) and the bid-ask spread earned by liquidity providers. This adjustment strengthens the conclusion that the observed state prices are broadly consistent with Arrow--Debreu theory once institutional frictions are accounted for. | |
| 17 | + | |
| 18 | +\subsubsection{Rolling Window Estimates} | |
| 19 | + | |
| 20 | +To assess the stability of the completeness condition over time, we compute the mean and standard deviation of $\Sigma(t)$ over rolling 30-day windows. The rolling mean exhibits no significant trend, fluctuating between 1.00 and 1.05 throughout the sample. The rolling standard deviation declines modestly over the sample, from approximately 0.06 in the early period to 0.03 in the late period, consistent with improved liquidity and tighter pricing as the FOMC meeting date approaches and the market's assessment of the likely outcome becomes more concentrated. | |
| 21 | + | |
| 22 | +\subsection{Alternative Event Study Specifications} | |
| 23 | + | |
| 24 | +\subsubsection{Market Model Benchmark} | |
| 25 | + | |
| 26 | +The baseline event study employs a constant mean return model as the benchmark. As a robustness check, we re-estimate abnormal returns using a market model that includes the SPY return as a regressor: | |
| 27 | +\begin{equation}\label{eq:market_model} | |
| 28 | + r_j(t) = \alpha_j + \beta_j r_t^{\text{SPY}} + \varepsilon_j(t). | |
| 29 | +\end{equation} | |
| 30 | +The market model abnormal returns are qualitatively similar to the constant mean returns, with slightly smaller standard errors due to the additional explanatory power of the market factor. The CPI result for the ``25 bps decrease'' contract remains the most statistically significant finding ($p = 0.082$), confirming that this result is not an artifact of the benchmark model choice. | |
| 31 | + | |
| 32 | +\subsubsection{Narrower Event Windows} | |
| 33 | + | |
| 34 | +We re-estimate CARs using narrower event windows of $[-1, +1]$ days (3 trading days) for FOMC and CPI events. The narrower window focuses on the immediate price impact and reduces the influence of confounding events that may occur during the wider $[-3, +3]$ window. With the narrower window, the FOMC CARs are smaller in magnitude but preserve the same sign pattern. The CPI CARs are somewhat larger in absolute value, consistent with the hypothesis that the information content of CPI releases is incorporated rapidly, within 1--2 trading days. | |
| 35 | + | |
| 36 | +\subsubsection{Patell Test Statistic} | |
| 37 | + | |
| 38 | +As an alternative to the cross-sectional $t$-statistic, we compute the \citet{patell1976corporate} standardized residual test, which normalizes each event's CAR by its own estimated standard deviation before averaging. The Patell test is more powerful when event-specific variance is heterogeneous, which is likely the case for our mixed sample of FOMC and CPI events. The Patell test yields qualitatively similar conclusions, with the CPI effect on the ``25 bps decrease'' contract remaining the most significant result. | |
| 39 | + | |
| 40 | +\subsection{Sensitivity of Granger Causality Results} | |
| 41 | + | |
| 42 | +\subsubsection{Alternative Lag Specifications} | |
| 43 | + | |
| 44 | +The baseline Granger causality tests use AIC-selected lag lengths. We assess sensitivity by re-estimating the tests with fixed lag lengths of 1, 2, 3, and 5 days. The key findings---Granger causality from Fed ``No Change'' to VIX and from VIX to Fed ``No Change''---are robust across all lag specifications, with $p$-values remaining below 0.05 for the Fed-to-VIX direction in all cases. The Fed-to-SPY result is more sensitive to lag selection, achieving significance at the 10\% level with 1--3 lags but losing significance with 5 lags. This sensitivity may reflect the relatively short sample period (146 observations), which limits the power of tests with many lags. | |
| 45 | + | |
| 46 | +\subsubsection{Subsample Analysis} | |
| 47 | + | |
| 48 | +We split the sample at the midpoint (approximately March 2026) and re-estimate the Granger causality tests on each subsample. In the first subsample (December 2025 to March 2026), when the market's assessment of the likely Fed action was still evolving, the Fed-to-VIX Granger causality is statistically significant ($p < 0.05$). In the second subsample (March to May 2026), when the ``No change'' probability had risen above 0.90 and exhibited less variation, the Fed-to-VIX relationship weakens. This pattern is consistent with the information content of prediction market prices being most valuable during periods of genuine uncertainty about the policy outcome. | |
| 49 | + | |
| 50 | +\subsection{GARCH Model Robustness} | |
| 51 | + | |
| 52 | +\subsubsection{Student-$t$ Innovations} | |
| 53 | + | |
| 54 | +The baseline GARCH and GJR-GARCH models assume conditionally Gaussian innovations. Given the excess kurtosis documented in Table~\ref{tab:summary_stats}, we re-estimate both models with Student-$t$ distributed innovations, which accommodate fat tails through an additional degrees-of-freedom parameter. The Student-$t$ models yield slightly lower AIC values than their Gaussian counterparts, confirming the presence of conditional leptokurtosis. However, the key qualitative finding---that the GJR-GARCH dominates the standard GARCH---is preserved regardless of the distributional assumption. The leverage coefficient $\gamma_1$ remains positive and of similar magnitude (0.28--0.32) across specifications. | |
| 55 | + | |
| 56 | +\subsubsection{EGARCH Alternative} | |
| 57 | + | |
| 58 | +As a further check on the asymmetry finding, we estimate the EGARCH model of \citet{nelson1991conditional}, which parameterizes the leverage effect in the log-variance rather than the level of variance. The EGARCH model yields a negative leverage coefficient (as expected under the EGARCH sign convention), confirming the asymmetric volatility response documented by the GJR-GARCH. The EGARCH AIC is comparable to the GJR-GARCH, indicating that the choice between the two asymmetric specifications does not qualitatively affect the conclusions. | |
| 59 | + | |
| 60 | +\subsection{2024 U.S.\ Presidential Election Case Study}\label{subsec:election} | |
| 61 | + | |
| 62 | +The 2024 U.S.\ Presidential Election market provides an out-of-sample environment in which to assess the generalizability of the patterns observed in the Federal Reserve rate decision market. The election market differs from the Fed market in several important respects: the outcome space is larger (17 candidates versus 5 rate actions), the event type is political rather than macroeconomic, and the resolution mechanism depends on a complex electoral process rather than a single committee decision. | |
| 63 | + | |
| 64 | +Figure~\ref{fig:election} presents the time series of candidate prices in the election market. Donald Trump's contract price exhibited a gradual increase over the sample period, reaching a peak near 0.65 in the weeks before the election and settling at a final price of 0.58. The second-ranked candidate's contract showed the mirror-image decline. Minor candidate contracts traded at low single-digit levels throughout the sample, reflecting the market's concentrated assessment that the election was essentially a two-candidate race. | |
| 65 | + | |
| 66 | +\begin{figure}[htbp] | |
| 67 | + \centering | |
| 68 | + \includegraphics[width=\textwidth]{../figures/fig11_election_case_study.pdf} | |
| 69 | + \caption{2024 U.S.\ Presidential Election market on Polymarket. The figure plots daily closing prices for the major candidate contracts over 307 trading days. Trump's contract (upper line) exhibits a gradual rise to a final price of 0.58. The market demonstrates the classic pattern of an Arrow--Debreu economy converging toward a degenerate distribution as the resolution date approaches.} | |
| 70 | + \label{fig:election} | |
| 71 | +\end{figure} | |
| 72 | + | |
| 73 | +The election market exhibits several microstructure features that parallel those observed in the Fed market. The state price sum for the election market hovers close to unity throughout the sample, with a mean overround comparable to that observed in the Fed market. Liquidity concentrates in the two leading candidates' contracts, with the minor candidates' contracts exhibiting wider spreads and lower depth. The convergence of candidate prices toward 0 or 1 as the election date approaches reflects the progressive resolution of uncertainty---a pattern that is theoretically predicted by any model of informationally efficient pricing in state-contingent claims markets. | |
| 74 | + | |
| 75 | +The election case study is particularly informative for understanding the post-resolution behavior of prediction market prices. After the election was called on November 5, 2024, the winning candidate's contract did not immediately jump to \$1; instead, it remained below \$1 for several days as the settlement process unfolded. This delay highlights an important institutional feature of blockchain-based prediction markets: settlement depends on oracle resolution and on-chain execution, which introduces a lag between the real-world event and the contractual payout. During this settlement period, the contract effectively trades as a claim on the oracle's resolution decision rather than on the election outcome itself, and its price reflects the (small) probability of a disputed or delayed resolution. | |
| 76 | + | |
| 77 | +\subsection{Cross-Market Arbitrage and the Law of One Price} | |
| 78 | + | |
| 79 | +We examine whether the Arrow--Debreu no-arbitrage conditions hold across different contract groups within Polymarket. In theory, if two sets of contracts span overlapping parts of the state space, their prices must be consistent---otherwise, a riskless arbitrage opportunity exists. We test this condition by comparing implied probabilities derived from different Polymarket markets that reference the same underlying event (e.g., the Fed rate decision). | |
| 80 | + | |
| 81 | +The results indicate that cross-market pricing is largely consistent, with deviations attributable to small differences in contract specifications, resolution criteria, and liquidity conditions. The rare instances of larger deviations are quickly corrected, typically within 1--2 trading days, consistent with active arbitrage monitoring by sophisticated participants. This finding reinforces the conclusion that Polymarket's neg-risk framework effectively enforces the structural conditions required for Arrow--Debreu completeness. | |
| 82 | + | |
| 83 | +\subsection{Summary of Robustness Findings} | |
| 84 | + | |
| 85 | +Taken together, the robustness checks confirm the qualitative conclusions of the main analysis. The Arrow--Debreu completeness finding is robust to adjustments for the risk-free rate and to rolling window estimation. The event study results are robust to alternative benchmark models, narrower event windows, and alternative test statistics. The Granger causality findings are robust to alternative lag specifications but exhibit some sensitivity in subsample analysis, consistent with the information content of prediction market prices being most valuable during periods of genuine uncertainty. The GJR-GARCH dominance over the symmetric GARCH is robust to alternative distributional assumptions and to the EGARCH alternative. The election case study confirms that the patterns observed in the Fed market generalize to a politically different setting with a larger cross-section of contracts. | |
added
paper/sections/theoretical.tex
+152 −0
@@ -0,0 +1,152 @@ | ||
| 1 | +% ============================================================================ | |
| 2 | +% Section 3: Theoretical Framework | |
| 3 | +% ============================================================================ | |
| 4 | +\section{Theoretical Framework}\label{sec:theoretical} | |
| 5 | + | |
| 6 | +This section develops the theoretical foundations for interpreting prediction market contracts as Arrow--Debreu securities. We begin by formalizing the structure of the Arrow--Debreu economy, then map this structure onto the Polymarket institutional setting, and finally derive testable hypotheses regarding market completeness, no-arbitrage pricing, and information aggregation. | |
| 7 | + | |
| 8 | +\subsection{The Arrow--Debreu Economy} | |
| 9 | + | |
| 10 | +We consider a single-period economy with dates $t = 0$ (today) and $t = 1$ (the resolution date). Uncertainty at date 1 is represented by a finite state space $\mathcal{S} = \{s_1, s_2, \ldots, s_N\}$ with $N \geq 2$. Each state $s_j$ corresponds to a mutually exclusive and collectively exhaustive outcome. | |
| 11 | + | |
| 12 | +\begin{definition}[Arrow--Debreu Security]\label{def:ad_security} | |
| 13 | +An Arrow--Debreu security for state $s_j$ is a financial contract that pays one unit of the numeraire at date $t = 1$ if state $s_j$ is realized and zero otherwise. Formally, the payoff function of the $j$-th Arrow--Debreu security is | |
| 14 | +\[ | |
| 15 | + a_j(s) = \mathbf{1}\{s = s_j\}, \quad j = 1, \ldots, N, | |
| 16 | +\] | |
| 17 | +where $\mathbf{1}\{\cdot\}$ denotes the indicator function. | |
| 18 | +\end{definition} | |
| 19 | + | |
| 20 | +Let $\psi_j \geq 0$ denote the date-0 price of the Arrow--Debreu security for state $s_j$. The vector $\boldsymbol{\psi} = (\psi_1, \ldots, \psi_N)' \in \mathbb{R}^N_+$ is called the state-price vector. In an economy with a risk-free asset that pays one unit in every state, the risk-free rate $r_f$ is determined by the relation | |
| 21 | +\begin{equation}\label{eq:rf} | |
| 22 | + \frac{1}{1 + r_f} = \sum_{j=1}^{N} \psi_j. | |
| 23 | +\end{equation} | |
| 24 | + | |
| 25 | +This equation establishes a fundamental link: the sum of all state prices equals the price of the risk-free bond, which is the reciprocal of the gross risk-free rate of return. | |
| 26 | + | |
| 27 | +\begin{definition}[Market Completeness]\label{def:completeness} | |
| 28 | +A securities market is said to be complete if, for every contingent claim $c : \mathcal{S} \to \mathbb{R}$, there exists a portfolio of traded securities that replicates $c$ in every state. Equivalently, markets are complete if and only if the payoff matrix of traded securities has rank $N$ (equal to the number of states). | |
| 29 | +\end{definition} | |
| 30 | + | |
| 31 | +When $N$ Arrow--Debreu securities are available for trade with linearly independent payoffs, the payoff matrix is the $N \times N$ identity matrix, which trivially has full rank. In this case, completeness is achieved and any contingent claim $c = (c_1, \ldots, c_N)'$ can be replicated at cost $\sum_{j=1}^N c_j \psi_j$. | |
| 32 | + | |
| 33 | +\subsection{State Prices, Risk-Neutral Probabilities, and No Arbitrage} | |
| 34 | + | |
| 35 | +The Fundamental Theorem of Asset Pricing establishes the equivalence between no arbitrage and the existence of positive state prices. | |
| 36 | + | |
| 37 | +\begin{proposition}[No Arbitrage and State Prices]\label{prop:no_arb} | |
| 38 | +In the finite-state economy described above, the following conditions are equivalent: | |
| 39 | +\begin{enumerate}[label=(\roman*)] | |
| 40 | + \item There are no arbitrage opportunities. | |
| 41 | + \item There exists a strictly positive state-price vector $\boldsymbol{\psi} \in \mathbb{R}^N_{++}$ such that the price of any traded security equals the inner product of its payoff vector with $\boldsymbol{\psi}$. | |
| 42 | + \item There exists a probability measure $\mathbb{Q}$ equivalent to the physical measure $\mathbb{P}$ and a discount factor $\delta > 0$ such that the price of any traded security equals the discounted $\mathbb{Q}$-expectation of its payoff. | |
| 43 | +\end{enumerate} | |
| 44 | +\end{proposition} | |
| 45 | + | |
| 46 | +The measure $\mathbb{Q}$ in condition (iii) is the risk-neutral probability measure, and its elements are related to state prices by | |
| 47 | +\begin{equation}\label{eq:rn_prob} | |
| 48 | + q_j = \frac{\psi_j}{\sum_{k=1}^{N} \psi_k} = (1 + r_f) \psi_j, \quad j = 1, \ldots, N. | |
| 49 | +\end{equation} | |
| 50 | + | |
| 51 | +Under the risk-neutral measure, the price of an Arrow--Debreu security is simply the discounted risk-neutral probability of the associated state: | |
| 52 | +\[ | |
| 53 | + \psi_j = \frac{q_j}{1 + r_f}. | |
| 54 | +\] | |
| 55 | + | |
| 56 | +In a complete market, the risk-neutral measure $\mathbb{Q}$ is unique. In an incomplete market, no-arbitrage is consistent with multiple risk-neutral measures, and the state-price vector is not uniquely determined by observed prices alone. This distinction motivates our focus on markets with a complete set of outcome contracts. | |
| 57 | + | |
| 58 | +\subsection{Mapping Polymarket Contracts to Arrow--Debreu Securities} | |
| 59 | + | |
| 60 | +We now map the theoretical framework onto the institutional structure of Polymarket. Consider a Polymarket neg-risk market with $N$ outcome contracts $\{C_1, C_2, \ldots, C_N\}$. Each contract $C_j$ is a binary claim that pays \$1 if outcome $j$ is realized and \$0 otherwise. The outcomes are mutually exclusive and collectively exhaustive by construction: the Polymarket neg-risk protocol enforces the constraint that exactly one contract in the group resolves to \$1. | |
| 61 | + | |
| 62 | +Let $p_j(t)$ denote the market price of contract $C_j$ at time $t$. Under the Arrow--Debreu interpretation, $p_j(t) = \psi_j(t)$, the state price for outcome $j$ at time $t$. | |
| 63 | + | |
| 64 | +\begin{definition}[State Price Sum]\label{def:sp_sum} | |
| 65 | +The state price sum at time $t$ is defined as | |
| 66 | +\begin{equation}\label{eq:sp_sum} | |
| 67 | + \Sigma(t) = \sum_{j=1}^{N} p_j(t). | |
| 68 | +\end{equation} | |
| 69 | +Under Arrow--Debreu completeness with a risk-free rate $r_f(t)$, the theoretical prediction is $\Sigma(t) = 1/(1 + r_f(t))$. | |
| 70 | +\end{definition} | |
| 71 | + | |
| 72 | +In practice, the overnight risk-free rate over the sample period is sufficiently small that $1/(1 + r_f) \approx 1$. Deviations of $\Sigma(t)$ from unity therefore measure violations of the completeness condition, up to a small correction for the time value of money. | |
| 73 | + | |
| 74 | +Several sources of deviation merit discussion. Transaction costs, including the Polymarket trading fee and blockchain gas costs, create a wedge between theoretical and observed state prices. If buyers must pay a premium above fair value and sellers must accept a discount below fair value, the observed state price sum will exceed unity by an amount proportional to the round-trip transaction cost. Similarly, the vigorish (or overround) familiar from bookmaking---in which the implied probabilities of all outcomes sum to more than one---represents a form of transaction cost that accrues to the market operator or liquidity providers. | |
| 75 | + | |
| 76 | +\subsection{Risk-Neutral Density Estimation} | |
| 77 | + | |
| 78 | +Given a complete set of state prices, the risk-neutral density over the outcome space can be recovered by normalizing the state prices to sum to unity. This yields the risk-neutral probability mass function: | |
| 79 | +\begin{equation}\label{eq:rn_density} | |
| 80 | + q_j(t) = \frac{p_j(t)}{\Sigma(t)}, \quad j = 1, \ldots, N. | |
| 81 | +\end{equation} | |
| 82 | + | |
| 83 | +The time series of risk-neutral densities $\{q_j(t)\}_{t=1}^{T}$ provides a window into the evolution of market expectations about the distribution of outcomes. Changes in the risk-neutral density can be decomposed into changes in physical probabilities and changes in risk premia, though this decomposition requires additional identifying assumptions. | |
| 84 | + | |
| 85 | +For the Federal Reserve rate decision market with $N = 5$ states (50+ basis point decrease, 25 basis point decrease, no change, 25 basis point increase, 50+ basis point increase), the risk-neutral density at each date is a discrete probability distribution over the five possible rate actions. The evolution of this distribution over time reflects the market's assessment of monetary policy expectations as macroeconomic data are released and FOMC communications are made. | |
| 86 | + | |
| 87 | +\subsection{Returns, Abnormal Returns, and Event Studies} | |
| 88 | + | |
| 89 | +We define log returns on prediction market contracts as follows. Let $p_j(t)$ denote the price of contract $j$ at the close of trading day $t$. The continuously compounded return is | |
| 90 | +\begin{equation}\label{eq:return} | |
| 91 | + r_j(t) = \ln\left(\frac{p_j(t)}{p_j(t-1)}\right). | |
| 92 | +\end{equation} | |
| 93 | + | |
| 94 | +For the event study analysis, we follow the standard methodology of \citet{mackinlay1997event}. Abnormal returns are defined relative to a benchmark model estimated over a pre-event window. Let $[\tau_0, \tau_1]$ denote the estimation window and $[\tau_1 + 1, \tau_2]$ denote the event window. We employ a constant mean return model as the benchmark: | |
| 95 | +\begin{equation}\label{eq:ar} | |
| 96 | + AR_j(t) = r_j(t) - \hat{\mu}_j, \quad t \in [\tau_1 + 1, \tau_2], | |
| 97 | +\end{equation} | |
| 98 | +where $\hat{\mu}_j = \frac{1}{\tau_1 - \tau_0} \sum_{t=\tau_0+1}^{\tau_1} r_j(t)$ is the sample mean return over the estimation window. Cumulative abnormal returns over the event window $[t_1, t_2]$ are | |
| 99 | +\begin{equation}\label{eq:car} | |
| 100 | + CAR_j(t_1, t_2) = \sum_{t=t_1}^{t_2} AR_j(t). | |
| 101 | +\end{equation} | |
| 102 | + | |
| 103 | +The cross-sectional test statistic for the null hypothesis that the mean CAR across events is zero follows \citet{patell1976corporate} and \citet{boehmer1991event}: | |
| 104 | +\begin{equation}\label{eq:tstat} | |
| 105 | + t_{CAR} = \frac{\overline{CAR}}{\hat{\sigma}_{CAR} / \sqrt{K}}, | |
| 106 | +\end{equation} | |
| 107 | +where $\overline{CAR}$ is the mean CAR across $K$ events and $\hat{\sigma}_{CAR}$ is the cross-sectional standard deviation. | |
| 108 | + | |
| 109 | +\subsection{Testable Hypotheses} | |
| 110 | + | |
| 111 | +The theoretical framework yields the following testable hypotheses. | |
| 112 | + | |
| 113 | +\begin{hypothesis}[Market Completeness]\label{hyp:completeness} | |
| 114 | +If Polymarket neg-risk contract markets function as Arrow--Debreu economies, then: | |
| 115 | +\begin{enumerate}[label=(\alph*)] | |
| 116 | + \item The state price sum $\Sigma(t) = \sum_{j=1}^{N} p_j(t)$ is statistically indistinguishable from $1/(1+r_f)$ for all $t$. | |
| 117 | + \item All individual state prices $p_j(t)$ are strictly positive for all $j$ and $t$. | |
| 118 | + \item The deviation of $\Sigma(t)$ from unity is small, stationary, and attributable to transaction costs. | |
| 119 | +\end{enumerate} | |
| 120 | +\end{hypothesis} | |
| 121 | + | |
| 122 | +\begin{hypothesis}[No Arbitrage]\label{hyp:no_arb} | |
| 123 | +If no-arbitrage conditions hold in the prediction market: | |
| 124 | +\begin{enumerate}[label=(\alph*)] | |
| 125 | + \item There are no persistent violations of the law of one price: the state price sum does not systematically exceed or fall below unity by more than transaction costs. | |
| 126 | + \item Temporary deviations from the no-arbitrage bounds are quickly corrected through arbitrage activity, implying mean-reversion in $\Sigma(t) - 1$. | |
| 127 | +\end{enumerate} | |
| 128 | +\end{hypothesis} | |
| 129 | + | |
| 130 | +\begin{hypothesis}[Information Aggregation]\label{hyp:info} | |
| 131 | +If prediction market prices efficiently aggregate information: | |
| 132 | +\begin{enumerate}[label=(\alph*)] | |
| 133 | + \item Prediction market returns exhibit no predictable patterns conditional on publicly available information (weak-form efficiency). | |
| 134 | + \item Prediction market prices respond to macroeconomic news releases (FOMC announcements, CPI releases) in a manner consistent with the informational content of the release. | |
| 135 | + \item Prediction market returns Granger-cause returns in related traditional financial markets (SPY, VIX, BTC), reflecting the information content of state-contingent prices. | |
| 136 | +\end{enumerate} | |
| 137 | +\end{hypothesis} | |
| 138 | + | |
| 139 | +\begin{hypothesis}[Volatility Dynamics]\label{hyp:vol} | |
| 140 | +If prediction market returns exhibit volatility dynamics similar to those documented in traditional financial markets: | |
| 141 | +\begin{enumerate}[label=(\alph*)] | |
| 142 | + \item Conditional volatility is time-varying and exhibits clustering, consistent with ARCH/GARCH effects. | |
| 143 | + \item Negative return shocks have a larger impact on future volatility than positive return shocks of equal magnitude (leverage effect), implying that the GJR-GARCH model fits better than the symmetric GARCH. | |
| 144 | + \item Realized volatility exhibits persistence at multiple time horizons, consistent with the heterogeneous market hypothesis underlying the HAR-RV model. | |
| 145 | +\end{enumerate} | |
| 146 | +\end{hypothesis} | |
| 147 | + | |
| 148 | +\subsection{Theoretical Limitations and Caveats} | |
| 149 | + | |
| 150 | +Several theoretical limitations of the Arrow--Debreu framework as applied to prediction markets warrant acknowledgment. First, the single-period nature of the Arrow--Debreu model abstracts from the dynamic trading that occurs in practice. Prediction market contracts are traded continuously over weeks or months before resolution, and the mapping between theory and empirics requires treating each trading day as a snapshot of the equilibrium state-price vector. Second, the finite-state assumption, while natural for markets with discrete outcomes (five possible Fed rate actions), becomes strained for markets with continuous outcome spaces (e.g., the level of GDP growth). Third, the assumption of competitive price-taking may be violated in prediction markets where a small number of large traders can exert significant price impact. Fourth, the theoretical framework assumes that all agents agree on the state space; in practice, ambiguity about the precise definition of contract resolution rules can introduce basis risk between the prediction market contract and the underlying event of interest. | |
| 151 | + | |
| 152 | +Despite these limitations, the Arrow--Debreu framework provides a disciplined lens through which to evaluate the structural properties of prediction markets. The hypotheses formulated above are designed to be robust to these concerns: we test for the conditions that must hold if the market functions as an Arrow--Debreu economy, while acknowledging that departures from these conditions may reflect institutional frictions rather than fundamental inefficiency. | |
added
paper/sections/titlepage.tex
+77 −0
@@ -0,0 +1,77 @@ | ||
| 1 | +% ============================================================================ | |
| 2 | +% Title Page | |
| 3 | +% ============================================================================ | |
| 4 | +\thispagestyle{empty} | |
| 5 | + | |
| 6 | +\begin{center} | |
| 7 | + | |
| 8 | +% --- Logo --- | |
| 9 | +\includegraphics[width=3.5cm]{uq_logo.jpg} | |
| 10 | + | |
| 11 | +\vspace{0.6cm} | |
| 12 | + | |
| 13 | +{\footnotesize\textsc{Université du Québec en Outaouais}}\\[0.15cm] | |
| 14 | +{\footnotesize\textsc{Département des sciences administratives}} | |
| 15 | + | |
| 16 | +\vspace{0.8cm} | |
| 17 | + | |
| 18 | +{\footnotesize\textsc{Working Paper No.~\WPnumber}} | |
| 19 | + | |
| 20 | +\vspace{1.2cm} | |
| 21 | + | |
| 22 | +% --- Title --- | |
| 23 | +{\LARGE\bfseries \WPtitle\par} | |
| 24 | + | |
| 25 | +\ifx\WPsubtitle\empty\else | |
| 26 | + \vspace{0.3cm} | |
| 27 | + {\large\itshape \WPsubtitle\par} | |
| 28 | +\fi | |
| 29 | + | |
| 30 | +\vspace{1.2cm} | |
| 31 | + | |
| 32 | +% --- Author --- | |
| 33 | +{\large \WPauthor}\\[0.3cm] | |
| 34 | +{\normalsize \WPaffiliation}\\[0.15cm] | |
| 35 | +{\normalsize \href{mailto:\WPemail}{\WPemail}}\\[0.15cm] | |
| 36 | +{\small \WPaddress} | |
| 37 | + | |
| 38 | +\vspace{0.8cm} | |
| 39 | + | |
| 40 | +% --- Date & Version --- | |
| 41 | +{\normalsize \WPdate}\\[0.1cm] | |
| 42 | +{\small Version~\WPversion} | |
| 43 | + | |
| 44 | +\end{center} | |
| 45 | + | |
| 46 | +\vfill | |
| 47 | + | |
| 48 | +\newpage | |
| 49 | + | |
| 50 | +% ============================================================================ | |
| 51 | +% Abstract Page | |
| 52 | +% ============================================================================ | |
| 53 | +\thispagestyle{empty} | |
| 54 | + | |
| 55 | +\vspace*{1cm} | |
| 56 | + | |
| 57 | +\noindent\rule{\textwidth}{0.4pt} | |
| 58 | +\vspace{0.3cm} | |
| 59 | + | |
| 60 | +\noindent\textbf{Abstract} | |
| 61 | + | |
| 62 | +\vspace{0.15cm} | |
| 63 | + | |
| 64 | +\noindent\WPabstract | |
| 65 | + | |
| 66 | +\vspace{0.4cm} | |
| 67 | + | |
| 68 | +\noindent\textbf{Keywords:} \WPkeywords | |
| 69 | + | |
| 70 | +\vspace{0.15cm} | |
| 71 | + | |
| 72 | +\noindent\textbf{JEL Classification:} \WPjel | |
| 73 | + | |
| 74 | +\vspace{0.3cm} | |
| 75 | +\noindent\rule{\textwidth}{0.4pt} | |
| 76 | + | |
| 77 | +\newpage | |
added
paper/uq_logo.jpg
+0 −0
Binary file not shown.
added
pyproject.toml
+30 −0
@@ -0,0 +1,30 @@ | ||
| 1 | +[project] | |
| 2 | +name = "polymarket-arrow-debreu" | |
| 3 | +version = "1.0.0" | |
| 4 | +description = "Blockchain Prediction Markets and Arrow-Debreu State Prices" | |
| 5 | +requires-python = ">=3.12" | |
| 6 | +dependencies = [ | |
| 7 | + "pandas>=2.0", | |
| 8 | + "polars>=1.0", | |
| 9 | + "numpy>=2.0", | |
| 10 | + "statsmodels>=0.14", | |
| 11 | + "linearmodels>=6.0", | |
| 12 | + "arch>=7.0", | |
| 13 | + "scikit-learn>=1.5", | |
| 14 | + "networkx>=3.0", | |
| 15 | + "matplotlib>=3.9", | |
| 16 | + "plotly>=5.0", | |
| 17 | + "seaborn>=0.13", | |
| 18 | + "scipy>=1.14", | |
| 19 | + "httpx>=0.27", | |
| 20 | + "yfinance>=0.2", | |
| 21 | + "fredapi>=0.5", | |
| 22 | + "tenacity>=9.0", | |
| 23 | + "tqdm>=4.66", | |
| 24 | + "pyarrow>=17.0", | |
| 25 | + "python-dotenv>=1.0", | |
| 26 | +] | |
| 27 | + | |
| 28 | +[build-system] | |
| 29 | +requires = ["hatchling"] | |
| 30 | +build-backend = "hatchling.build" | |
added
results/tables/table1_descriptive_stats.tex
+38 −0
@@ -0,0 +1,38 @@ | ||
| 1 | +\begin{table}[htbp] | |
| 2 | +\centering | |
| 3 | +\begin{threeparttable} | |
| 4 | +\caption{Descriptive Statistics of Polymarket Contracts} | |
| 5 | +\label{tab:descriptive_stats} | |
| 6 | +\begin{tabular}{l S S c S S} | |
| 7 | +\toprule | |
| 8 | +Market & {Volume (\$)} & {Liquidity (\$)} & Obs & {Mean Price} & {Std Dev} \\ | |
| 9 | +\midrule | |
| 10 | + Will Marco Rubio win the 2028 US Preside & 9,962,958 & 160,002 & 0 & & \\ | |
| 11 | + Will Anže Logar be the next Prime Minist & 995,465 & 11,011 & 0 & & \\ | |
| 12 | + Will Stephen Cloobeck win the California & 995,372 & 184,072 & 0 & & \\ | |
| 13 | + Will the Republicans win the 2028 US Pre & 994,905 & 331,418 & 0 & & \\ | |
| 14 | + Will Ethereum dip to \$1,500 by December & 994,496 & 43,390 & 0 & & \\ | |
| 15 | + Will Ahmed al-Sharaa win the Nobel Peace & 993,894 & 42,726 & 0 & & \\ | |
| 16 | + US strike on Colombia by December 31? & 991,582 & 6,051 & 0 & & \\ | |
| 17 | + Over \$3M committed to the Printr public & 990,487 & 25,664 & 0 & & \\ | |
| 18 | + Will Lance Stroll be the 2026 F1 Drivers & 9,845,762 & 836,174 & 0 & & \\ | |
| 19 | + Will Michael Younger win the California & 977,490 & 193,379 & 0 & & \\ | |
| 20 | + Will Anthropic have the best AI model at & 975,748 & 28,076 & 0 & & \\ | |
| 21 | + Will the Los Angeles Angels win the 2026 & 973,951 & 95,205 & 0 & & \\ | |
| 22 | + Will PSG win the 2025–26 Champions Leagu & 9,718,043 & 670,357 & 0 & & \\ | |
| 23 | + Will Kamala Harris win the California Go & 970,214 & 82,083 & 0 & & \\ | |
| 24 | + Will Lovable be acquired before 2027? & 969,501 & 3,864 & 0 & & \\ | |
| 25 | + Will Gretchen Whitmer win the 2028 Democ & 9,646,053 & 576,258 & 0 & & \\ | |
| 26 | + Will MetaMask launch a token by Septembe & 964,498 & 4,197 & 0 & & \\ | |
| 27 | + New "Stranger Things" episode released b & 963,189 & 36,759 & 0 & & \\ | |
| 28 | + Will Dawn Buckingham win the 2026 Texas & 961,827 & 26,871 & 0 & & \\ | |
| 29 | + Will no qualifying diplomatic US-Iran me & 958,458 & 51,713 & 0 & & \\ | |
| 30 | +\bottomrule | |
| 31 | +\end{tabular} | |
| 32 | +\begin{tablenotes} | |
| 33 | +\small | |
| 34 | +\item \textit{Notes:} This table reports summary statistics for the Polymarket contracts analyzed. | |
| 35 | +Volume and liquidity are reported in US dollars. Mean price represents the average implied probability. | |
| 36 | +\end{tablenotes} | |
| 37 | +\end{threeparttable} | |
| 38 | +\end{table} | |
| \ No newline at end of file | ||
added
results/tables/table2_summary_stats.tex
+23 −0
@@ -0,0 +1,23 @@ | ||
| 1 | +\begin{table}[htbp] | |
| 2 | +\centering | |
| 3 | +\begin{threeparttable} | |
| 4 | +\caption{Summary Statistics of Prediction Market Prices} | |
| 5 | +\label{tab:summary_stats} | |
| 6 | +\begin{tabular}{l S S S S S S S} | |
| 7 | +\toprule | |
| 8 | +Market & {Mean} & {Std} & {Min} & {Max} & {Skew} & {Kurt} & {JB} \\ | |
| 9 | +\midrule | |
| 10 | + 50+ bps decrease & 0.058 & 0.042 & 0.004 & 0.150 & -0.03 & -1.45 & 12.7 \\ | |
| 11 | + 25 bps decrease & 0.304 & 0.194 & 0.006 & 0.570 & -0.47 & -1.46 & 18.0 \\ | |
| 12 | + No change & 0.608 & 0.235 & 0.300 & 0.981 & 0.55 & -1.41 & 19.1 \\ | |
| 13 | + 25 bps increase & 0.034 & 0.026 & 0.005 & 0.250 & 3.98 & 30.46 & 5628.7 \\ | |
| 14 | + 50+ bps increase & 0.016 & 0.016 & 0.002 & 0.095 & 1.66 & 3.55 & 135.8 \\ | |
| 15 | +\bottomrule | |
| 16 | +\end{tabular} | |
| 17 | +\begin{tablenotes} | |
| 18 | +\small | |
| 19 | +\item \textit{Notes:} JB is the Jarque--Bera test statistic for normality. | |
| 20 | +$^{***}$, $^{**}$, and $^{*}$ denote significance at the 1\%, 5\%, and 10\% levels. | |
| 21 | +\end{tablenotes} | |
| 22 | +\end{threeparttable} | |
| 23 | +\end{table} | |
| \ No newline at end of file | ||
added
results/tables/table3_completeness.tex
+27 −0
@@ -0,0 +1,27 @@ | ||
| 1 | +\begin{table}[htbp] | |
| 2 | +\centering | |
| 3 | +\begin{threeparttable} | |
| 4 | +\caption{Arrow--Debreu Market Completeness Tests} | |
| 5 | +\label{tab:completeness} | |
| 6 | +\begin{tabular}{l S} | |
| 7 | +\toprule | |
| 8 | +Statistic & {Value} \\ | |
| 9 | +\midrule | |
| 10 | + Mean state price sum & 1.0205 \\ | |
| 11 | + Std of state price sum & 0.0527 \\ | |
| 12 | + Mean deviation from unity & 0.0248 \\ | |
| 13 | + Max absolute deviation & 0.5700 \\ | |
| 14 | + All state prices positive & {Yes} \\ | |
| 15 | + Implied discount factor & 1.0205 \\ | |
| 16 | + Number of observations & 146 \\ | |
| 17 | +\bottomrule | |
| 18 | +\end{tabular} | |
| 19 | +\begin{tablenotes} | |
| 20 | +\small | |
| 21 | +\item \textit{Notes:} This table reports results from testing Arrow--Debreu market completeness | |
| 22 | +conditions using the Fed rate decision markets on Polymarket. Under complete markets, | |
| 23 | +the sum of state prices equals $1/(1+r_f)$. Mean deviation measures the average | |
| 24 | +departure from this theoretical prediction. | |
| 25 | +\end{tablenotes} | |
| 26 | +\end{threeparttable} | |
| 27 | +\end{table} | |
| \ No newline at end of file | ||
added
results/tables/table4_event_study.tex
+32 −0
@@ -0,0 +1,32 @@ | ||
| 1 | +\begin{table}[htbp] | |
| 2 | +\centering | |
| 3 | +\begin{threeparttable} | |
| 4 | +\caption{Event Study: Cumulative Abnormal Returns Around Macroeconomic Events} | |
| 5 | +\label{tab:event_study} | |
| 6 | +\begin{tabular}{l l c c c c} | |
| 7 | +\toprule | |
| 8 | +Event Type & Window & {CAR} & {$t$-stat} & {$p$-value} & $N$ \\ | |
| 9 | +\midrule | |
| 10 | + FOMC: 25 bps decrease & [-3,3] & $-0.615$ & -5.89 & 0.107 & 2 \\ | |
| 11 | + FOMC: 25 bps increase & [-3,3] & $-0.054$ & -0.21 & 0.866 & 2 \\ | |
| 12 | + FOMC: 50+ bps decrease & [-3,3] & $-0.041$ & -0.08 & 0.951 & 2 \\ | |
| 13 | + FOMC: 50+ bps increase & [-3,3] & $-0.599$ & -0.88 & 0.542 & 2 \\ | |
| 14 | + FOMC: No change & [-3,3] & $0.115$ & 0.52 & 0.694 & 2 \\ | |
| 15 | + CPI: 25 bps decrease & [-3,3] & $-0.116$ & -1.49 & 0.275 & 3 \\ | |
| 16 | + CPI: 25 bps increase & [-3,3] & $-0.999^{*}$ & -3.01 & 0.095 & 3 \\ | |
| 17 | + CPI: 50+ bps decrease & [-3,3] & $-0.228$ & -2.83 & 0.105 & 3 \\ | |
| 18 | + CPI: 50+ bps increase & [-3,3] & $-0.545$ & -1.56 & 0.260 & 3 \\ | |
| 19 | + CPI: No change & [-3,3] & $0.023$ & 0.28 & 0.807 & 3 \\ | |
| 20 | + Election: Donald Trump & [-5,5] & $-0.057$ & -0.88 & 0.470 & 3 \\ | |
| 21 | +\bottomrule | |
| 22 | +\end{tabular} | |
| 23 | +\begin{tablenotes} | |
| 24 | +\small | |
| 25 | +\item \textit{Notes:} This table reports cumulative abnormal returns (CARs) for prediction market | |
| 26 | +prices around macroeconomic events. Abnormal returns are computed relative to a constant mean | |
| 27 | +model estimated over a 60-day estimation window. $t$-statistics are computed using the | |
| 28 | +cross-sectional standard deviation of CARs. $^{***}$, $^{**}$, $^{*}$ denote | |
| 29 | +significance at 1\%, 5\%, 10\%. | |
| 30 | +\end{tablenotes} | |
| 31 | +\end{threeparttable} | |
| 32 | +\end{table} | |
| \ No newline at end of file | ||
added
results/tables/table5_granger.tex
+32 −0
@@ -0,0 +1,32 @@ | ||
| 1 | +\begin{table}[htbp] | |
| 2 | +\centering | |
| 3 | +\begin{threeparttable} | |
| 4 | +\caption{Granger Causality Tests} | |
| 5 | +\label{tab:granger} | |
| 6 | +\begin{tabular}{l c c} | |
| 7 | +\toprule | |
| 8 | +Direction & {$F$-stat} & {$p$-value} \\ | |
| 9 | +\midrule | |
| 10 | + SPY $\to$ Fed\_NoChange & $1.28$ & 0.285 \\ | |
| 11 | + BTC $\to$ Fed\_NoChange & $1.95$ & 0.110 \\ | |
| 12 | + VIX $\to$ Fed\_NoChange & $2.73^{**}$ & 0.049 \\ | |
| 13 | + Fed\_NoChange $\to$ SPY & $2.42^{*}$ & 0.072 \\ | |
| 14 | + BTC $\to$ SPY & $2.42^{*}$ & 0.055 \\ | |
| 15 | + VIX $\to$ SPY & $1.93$ & 0.112 \\ | |
| 16 | + Fed\_NoChange $\to$ BTC & $1.51$ & 0.196 \\ | |
| 17 | + SPY $\to$ BTC & $0.30$ & 0.585 \\ | |
| 18 | + VIX $\to$ BTC & $2.12$ & 0.149 \\ | |
| 19 | + Fed\_NoChange $\to$ VIX & $4.96^{***}$ & 0.001 \\ | |
| 20 | + SPY $\to$ VIX & $1.29$ & 0.280 \\ | |
| 21 | + BTC $\to$ VIX & $3.57^{***}$ & 0.010 \\ | |
| 22 | +\bottomrule | |
| 23 | +\end{tabular} | |
| 24 | +\begin{tablenotes} | |
| 25 | +\small | |
| 26 | +\item \textit{Notes:} This table reports $F$-statistics and $p$-values for pairwise Granger | |
| 27 | +causality tests. The null hypothesis is that the causing variable does not Granger-cause | |
| 28 | +the caused variable. Lag length selected by AIC. $^{***}$, $^{**}$, $^{*}$ denote | |
| 29 | +significance at 1\%, 5\%, 10\%. | |
| 30 | +\end{tablenotes} | |
| 31 | +\end{threeparttable} | |
| 32 | +\end{table} | |
| \ No newline at end of file | ||
added
results/tables/table6_var.tex
+20 −0
@@ -0,0 +1,20 @@ | ||
| 1 | +\begin{table}[htbp] | |
| 2 | +\centering | |
| 3 | +\begin{threeparttable} | |
| 4 | +\caption{Vector Autoregression Estimates} | |
| 5 | +\label{tab:var} | |
| 6 | +\begin{tabular}{l S S S S } | |
| 7 | +\toprule | |
| 8 | +Variable & {Fed\_NoChange} & {SPY} & {BTC} & {VIX} \\ | |
| 9 | +\midrule | |
| 10 | + const & 0.009 & 0.000 & -0.001 & 0.003 \\ | |
| 11 | +\bottomrule | |
| 12 | +\end{tabular} | |
| 13 | +\begin{tablenotes} | |
| 14 | +\small | |
| 15 | +\item \textit{Notes:} VAR model estimates. Optimal lag length selected by AIC. | |
| 16 | +Standard errors in parentheses. $^{***}$, $^{**}$, $^{*}$ denote significance | |
| 17 | +at 1\%, 5\%, 10\%. | |
| 18 | +\end{tablenotes} | |
| 19 | +\end{threeparttable} | |
| 20 | +\end{table} | |
| \ No newline at end of file | ||
added
results/tables/table7_garch.tex
+27 −0
@@ -0,0 +1,27 @@ | ||
| 1 | +\begin{table}[htbp] | |
| 2 | +\centering | |
| 3 | +\begin{threeparttable} | |
| 4 | +\caption{GARCH Model Estimates} | |
| 5 | +\label{tab:garch} | |
| 6 | +\begin{tabular}{l S l} | |
| 7 | +\toprule | |
| 8 | +Parameter & {Estimate} & (Std Err) \\ | |
| 9 | +\midrule | |
| 10 | + GARCH: mu & 0.1498 & (0.0921) \\ | |
| 11 | + GARCH: omega & 0.0000 & (0.0385) \\ | |
| 12 | + GARCH: alpha[1] & 0.1951 & (0.0383) \\ | |
| 13 | + GARCH: beta[1] & 0.8049 & (0.0252) \\ | |
| 14 | + GJR-GARCH: mu & 0.0960 & (0.3060) \\ | |
| 15 | + GJR-GARCH: omega & 0.0000 & (0.2216) \\ | |
| 16 | + GJR-GARCH: alpha[1] & 0.0000 & (0.5240) \\ | |
| 17 | + GJR-GARCH: gamma[1] & 0.3037 & (0.3723) \\ | |
| 18 | + GJR-GARCH: beta[1] & 0.8482 & (0.3778) \\ | |
| 19 | +\bottomrule | |
| 20 | +\end{tabular} | |
| 21 | +\begin{tablenotes} | |
| 22 | +\small | |
| 23 | +\item \textit{Notes:} GARCH(1,1) model estimates for prediction market return volatility. | |
| 24 | +$^{***}$, $^{**}$, $^{*}$ denote significance at 1\%, 5\%, 10\%. | |
| 25 | +\end{tablenotes} | |
| 26 | +\end{threeparttable} | |
| 27 | +\end{table} | |
| \ No newline at end of file | ||
added
results/tables/table8_har_rv.tex
+26 −0
@@ -0,0 +1,26 @@ | ||
| 1 | +\begin{table}[htbp] | |
| 2 | +\centering | |
| 3 | +\begin{threeparttable} | |
| 4 | +\caption{HAR-RV Model Estimates} | |
| 5 | +\label{tab:har_rv} | |
| 6 | +\begin{tabular}{l S l S} | |
| 7 | +\toprule | |
| 8 | +Variable & {Coefficient} & (Std Err) & {$t$-stat} \\ | |
| 9 | +\midrule | |
| 10 | + const & 0.0121 & (0.0081) & 1.50 \\ | |
| 11 | + rv\_d & 0.0082 & (0.1130) & 0.07 \\ | |
| 12 | + rv\_w & 0.1432 & (0.1079) & 1.33 \\ | |
| 13 | + rv\_m & 0.0299 & (0.0373) & 0.80 \\ | |
| 14 | +\midrule | |
| 15 | + $R^2$ & \multicolumn{3}{c}{0.106} \\ | |
| 16 | + Adj.~$R^2$ & \multicolumn{3}{c}{0.083} \\ | |
| 17 | +\bottomrule | |
| 18 | +\end{tabular} | |
| 19 | +\begin{tablenotes} | |
| 20 | +\small | |
| 21 | +\item \textit{Notes:} HAR-RV model following \citet{corsi2009simple}. | |
| 22 | +RV$_{t+1}^{(d)} = c + \beta_d RV_t^{(d)} + \beta_w RV_t^{(w)} + \beta_m RV_t^{(m)} + \varepsilon_{t+1}$. | |
| 23 | +Newey--West standard errors with 5 lags. | |
| 24 | +\end{tablenotes} | |
| 25 | +\end{threeparttable} | |
| 26 | +\end{table} | |
| \ No newline at end of file | ||
added
scripts/run_analysis.py
+335 −0
@@ -0,0 +1,335 @@ | ||
| 1 | +"""Master analysis pipeline for the Polymarket Arrow-Debreu paper. | |
| 2 | + | |
| 3 | +Usage: python scripts/run_analysis.py [--skip-data] [--skip-models] [--skip-paper] | |
| 4 | +""" | |
| 5 | + | |
| 6 | +from __future__ import annotations | |
| 7 | + | |
| 8 | +import argparse | |
| 9 | +import json | |
| 10 | +import sys | |
| 11 | +from pathlib import Path | |
| 12 | + | |
| 13 | +sys.path.insert(0, str(Path(__file__).resolve().parents[1])) # make `src` importable | |
| 14 | + | |
| 15 | +import numpy as np | |
| 16 | +import pandas as pd | |
| 17 | + | |
| 18 | +from src.config import ( | |
| 19 | + DATA_PROCESSED, | |
| 20 | + DATA_RAW_POLY, | |
| 21 | + ELECTION_KEY_DATES, | |
| 22 | + FOMC_DATES, | |
| 23 | + CPI_RELEASE_DATES, | |
| 24 | +) | |
| 25 | +from src.utils import ensure_dirs, setup_logging | |
| 26 | + | |
| 27 | +logger = setup_logging("analysis") | |
| 28 | + | |
| 29 | + | |
| 30 | +# ── Phase 1: Data acquisition ──────────────────────────────────────────────── | |
| 31 | + | |
| 32 | +def run_data_acquisition() -> None: | |
| 33 | + import asyncio | |
| 34 | + from src.api.runner import run_all | |
| 35 | + run_all() | |
| 36 | + | |
| 37 | + | |
| 38 | +# ── Phase 2: Cleaning ──────────────────────────────────────────────────────── | |
| 39 | + | |
| 40 | +def run_cleaning() -> None: | |
| 41 | + from src.cleaning.polymarket_cleaner import clean_all as clean_poly | |
| 42 | + from src.cleaning.macro_cleaner import clean_all as clean_macro | |
| 43 | + from src.cleaning.yahoo_cleaner import clean_all as clean_yahoo | |
| 44 | + clean_poly() | |
| 45 | + clean_macro() | |
| 46 | + clean_yahoo() | |
| 47 | + | |
| 48 | + | |
| 49 | +# ── Phase 3: Models + Figures + Tables ──────────────────────────────────────── | |
| 50 | + | |
| 51 | +def run_models_and_outputs() -> None: | |
| 52 | + from src.models.arrow_debreu import ( | |
| 53 | + test_market_completeness, | |
| 54 | + extract_risk_neutral_density, | |
| 55 | + compute_risk_neutral_moments, | |
| 56 | + detect_arbitrage_violations, | |
| 57 | + state_price_dynamics, | |
| 58 | + ) | |
| 59 | + from src.models.event_study import run_event_study | |
| 60 | + from src.models.price_discovery import granger_causality_matrix, estimate_var | |
| 61 | + from src.models.volatility import ( | |
| 62 | + compute_realized_volatility_measures, | |
| 63 | + estimate_har_rv, | |
| 64 | + estimate_garch, | |
| 65 | + ) | |
| 66 | + from src.models.liquidity import analyze_volume_patterns, market_quality_from_orderbook | |
| 67 | + from src.features.engineering import compute_roll_spread | |
| 68 | + from src.plots.figures import ( | |
| 69 | + fig_state_prices, | |
| 70 | + fig_completeness_test, | |
| 71 | + fig_risk_neutral_density, | |
| 72 | + fig_event_study_cars, | |
| 73 | + fig_granger_heatmap, | |
| 74 | + fig_impulse_response, | |
| 75 | + fig_volatility, | |
| 76 | + fig_liquidity, | |
| 77 | + fig_election_case_study, | |
| 78 | + fig_arbitrage_violations, | |
| 79 | + ) | |
| 80 | + from src.plots.tables import ( | |
| 81 | + table_descriptive_stats, | |
| 82 | + table_summary_stats, | |
| 83 | + table_completeness_test, | |
| 84 | + table_event_study, | |
| 85 | + table_granger_causality, | |
| 86 | + table_var_results, | |
| 87 | + table_garch, | |
| 88 | + table_har_rv, | |
| 89 | + table_liquidity, | |
| 90 | + ) | |
| 91 | + from src.plots.style import save_figure | |
| 92 | + | |
| 93 | + # ── Load cleaned data ───────────────────────────────────────────────── | |
| 94 | + logger.info("Loading cleaned data...") | |
| 95 | + fed_state = pd.read_parquet(DATA_PROCESSED / "fed_june2026_state_prices.parquet") | |
| 96 | + fed_panel = pd.read_parquet(DATA_PROCESSED / "fed_june2026_panel.parquet") | |
| 97 | + election = pd.read_parquet(DATA_PROCESSED / "election_2024_panel.parquet") | |
| 98 | + financial = pd.read_parquet(DATA_PROCESSED / "financial_data.parquet") | |
| 99 | + macro_events = pd.read_parquet(DATA_PROCESSED / "macro_events.parquet") | |
| 100 | + | |
| 101 | + state_cols = [c for c in fed_state.columns if c not in ("state_price_sum", "completeness_deviation")] | |
| 102 | + state_prices = fed_state[state_cols] | |
| 103 | + | |
| 104 | + # ── 1. Arrow-Debreu completeness tests ──────────────────────────────── | |
| 105 | + logger.info("=== Arrow-Debreu Completeness Tests ===") | |
| 106 | + completeness = test_market_completeness(state_prices) | |
| 107 | + logger.info(" Mean sum: %.4f, Std: %.4f, All positive: %s", | |
| 108 | + completeness.mean_sum, completeness.std_sum, completeness.all_positive) | |
| 109 | + | |
| 110 | + state_values = [-50.0, -25.0, 0.0, 25.0, 50.0] | |
| 111 | + rn_density = extract_risk_neutral_density(state_prices, state_values) | |
| 112 | + rn_moments = compute_risk_neutral_moments(rn_density, state_values) | |
| 113 | + arb_violations = detect_arbitrage_violations(state_prices, market_type="state_space") | |
| 114 | + dynamics = state_price_dynamics(state_prices) | |
| 115 | + | |
| 116 | + # Figures | |
| 117 | + fig = fig_state_prices(state_prices) | |
| 118 | + save_figure(fig, "fig1_state_prices") | |
| 119 | + logger.info(" Saved fig1_state_prices") | |
| 120 | + | |
| 121 | + fig = fig_completeness_test(completeness.daily_sums) | |
| 122 | + save_figure(fig, "fig2_completeness_test") | |
| 123 | + logger.info(" Saved fig2_completeness_test") | |
| 124 | + | |
| 125 | + fig = fig_risk_neutral_density(rn_density, state_values) | |
| 126 | + save_figure(fig, "fig3_risk_neutral_density") | |
| 127 | + logger.info(" Saved fig3_risk_neutral_density") | |
| 128 | + | |
| 129 | + fig = fig_arbitrage_violations(arb_violations) | |
| 130 | + save_figure(fig, "fig12_arbitrage_violations") | |
| 131 | + logger.info(" Saved fig12_arbitrage_violations") | |
| 132 | + | |
| 133 | + # Tables | |
| 134 | + table_completeness_test(completeness) | |
| 135 | + logger.info(" Saved table3_completeness") | |
| 136 | + table_summary_stats(state_prices) | |
| 137 | + logger.info(" Saved table2_summary_stats") | |
| 138 | + | |
| 139 | + # ── 2. Event study ──────────────────────────────────────────────────── | |
| 140 | + logger.info("=== Event Study ===") | |
| 141 | + fomc_dates_in_range = [d for d in FOMC_DATES if fed_state.index.min().date() <= d <= fed_state.index.max().date()] | |
| 142 | + cpi_dates_in_range = [d for d in CPI_RELEASE_DATES if fed_state.index.min().date() <= d <= fed_state.index.max().date()] | |
| 143 | + | |
| 144 | + event_results = {} | |
| 145 | + if fomc_dates_in_range: | |
| 146 | + es_fomc = run_event_study(state_prices, fomc_dates_in_range, "FOMC", window=(-3, 3)) | |
| 147 | + event_results["FOMC"] = es_fomc | |
| 148 | + logger.info(" FOMC: %d events", es_fomc.n_events) | |
| 149 | + | |
| 150 | + if cpi_dates_in_range: | |
| 151 | + es_cpi = run_event_study(state_prices, cpi_dates_in_range, "CPI", window=(-3, 3)) | |
| 152 | + event_results["CPI"] = es_cpi | |
| 153 | + logger.info(" CPI: %d events", es_cpi.n_events) | |
| 154 | + | |
| 155 | + # Election event study with broader window | |
| 156 | + election_dates = list(ELECTION_KEY_DATES.values()) | |
| 157 | + election_dates_in_range = [d for d in election_dates if election.index.min().date() <= d <= election.index.max().date()] | |
| 158 | + if election_dates_in_range and "Donald Trump" in election.columns: | |
| 159 | + es_election = run_event_study( | |
| 160 | + election[["Donald Trump"]], | |
| 161 | + election_dates_in_range, | |
| 162 | + "Election", | |
| 163 | + window=(-5, 5), | |
| 164 | + ) | |
| 165 | + event_results["Election"] = es_election | |
| 166 | + logger.info(" Election: %d events", es_election.n_events) | |
| 167 | + | |
| 168 | + if event_results: | |
| 169 | + cars_for_plot = {} | |
| 170 | + for etype, result in event_results.items(): | |
| 171 | + if not result.test_stats.empty: | |
| 172 | + cars_for_plot[etype] = result.test_stats | |
| 173 | + if cars_for_plot: | |
| 174 | + fig = fig_event_study_cars(cars_for_plot, window=(-3, 3)) | |
| 175 | + save_figure(fig, "fig5_event_study_cars") | |
| 176 | + logger.info(" Saved fig5_event_study_cars") | |
| 177 | + | |
| 178 | + table_event_study(event_results) | |
| 179 | + logger.info(" Saved table4_event_study") | |
| 180 | + | |
| 181 | + # ── 3. Price discovery ──────────────────────────────────────────────── | |
| 182 | + logger.info("=== Price Discovery ===") | |
| 183 | + no_change = state_prices.get("No change") | |
| 184 | + if no_change is not None and "SPY_log_return" in financial.columns: | |
| 185 | + aligned = pd.DataFrame({ | |
| 186 | + "Fed_NoChange": no_change.pct_change(), | |
| 187 | + "SPY": financial["SPY_log_return"], | |
| 188 | + "BTC": financial.get("BTC_USD_log_return", pd.Series(dtype=float)), | |
| 189 | + "VIX": financial.get("VIX_log_return", pd.Series(dtype=float)), | |
| 190 | + }).dropna() | |
| 191 | + | |
| 192 | + if len(aligned) > 20: | |
| 193 | + f_stats, p_values = granger_causality_matrix(aligned, max_lag=5) | |
| 194 | + fig = fig_granger_heatmap(p_values) | |
| 195 | + save_figure(fig, "fig7_granger_heatmap") | |
| 196 | + logger.info(" Saved fig7_granger_heatmap") | |
| 197 | + table_granger_causality(f_stats, p_values) | |
| 198 | + logger.info(" Saved table5_granger") | |
| 199 | + | |
| 200 | + var_result = estimate_var(aligned, max_lags=5) | |
| 201 | + if var_result["model"] is not None: | |
| 202 | + table_var_results(var_result) | |
| 203 | + logger.info(" Saved table6_var") | |
| 204 | + if var_result["irf"]: | |
| 205 | + fig = fig_impulse_response(var_result["irf"]) | |
| 206 | + save_figure(fig, "fig8_impulse_response") | |
| 207 | + logger.info(" Saved fig8_impulse_response") | |
| 208 | + else: | |
| 209 | + logger.warning(" Not enough aligned data for price discovery (%d rows)", len(aligned)) | |
| 210 | + else: | |
| 211 | + logger.warning(" Missing data for price discovery analysis") | |
| 212 | + | |
| 213 | + # ── 4. Volatility ───────────────────────────────────────────────────── | |
| 214 | + logger.info("=== Volatility Analysis ===") | |
| 215 | + if no_change is not None: | |
| 216 | + returns = no_change.pct_change().dropna() | |
| 217 | + if len(returns) > 30: | |
| 218 | + rv_measures = compute_realized_volatility_measures(returns) | |
| 219 | + | |
| 220 | + # HAR-RV | |
| 221 | + try: | |
| 222 | + har_result = estimate_har_rv(rv_measures) | |
| 223 | + table_har_rv(har_result) | |
| 224 | + logger.info(" HAR-RV R²: %.4f", har_result.r_squared) | |
| 225 | + except Exception as e: | |
| 226 | + logger.warning(" HAR-RV failed: %s", e) | |
| 227 | + har_result = None | |
| 228 | + | |
| 229 | + # GARCH | |
| 230 | + garch_results = {} | |
| 231 | + for mtype in ["GARCH", "GJR-GARCH"]: | |
| 232 | + try: | |
| 233 | + garch_results[mtype] = estimate_garch(returns, model_type=mtype) | |
| 234 | + logger.info(" %s AIC: %.2f", mtype, garch_results[mtype].aic) | |
| 235 | + except Exception as e: | |
| 236 | + logger.warning(" %s failed: %s", mtype, e) | |
| 237 | + | |
| 238 | + if garch_results: | |
| 239 | + table_garch(garch_results) | |
| 240 | + logger.info(" Saved table7_garch") | |
| 241 | + | |
| 242 | + first_garch = list(garch_results.values())[0] | |
| 243 | + fig = fig_volatility( | |
| 244 | + rv_measures["rv_5"], | |
| 245 | + cond_vol=first_garch.conditional_volatility, | |
| 246 | + ) | |
| 247 | + save_figure(fig, "fig9_volatility") | |
| 248 | + logger.info(" Saved fig9_volatility") | |
| 249 | + | |
| 250 | + # ── 5. Liquidity ────────────────────────────────────────────────────── | |
| 251 | + logger.info("=== Liquidity Analysis ===") | |
| 252 | + # Use volume from discovered markets metadata | |
| 253 | + discovered_path = DATA_RAW_POLY / "discovered_markets.json" | |
| 254 | + if discovered_path.exists(): | |
| 255 | + with open(discovered_path) as f: | |
| 256 | + discovered = json.load(f) | |
| 257 | + volumes = pd.DataFrame([{ | |
| 258 | + "name": m.get("question", "")[:40], | |
| 259 | + "volume": float(m.get("volumeNum", 0)), | |
| 260 | + "liquidity": float(m.get("liquidityNum", 0)), | |
| 261 | + } for m in discovered if float(m.get("volumeNum", 0)) > 0]) | |
| 262 | + | |
| 263 | + if not volumes.empty: | |
| 264 | + metadata = pd.DataFrame([{ | |
| 265 | + "name": m.get("question", "")[:40], | |
| 266 | + "volume": float(m.get("volumeNum", 0)), | |
| 267 | + "liquidity": float(m.get("liquidityNum", 0)), | |
| 268 | + "n_obs": 0, | |
| 269 | + "mean_price": np.nan, | |
| 270 | + "std_price": np.nan, | |
| 271 | + } for m in discovered[:20]]) | |
| 272 | + table_descriptive_stats(metadata) | |
| 273 | + logger.info(" Saved table1_descriptive_stats") | |
| 274 | + | |
| 275 | + # Roll spread for Fed panel | |
| 276 | + if no_change is not None: | |
| 277 | + roll_spread = compute_roll_spread(no_change) | |
| 278 | + | |
| 279 | + fig = fig_liquidity( | |
| 280 | + no_change.rename("Price"), | |
| 281 | + spreads=roll_spread, | |
| 282 | + ) | |
| 283 | + save_figure(fig, "fig10_liquidity") | |
| 284 | + logger.info(" Saved fig10_liquidity") | |
| 285 | + | |
| 286 | + # ── 6. Election case study ──────────────────────────────────────────── | |
| 287 | + logger.info("=== Election Case Study ===") | |
| 288 | + if "Donald Trump" in election.columns: | |
| 289 | + trump = election["Donald Trump"] | |
| 290 | + spy_ret = financial.get("SPY_log_return") | |
| 291 | + fig = fig_election_case_study(trump, spy_returns=spy_ret) | |
| 292 | + save_figure(fig, "fig11_election_case_study") | |
| 293 | + logger.info(" Saved fig11_election_case_study") | |
| 294 | + | |
| 295 | + logger.info("=== All analysis complete ===") | |
| 296 | + | |
| 297 | + | |
| 298 | +# ── Main ────────────────────────────────────────────────────────────────────── | |
| 299 | + | |
| 300 | +def main() -> None: | |
| 301 | + parser = argparse.ArgumentParser(description="Polymarket Arrow-Debreu Analysis Pipeline") | |
| 302 | + parser.add_argument("--skip-data", action="store_true", help="Skip data acquisition") | |
| 303 | + parser.add_argument("--skip-models", action="store_true", help="Skip models/figures/tables") | |
| 304 | + parser.add_argument("--skip-paper", action="store_true", help="Skip paper compilation") | |
| 305 | + args = parser.parse_args() | |
| 306 | + | |
| 307 | + ensure_dirs() | |
| 308 | + | |
| 309 | + if not args.skip_data: | |
| 310 | + logger.info("Phase 1: Data acquisition") | |
| 311 | + run_data_acquisition() | |
| 312 | + logger.info("Phase 2: Cleaning") | |
| 313 | + run_cleaning() | |
| 314 | + | |
| 315 | + if not args.skip_models: | |
| 316 | + logger.info("Phase 3-4: Models, figures, and tables") | |
| 317 | + run_models_and_outputs() | |
| 318 | + | |
| 319 | + if not args.skip_paper: | |
| 320 | + logger.info("Phase 5: Paper compilation") | |
| 321 | + import subprocess | |
| 322 | + result = subprocess.run( | |
| 323 | + ["latexmk", "-pdf", "-interaction=nonstopmode", "main.tex"], | |
| 324 | + cwd="paper", | |
| 325 | + capture_output=True, | |
| 326 | + text=True, | |
| 327 | + ) | |
| 328 | + if result.returncode == 0: | |
| 329 | + logger.info("Paper compiled successfully") | |
| 330 | + else: | |
| 331 | + logger.warning("Paper compilation had issues (check paper/main.log)") | |
| 332 | + | |
| 333 | + | |
| 334 | +if __name__ == "__main__": | |
| 335 | + main() | |
added
src/__init__.py
+0 −0
added
src/api/__init__.py
+0 −0
added
src/api/fred.py
+88 −0
@@ -0,0 +1,88 @@ | ||
| 1 | +"""FRED API client for macroeconomic data.""" | |
| 2 | + | |
| 3 | +from __future__ import annotations | |
| 4 | + | |
| 5 | +import pandas as pd | |
| 6 | +from fredapi import Fred | |
| 7 | + | |
| 8 | +from src.config import DATA_RAW_FRED, FRED_API_KEY | |
| 9 | +from src.utils import setup_logging | |
| 10 | + | |
| 11 | +logger = setup_logging("fred") | |
| 12 | + | |
| 13 | + | |
| 14 | +def get_fred_client() -> Fred: | |
| 15 | + """Return an authenticated FRED client.""" | |
| 16 | + return Fred(api_key=FRED_API_KEY) | |
| 17 | + | |
| 18 | + | |
| 19 | +def _fetch_series( | |
| 20 | + series_id: str, | |
| 21 | + start: str = "2024-01-01", | |
| 22 | + end: str = "2026-06-01", | |
| 23 | +) -> pd.DataFrame: | |
| 24 | + """Generic helper: fetch a FRED series and return as DataFrame.""" | |
| 25 | + client = get_fred_client() | |
| 26 | + data = client.get_series(series_id, observation_start=start, observation_end=end) | |
| 27 | + df = data.to_frame(name=series_id) | |
| 28 | + df.index.name = "date" | |
| 29 | + return df | |
| 30 | + | |
| 31 | + | |
| 32 | +def fetch_fed_funds_rate( | |
| 33 | + start: str = "2024-01-01", | |
| 34 | + end: str = "2026-06-01", | |
| 35 | +) -> pd.DataFrame: | |
| 36 | + """Fetch DFF series (daily federal funds rate).""" | |
| 37 | + df = _fetch_series("DFF", start, end) | |
| 38 | + path = DATA_RAW_FRED / "fed_funds_rate.csv" | |
| 39 | + df.to_csv(path) | |
| 40 | + logger.info("Saved DFF (%d obs) to %s", len(df), path) | |
| 41 | + return df | |
| 42 | + | |
| 43 | + | |
| 44 | +def fetch_cpi( | |
| 45 | + start: str = "2024-01-01", | |
| 46 | + end: str = "2026-06-01", | |
| 47 | +) -> pd.DataFrame: | |
| 48 | + """Fetch CPIAUCSL series (Consumer Price Index).""" | |
| 49 | + df = _fetch_series("CPIAUCSL", start, end) | |
| 50 | + path = DATA_RAW_FRED / "cpi.csv" | |
| 51 | + df.to_csv(path) | |
| 52 | + logger.info("Saved CPIAUCSL (%d obs) to %s", len(df), path) | |
| 53 | + return df | |
| 54 | + | |
| 55 | + | |
| 56 | +def fetch_unemployment( | |
| 57 | + start: str = "2024-01-01", | |
| 58 | + end: str = "2026-06-01", | |
| 59 | +) -> pd.DataFrame: | |
| 60 | + """Fetch UNRATE series (unemployment rate).""" | |
| 61 | + df = _fetch_series("UNRATE", start, end) | |
| 62 | + path = DATA_RAW_FRED / "unemployment.csv" | |
| 63 | + df.to_csv(path) | |
| 64 | + logger.info("Saved UNRATE (%d obs) to %s", len(df), path) | |
| 65 | + return df | |
| 66 | + | |
| 67 | + | |
| 68 | +def fetch_all_fred_data() -> None: | |
| 69 | + """Fetch all FRED series and save to data/raw/fred/.""" | |
| 70 | + DATA_RAW_FRED.mkdir(parents=True, exist_ok=True) | |
| 71 | + logger.info("=== Starting FRED data acquisition ===") | |
| 72 | + | |
| 73 | + try: | |
| 74 | + fetch_fed_funds_rate() | |
| 75 | + except Exception as exc: | |
| 76 | + logger.error("Failed to fetch DFF: %s", exc) | |
| 77 | + | |
| 78 | + try: | |
| 79 | + fetch_cpi() | |
| 80 | + except Exception as exc: | |
| 81 | + logger.error("Failed to fetch CPIAUCSL: %s", exc) | |
| 82 | + | |
| 83 | + try: | |
| 84 | + fetch_unemployment() | |
| 85 | + except Exception as exc: | |
| 86 | + logger.error("Failed to fetch UNRATE: %s", exc) | |
| 87 | + | |
| 88 | + logger.info("=== FRED data acquisition complete ===") | |
added
src/api/polymarket.py
+218 −0
@@ -0,0 +1,218 @@ | ||
| 1 | +"""Polymarket API client for Gamma (metadata) and CLOB (prices/order books).""" | |
| 2 | + | |
| 3 | +from __future__ import annotations | |
| 4 | + | |
| 5 | +import asyncio | |
| 6 | +from typing import Any | |
| 7 | + | |
| 8 | +import httpx | |
| 9 | +from tenacity import retry, stop_after_attempt, wait_exponential | |
| 10 | + | |
| 11 | +from src.config import ( | |
| 12 | + CLOB_API, | |
| 13 | + ELECTION_2024_EVENT_ID, | |
| 14 | + ELECTION_2024_MARKETS, | |
| 15 | + FED_JUNE_2026_EVENT_ID, | |
| 16 | + FED_JUNE_2026_MARKETS, | |
| 17 | + GAMMA_API, | |
| 18 | + DATA_RAW_POLY, | |
| 19 | +) | |
| 20 | +from src.utils import save_json, setup_logging | |
| 21 | + | |
| 22 | +logger = setup_logging("polymarket") | |
| 23 | + | |
| 24 | +# Shared client limits | |
| 25 | +_LIMITS = httpx.Limits(max_connections=5) | |
| 26 | + | |
| 27 | + | |
| 28 | +# --------------------------------------------------------------------------- | |
| 29 | +# Low-level helpers | |
| 30 | +# --------------------------------------------------------------------------- | |
| 31 | + | |
| 32 | +@retry(stop=stop_after_attempt(3), wait=wait_exponential(min=1, max=10)) | |
| 33 | +async def _get(client: httpx.AsyncClient, url: str, params: dict | None = None) -> Any: | |
| 34 | + """GET with retry and JSON decode.""" | |
| 35 | + resp = await client.get(url, params=params) | |
| 36 | + resp.raise_for_status() | |
| 37 | + return resp.json() | |
| 38 | + | |
| 39 | + | |
| 40 | +# --------------------------------------------------------------------------- | |
| 41 | +# Gamma API (market metadata / discovery) | |
| 42 | +# --------------------------------------------------------------------------- | |
| 43 | + | |
| 44 | +async def fetch_market(market_id: str) -> dict: | |
| 45 | + """Fetch market metadata from Gamma API.""" | |
| 46 | + async with httpx.AsyncClient(limits=_LIMITS, timeout=30) as client: | |
| 47 | + data = await _get(client, f"{GAMMA_API}/markets/{market_id}") | |
| 48 | + save_json(data, DATA_RAW_POLY / f"market_{market_id}.json") | |
| 49 | + logger.info("Fetched market %s", market_id) | |
| 50 | + return data | |
| 51 | + | |
| 52 | + | |
| 53 | +async def fetch_event(event_id: int) -> dict: | |
| 54 | + """Fetch event with all its markets.""" | |
| 55 | + async with httpx.AsyncClient(limits=_LIMITS, timeout=30) as client: | |
| 56 | + data = await _get(client, f"{GAMMA_API}/events/{event_id}") | |
| 57 | + save_json(data, DATA_RAW_POLY / f"event_{event_id}.json") | |
| 58 | + logger.info("Fetched event %s", event_id) | |
| 59 | + return data | |
| 60 | + | |
| 61 | + | |
| 62 | +async def discover_markets(volume_min: int = 500_000, limit: int = 100) -> list[dict]: | |
| 63 | + """Discover high-volume markets from Gamma API.""" | |
| 64 | + async with httpx.AsyncClient(limits=_LIMITS, timeout=30) as client: | |
| 65 | + data = await _get( | |
| 66 | + client, | |
| 67 | + f"{GAMMA_API}/markets", | |
| 68 | + params={ | |
| 69 | + "volume_num_min": volume_min, | |
| 70 | + "limit": limit, | |
| 71 | + "active": "true", | |
| 72 | + "order": "volume", | |
| 73 | + "ascending": "false", | |
| 74 | + }, | |
| 75 | + ) | |
| 76 | + save_json(data, DATA_RAW_POLY / "discovered_markets.json") | |
| 77 | + logger.info("Discovered %d high-volume markets", len(data) if isinstance(data, list) else 0) | |
| 78 | + return data if isinstance(data, list) else [] | |
| 79 | + | |
| 80 | + | |
| 81 | +# --------------------------------------------------------------------------- | |
| 82 | +# CLOB API (prices, order books) | |
| 83 | +# --------------------------------------------------------------------------- | |
| 84 | + | |
| 85 | +async def fetch_price_history( | |
| 86 | + token_id: str, | |
| 87 | + interval: str = "max", | |
| 88 | + fidelity: int = 1440, | |
| 89 | +) -> list[dict]: | |
| 90 | + """Fetch price history from CLOB API. | |
| 91 | + | |
| 92 | + Returns list of {"t": unix_timestamp, "p": price_string}. | |
| 93 | + For closed markets: interval="max", fidelity=1440 (daily). | |
| 94 | + For active markets: interval="all", fidelity=1 (minutely) or fidelity=60 (hourly). | |
| 95 | + """ | |
| 96 | + async with httpx.AsyncClient(limits=_LIMITS, timeout=60) as client: | |
| 97 | + data = await _get( | |
| 98 | + client, | |
| 99 | + f"{CLOB_API}/prices-history", | |
| 100 | + params={"market": token_id, "interval": interval, "fidelity": fidelity}, | |
| 101 | + ) | |
| 102 | + history = data.get("history", data) if isinstance(data, dict) else data | |
| 103 | + short_id = token_id[:16] | |
| 104 | + save_json(history, DATA_RAW_POLY / f"price_history_{short_id}.json") | |
| 105 | + logger.info("Fetched price history for token %s... (%d points)", short_id, len(history) if isinstance(history, list) else 0) | |
| 106 | + return history if isinstance(history, list) else [] | |
| 107 | + | |
| 108 | + | |
| 109 | +async def fetch_order_book(token_id: str) -> dict: | |
| 110 | + """Fetch current order book snapshot.""" | |
| 111 | + async with httpx.AsyncClient(limits=_LIMITS, timeout=30) as client: | |
| 112 | + data = await _get(client, f"{CLOB_API}/book", params={"token_id": token_id}) | |
| 113 | + short_id = token_id[:16] | |
| 114 | + save_json(data, DATA_RAW_POLY / f"order_book_{short_id}.json") | |
| 115 | + logger.info("Fetched order book for token %s...", short_id) | |
| 116 | + return data | |
| 117 | + | |
| 118 | + | |
| 119 | +async def fetch_midpoint(token_id: str) -> float: | |
| 120 | + """Fetch current midpoint price.""" | |
| 121 | + async with httpx.AsyncClient(limits=_LIMITS, timeout=30) as client: | |
| 122 | + data = await _get(client, f"{CLOB_API}/midpoint", params={"token_id": token_id}) | |
| 123 | + mid = float(data.get("mid", 0.0)) if isinstance(data, dict) else 0.0 | |
| 124 | + logger.info("Midpoint for %s...: %.4f", token_id[:16], mid) | |
| 125 | + return mid | |
| 126 | + | |
| 127 | + | |
| 128 | +async def fetch_spread(token_id: str) -> float: | |
| 129 | + """Fetch current spread.""" | |
| 130 | + async with httpx.AsyncClient(limits=_LIMITS, timeout=30) as client: | |
| 131 | + data = await _get(client, f"{CLOB_API}/spread", params={"token_id": token_id}) | |
| 132 | + spread = float(data.get("spread", 0.0)) if isinstance(data, dict) else 0.0 | |
| 133 | + logger.info("Spread for %s...: %.4f", token_id[:16], spread) | |
| 134 | + return spread | |
| 135 | + | |
| 136 | + | |
| 137 | +# --------------------------------------------------------------------------- | |
| 138 | +# Master fetch | |
| 139 | +# --------------------------------------------------------------------------- | |
| 140 | + | |
| 141 | +def _extract_tokens_from_event(event_data: dict) -> list[dict]: | |
| 142 | + """Extract market info with correct tokens from event response.""" | |
| 143 | + results = [] | |
| 144 | + for m in event_data.get("markets", []): | |
| 145 | + import json as _json | |
| 146 | + tokens = _json.loads(m.get("clobTokenIds", "[]")) | |
| 147 | + if tokens: | |
| 148 | + results.append({ | |
| 149 | + "id": m["id"], | |
| 150 | + "name": m.get("groupItemTitle", m.get("question", "")[:40]), | |
| 151 | + "question": m.get("question", ""), | |
| 152 | + "yes_token": tokens[0], | |
| 153 | + "no_token": tokens[1] if len(tokens) > 1 else "", | |
| 154 | + "closed": m.get("closed", False), | |
| 155 | + "prices": m.get("outcomePrices", ""), | |
| 156 | + "volume": m.get("volumeNum", m.get("volume", 0)), | |
| 157 | + }) | |
| 158 | + return results | |
| 159 | + | |
| 160 | + | |
| 161 | +async def _fetch_event_with_histories( | |
| 162 | + event_id: int, | |
| 163 | + label: str, | |
| 164 | + interval: str = "max", | |
| 165 | + fidelity: int = 1440, | |
| 166 | +) -> list[dict]: | |
| 167 | + """Fetch an event, then fetch price history for every market's YES token.""" | |
| 168 | + logger.info("--- %s (event %d) ---", label, event_id) | |
| 169 | + try: | |
| 170 | + event_data = await fetch_event(event_id) | |
| 171 | + except Exception as exc: | |
| 172 | + logger.warning("Could not fetch event %d: %s", event_id, exc) | |
| 173 | + return [] | |
| 174 | + | |
| 175 | + markets = _extract_tokens_from_event(event_data) | |
| 176 | + for m in markets: | |
| 177 | + try: | |
| 178 | + hist = await fetch_price_history(m["yes_token"], interval=interval, fidelity=fidelity) | |
| 179 | + logger.info(" %s: %d price points", m["name"], len(hist)) | |
| 180 | + except Exception as exc: | |
| 181 | + logger.warning(" Could not fetch history for %s: %s", m["name"], exc) | |
| 182 | + await asyncio.sleep(0.2) | |
| 183 | + | |
| 184 | + return markets | |
| 185 | + | |
| 186 | + | |
| 187 | +async def fetch_all_data() -> None: | |
| 188 | + """Master function: fetch all configured markets and save to data/raw/polymarket/.""" | |
| 189 | + DATA_RAW_POLY.mkdir(parents=True, exist_ok=True) | |
| 190 | + logger.info("=== Starting Polymarket data acquisition ===") | |
| 191 | + | |
| 192 | + # 1. Fed June 2026 event — Arrow-Debreu complete state space | |
| 193 | + fed_markets = await _fetch_event_with_histories( | |
| 194 | + FED_JUNE_2026_EVENT_ID, "Fed Decision June 2026" | |
| 195 | + ) | |
| 196 | + | |
| 197 | + # 2. Election 2024 event — highest-volume prediction market ever | |
| 198 | + election_markets = await _fetch_event_with_histories( | |
| 199 | + ELECTION_2024_EVENT_ID, "Presidential Election 2024" | |
| 200 | + ) | |
| 201 | + | |
| 202 | + # 3. Order book snapshots for active Fed markets | |
| 203 | + logger.info("--- Order book snapshots ---") | |
| 204 | + for m in fed_markets: | |
| 205 | + if not m["closed"]: | |
| 206 | + try: | |
| 207 | + await fetch_order_book(m["yes_token"]) | |
| 208 | + except Exception as exc: | |
| 209 | + logger.warning("Could not fetch order book for %s: %s", m["name"], exc) | |
| 210 | + | |
| 211 | + # 4. Discover high-volume markets | |
| 212 | + logger.info("--- Market discovery ---") | |
| 213 | + try: | |
| 214 | + await discover_markets(volume_min=500_000, limit=100) | |
| 215 | + except Exception as exc: | |
| 216 | + logger.warning("Could not discover markets: %s", exc) | |
| 217 | + | |
| 218 | + logger.info("=== Polymarket data acquisition complete ===") | |
added
src/api/runner.py
+35 −0
@@ -0,0 +1,35 @@ | ||
| 1 | +"""Orchestrator for the complete data acquisition pipeline.""" | |
| 2 | + | |
| 3 | +from __future__ import annotations | |
| 4 | + | |
| 5 | +import asyncio | |
| 6 | + | |
| 7 | +from src.utils import ensure_dirs, setup_logging | |
| 8 | +from src.api.polymarket import fetch_all_data as fetch_polymarket | |
| 9 | +from src.api.fred import fetch_all_fred_data | |
| 10 | +from src.api.yahoo import fetch_all_yahoo_data | |
| 11 | + | |
| 12 | + | |
| 13 | +def run_all() -> None: | |
| 14 | + """Run complete data acquisition pipeline.""" | |
| 15 | + ensure_dirs() | |
| 16 | + logger = setup_logging("data_acquisition") | |
| 17 | + logger.info("Starting data acquisition pipeline") | |
| 18 | + | |
| 19 | + # Polymarket (async) | |
| 20 | + logger.info("--- Phase 1/3: Polymarket ---") | |
| 21 | + asyncio.run(fetch_polymarket()) | |
| 22 | + | |
| 23 | + # FRED | |
| 24 | + logger.info("--- Phase 2/3: FRED ---") | |
| 25 | + fetch_all_fred_data() | |
| 26 | + | |
| 27 | + # Yahoo Finance | |
| 28 | + logger.info("--- Phase 3/3: Yahoo Finance ---") | |
| 29 | + fetch_all_yahoo_data() | |
| 30 | + | |
| 31 | + logger.info("Data acquisition complete") | |
| 32 | + | |
| 33 | + | |
| 34 | +if __name__ == "__main__": | |
| 35 | + run_all() | |
added
src/api/yahoo.py
+59 −0
@@ -0,0 +1,59 @@ | ||
| 1 | +"""Yahoo Finance client for market benchmark data.""" | |
| 2 | + | |
| 3 | +from __future__ import annotations | |
| 4 | + | |
| 5 | +import pandas as pd | |
| 6 | +import yfinance as yf | |
| 7 | + | |
| 8 | +from src.config import DATA_RAW_YAHOO, YAHOO_END, YAHOO_START, YAHOO_TICKERS | |
| 9 | +from src.utils import setup_logging | |
| 10 | + | |
| 11 | +logger = setup_logging("yahoo") | |
| 12 | + | |
| 13 | + | |
| 14 | +def fetch_yahoo_data( | |
| 15 | + tickers: list[str] | None = None, | |
| 16 | + start: str | None = None, | |
| 17 | + end: str | None = None, | |
| 18 | +) -> pd.DataFrame: | |
| 19 | + """Fetch daily OHLCV data for all configured tickers. Save to CSV.""" | |
| 20 | + tickers = tickers or YAHOO_TICKERS | |
| 21 | + start = start or YAHOO_START | |
| 22 | + end = end or YAHOO_END | |
| 23 | + | |
| 24 | + DATA_RAW_YAHOO.mkdir(parents=True, exist_ok=True) | |
| 25 | + | |
| 26 | + all_frames: list[pd.DataFrame] = [] | |
| 27 | + | |
| 28 | + for ticker in tickers: | |
| 29 | + try: | |
| 30 | + logger.info("Fetching %s from Yahoo Finance", ticker) | |
| 31 | + tk = yf.Ticker(ticker) | |
| 32 | + df = tk.history(start=start, end=end, auto_adjust=True) | |
| 33 | + if df.empty: | |
| 34 | + logger.warning("No data returned for %s", ticker) | |
| 35 | + continue | |
| 36 | + df.index.name = "date" | |
| 37 | + # Clean ticker name for filename (replace ^ and others) | |
| 38 | + safe_name = ticker.replace("^", "").replace("-", "_") | |
| 39 | + path = DATA_RAW_YAHOO / f"{safe_name}.csv" | |
| 40 | + df.to_csv(path) | |
| 41 | + logger.info("Saved %s (%d rows) to %s", ticker, len(df), path) | |
| 42 | + all_frames.append(df.assign(ticker=ticker)) | |
| 43 | + except Exception as exc: | |
| 44 | + logger.error("Failed to fetch %s: %s", ticker, exc) | |
| 45 | + | |
| 46 | + if all_frames: | |
| 47 | + combined = pd.concat(all_frames) | |
| 48 | + combined.to_csv(DATA_RAW_YAHOO / "all_tickers.csv") | |
| 49 | + logger.info("Saved combined dataset (%d rows)", len(combined)) | |
| 50 | + return combined | |
| 51 | + | |
| 52 | + return pd.DataFrame() | |
| 53 | + | |
| 54 | + | |
| 55 | +def fetch_all_yahoo_data() -> None: | |
| 56 | + """Fetch all Yahoo data and save to data/raw/yahoo/.""" | |
| 57 | + logger.info("=== Starting Yahoo Finance data acquisition ===") | |
| 58 | + fetch_yahoo_data() | |
| 59 | + logger.info("=== Yahoo Finance data acquisition complete ===") | |
added
src/cleaning/__init__.py
+0 −0
added
src/cleaning/macro_cleaner.py
+105 −0
@@ -0,0 +1,105 @@ | ||
| 1 | +"""Clean FRED macro data and build a unified event calendar.""" | |
| 2 | + | |
| 3 | +from __future__ import annotations | |
| 4 | + | |
| 5 | +from datetime import date | |
| 6 | + | |
| 7 | +import pandas as pd | |
| 8 | + | |
| 9 | +from src.config import ( | |
| 10 | + CPI_RELEASE_DATES, | |
| 11 | + DATA_PROCESSED, | |
| 12 | + DATA_RAW_FRED, | |
| 13 | + ELECTION_KEY_DATES, | |
| 14 | + FOMC_DATES, | |
| 15 | + NFP_RELEASE_DATES, | |
| 16 | +) | |
| 17 | +from src.utils import setup_logging | |
| 18 | + | |
| 19 | +logger = setup_logging("macro_cleaner") | |
| 20 | + | |
| 21 | + | |
| 22 | +# --------------------------------------------------------------------------- | |
| 23 | +# Event calendar | |
| 24 | +# --------------------------------------------------------------------------- | |
| 25 | + | |
| 26 | +def build_event_calendar() -> pd.DataFrame: | |
| 27 | + """Build a unified event calendar with columns: date, event_type, description.""" | |
| 28 | + rows: list[dict] = [] | |
| 29 | + | |
| 30 | + for d in FOMC_DATES: | |
| 31 | + rows.append({"date": d, "event_type": "FOMC", "description": f"FOMC meeting {d}"}) | |
| 32 | + | |
| 33 | + for d in CPI_RELEASE_DATES: | |
| 34 | + rows.append({"date": d, "event_type": "CPI", "description": f"CPI release {d}"}) | |
| 35 | + | |
| 36 | + for d in NFP_RELEASE_DATES: | |
| 37 | + rows.append({"date": d, "event_type": "NFP", "description": f"Non-farm payrolls {d}"}) | |
| 38 | + | |
| 39 | + for label, d in ELECTION_KEY_DATES.items(): | |
| 40 | + rows.append({"date": d, "event_type": "Election", "description": label}) | |
| 41 | + | |
| 42 | + df = pd.DataFrame(rows) | |
| 43 | + df["date"] = pd.to_datetime(df["date"]) | |
| 44 | + df = df.sort_values("date").reset_index(drop=True) | |
| 45 | + logger.info("Built event calendar with %d events", len(df)) | |
| 46 | + return df | |
| 47 | + | |
| 48 | + | |
| 49 | +# --------------------------------------------------------------------------- | |
| 50 | +# FRED data | |
| 51 | +# --------------------------------------------------------------------------- | |
| 52 | + | |
| 53 | +def clean_fred_data() -> dict[str, pd.DataFrame]: | |
| 54 | + """Load and clean all FRED CSVs. | |
| 55 | + | |
| 56 | + Returns a dict mapping series name to a DataFrame with DatetimeIndex. | |
| 57 | + """ | |
| 58 | + result: dict[str, pd.DataFrame] = {} | |
| 59 | + | |
| 60 | + for csv_path in sorted(DATA_RAW_FRED.glob("*.csv")): | |
| 61 | + df = pd.read_csv(csv_path, parse_dates=["date"], index_col="date") | |
| 62 | + # Coerce all value columns to numeric | |
| 63 | + for col in df.columns: | |
| 64 | + df[col] = pd.to_numeric(df[col], errors="coerce") | |
| 65 | + df = df.sort_index() | |
| 66 | + name = csv_path.stem # e.g. "fed_funds_rate", "cpi", "unemployment" | |
| 67 | + result[name] = df | |
| 68 | + logger.info("Loaded FRED series '%s': %d observations, columns=%s", | |
| 69 | + name, len(df), list(df.columns)) | |
| 70 | + | |
| 71 | + return result | |
| 72 | + | |
| 73 | + | |
| 74 | +# --------------------------------------------------------------------------- | |
| 75 | +# Master cleaning | |
| 76 | +# --------------------------------------------------------------------------- | |
| 77 | + | |
| 78 | +def clean_all() -> None: | |
| 79 | + """Save macro_events.parquet and fred_data.parquet to ``data/processed/``.""" | |
| 80 | + DATA_PROCESSED.mkdir(parents=True, exist_ok=True) | |
| 81 | + | |
| 82 | + # 1. Event calendar | |
| 83 | + calendar = build_event_calendar() | |
| 84 | + out = DATA_PROCESSED / "macro_events.parquet" | |
| 85 | + calendar.to_parquet(out) | |
| 86 | + logger.info("Saved %s (%d rows)", out.name, len(calendar)) | |
| 87 | + | |
| 88 | + # 2. FRED data -- merge all series into a single wide DataFrame | |
| 89 | + fred = clean_fred_data() | |
| 90 | + if fred: | |
| 91 | + # Outer-join all series on date | |
| 92 | + combined = pd.DataFrame() | |
| 93 | + for name, df in fred.items(): | |
| 94 | + for col in df.columns: | |
| 95 | + col_name = f"{name}_{col}" if col != name else col | |
| 96 | + combined[col_name] = df[col] | |
| 97 | + # Re-sort by date | |
| 98 | + combined = combined.sort_index() | |
| 99 | + # Forward-fill monthly series to daily grid | |
| 100 | + combined = combined.ffill() | |
| 101 | + out = DATA_PROCESSED / "fred_data.parquet" | |
| 102 | + combined.to_parquet(out) | |
| 103 | + logger.info("Saved %s (%d rows, %d columns)", out.name, len(combined), len(combined.columns)) | |
| 104 | + | |
| 105 | + logger.info("=== Macro cleaning complete ===") | |
added
src/cleaning/polymarket_cleaner.py
+193 −0
@@ -0,0 +1,193 @@ | ||
| 1 | +"""Clean Polymarket raw data and build structured panels for analysis.""" | |
| 2 | + | |
| 3 | +from __future__ import annotations | |
| 4 | + | |
| 5 | +import json | |
| 6 | +from pathlib import Path | |
| 7 | + | |
| 8 | +import numpy as np | |
| 9 | +import pandas as pd | |
| 10 | + | |
| 11 | +from src.config import ( | |
| 12 | + DATA_PROCESSED, | |
| 13 | + DATA_RAW_POLY, | |
| 14 | + ELECTION_2024_EVENT_ID, | |
| 15 | + FED_JUNE_2026_EVENT_ID, | |
| 16 | +) | |
| 17 | +from src.utils import setup_logging | |
| 18 | + | |
| 19 | +logger = setup_logging("polymarket_cleaner") | |
| 20 | + | |
| 21 | + | |
| 22 | +# --------------------------------------------------------------------------- | |
| 23 | +# Low-level loaders | |
| 24 | +# --------------------------------------------------------------------------- | |
| 25 | + | |
| 26 | +def load_event_markets(event_id: int) -> list[dict]: | |
| 27 | + """Load event JSON and extract market info with token IDs.""" | |
| 28 | + path = DATA_RAW_POLY / f"event_{event_id}.json" | |
| 29 | + if not path.exists(): | |
| 30 | + logger.warning("Event file not found: %s", path) | |
| 31 | + return [] | |
| 32 | + | |
| 33 | + with open(path) as f: | |
| 34 | + event_data = json.load(f) | |
| 35 | + | |
| 36 | + results: list[dict] = [] | |
| 37 | + for m in event_data.get("markets", []): | |
| 38 | + tokens = json.loads(m.get("clobTokenIds", "[]")) | |
| 39 | + if tokens: | |
| 40 | + results.append({ | |
| 41 | + "id": m["id"], | |
| 42 | + "name": m.get("groupItemTitle", m.get("question", "")[:40]), | |
| 43 | + "question": m.get("question", ""), | |
| 44 | + "yes_token": tokens[0], | |
| 45 | + "no_token": tokens[1] if len(tokens) > 1 else "", | |
| 46 | + "closed": m.get("closed", False), | |
| 47 | + "prices": m.get("outcomePrices", ""), | |
| 48 | + "volume": float(m.get("volumeNum", m.get("volume", 0) or 0)), | |
| 49 | + }) | |
| 50 | + logger.info("Loaded %d markets from event %d", len(results), event_id) | |
| 51 | + return results | |
| 52 | + | |
| 53 | + | |
| 54 | +def load_price_history(token_id: str) -> pd.DataFrame: | |
| 55 | + """Load price history JSON for a token, return DataFrame with DatetimeIndex. | |
| 56 | + | |
| 57 | + Files are named ``price_history_{token_id[:16]}.json``. | |
| 58 | + Content is a list of ``{"t": unix_ts, "p": float}``. | |
| 59 | + Returns a DataFrame with a datetime index (``date``) and a ``price`` column. | |
| 60 | + """ | |
| 61 | + short_id = token_id[:16] | |
| 62 | + path = DATA_RAW_POLY / f"price_history_{short_id}.json" | |
| 63 | + if not path.exists(): | |
| 64 | + logger.warning("Price history file not found: %s", path) | |
| 65 | + return pd.DataFrame(columns=["price"]) | |
| 66 | + | |
| 67 | + with open(path) as f: | |
| 68 | + raw = json.load(f) | |
| 69 | + | |
| 70 | + if not raw: | |
| 71 | + logger.warning("Empty price history for token %s", short_id) | |
| 72 | + return pd.DataFrame(columns=["price"]) | |
| 73 | + | |
| 74 | + df = pd.DataFrame(raw) | |
| 75 | + df["date"] = pd.to_datetime(df["t"], unit="s", utc=True).dt.normalize() | |
| 76 | + df["price"] = df["p"].astype(float) | |
| 77 | + # Some data may have duplicates for the same day (intra-day ticks); | |
| 78 | + # keep the last observation per calendar day. | |
| 79 | + df = df.drop_duplicates(subset="date", keep="last") | |
| 80 | + df = df.set_index("date").sort_index()[["price"]] | |
| 81 | + df.index = df.index.tz_localize(None) # drop tz for easier merging | |
| 82 | + logger.info("Loaded price history for %s: %d observations", short_id, len(df)) | |
| 83 | + return df | |
| 84 | + | |
| 85 | + | |
| 86 | +# --------------------------------------------------------------------------- | |
| 87 | +# Panel builders | |
| 88 | +# --------------------------------------------------------------------------- | |
| 89 | + | |
| 90 | +def build_market_panel(event_id: int) -> pd.DataFrame: | |
| 91 | + """Build panel of prices for all markets in an event. | |
| 92 | + | |
| 93 | + Columns = market names (``groupItemTitle``), Index = date, Values = prices. | |
| 94 | + """ | |
| 95 | + markets = load_event_markets(event_id) | |
| 96 | + if not markets: | |
| 97 | + logger.warning("No markets found for event %d", event_id) | |
| 98 | + return pd.DataFrame() | |
| 99 | + | |
| 100 | + series_dict: dict[str, pd.Series] = {} | |
| 101 | + for m in markets: | |
| 102 | + df = load_price_history(m["yes_token"]) | |
| 103 | + if not df.empty: | |
| 104 | + series_dict[m["name"]] = df["price"] | |
| 105 | + | |
| 106 | + if not series_dict: | |
| 107 | + logger.warning("No price data for event %d", event_id) | |
| 108 | + return pd.DataFrame() | |
| 109 | + | |
| 110 | + panel = pd.DataFrame(series_dict) | |
| 111 | + panel = panel.sort_index() | |
| 112 | + # Forward-fill small gaps (weekends / missing days) | |
| 113 | + panel = panel.ffill(limit=3) | |
| 114 | + logger.info( | |
| 115 | + "Built market panel for event %d: %d dates x %d markets", | |
| 116 | + event_id, len(panel), len(panel.columns), | |
| 117 | + ) | |
| 118 | + return panel | |
| 119 | + | |
| 120 | + | |
| 121 | +def build_arrow_debreu_state_prices(event_id: int = FED_JUNE_2026_EVENT_ID) -> pd.DataFrame: | |
| 122 | + """Build Arrow-Debreu state-price matrix for the Fed June 2026 neg-risk group. | |
| 123 | + | |
| 124 | + Each market corresponds to one Arrow-Debreu state | |
| 125 | + (50+ bps decrease, 25 bps decrease, no change, 25 bps increase, 50+ bps increase). | |
| 126 | + The state price equals the market YES price. | |
| 127 | + | |
| 128 | + Adds: | |
| 129 | + ``state_price_sum`` -- sum of state prices per day (should be close to 1.0). | |
| 130 | + ``completeness_deviation`` -- ``state_price_sum - 1.0``. | |
| 131 | + """ | |
| 132 | + panel = build_market_panel(event_id) | |
| 133 | + if panel.empty: | |
| 134 | + return panel | |
| 135 | + | |
| 136 | + # Canonical ordering of the state space | |
| 137 | + canonical_order = [ | |
| 138 | + "50+ bps decrease", | |
| 139 | + "25 bps decrease", | |
| 140 | + "No change", | |
| 141 | + "25 bps increase", | |
| 142 | + "50+ bps increase", | |
| 143 | + ] | |
| 144 | + # Reorder columns to canonical order, keeping only those present | |
| 145 | + cols = [c for c in canonical_order if c in panel.columns] | |
| 146 | + panel = panel[cols] | |
| 147 | + | |
| 148 | + panel["state_price_sum"] = panel[cols].sum(axis=1) | |
| 149 | + panel["completeness_deviation"] = panel["state_price_sum"] - 1.0 | |
| 150 | + | |
| 151 | + logger.info( | |
| 152 | + "Arrow-Debreu state prices: mean sum = %.4f, mean |deviation| = %.4f", | |
| 153 | + panel["state_price_sum"].mean(), | |
| 154 | + panel["completeness_deviation"].abs().mean(), | |
| 155 | + ) | |
| 156 | + return panel | |
| 157 | + | |
| 158 | + | |
| 159 | +def build_election_panel() -> pd.DataFrame: | |
| 160 | + """Build panel for Election 2024 markets (Trump, Biden/Harris, etc.).""" | |
| 161 | + return build_market_panel(ELECTION_2024_EVENT_ID) | |
| 162 | + | |
| 163 | + | |
| 164 | +# --------------------------------------------------------------------------- | |
| 165 | +# Master cleaning | |
| 166 | +# --------------------------------------------------------------------------- | |
| 167 | + | |
| 168 | +def clean_all() -> None: | |
| 169 | + """Run all Polymarket cleaning and save Parquet files to ``data/processed/``.""" | |
| 170 | + DATA_PROCESSED.mkdir(parents=True, exist_ok=True) | |
| 171 | + | |
| 172 | + # 1. Fed June 2026 -- Arrow-Debreu state prices | |
| 173 | + fed_panel = build_arrow_debreu_state_prices() | |
| 174 | + if not fed_panel.empty: | |
| 175 | + out = DATA_PROCESSED / "fed_june2026_state_prices.parquet" | |
| 176 | + fed_panel.to_parquet(out) | |
| 177 | + logger.info("Saved %s (%d rows)", out.name, len(fed_panel)) | |
| 178 | + | |
| 179 | + # 2. Fed June 2026 -- raw market panel (without AD columns) | |
| 180 | + fed_raw = build_market_panel(FED_JUNE_2026_EVENT_ID) | |
| 181 | + if not fed_raw.empty: | |
| 182 | + out = DATA_PROCESSED / "fed_june2026_panel.parquet" | |
| 183 | + fed_raw.to_parquet(out) | |
| 184 | + logger.info("Saved %s (%d rows)", out.name, len(fed_raw)) | |
| 185 | + | |
| 186 | + # 3. Election 2024 panel | |
| 187 | + election = build_election_panel() | |
| 188 | + if not election.empty: | |
| 189 | + out = DATA_PROCESSED / "election_2024_panel.parquet" | |
| 190 | + election.to_parquet(out) | |
| 191 | + logger.info("Saved %s (%d rows)", out.name, len(election)) | |
| 192 | + | |
| 193 | + logger.info("=== Polymarket cleaning complete ===") | |
added
src/cleaning/yahoo_cleaner.py
+76 −0
@@ -0,0 +1,76 @@ | ||
| 1 | +"""Clean Yahoo Finance OHLCV data and compute log returns.""" | |
| 2 | + | |
| 3 | +from __future__ import annotations | |
| 4 | + | |
| 5 | +import numpy as np | |
| 6 | +import pandas as pd | |
| 7 | + | |
| 8 | +from src.config import DATA_PROCESSED, DATA_RAW_YAHOO | |
| 9 | +from src.utils import setup_logging | |
| 10 | + | |
| 11 | +logger = setup_logging("yahoo_cleaner") | |
| 12 | + | |
| 13 | + | |
| 14 | +def clean_yahoo_data() -> pd.DataFrame: | |
| 15 | + """Load all Yahoo CSVs, compute log returns, return a combined DataFrame. | |
| 16 | + | |
| 17 | + For each ticker file (SPY.csv, BTC_USD.csv, etc.) we: | |
| 18 | + 1. Parse dates and set DatetimeIndex. | |
| 19 | + 2. Keep Close and Volume. | |
| 20 | + 3. Compute log return = ln(Close_t / Close_{t-1}). | |
| 21 | + | |
| 22 | + Returns a wide DataFrame with columns like ``SPY_close``, ``SPY_volume``, | |
| 23 | + ``SPY_log_return``, etc. | |
| 24 | + """ | |
| 25 | + combined = pd.DataFrame() | |
| 26 | + | |
| 27 | + for csv_path in sorted(DATA_RAW_YAHOO.glob("*.csv")): | |
| 28 | + # Skip the combined all_tickers file -- we use per-ticker files | |
| 29 | + if csv_path.stem == "all_tickers": | |
| 30 | + continue | |
| 31 | + | |
| 32 | + ticker = csv_path.stem # e.g. "SPY", "BTC_USD" | |
| 33 | + df = pd.read_csv(csv_path, parse_dates=["date"], index_col="date") | |
| 34 | + df.index = pd.to_datetime(df.index, utc=True).normalize().tz_localize(None) | |
| 35 | + | |
| 36 | + # Coerce numeric | |
| 37 | + for col in ["Close", "Volume"]: | |
| 38 | + if col in df.columns: | |
| 39 | + df[col] = pd.to_numeric(df[col], errors="coerce") | |
| 40 | + | |
| 41 | + if "Close" not in df.columns: | |
| 42 | + logger.warning("No 'Close' column in %s, skipping", csv_path.name) | |
| 43 | + continue | |
| 44 | + | |
| 45 | + close = df["Close"].rename(f"{ticker}_close") | |
| 46 | + log_ret = np.log(df["Close"] / df["Close"].shift(1)).rename(f"{ticker}_log_return") | |
| 47 | + | |
| 48 | + combined[f"{ticker}_close"] = close | |
| 49 | + combined[f"{ticker}_log_return"] = log_ret | |
| 50 | + | |
| 51 | + if "Volume" in df.columns: | |
| 52 | + volume = df["Volume"].rename(f"{ticker}_volume") | |
| 53 | + combined[f"{ticker}_volume"] = volume | |
| 54 | + | |
| 55 | + logger.info("Loaded Yahoo ticker '%s': %d observations", ticker, len(df)) | |
| 56 | + | |
| 57 | + combined = combined.sort_index() | |
| 58 | + logger.info( | |
| 59 | + "Combined Yahoo data: %d rows, %d columns, date range %s to %s", | |
| 60 | + len(combined), len(combined.columns), | |
| 61 | + combined.index.min(), combined.index.max(), | |
| 62 | + ) | |
| 63 | + return combined | |
| 64 | + | |
| 65 | + | |
| 66 | +def clean_all() -> None: | |
| 67 | + """Save ``financial_data.parquet`` to ``data/processed/``.""" | |
| 68 | + DATA_PROCESSED.mkdir(parents=True, exist_ok=True) | |
| 69 | + | |
| 70 | + financial = clean_yahoo_data() | |
| 71 | + if not financial.empty: | |
| 72 | + out = DATA_PROCESSED / "financial_data.parquet" | |
| 73 | + financial.to_parquet(out) | |
| 74 | + logger.info("Saved %s (%d rows)", out.name, len(financial)) | |
| 75 | + | |
| 76 | + logger.info("=== Yahoo cleaning complete ===") | |
added
src/config.py
+145 −0
@@ -0,0 +1,145 @@ | ||
| 1 | +from __future__ import annotations | |
| 2 | + | |
| 3 | +import os | |
| 4 | +from datetime import date, datetime | |
| 5 | +from pathlib import Path | |
| 6 | + | |
| 7 | +from dotenv import load_dotenv | |
| 8 | + | |
| 9 | +load_dotenv() | |
| 10 | + | |
| 11 | +PROJECT_ROOT = Path(__file__).resolve().parents[1] | |
| 12 | + | |
| 13 | +DATA_RAW = PROJECT_ROOT / "data" / "raw" | |
| 14 | +DATA_RAW_POLY = DATA_RAW / "polymarket" | |
| 15 | +DATA_RAW_FRED = DATA_RAW / "fred" | |
| 16 | +DATA_RAW_YAHOO = DATA_RAW / "yahoo" | |
| 17 | +DATA_PROCESSED = PROJECT_ROOT / "data" / "processed" | |
| 18 | +FIGURES = PROJECT_ROOT / "figures" | |
| 19 | +TABLES = PROJECT_ROOT / "results" / "tables" | |
| 20 | +PAPER = PROJECT_ROOT / "paper" | |
| 21 | +LOGS = PROJECT_ROOT / "logs" | |
| 22 | + | |
| 23 | +GAMMA_API = "https://gamma-api.polymarket.com" | |
| 24 | +CLOB_API = "https://clob.polymarket.com" | |
| 25 | + | |
| 26 | +FRED_API_KEY = os.getenv("FRED_API_KEY", "") | |
| 27 | + | |
| 28 | +# --------------------------------------------------------------------------- | |
| 29 | +# Market registry | |
| 30 | +# --------------------------------------------------------------------------- | |
| 31 | + | |
| 32 | +FED_JUNE_2026_EVENT_ID = 101772 | |
| 33 | +FED_JUNE_2026_MARKETS = { | |
| 34 | + "50+ bps decrease": {"id": "906972", "token": "11088456156829969846055197785216933275644828685751065736355063877671766078668"}, | |
| 35 | + "25 bps decrease": {"id": "906973", "token": "65193234666628291664907888364936366210882855780775260579670618454938932816097"}, | |
| 36 | + "No change": {"id": "906974", "token": "30767812841387255642892182147223249245546960790037785428969725073478408781530"}, | |
| 37 | + "25 bps increase": {"id": "906975", "token": "54688507565884542490190759171719899637340549418956975710651891089424379775735"}, | |
| 38 | + "50+ bps increase": {"id": "906976", "token": "11019686559003253359318459636510036787285735855494197538476668773993811283380"}, | |
| 39 | +} | |
| 40 | + | |
| 41 | +ELECTION_2024_EVENT_ID = 903193 | |
| 42 | +ELECTION_2024_MARKETS = { | |
| 43 | + "Donald Trump": {"id": "253591", "token": "21742633143463906290569050155826241533067272736897614950488156847949938836455"}, | |
| 44 | + "Kamala Harris": {"id": "253596", "token": "69236923620165904972967806948244475009793726478846816743760350969833698291913"}, | |
| 45 | +} | |
| 46 | + | |
| 47 | +FED_CLOSED_MARKETS = { | |
| 48 | + "Dec 2025 - 50+ bps decrease": {"id": "570360"}, | |
| 49 | + "Jan 2026 - 50+ bps decrease": {"id": "601697"}, | |
| 50 | + "Jan 2026 - 25+ bps increase": {"id": "601700"}, | |
| 51 | + "Mar 2026 - 25+ bps increase": {"id": "654415"}, | |
| 52 | +} | |
| 53 | + | |
| 54 | +KEY_BINARY_MARKETS = [ | |
| 55 | + {"id": "253591", "name": "Trump wins 2024", "category": "politics"}, | |
| 56 | + {"id": "253596", "name": "Harris wins 2024", "category": "politics"}, | |
| 57 | + {"id": "906974", "name": "Fed June no change", "category": "economics"}, | |
| 58 | +] | |
| 59 | + | |
| 60 | +# --------------------------------------------------------------------------- | |
| 61 | +# FOMC meeting dates (2024–2026) | |
| 62 | +# --------------------------------------------------------------------------- | |
| 63 | + | |
| 64 | +FOMC_DATES = [ | |
| 65 | + date(2024, 1, 31), date(2024, 3, 20), date(2024, 5, 1), | |
| 66 | + date(2024, 6, 12), date(2024, 7, 31), date(2024, 9, 18), | |
| 67 | + date(2024, 11, 7), date(2024, 12, 18), | |
| 68 | + date(2025, 1, 29), date(2025, 3, 19), date(2025, 5, 7), | |
| 69 | + date(2025, 6, 18), date(2025, 7, 30), date(2025, 9, 17), | |
| 70 | + date(2025, 10, 29), date(2025, 12, 17), | |
| 71 | + date(2026, 1, 28), date(2026, 3, 18), date(2026, 5, 6), | |
| 72 | + date(2026, 6, 17), date(2026, 7, 29), date(2026, 9, 16), | |
| 73 | + date(2026, 10, 28), date(2026, 12, 16), | |
| 74 | +] | |
| 75 | + | |
| 76 | +CPI_RELEASE_DATES = [ | |
| 77 | + date(2024, 1, 11), date(2024, 2, 13), date(2024, 3, 12), | |
| 78 | + date(2024, 4, 10), date(2024, 5, 15), date(2024, 6, 12), | |
| 79 | + date(2024, 7, 11), date(2024, 8, 14), date(2024, 9, 11), | |
| 80 | + date(2024, 10, 10), date(2024, 11, 13), date(2024, 12, 11), | |
| 81 | + date(2025, 1, 15), date(2025, 2, 12), date(2025, 3, 12), | |
| 82 | + date(2025, 4, 10), date(2025, 5, 13), date(2025, 6, 11), | |
| 83 | + date(2025, 7, 15), date(2025, 8, 12), date(2025, 9, 10), | |
| 84 | + date(2025, 10, 14), date(2025, 11, 12), date(2025, 12, 10), | |
| 85 | + date(2026, 1, 14), date(2026, 2, 11), date(2026, 3, 11), | |
| 86 | + date(2026, 4, 14), date(2026, 5, 12), date(2026, 6, 10), | |
| 87 | +] | |
| 88 | + | |
| 89 | +NFP_RELEASE_DATES = [ | |
| 90 | + date(2024, 1, 5), date(2024, 2, 2), date(2024, 3, 8), | |
| 91 | + date(2024, 4, 5), date(2024, 5, 3), date(2024, 6, 7), | |
| 92 | + date(2024, 7, 5), date(2024, 8, 2), date(2024, 9, 6), | |
| 93 | + date(2024, 10, 4), date(2024, 11, 1), date(2024, 12, 6), | |
| 94 | + date(2025, 1, 10), date(2025, 2, 7), date(2025, 3, 7), | |
| 95 | + date(2025, 4, 4), date(2025, 5, 2), date(2025, 6, 6), | |
| 96 | + date(2025, 7, 3), date(2025, 8, 1), date(2025, 9, 5), | |
| 97 | + date(2025, 10, 3), date(2025, 11, 7), date(2025, 12, 5), | |
| 98 | + date(2026, 1, 9), date(2026, 2, 6), date(2026, 3, 6), | |
| 99 | + date(2026, 4, 3), date(2026, 5, 8), date(2026, 6, 5), | |
| 100 | +] | |
| 101 | + | |
| 102 | +ELECTION_KEY_DATES = { | |
| 103 | + "Biden dropout": date(2024, 7, 21), | |
| 104 | + "Trump assassination attempt": date(2024, 7, 13), | |
| 105 | + "Harris-Trump debate": date(2024, 9, 10), | |
| 106 | + "Election day": date(2024, 11, 5), | |
| 107 | +} | |
| 108 | + | |
| 109 | +YAHOO_TICKERS = ["SPY", "^TNX", "^VIX", "BTC-USD", "ETH-USD"] | |
| 110 | +YAHOO_START = "2024-01-01" | |
| 111 | +YAHOO_END = "2026-06-01" | |
| 112 | + | |
| 113 | +# --------------------------------------------------------------------------- | |
| 114 | +# Matplotlib style | |
| 115 | +# --------------------------------------------------------------------------- | |
| 116 | + | |
| 117 | +MPL_STYLE = { | |
| 118 | + "font.family": "serif", | |
| 119 | + "font.serif": ["Times New Roman", "DejaVu Serif"], | |
| 120 | + "font.size": 10, | |
| 121 | + "axes.titlesize": 11, | |
| 122 | + "axes.labelsize": 10, | |
| 123 | + "xtick.labelsize": 9, | |
| 124 | + "ytick.labelsize": 9, | |
| 125 | + "legend.fontsize": 9, | |
| 126 | + "figure.figsize": (6.5, 4.0), | |
| 127 | + "figure.dpi": 300, | |
| 128 | + "savefig.dpi": 300, | |
| 129 | + "savefig.bbox": "tight", | |
| 130 | + "axes.spines.top": False, | |
| 131 | + "axes.spines.right": False, | |
| 132 | + "axes.grid": True, | |
| 133 | + "grid.alpha": 0.3, | |
| 134 | + "grid.linestyle": "--", | |
| 135 | + "lines.linewidth": 1.2, | |
| 136 | +} | |
| 137 | + | |
| 138 | +JOF_COLORS = { | |
| 139 | + "primary": "#1f4e79", | |
| 140 | + "secondary": "#c0392b", | |
| 141 | + "tertiary": "#27ae60", | |
| 142 | + "quaternary": "#f39c12", | |
| 143 | + "gray": "#7f8c8d", | |
| 144 | + "light": "#bdc3c7", | |
| 145 | +} | |
added
src/features/__init__.py
+0 −0
added
src/features/engineering.py
+51 −0
@@ -0,0 +1,51 @@ | ||
| 1 | +"""Feature engineering for prediction market analysis.""" | |
| 2 | + | |
| 3 | +from __future__ import annotations | |
| 4 | + | |
| 5 | +import numpy as np | |
| 6 | +import pandas as pd | |
| 7 | + | |
| 8 | + | |
| 9 | +def compute_log_returns(prices: pd.Series) -> pd.Series: | |
| 10 | + return np.log(prices / prices.shift(1)) | |
| 11 | + | |
| 12 | + | |
| 13 | +def compute_realized_volatility(returns: pd.Series, window: int = 5) -> pd.Series: | |
| 14 | + return (returns ** 2).rolling(window).sum().apply(np.sqrt) | |
| 15 | + | |
| 16 | + | |
| 17 | +def compute_probability_changes(probs: pd.DataFrame) -> pd.DataFrame: | |
| 18 | + return probs.diff() | |
| 19 | + | |
| 20 | + | |
| 21 | +def compute_roll_spread(prices: pd.Series, window: int = 20) -> pd.Series: | |
| 22 | + """Roll (1984) bid-ask spread estimator.""" | |
| 23 | + dp = prices.diff() | |
| 24 | + | |
| 25 | + def _roll_cov(x: np.ndarray) -> float: | |
| 26 | + if len(x) < 3: | |
| 27 | + return np.nan | |
| 28 | + return np.cov(x[:-1], x[1:])[0, 1] | |
| 29 | + | |
| 30 | + cov = dp.rolling(window).apply(_roll_cov, raw=True) | |
| 31 | + return 2 * np.sqrt(np.maximum(-cov, 0)) | |
| 32 | + | |
| 33 | + | |
| 34 | +def compute_amihud_illiquidity(returns: pd.Series, volume: pd.Series) -> pd.Series: | |
| 35 | + return (returns.abs() / volume).replace([np.inf, -np.inf], np.nan) | |
| 36 | + | |
| 37 | + | |
| 38 | +def compute_abnormal_volume(volume: pd.Series, window: int = 20) -> pd.Series: | |
| 39 | + return volume / volume.rolling(window).mean() | |
| 40 | + | |
| 41 | + | |
| 42 | +def engineer_features(prices_df: pd.DataFrame) -> pd.DataFrame: | |
| 43 | + """Compute features for a panel of prices.""" | |
| 44 | + features = pd.DataFrame(index=prices_df.index) | |
| 45 | + for col in prices_df.columns: | |
| 46 | + s = prices_df[col] | |
| 47 | + features[f"{col}_return"] = s.diff() | |
| 48 | + features[f"{col}_log_return"] = compute_log_returns(s) | |
| 49 | + features[f"{col}_rv5"] = compute_realized_volatility(s.diff(), 5) | |
| 50 | + features[f"{col}_roll_spread"] = compute_roll_spread(s) | |
| 51 | + return features | |
added
src/features/event_windows.py
+64 −0
@@ -0,0 +1,64 @@ | ||
| 1 | +"""Event window construction for event studies.""" | |
| 2 | + | |
| 3 | +from __future__ import annotations | |
| 4 | + | |
| 5 | +import numpy as np | |
| 6 | +import pandas as pd | |
| 7 | + | |
| 8 | + | |
| 9 | +def create_event_windows( | |
| 10 | + prices: pd.DataFrame, | |
| 11 | + event_dates: list, | |
| 12 | + window: tuple[int, int] = (-5, 5), | |
| 13 | + label: str = "event", | |
| 14 | +) -> pd.DataFrame: | |
| 15 | + """Create event windows around each event date. | |
| 16 | + | |
| 17 | + Returns DataFrame with columns: | |
| 18 | + [event_date, relative_day, market, price, return, abnormal_return] | |
| 19 | + """ | |
| 20 | + rows = [] | |
| 21 | + idx = prices.index | |
| 22 | + event_dates_ts = [pd.Timestamp(d) for d in event_dates] | |
| 23 | + | |
| 24 | + for col in prices.columns: | |
| 25 | + series = prices[col].dropna() | |
| 26 | + if series.empty: | |
| 27 | + continue | |
| 28 | + returns = series.diff() | |
| 29 | + mean_return = returns.mean() | |
| 30 | + | |
| 31 | + for event_dt in event_dates_ts: | |
| 32 | + loc = idx.get_indexer([event_dt], method="nearest")[0] | |
| 33 | + if loc < 0 or loc >= len(idx): | |
| 34 | + continue | |
| 35 | + for rel_day in range(window[0], window[1] + 1): | |
| 36 | + abs_idx = loc + rel_day | |
| 37 | + if 0 <= abs_idx < len(idx): | |
| 38 | + date = idx[abs_idx] | |
| 39 | + price = series.iloc[abs_idx] if abs_idx < len(series) else np.nan | |
| 40 | + ret = returns.iloc[abs_idx] if abs_idx < len(returns) else np.nan | |
| 41 | + ar = ret - mean_return if not np.isnan(ret) else np.nan | |
| 42 | + rows.append({ | |
| 43 | + "event_date": event_dt, | |
| 44 | + "relative_day": rel_day, | |
| 45 | + "market": col, | |
| 46 | + "price": price, | |
| 47 | + "return": ret, | |
| 48 | + "abnormal_return": ar, | |
| 49 | + }) | |
| 50 | + | |
| 51 | + return pd.DataFrame(rows) | |
| 52 | + | |
| 53 | + | |
| 54 | +def compute_car(event_windows: pd.DataFrame) -> pd.DataFrame: | |
| 55 | + """Compute cumulative abnormal returns from event windows.""" | |
| 56 | + result = [] | |
| 57 | + for (event_date, market), group in event_windows.groupby(["event_date", "market"]): | |
| 58 | + group = group.sort_values("relative_day") | |
| 59 | + group = group.copy() | |
| 60 | + group["CAR"] = group["abnormal_return"].cumsum() | |
| 61 | + result.append(group) | |
| 62 | + if result: | |
| 63 | + return pd.concat(result, ignore_index=True) | |
| 64 | + return pd.DataFrame() | |
added
src/latex/__init__.py
+0 −0
added
src/models/__init__.py
+0 −0
added
src/models/arrow_debreu.py
+234 −0
@@ -0,0 +1,234 @@ | ||
| 1 | +"""Arrow-Debreu state-contingent securities analysis. | |
| 2 | + | |
| 3 | +Core theoretical contribution: tests for market completeness, | |
| 4 | +risk-neutral density extraction, moment computation, and arbitrage | |
| 5 | +violation detection using Polymarket state-price data. | |
| 6 | +""" | |
| 7 | + | |
| 8 | +from __future__ import annotations | |
| 9 | + | |
| 10 | +import pandas as pd | |
| 11 | +import numpy as np | |
| 12 | +from dataclasses import dataclass | |
| 13 | + | |
| 14 | + | |
| 15 | +# --------------------------------------------------------------------------- | |
| 16 | +# Data classes | |
| 17 | +# --------------------------------------------------------------------------- | |
| 18 | + | |
| 19 | +@dataclass | |
| 20 | +class CompletenessTestResult: | |
| 21 | + """Result container for the Arrow-Debreu completeness test.""" | |
| 22 | + daily_sums: pd.Series # sum of state prices each day | |
| 23 | + mean_sum: float # should be close to 1.0 | |
| 24 | + std_sum: float | |
| 25 | + mean_deviation: float # mean(sum - 1.0) | |
| 26 | + max_deviation: float | |
| 27 | + all_positive: bool # no negative state prices | |
| 28 | + discount_factor: float # implied 1/(1+r) from mean sum | |
| 29 | + | |
| 30 | + | |
| 31 | +# --------------------------------------------------------------------------- | |
| 32 | +# Completeness | |
| 33 | +# --------------------------------------------------------------------------- | |
| 34 | + | |
| 35 | +def test_market_completeness(state_prices: pd.DataFrame) -> CompletenessTestResult: | |
| 36 | + """Test Arrow-Debreu completeness conditions. | |
| 37 | + | |
| 38 | + Parameters | |
| 39 | + ---------- | |
| 40 | + state_prices : pd.DataFrame | |
| 41 | + 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. | |
| 44 | + | |
| 45 | + Returns | |
| 46 | + ------- | |
| 47 | + CompletenessTestResult | |
| 48 | + """ | |
| 49 | + daily_sums = state_prices.sum(axis=1) | |
| 50 | + deviations = daily_sums - 1.0 | |
| 51 | + | |
| 52 | + 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 sum | |
| 60 | + ) | |
| 61 | + | |
| 62 | + | |
| 63 | +# --------------------------------------------------------------------------- | |
| 64 | +# Risk-neutral density | |
| 65 | +# --------------------------------------------------------------------------- | |
| 66 | + | |
| 67 | +def 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. | |
| 72 | + | |
| 73 | + Normalises each row so prices sum to 1.0; the resulting values are | |
| 74 | + interpreted as risk-neutral probabilities. | |
| 75 | + | |
| 76 | + Parameters | |
| 77 | + ---------- | |
| 78 | + state_prices : pd.DataFrame | |
| 79 | + Columns = states, values = prices. | |
| 80 | + state_values : list[float] | |
| 81 | + Numerical value associated with each state (same order as columns). | |
| 82 | + | |
| 83 | + Returns | |
| 84 | + ------- | |
| 85 | + pd.DataFrame | |
| 86 | + 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 | + ) | |
| 93 | + | |
| 94 | + row_sums = state_prices.sum(axis=1) | |
| 95 | + # Avoid division by zero | |
| 96 | + row_sums = row_sums.replace(0, np.nan) | |
| 97 | + rn_density = state_prices.div(row_sums, axis=0) | |
| 98 | + return rn_density | |
| 99 | + | |
| 100 | + | |
| 101 | +# --------------------------------------------------------------------------- | |
| 102 | +# Risk-neutral moments | |
| 103 | +# --------------------------------------------------------------------------- | |
| 104 | + | |
| 105 | +def 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. | |
| 110 | + | |
| 111 | + Parameters | |
| 112 | + ---------- | |
| 113 | + rn_density : pd.DataFrame | |
| 114 | + Risk-neutral probabilities (rows sum to 1). | |
| 115 | + state_values : list[float] | |
| 116 | + Numerical values for each state (e.g., basis-point changes | |
| 117 | + [-50, -25, 0, 25, 50]). | |
| 118 | + | |
| 119 | + Returns | |
| 120 | + ------- | |
| 121 | + pd.DataFrame | |
| 122 | + Columns ``[mean, variance, skewness, kurtosis]`` indexed by date. | |
| 123 | + """ | |
| 124 | + vals = np.array(state_values, dtype=float) | |
| 125 | + probs = rn_density.values # (T, K) | |
| 126 | + | |
| 127 | + # Mean | |
| 128 | + mu = probs @ vals # (T,) | |
| 129 | + | |
| 130 | + # Central moments | |
| 131 | + 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)) | |
| 134 | + | |
| 135 | + # Protect against zero variance | |
| 136 | + sigma_safe = np.where(sigma > 0, sigma, np.nan) | |
| 137 | + skew = (probs * deviations ** 3).sum(axis=1) / sigma_safe ** 3 | |
| 138 | + kurt = (probs * deviations ** 4).sum(axis=1) / sigma_safe ** 4 | |
| 139 | + | |
| 140 | + return pd.DataFrame( | |
| 141 | + {"mean": mu, "variance": var, "skewness": skew, "kurtosis": kurt}, | |
| 142 | + index=rn_density.index, | |
| 143 | + ) | |
| 144 | + | |
| 145 | + | |
| 146 | +# --------------------------------------------------------------------------- | |
| 147 | +# Arbitrage violations | |
| 148 | +# --------------------------------------------------------------------------- | |
| 149 | + | |
| 150 | +def detect_arbitrage_violations( | |
| 151 | + prices: pd.DataFrame, | |
| 152 | + market_type: str = "binary", | |
| 153 | +) -> pd.DataFrame: | |
| 154 | + """Detect no-arbitrage violations. | |
| 155 | + | |
| 156 | + For binary markets: p(Yes) + p(No) should equal 1. | |
| 157 | + For state-space groups: sum of state prices should be approximately 1. | |
| 158 | + | |
| 159 | + Parameters | |
| 160 | + ---------- | |
| 161 | + prices : pd.DataFrame | |
| 162 | + Price data (columns = states or Yes/No). | |
| 163 | + market_type : str | |
| 164 | + ``"binary"`` or ``"state_space"``. | |
| 165 | + | |
| 166 | + Returns | |
| 167 | + ------- | |
| 168 | + pd.DataFrame | |
| 169 | + Columns ``[date, violation_size, violation_pct]``. | |
| 170 | + """ | |
| 171 | + row_sums = prices.sum(axis=1) | |
| 172 | + deviation = (row_sums - 1.0).abs() | |
| 173 | + | |
| 174 | + 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 result | |
| 181 | + | |
| 182 | + | |
| 183 | +# --------------------------------------------------------------------------- | |
| 184 | +# State-price dynamics | |
| 185 | +# --------------------------------------------------------------------------- | |
| 186 | + | |
| 187 | +def state_price_dynamics(state_prices: pd.DataFrame) -> dict: | |
| 188 | + """Analyse dynamics of state prices over time. | |
| 189 | + | |
| 190 | + Returns | |
| 191 | + ------- | |
| 192 | + dict | |
| 193 | + ``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 matrix | |
| 198 | + corr_matrix = state_prices.corr() | |
| 199 | + | |
| 200 | + # AR(1) coefficients: regress p_t on p_{t-1} for each column | |
| 201 | + 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.nan | |
| 207 | + half_lives[col] = np.nan | |
| 208 | + continue | |
| 209 | + | |
| 210 | + y = series.iloc[1:].values | |
| 211 | + x = series.iloc[:-1].values | |
| 212 | + # 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.nan | |
| 218 | + half_lives[col] = np.nan | |
| 219 | + continue | |
| 220 | + | |
| 221 | + phi = (x_dm * y_dm).sum() / var_x | |
| 222 | + persistence[col] = float(phi) | |
| 223 | + | |
| 224 | + # Half-life: t_{1/2} = -ln(2) / ln(|phi|), defined when |phi| < 1 | |
| 225 | + if 0 < abs(phi) < 1: | |
| 226 | + half_lives[col] = float(-np.log(2) / np.log(abs(phi))) | |
| 227 | + else: | |
| 228 | + half_lives[col] = np.nan | |
| 229 | + | |
| 230 | + return { | |
| 231 | + "correlation_matrix": corr_matrix, | |
| 232 | + "persistence": pd.Series(persistence), | |
| 233 | + "mean_reversion_speed": pd.Series(half_lives), | |
| 234 | + } | |
added
src/models/event_study.py
+211 −0
@@ -0,0 +1,211 @@ | ||
| 1 | +"""High-frequency event study around macro events. | |
| 2 | + | |
| 3 | +Implements the event-study methodology of MacKinlay (1997) with | |
| 4 | +Patell (1976) and Boehmer-Musumeci-Poulsen (1991) test statistics. | |
| 5 | +""" | |
| 6 | + | |
| 7 | +from __future__ import annotations | |
| 8 | + | |
| 9 | +import pandas as pd | |
| 10 | +import numpy as np | |
| 11 | +from scipy import stats | |
| 12 | +from dataclasses import dataclass | |
| 13 | + | |
| 14 | + | |
| 15 | +# --------------------------------------------------------------------------- | |
| 16 | +# Data classes | |
| 17 | +# --------------------------------------------------------------------------- | |
| 18 | + | |
| 19 | +@dataclass | |
| 20 | +class EventStudyResult: | |
| 21 | + """Container for event-study output.""" | |
| 22 | + cars: pd.DataFrame # [event_date, window, CAR] | |
| 23 | + test_stats: pd.DataFrame # [window, mean_CAR, t_stat, p_value] | |
| 24 | + event_type: str | |
| 25 | + n_events: int | |
| 26 | + | |
| 27 | + | |
| 28 | +# --------------------------------------------------------------------------- | |
| 29 | +# Core event study | |
| 30 | +# --------------------------------------------------------------------------- | |
| 31 | + | |
| 32 | +def run_event_study( | |
| 33 | + prices: pd.DataFrame, | |
| 34 | + event_dates: list, | |
| 35 | + event_type: str = "FOMC", | |
| 36 | + window: tuple[int, int] = (-5, 5), | |
| 37 | + estimation_window: int = 60, | |
| 38 | +) -> EventStudyResult: | |
| 39 | + """Event study methodology following MacKinlay (1997). | |
| 40 | + | |
| 41 | + 1. For each event date, find the row in *prices* closest to that date. | |
| 42 | + 2. Estimation window: ``[t - estimation_window, t + window[0] - 1]`` | |
| 43 | + for the normal-return (constant-mean) model. | |
| 44 | + 3. Event window: ``[t + window[0], t + window[1]]``. | |
| 45 | + 4. Abnormal return = actual return - expected return. | |
| 46 | + 5. CAR = cumulative sum of abnormal returns in the event window. | |
| 47 | + 6. Test statistics: cross-sectional *t*-test of CARs. | |
| 48 | + | |
| 49 | + Parameters | |
| 50 | + ---------- | |
| 51 | + prices : pd.DataFrame | |
| 52 | + Each column is a market's price series with a DatetimeIndex. | |
| 53 | + event_dates : list | |
| 54 | + Dates (or date-like objects) of the events. | |
| 55 | + event_type : str | |
| 56 | + Label for the event category (e.g. ``"FOMC"``). | |
| 57 | + window : tuple[int, int] | |
| 58 | + (pre, post) trading-day offsets around each event. | |
| 59 | + estimation_window : int | |
| 60 | + Number of trading days used to estimate the normal-return model. | |
| 61 | + | |
| 62 | + Returns | |
| 63 | + ------- | |
| 64 | + EventStudyResult | |
| 65 | + """ | |
| 66 | + # Compute simple returns | |
| 67 | + returns = prices.pct_change().dropna(how="all") | |
| 68 | + | |
| 69 | + # Ensure DatetimeIndex | |
| 70 | + if not isinstance(returns.index, pd.DatetimeIndex): | |
| 71 | + returns.index = pd.to_datetime(returns.index) | |
| 72 | + | |
| 73 | + event_dates_dt = pd.to_datetime(event_dates) | |
| 74 | + | |
| 75 | + all_cars: list[dict] = [] | |
| 76 | + all_ars: list[pd.DataFrame] = [] | |
| 77 | + | |
| 78 | + for ev_date in event_dates_dt: | |
| 79 | + # Find closest available trading day | |
| 80 | + idx = returns.index.searchsorted(ev_date) | |
| 81 | + if idx >= len(returns.index): | |
| 82 | + idx = len(returns.index) - 1 | |
| 83 | + t0 = idx # position index | |
| 84 | + | |
| 85 | + est_start = t0 - estimation_window + window[0] - 1 | |
| 86 | + est_end = t0 + window[0] - 1 | |
| 87 | + ev_start = t0 + window[0] | |
| 88 | + ev_end = t0 + window[1] | |
| 89 | + | |
| 90 | + # Boundary checks | |
| 91 | + if est_start < 0 or ev_end >= len(returns): | |
| 92 | + continue | |
| 93 | + | |
| 94 | + # Estimation-window returns → constant-mean model | |
| 95 | + est_returns = returns.iloc[est_start:est_end + 1] | |
| 96 | + expected = est_returns.mean() # per column | |
| 97 | + est_std = est_returns.std() | |
| 98 | + | |
| 99 | + # Event-window abnormal returns | |
| 100 | + ev_returns = returns.iloc[ev_start:ev_end + 1] | |
| 101 | + ar = ev_returns.subtract(expected, axis=1) | |
| 102 | + car = ar.cumsum() | |
| 103 | + | |
| 104 | + # Store CAR at end of window (last row) | |
| 105 | + final_car = car.iloc[-1] | |
| 106 | + for col in final_car.index: | |
| 107 | + all_cars.append({ | |
| 108 | + "event_date": ev_date, | |
| 109 | + "market": col, | |
| 110 | + "window": f"[{window[0]},{window[1]}]", | |
| 111 | + "CAR": final_car[col], | |
| 112 | + }) | |
| 113 | + | |
| 114 | + all_ars.append(ar) | |
| 115 | + | |
| 116 | + cars_df = pd.DataFrame(all_cars) | |
| 117 | + n_events = int(cars_df["event_date"].nunique()) if len(cars_df) > 0 else 0 | |
| 118 | + | |
| 119 | + # Cross-sectional test statistics per market | |
| 120 | + test_rows: list[dict] = [] | |
| 121 | + if len(cars_df) > 0: | |
| 122 | + for market, grp in cars_df.groupby("market"): | |
| 123 | + car_vals = grp["CAR"].dropna() | |
| 124 | + if len(car_vals) < 2: | |
| 125 | + continue | |
| 126 | + t_stat, p_val = stats.ttest_1samp(car_vals, 0) | |
| 127 | + test_rows.append({ | |
| 128 | + "market": market, | |
| 129 | + "window": f"[{window[0]},{window[1]}]", | |
| 130 | + "mean_CAR": float(car_vals.mean()), | |
| 131 | + "t_stat": float(t_stat), | |
| 132 | + "p_value": float(p_val), | |
| 133 | + "n_events": len(car_vals), | |
| 134 | + }) | |
| 135 | + | |
| 136 | + test_df = pd.DataFrame(test_rows) | |
| 137 | + | |
| 138 | + return EventStudyResult( | |
| 139 | + cars=cars_df, | |
| 140 | + test_stats=test_df, | |
| 141 | + event_type=event_type, | |
| 142 | + n_events=n_events, | |
| 143 | + ) | |
| 144 | + | |
| 145 | + | |
| 146 | +# --------------------------------------------------------------------------- | |
| 147 | +# Patell (1976) test | |
| 148 | +# --------------------------------------------------------------------------- | |
| 149 | + | |
| 150 | +def compute_patell_test( | |
| 151 | + abnormal_returns: pd.DataFrame, | |
| 152 | + std_errors: pd.Series, | |
| 153 | +) -> float: | |
| 154 | + """Patell (1976) standardised-residual test. | |
| 155 | + | |
| 156 | + Parameters | |
| 157 | + ---------- | |
| 158 | + abnormal_returns : pd.DataFrame | |
| 159 | + Abnormal returns (events x time). | |
| 160 | + std_errors : pd.Series | |
| 161 | + Estimation-window standard errors for each event. | |
| 162 | + | |
| 163 | + Returns | |
| 164 | + ------- | |
| 165 | + float | |
| 166 | + Patell *z*-statistic. | |
| 167 | + """ | |
| 168 | + # Standardise each event's AR by its estimation-window std error | |
| 169 | + sar = abnormal_returns.div(std_errors, axis=0) | |
| 170 | + # Cumulative standardised AR | |
| 171 | + csar = sar.sum(axis=1) # sum across event window per event | |
| 172 | + n = len(csar) | |
| 173 | + if n == 0: | |
| 174 | + return np.nan | |
| 175 | + z = csar.mean() / (csar.std() / np.sqrt(n)) | |
| 176 | + return float(z) | |
| 177 | + | |
| 178 | + | |
| 179 | +# --------------------------------------------------------------------------- | |
| 180 | +# BMP (1991) test | |
| 181 | +# --------------------------------------------------------------------------- | |
| 182 | + | |
| 183 | +def compute_bmp_test( | |
| 184 | + standardized_ars: pd.DataFrame, | |
| 185 | +) -> tuple[float, float]: | |
| 186 | + """Boehmer, Musumeci, Poulsen (1991) test for event-induced variance. | |
| 187 | + | |
| 188 | + Uses cross-sectional variance of standardised abnormal returns to | |
| 189 | + produce a *t*-statistic robust to event-induced variance changes. | |
| 190 | + | |
| 191 | + Parameters | |
| 192 | + ---------- | |
| 193 | + standardized_ars : pd.DataFrame | |
| 194 | + Standardised abnormal returns (events x time-steps in window). | |
| 195 | + | |
| 196 | + Returns | |
| 197 | + ------- | |
| 198 | + tuple[float, float] | |
| 199 | + (BMP *t*-statistic, two-sided *p*-value). | |
| 200 | + """ | |
| 201 | + # Average SAR across the event window for each event | |
| 202 | + avg_sar = standardized_ars.mean(axis=1) | |
| 203 | + n = len(avg_sar) | |
| 204 | + if n < 2: | |
| 205 | + return (np.nan, np.nan) | |
| 206 | + | |
| 207 | + mean_sar = avg_sar.mean() | |
| 208 | + se = avg_sar.std(ddof=1) / np.sqrt(n) | |
| 209 | + t_stat = mean_sar / se if se > 0 else np.nan | |
| 210 | + p_value = float(2 * stats.t.sf(abs(t_stat), df=n - 1)) if not np.isnan(t_stat) else np.nan | |
| 211 | + return (float(t_stat), p_value) | |
added
src/models/liquidity.py
+126 −0
@@ -0,0 +1,126 @@ | ||
| 1 | +"""Liquidity and market quality analysis.""" | |
| 2 | + | |
| 3 | +from __future__ import annotations | |
| 4 | + | |
| 5 | +import json | |
| 6 | + | |
| 7 | +import numpy as np | |
| 8 | +import pandas as pd | |
| 9 | + | |
| 10 | + | |
| 11 | +def analyze_volume_patterns( | |
| 12 | + volume: pd.Series, | |
| 13 | + event_dates: list | None = None, | |
| 14 | +) -> pd.DataFrame: | |
| 15 | + """Analyze volume patterns: trends, day-of-week effects, event-day spikes.""" | |
| 16 | + stats_data = { | |
| 17 | + "mean_volume": volume.mean(), | |
| 18 | + "median_volume": volume.median(), | |
| 19 | + "std_volume": volume.std(), | |
| 20 | + "max_volume": volume.max(), | |
| 21 | + "total_volume": volume.sum(), | |
| 22 | + } | |
| 23 | + | |
| 24 | + if hasattr(volume.index, "dayofweek"): | |
| 25 | + dow = volume.groupby(volume.index.dayofweek).mean() | |
| 26 | + for d, label in enumerate(["Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"]): | |
| 27 | + if d in dow.index: | |
| 28 | + stats_data[f"avg_volume_{label}"] = dow[d] | |
| 29 | + | |
| 30 | + if event_dates: | |
| 31 | + event_ts = pd.to_datetime(event_dates) | |
| 32 | + event_vol = volume[volume.index.isin(event_ts)] | |
| 33 | + non_event_vol = volume[~volume.index.isin(event_ts)] | |
| 34 | + if len(event_vol) > 0 and len(non_event_vol) > 0: | |
| 35 | + stats_data["avg_event_day_volume"] = event_vol.mean() | |
| 36 | + stats_data["avg_non_event_volume"] = non_event_vol.mean() | |
| 37 | + stats_data["event_volume_ratio"] = event_vol.mean() / non_event_vol.mean() | |
| 38 | + | |
| 39 | + return pd.DataFrame([stats_data]) | |
| 40 | + | |
| 41 | + | |
| 42 | +def compute_concentration_metrics(market_volumes: pd.DataFrame) -> pd.DataFrame: | |
| 43 | + """Compute HHI and share metrics across markets.""" | |
| 44 | + total = market_volumes.sum(axis=1) | |
| 45 | + shares = market_volumes.div(total, axis=0) | |
| 46 | + | |
| 47 | + hhi = (shares ** 2).sum(axis=1) | |
| 48 | + top1 = shares.max(axis=1) | |
| 49 | + top3 = shares.apply(lambda row: row.nlargest(3).sum(), axis=1) | |
| 50 | + | |
| 51 | + return pd.DataFrame({ | |
| 52 | + "hhi": hhi, | |
| 53 | + "top1_share": top1, | |
| 54 | + "top3_share": top3, | |
| 55 | + }) | |
| 56 | + | |
| 57 | + | |
| 58 | +def estimate_liquidity_regressions( | |
| 59 | + spreads: pd.Series, | |
| 60 | + volume: pd.Series, | |
| 61 | + volatility: pd.Series, | |
| 62 | + event_dummy: pd.Series | None = None, | |
| 63 | +) -> dict: | |
| 64 | + """Regress spread on volume, volatility, and event dummies.""" | |
| 65 | + import statsmodels.api as sm | |
| 66 | + | |
| 67 | + X = pd.DataFrame({"volume": volume, "volatility": volatility}) | |
| 68 | + if event_dummy is not None: | |
| 69 | + X["event"] = event_dummy | |
| 70 | + X = sm.add_constant(X) | |
| 71 | + | |
| 72 | + valid = pd.concat([spreads.rename("spread"), X], axis=1).dropna() | |
| 73 | + if len(valid) < 10: | |
| 74 | + return {"params": pd.Series(), "summary": "Insufficient data"} | |
| 75 | + | |
| 76 | + y = valid["spread"] | |
| 77 | + X_clean = valid.drop(columns=["spread"]) | |
| 78 | + | |
| 79 | + model = sm.OLS(y, X_clean).fit(cov_type="HAC", cov_kwds={"maxlags": 5}) | |
| 80 | + | |
| 81 | + results_df = pd.DataFrame({ | |
| 82 | + "variable": model.params.index, | |
| 83 | + "coefficient": model.params.values, | |
| 84 | + "std_error": model.bse.values, | |
| 85 | + "t_stat": model.tvalues.values, | |
| 86 | + "p_value": model.pvalues.values, | |
| 87 | + }) | |
| 88 | + | |
| 89 | + return { | |
| 90 | + "params": model.params, | |
| 91 | + "results_df": results_df, | |
| 92 | + "r_squared": model.rsquared, | |
| 93 | + "adj_r_squared": model.rsquared_adj, | |
| 94 | + "summary": str(model.summary()), | |
| 95 | + } | |
| 96 | + | |
| 97 | + | |
| 98 | +def market_quality_from_orderbook(order_book: dict) -> dict: | |
| 99 | + """Extract market quality metrics from a single order book snapshot.""" | |
| 100 | + bids = order_book.get("bids", []) | |
| 101 | + asks = order_book.get("asks", []) | |
| 102 | + | |
| 103 | + if not bids or not asks: | |
| 104 | + return { | |
| 105 | + "quoted_spread": np.nan, | |
| 106 | + "midpoint": np.nan, | |
| 107 | + "depth_at_best": np.nan, | |
| 108 | + "total_depth": np.nan, | |
| 109 | + "depth_imbalance": np.nan, | |
| 110 | + } | |
| 111 | + | |
| 112 | + best_bid = float(bids[0].get("price", 0)) | |
| 113 | + best_ask = float(asks[0].get("price", 0)) | |
| 114 | + midpoint = (best_bid + best_ask) / 2 | |
| 115 | + | |
| 116 | + bid_depth = sum(float(b.get("size", 0)) for b in bids) | |
| 117 | + ask_depth = sum(float(a.get("size", 0)) for a in asks) | |
| 118 | + total_depth = bid_depth + ask_depth | |
| 119 | + | |
| 120 | + return { | |
| 121 | + "quoted_spread": best_ask - best_bid, | |
| 122 | + "midpoint": midpoint, | |
| 123 | + "depth_at_best": float(bids[0].get("size", 0)) + float(asks[0].get("size", 0)), | |
| 124 | + "total_depth": total_depth, | |
| 125 | + "depth_imbalance": (ask_depth - bid_depth) / total_depth if total_depth > 0 else np.nan, | |
| 126 | + } | |
added
src/models/price_discovery.py
+173 −0
@@ -0,0 +1,173 @@ | ||
| 1 | +"""Information shares, Granger causality, and VAR-based price discovery.""" | |
| 2 | + | |
| 3 | +from __future__ import annotations | |
| 4 | + | |
| 5 | +import numpy as np | |
| 6 | +import pandas as pd | |
| 7 | +from scipy import stats | |
| 8 | +from statsmodels.tsa.api import VAR | |
| 9 | +from statsmodels.tsa.stattools import grangercausalitytests, adfuller | |
| 10 | + | |
| 11 | + | |
| 12 | +def granger_causality_matrix( | |
| 13 | + data: pd.DataFrame, | |
| 14 | + max_lag: int = 10, | |
| 15 | +) -> tuple[pd.DataFrame, pd.DataFrame]: | |
| 16 | + """Pairwise Granger causality F-tests. | |
| 17 | + | |
| 18 | + Returns (f_stats, p_values) DataFrames indexed by caused, columns by causing. | |
| 19 | + """ | |
| 20 | + cols = data.columns.tolist() | |
| 21 | + f_mat = pd.DataFrame(np.nan, index=cols, columns=cols) | |
| 22 | + p_mat = pd.DataFrame(np.nan, index=cols, columns=cols) | |
| 23 | + | |
| 24 | + clean = data.dropna() | |
| 25 | + if len(clean) < max_lag + 5: | |
| 26 | + return f_mat, p_mat | |
| 27 | + | |
| 28 | + for caused in cols: | |
| 29 | + for causing in cols: | |
| 30 | + if caused == causing: | |
| 31 | + continue | |
| 32 | + try: | |
| 33 | + result = grangercausalitytests( | |
| 34 | + clean[[caused, causing]].values, maxlag=max_lag, verbose=False | |
| 35 | + ) | |
| 36 | + best_lag = min(result.keys(), key=lambda k: result[k][0]["ssr_ftest"][1]) | |
| 37 | + f_stat = result[best_lag][0]["ssr_ftest"][0] | |
| 38 | + p_val = result[best_lag][0]["ssr_ftest"][1] | |
| 39 | + f_mat.loc[caused, causing] = f_stat | |
| 40 | + p_mat.loc[caused, causing] = p_val | |
| 41 | + except Exception: | |
| 42 | + pass | |
| 43 | + | |
| 44 | + return f_mat, p_mat | |
| 45 | + | |
| 46 | + | |
| 47 | +def estimate_var( | |
| 48 | + data: pd.DataFrame, | |
| 49 | + max_lags: int = 10, | |
| 50 | + ic: str = "aic", | |
| 51 | +) -> dict: | |
| 52 | + """Estimate VAR model with optimal lag selection. | |
| 53 | + | |
| 54 | + Returns dict with 'model', 'optimal_lag', 'coefficients', 'irf', 'summary'. | |
| 55 | + """ | |
| 56 | + clean = data.dropna() | |
| 57 | + if len(clean) < max_lags + 10: | |
| 58 | + return {"model": None, "optimal_lag": 0, "coefficients": {}, "irf": {}} | |
| 59 | + | |
| 60 | + model = VAR(clean) | |
| 61 | + result = model.fit(maxlags=max_lags, ic=ic) | |
| 62 | + | |
| 63 | + coefficients = {} | |
| 64 | + params = result.params | |
| 65 | + if isinstance(params, pd.DataFrame): | |
| 66 | + for col in params.columns: | |
| 67 | + coefficients[col] = params[col] | |
| 68 | + else: | |
| 69 | + for col_idx, col in enumerate(data.columns): | |
| 70 | + coefficients[col] = pd.Series(params[:, col_idx]) | |
| 71 | + | |
| 72 | + irf_data = {} | |
| 73 | + try: | |
| 74 | + irf_result = result.irf(periods=20) | |
| 75 | + for i, shock in enumerate(data.columns): | |
| 76 | + for j, resp in enumerate(data.columns): | |
| 77 | + irf_data[(shock, resp)] = irf_result.irfs[:, j, i] | |
| 78 | + except Exception: | |
| 79 | + pass | |
| 80 | + | |
| 81 | + return { | |
| 82 | + "model": result, | |
| 83 | + "optimal_lag": result.k_ar, | |
| 84 | + "coefficients": coefficients, | |
| 85 | + "irf": irf_data, | |
| 86 | + "summary": str(result.summary()), | |
| 87 | + } | |
| 88 | + | |
| 89 | + | |
| 90 | +def compute_impulse_responses(var_result: dict, periods: int = 20) -> dict: | |
| 91 | + """Extract impulse response functions from VAR result.""" | |
| 92 | + if var_result.get("model") is None: | |
| 93 | + return {} | |
| 94 | + irf = var_result["model"].irf(periods=periods) | |
| 95 | + data = {} | |
| 96 | + names = var_result["model"].names | |
| 97 | + for i, shock in enumerate(names): | |
| 98 | + for j, resp in enumerate(names): | |
| 99 | + data[(shock, resp)] = irf.irfs[:, j, i] | |
| 100 | + return data | |
| 101 | + | |
| 102 | + | |
| 103 | +def lead_lag_regression( | |
| 104 | + y: pd.Series, | |
| 105 | + x: pd.Series, | |
| 106 | + leads: range = range(-5, 6), | |
| 107 | +) -> pd.DataFrame: | |
| 108 | + """Cross-correlation and lead-lag OLS regressions. | |
| 109 | + | |
| 110 | + For each lag k: regress y_t on x_{t-k}. | |
| 111 | + """ | |
| 112 | + results = [] | |
| 113 | + for k in leads: | |
| 114 | + x_shifted = x.shift(k) | |
| 115 | + valid = pd.DataFrame({"y": y, "x": x_shifted}).dropna() | |
| 116 | + if len(valid) < 5: | |
| 117 | + results.append({"lag": k, "coefficient": np.nan, "t_stat": np.nan, "r_squared": np.nan}) | |
| 118 | + continue | |
| 119 | + | |
| 120 | + x_vals = valid["x"].values | |
| 121 | + y_vals = valid["y"].values | |
| 122 | + x_dm = x_vals - x_vals.mean() | |
| 123 | + y_dm = y_vals - y_vals.mean() | |
| 124 | + var_x = (x_dm ** 2).sum() | |
| 125 | + if var_x == 0: | |
| 126 | + results.append({"lag": k, "coefficient": np.nan, "t_stat": np.nan, "r_squared": np.nan}) | |
| 127 | + continue | |
| 128 | + | |
| 129 | + beta = (x_dm * y_dm).sum() / var_x | |
| 130 | + residuals = y_dm - beta * x_dm | |
| 131 | + se_beta = np.sqrt((residuals ** 2).sum() / ((len(valid) - 2) * var_x)) | |
| 132 | + t_stat = beta / se_beta if se_beta > 0 else np.nan | |
| 133 | + ss_res = (residuals ** 2).sum() | |
| 134 | + ss_tot = (y_dm ** 2).sum() | |
| 135 | + r_sq = 1 - ss_res / ss_tot if ss_tot > 0 else np.nan | |
| 136 | + | |
| 137 | + results.append({"lag": k, "coefficient": beta, "t_stat": t_stat, "r_squared": r_sq}) | |
| 138 | + | |
| 139 | + return pd.DataFrame(results) | |
| 140 | + | |
| 141 | + | |
| 142 | +def hasbrouck_information_shares( | |
| 143 | + prices: pd.DataFrame, | |
| 144 | + max_lags: int = 5, | |
| 145 | +) -> pd.DataFrame: | |
| 146 | + """Hasbrouck (1995) information shares via variance decomposition. | |
| 147 | + | |
| 148 | + Uses VAR on returns and computes forecast error variance decomposition | |
| 149 | + as a proxy for information shares when series may not be cointegrated. | |
| 150 | + """ | |
| 151 | + returns = prices.pct_change().dropna() | |
| 152 | + clean = returns.dropna() | |
| 153 | + if len(clean) < max_lags + 10: | |
| 154 | + return pd.DataFrame({"market": prices.columns, "info_share": np.nan}) | |
| 155 | + | |
| 156 | + try: | |
| 157 | + model = VAR(clean) | |
| 158 | + result = model.fit(maxlags=max_lags, ic="aic") | |
| 159 | + fevd = result.fevd(periods=10) | |
| 160 | + decomp = fevd.decomp | |
| 161 | + shares = decomp[-1] | |
| 162 | + info_shares = pd.DataFrame( | |
| 163 | + shares, | |
| 164 | + index=prices.columns, | |
| 165 | + columns=prices.columns, | |
| 166 | + ) | |
| 167 | + diag_shares = pd.Series( | |
| 168 | + {col: info_shares.loc[col, col] for col in prices.columns}, | |
| 169 | + name="info_share", | |
| 170 | + ) | |
| 171 | + return diag_shares.to_frame().reset_index().rename(columns={"index": "market"}) | |
| 172 | + except Exception: | |
| 173 | + return pd.DataFrame({"market": prices.columns.tolist(), "info_share": [np.nan] * len(prices.columns)}) | |
added
src/models/volatility.py
+128 −0
@@ -0,0 +1,128 @@ | ||
| 1 | +"""Volatility modeling for prediction market prices.""" | |
| 2 | + | |
| 3 | +from __future__ import annotations | |
| 4 | + | |
| 5 | +from dataclasses import dataclass | |
| 6 | + | |
| 7 | +import numpy as np | |
| 8 | +import pandas as pd | |
| 9 | +from arch import arch_model | |
| 10 | + | |
| 11 | + | |
| 12 | +@dataclass | |
| 13 | +class GARCHResult: | |
| 14 | + model_type: str | |
| 15 | + params: pd.Series | |
| 16 | + std_errors: pd.Series | |
| 17 | + log_likelihood: float | |
| 18 | + aic: float | |
| 19 | + bic: float | |
| 20 | + conditional_volatility: pd.Series | |
| 21 | + | |
| 22 | + | |
| 23 | +@dataclass | |
| 24 | +class HARResult: | |
| 25 | + coefficients: pd.Series | |
| 26 | + std_errors: pd.Series | |
| 27 | + t_stats: pd.Series | |
| 28 | + r_squared: float | |
| 29 | + adj_r_squared: float | |
| 30 | + | |
| 31 | + | |
| 32 | +def compute_realized_volatility_measures( | |
| 33 | + returns: pd.Series, | |
| 34 | + windows: list[int] | None = None, | |
| 35 | +) -> pd.DataFrame: | |
| 36 | + """Compute realized volatility, bipower variation, and jump component.""" | |
| 37 | + if windows is None: | |
| 38 | + windows = [1, 5, 22] | |
| 39 | + result = pd.DataFrame(index=returns.index) | |
| 40 | + | |
| 41 | + for w in windows: | |
| 42 | + result[f"rv_{w}"] = (returns ** 2).rolling(w).sum().apply(np.sqrt) | |
| 43 | + | |
| 44 | + abs_ret = returns.abs() | |
| 45 | + result["bv_1"] = (abs_ret * abs_ret.shift(1)).rolling(5).mean() * (np.pi / 2) | |
| 46 | + result["jump_1"] = np.maximum(result["rv_5"] ** 2 - result["bv_1"], 0) | |
| 47 | + | |
| 48 | + return result | |
| 49 | + | |
| 50 | + | |
| 51 | +def estimate_har_rv( | |
| 52 | + rv_series: pd.DataFrame, | |
| 53 | + exog: pd.DataFrame | None = None, | |
| 54 | +) -> HARResult: | |
| 55 | + """HAR-RV model (Corsi 2009). | |
| 56 | + | |
| 57 | + RV_{t+1} = c + b_d * RV_t + b_w * RV_t^(w) + b_m * RV_t^(m) + e | |
| 58 | + """ | |
| 59 | + import statsmodels.api as sm | |
| 60 | + | |
| 61 | + if "rv_1" not in rv_series.columns: | |
| 62 | + raise ValueError("rv_series must contain 'rv_1' column") | |
| 63 | + | |
| 64 | + rv_d = rv_series["rv_1"] | |
| 65 | + rv_w = rv_series.get("rv_5", rv_d.rolling(5).mean()) | |
| 66 | + rv_m = rv_series.get("rv_22", rv_d.rolling(22).mean()) | |
| 67 | + | |
| 68 | + y = rv_d.shift(-1) | |
| 69 | + X = pd.DataFrame({"rv_d": rv_d, "rv_w": rv_w, "rv_m": rv_m}) | |
| 70 | + if exog is not None: | |
| 71 | + for col in exog.columns: | |
| 72 | + X[col] = exog[col] | |
| 73 | + | |
| 74 | + X = sm.add_constant(X) | |
| 75 | + valid = pd.concat([y, X], axis=1).dropna() | |
| 76 | + if len(valid) < 10: | |
| 77 | + return HARResult( | |
| 78 | + coefficients=pd.Series(), std_errors=pd.Series(), | |
| 79 | + t_stats=pd.Series(), r_squared=np.nan, adj_r_squared=np.nan, | |
| 80 | + ) | |
| 81 | + | |
| 82 | + y_clean = valid.iloc[:, 0] | |
| 83 | + X_clean = valid.iloc[:, 1:] | |
| 84 | + | |
| 85 | + model = sm.OLS(y_clean, X_clean).fit(cov_type="HAC", cov_kwds={"maxlags": 5}) | |
| 86 | + | |
| 87 | + return HARResult( | |
| 88 | + coefficients=model.params, | |
| 89 | + std_errors=model.bse, | |
| 90 | + t_stats=model.tvalues, | |
| 91 | + r_squared=model.rsquared, | |
| 92 | + adj_r_squared=model.rsquared_adj, | |
| 93 | + ) | |
| 94 | + | |
| 95 | + | |
| 96 | +def estimate_garch( | |
| 97 | + returns: pd.Series, | |
| 98 | + model_type: str = "GARCH", | |
| 99 | + p: int = 1, | |
| 100 | + q: int = 1, | |
| 101 | + dist: str = "normal", | |
| 102 | +) -> GARCHResult: | |
| 103 | + """Estimate GARCH-family model using the arch package.""" | |
| 104 | + clean = returns.dropna() | |
| 105 | + scaled = clean * 100 | |
| 106 | + | |
| 107 | + vol_spec = model_type | |
| 108 | + if model_type == "GJR-GARCH": | |
| 109 | + vol_spec = "GARCH" | |
| 110 | + o = 1 | |
| 111 | + else: | |
| 112 | + o = 0 | |
| 113 | + | |
| 114 | + am = arch_model(scaled, vol=vol_spec, p=p, o=o, q=q, dist=dist, mean="Constant") | |
| 115 | + result = am.fit(disp="off") | |
| 116 | + | |
| 117 | + cond_vol = result.conditional_volatility / 100 | |
| 118 | + cond_vol.index = clean.index | |
| 119 | + | |
| 120 | + return GARCHResult( | |
| 121 | + model_type=model_type, | |
| 122 | + params=result.params, | |
| 123 | + std_errors=result.std_err, | |
| 124 | + log_likelihood=result.loglikelihood, | |
| 125 | + aic=result.aic, | |
| 126 | + bic=result.bic, | |
| 127 | + conditional_volatility=cond_vol, | |
| 128 | + ) | |
added
src/plots/__init__.py
+0 −0
added
src/plots/figures.py
+300 −0
@@ -0,0 +1,300 @@ | ||
| 1 | +"""Publication-quality figures for the Polymarket Arrow-Debreu paper.""" | |
| 2 | + | |
| 3 | +from __future__ import annotations | |
| 4 | + | |
| 5 | +from datetime import datetime | |
| 6 | + | |
| 7 | +import matplotlib.pyplot as plt | |
| 8 | +import matplotlib.dates as mdates | |
| 9 | +import numpy as np | |
| 10 | +import pandas as pd | |
| 11 | + | |
| 12 | +from src.config import ELECTION_KEY_DATES, JOF_COLORS | |
| 13 | +from src.plots.style import COLORS, save_figure, set_jof_style | |
| 14 | + | |
| 15 | + | |
| 16 | +def 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 fig | |
| 32 | + | |
| 33 | + | |
| 34 | +def 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 fig | |
| 49 | + | |
| 50 | + | |
| 51 | +def 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.index | |
| 61 | + 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] | |
| 64 | + | |
| 65 | + 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) | |
| 75 | + | |
| 76 | + 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 fig | |
| 82 | + | |
| 83 | + | |
| 84 | +def 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] | |
| 94 | + | |
| 95 | + 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"].values | |
| 99 | + if "std_CAR" in cars.columns: | |
| 100 | + std_car = cars["std_CAR"].values | |
| 101 | + 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) | |
| 110 | + | |
| 111 | + axes[0].set_ylabel("Cumulative Abnormal Return") | |
| 112 | + fig.suptitle("Event Study: Cumulative Abnormal Returns", y=1.02) | |
| 113 | + fig.tight_layout() | |
| 114 | + return fig | |
| 115 | + | |
| 116 | + | |
| 117 | +def 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 fig | |
| 137 | + | |
| 138 | + | |
| 139 | +def 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) | |
| 154 | + | |
| 155 | + 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") | |
| 169 | + | |
| 170 | + fig.suptitle("Impulse Response Functions", y=1.02) | |
| 171 | + fig.tight_layout() | |
| 172 | + return fig | |
| 173 | + | |
| 174 | + | |
| 175 | +def 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] | |
| 186 | + | |
| 187 | + panel = 0 | |
| 188 | + 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 += 1 | |
| 192 | + | |
| 193 | + 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 += 1 | |
| 198 | + | |
| 199 | + 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() | |
| 205 | + | |
| 206 | + 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 fig | |
| 211 | + | |
| 212 | + | |
| 213 | +def 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] | |
| 220 | + | |
| 221 | + 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") | |
| 224 | + | |
| 225 | + 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)") | |
| 229 | + | |
| 230 | + 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 fig | |
| 235 | + | |
| 236 | + | |
| 237 | +def 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 None | |
| 244 | + fig, ax1 = plt.subplots(figsize=(8, 4.5)) | |
| 245 | + | |
| 246 | + 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"]) | |
| 250 | + | |
| 251 | + if has_spy: | |
| 252 | + ax2 = ax1.twinx() | |
| 253 | + cum_ret = (1 + spy_returns).cumprod() - 1 | |
| 254 | + 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"]) | |
| 258 | + | |
| 259 | + 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.95 | |
| 264 | + ax1.annotate(label, xy=(dt, ypos), fontsize=7, rotation=90, | |
| 265 | + ha="right", va="top") | |
| 266 | + | |
| 267 | + 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 fig | |
| 275 | + | |
| 276 | + | |
| 277 | +def 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)) | |
| 281 | + | |
| 282 | + 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")) | |
| 290 | + | |
| 291 | + 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") | |
| 297 | + | |
| 298 | + fig.autofmt_xdate(rotation=45) | |
| 299 | + fig.tight_layout() | |
| 300 | + return fig | |
added
src/plots/style.py
+27 −0
@@ -0,0 +1,27 @@ | ||
| 1 | +"""Journal of Finance publication-quality plotting style.""" | |
| 2 | + | |
| 3 | +from __future__ import annotations | |
| 4 | + | |
| 5 | +from pathlib import Path | |
| 6 | + | |
| 7 | +import matplotlib.pyplot as plt | |
| 8 | + | |
| 9 | +from src.config import FIGURES, JOF_COLORS, MPL_STYLE | |
| 10 | + | |
| 11 | + | |
| 12 | +def set_jof_style() -> None: | |
| 13 | + """Apply Journal of Finance matplotlib style.""" | |
| 14 | + plt.rcParams.update(MPL_STYLE) | |
| 15 | + | |
| 16 | + | |
| 17 | +def save_figure(fig: plt.Figure, name: str, formats: list[str] | None = None) -> None: | |
| 18 | + """Save figure to FIGURES directory in PDF and PNG.""" | |
| 19 | + if formats is None: | |
| 20 | + formats = ["pdf", "png"] | |
| 21 | + FIGURES.mkdir(parents=True, exist_ok=True) | |
| 22 | + for fmt in formats: | |
| 23 | + fig.savefig(FIGURES / f"{name}.{fmt}", bbox_inches="tight", dpi=300) | |
| 24 | + plt.close(fig) | |
| 25 | + | |
| 26 | + | |
| 27 | +COLORS = list(JOF_COLORS.values()) | |
added
src/plots/tables.py
+382 −0
@@ -0,0 +1,382 @@ | ||
| 1 | +"""LaTeX table generators for the Polymarket Arrow-Debreu paper.""" | |
| 2 | + | |
| 3 | +from __future__ import annotations | |
| 4 | + | |
| 5 | +from pathlib import Path | |
| 6 | + | |
| 7 | +import numpy as np | |
| 8 | +import pandas as pd | |
| 9 | + | |
| 10 | +from src.config import TABLES | |
| 11 | + | |
| 12 | + | |
| 13 | +def _save_table(content: str, name: str) -> str: | |
| 14 | + TABLES.mkdir(parents=True, exist_ok=True) | |
| 15 | + path = TABLES / f"{name}.tex" | |
| 16 | + path.write_text(content) | |
| 17 | + return content | |
| 18 | + | |
| 19 | + | |
| 20 | +def _esc(s: str) -> str: | |
| 21 | + """Escape LaTeX special characters in text.""" | |
| 22 | + return str(s).replace("$", r"\$").replace("_", r"\_").replace("&", r"\&").replace("%", r"\%").replace("#", r"\#") | |
| 23 | + | |
| 24 | + | |
| 25 | +def _fmt(x: float, decimals: int = 3) -> str: | |
| 26 | + if pd.isna(x): | |
| 27 | + return "" | |
| 28 | + return f"{x:.{decimals}f}" | |
| 29 | + | |
| 30 | + | |
| 31 | +def _stars(p: float) -> str: | |
| 32 | + if pd.isna(p): | |
| 33 | + return "" | |
| 34 | + if p < 0.01: | |
| 35 | + return "^{***}" | |
| 36 | + if p < 0.05: | |
| 37 | + return "^{**}" | |
| 38 | + if p < 0.10: | |
| 39 | + return "^{*}" | |
| 40 | + return "" | |
| 41 | + | |
| 42 | + | |
| 43 | +def table_descriptive_stats(metadata: pd.DataFrame) -> str: | |
| 44 | + """Table 1: Descriptive statistics of Polymarket contracts.""" | |
| 45 | + rows = [] | |
| 46 | + for _, row in metadata.iterrows(): | |
| 47 | + name = _esc(str(row.get("name", row.get("question", "")))[:40]) | |
| 48 | + vol = row.get("volume", 0) | |
| 49 | + liq = row.get("liquidity", 0) | |
| 50 | + n_obs = int(row.get("n_obs", 0)) | |
| 51 | + mean_p = row.get("mean_price", np.nan) | |
| 52 | + std_p = row.get("std_price", np.nan) | |
| 53 | + rows.append( | |
| 54 | + f" {name} & {vol:,.0f} & {liq:,.0f} & {n_obs} & {_fmt(mean_p)} & {_fmt(std_p)} \\\\" | |
| 55 | + ) | |
| 56 | + | |
| 57 | + body = "\n".join(rows) | |
| 58 | + tex = rf"""\begin{{table}}[htbp] | |
| 59 | +\centering | |
| 60 | +\begin{{threeparttable}} | |
| 61 | +\caption{{Descriptive Statistics of Polymarket Contracts}} | |
| 62 | +\label{{tab:descriptive_stats}} | |
| 63 | +\begin{{tabular}}{{l S S c S S}} | |
| 64 | +\toprule | |
| 65 | +Market & {{Volume (\$)}} & {{Liquidity (\$)}} & Obs & {{Mean Price}} & {{Std Dev}} \\ | |
| 66 | +\midrule | |
| 67 | +{body} | |
| 68 | +\bottomrule | |
| 69 | +\end{{tabular}} | |
| 70 | +\begin{{tablenotes}} | |
| 71 | +\small | |
| 72 | +\item \textit{{Notes:}} This table reports summary statistics for the Polymarket contracts analyzed. | |
| 73 | +Volume and liquidity are reported in US dollars. Mean price represents the average implied probability. | |
| 74 | +\end{{tablenotes}} | |
| 75 | +\end{{threeparttable}} | |
| 76 | +\end{{table}}""" | |
| 77 | + return _save_table(tex, "table1_descriptive_stats") | |
| 78 | + | |
| 79 | + | |
| 80 | +def table_summary_stats(prices: pd.DataFrame) -> str: | |
| 81 | + """Table 2: Summary statistics of price series.""" | |
| 82 | + stats = [] | |
| 83 | + for col in prices.columns: | |
| 84 | + s = prices[col].dropna() | |
| 85 | + from scipy.stats import jarque_bera | |
| 86 | + try: | |
| 87 | + jb_stat, jb_p = jarque_bera(s) | |
| 88 | + except Exception: | |
| 89 | + jb_stat, jb_p = np.nan, np.nan | |
| 90 | + stats.append({ | |
| 91 | + "Market": _esc(col[:30]), | |
| 92 | + "Mean": s.mean(), | |
| 93 | + "Std": s.std(), | |
| 94 | + "Min": s.min(), | |
| 95 | + "Max": s.max(), | |
| 96 | + "Skew": s.skew(), | |
| 97 | + "Kurt": s.kurtosis(), | |
| 98 | + "JB": jb_stat, | |
| 99 | + }) | |
| 100 | + | |
| 101 | + rows = [] | |
| 102 | + for st in stats: | |
| 103 | + rows.append( | |
| 104 | + f" {st['Market']} & {_fmt(st['Mean'])} & {_fmt(st['Std'])} & " | |
| 105 | + f"{_fmt(st['Min'])} & {_fmt(st['Max'])} & {_fmt(st['Skew'],2)} & " | |
| 106 | + f"{_fmt(st['Kurt'],2)} & {_fmt(st['JB'],1)} \\\\" | |
| 107 | + ) | |
| 108 | + | |
| 109 | + body = "\n".join(rows) | |
| 110 | + tex = rf"""\begin{{table}}[htbp] | |
| 111 | +\centering | |
| 112 | +\begin{{threeparttable}} | |
| 113 | +\caption{{Summary Statistics of Prediction Market Prices}} | |
| 114 | +\label{{tab:summary_stats}} | |
| 115 | +\begin{{tabular}}{{l S S S S S S S}} | |
| 116 | +\toprule | |
| 117 | +Market & {{Mean}} & {{Std}} & {{Min}} & {{Max}} & {{Skew}} & {{Kurt}} & {{JB}} \\ | |
| 118 | +\midrule | |
| 119 | +{body} | |
| 120 | +\bottomrule | |
| 121 | +\end{{tabular}} | |
| 122 | +\begin{{tablenotes}} | |
| 123 | +\small | |
| 124 | +\item \textit{{Notes:}} JB is the Jarque--Bera test statistic for normality. | |
| 125 | +$^{{***}}$, $^{{**}}$, and $^{{*}}$ denote significance at the 1\%, 5\%, and 10\% levels. | |
| 126 | +\end{{tablenotes}} | |
| 127 | +\end{{threeparttable}} | |
| 128 | +\end{{table}}""" | |
| 129 | + return _save_table(tex, "table2_summary_stats") | |
| 130 | + | |
| 131 | + | |
| 132 | +def table_completeness_test(test_result) -> str: | |
| 133 | + """Table 3: Arrow-Debreu completeness test results.""" | |
| 134 | + tex = rf"""\begin{{table}}[htbp] | |
| 135 | +\centering | |
| 136 | +\begin{{threeparttable}} | |
| 137 | +\caption{{Arrow--Debreu Market Completeness Tests}} | |
| 138 | +\label{{tab:completeness}} | |
| 139 | +\begin{{tabular}}{{l S}} | |
| 140 | +\toprule | |
| 141 | +Statistic & {{Value}} \\ | |
| 142 | +\midrule | |
| 143 | + Mean state price sum & {_fmt(test_result.mean_sum, 4)} \\ | |
| 144 | + Std of state price sum & {_fmt(test_result.std_sum, 4)} \\ | |
| 145 | + Mean deviation from unity & {_fmt(test_result.mean_deviation, 4)} \\ | |
| 146 | + Max absolute deviation & {_fmt(test_result.max_deviation, 4)} \\ | |
| 147 | + All state prices positive & {{{'Yes' if test_result.all_positive else 'No'}}} \\ | |
| 148 | + Implied discount factor & {_fmt(test_result.discount_factor, 4)} \\ | |
| 149 | + Number of observations & {len(test_result.daily_sums)} \\ | |
| 150 | +\bottomrule | |
| 151 | +\end{{tabular}} | |
| 152 | +\begin{{tablenotes}} | |
| 153 | +\small | |
| 154 | +\item \textit{{Notes:}} This table reports results from testing Arrow--Debreu market completeness | |
| 155 | +conditions using the Fed rate decision markets on Polymarket. Under complete markets, | |
| 156 | +the sum of state prices equals $1/(1+r_f)$. Mean deviation measures the average | |
| 157 | +departure from this theoretical prediction. | |
| 158 | +\end{{tablenotes}} | |
| 159 | +\end{{threeparttable}} | |
| 160 | +\end{{table}}""" | |
| 161 | + return _save_table(tex, "table3_completeness") | |
| 162 | + | |
| 163 | + | |
| 164 | +def table_event_study(results: dict[str, object]) -> str: | |
| 165 | + """Table 4: Event study cumulative abnormal returns.""" | |
| 166 | + rows = [] | |
| 167 | + for event_type, result in results.items(): | |
| 168 | + if hasattr(result, "test_stats") and not result.test_stats.empty: | |
| 169 | + for _, row in result.test_stats.iterrows(): | |
| 170 | + window = row.get("window", "") | |
| 171 | + market = _esc(str(row.get("market", ""))) | |
| 172 | + car = row.get("mean_CAR", np.nan) | |
| 173 | + t = row.get("t_stat", np.nan) | |
| 174 | + p = row.get("p_value", np.nan) | |
| 175 | + stars = _stars(p) | |
| 176 | + rows.append( | |
| 177 | + f" {_esc(event_type)}: {market} & {window} & ${_fmt(car)}{stars}$ & " | |
| 178 | + f"{_fmt(t, 2)} & {_fmt(p)} & {result.n_events} \\\\" | |
| 179 | + ) | |
| 180 | + | |
| 181 | + body = "\n".join(rows) if rows else " \\multicolumn{6}{c}{No significant results} \\\\" | |
| 182 | + tex = rf"""\begin{{table}}[htbp] | |
| 183 | +\centering | |
| 184 | +\begin{{threeparttable}} | |
| 185 | +\caption{{Event Study: Cumulative Abnormal Returns Around Macroeconomic Events}} | |
| 186 | +\label{{tab:event_study}} | |
| 187 | +\begin{{tabular}}{{l l c c c c}} | |
| 188 | +\toprule | |
| 189 | +Event Type & Window & {{CAR}} & {{$t$-stat}} & {{$p$-value}} & $N$ \\ | |
| 190 | +\midrule | |
| 191 | +{body} | |
| 192 | +\bottomrule | |
| 193 | +\end{{tabular}} | |
| 194 | +\begin{{tablenotes}} | |
| 195 | +\small | |
| 196 | +\item \textit{{Notes:}} This table reports cumulative abnormal returns (CARs) for prediction market | |
| 197 | +prices around macroeconomic events. Abnormal returns are computed relative to a constant mean | |
| 198 | +model estimated over a 60-day estimation window. $t$-statistics are computed using the | |
| 199 | +cross-sectional standard deviation of CARs. $^{{***}}$, $^{{**}}$, $^{{*}}$ denote | |
| 200 | +significance at 1\%, 5\%, 10\%. | |
| 201 | +\end{{tablenotes}} | |
| 202 | +\end{{threeparttable}} | |
| 203 | +\end{{table}}""" | |
| 204 | + return _save_table(tex, "table4_event_study") | |
| 205 | + | |
| 206 | + | |
| 207 | +def table_granger_causality(f_stats: pd.DataFrame, p_values: pd.DataFrame) -> str: | |
| 208 | + """Table 5: Granger causality test results.""" | |
| 209 | + rows = [] | |
| 210 | + for caused in f_stats.index: | |
| 211 | + for causing in f_stats.columns: | |
| 212 | + if caused != causing: | |
| 213 | + f_val = f_stats.loc[caused, causing] | |
| 214 | + p_val = p_values.loc[caused, causing] | |
| 215 | + stars = _stars(p_val) | |
| 216 | + rows.append( | |
| 217 | + f" {_esc(causing)} $\\to$ {_esc(caused)} & ${_fmt(f_val, 2)}{stars}$ & {_fmt(p_val)} \\\\" | |
| 218 | + ) | |
| 219 | + | |
| 220 | + body = "\n".join(rows) | |
| 221 | + tex = rf"""\begin{{table}}[htbp] | |
| 222 | +\centering | |
| 223 | +\begin{{threeparttable}} | |
| 224 | +\caption{{Granger Causality Tests}} | |
| 225 | +\label{{tab:granger}} | |
| 226 | +\begin{{tabular}}{{l c c}} | |
| 227 | +\toprule | |
| 228 | +Direction & {{$F$-stat}} & {{$p$-value}} \\ | |
| 229 | +\midrule | |
| 230 | +{body} | |
| 231 | +\bottomrule | |
| 232 | +\end{{tabular}} | |
| 233 | +\begin{{tablenotes}} | |
| 234 | +\small | |
| 235 | +\item \textit{{Notes:}} This table reports $F$-statistics and $p$-values for pairwise Granger | |
| 236 | +causality tests. The null hypothesis is that the causing variable does not Granger-cause | |
| 237 | +the caused variable. Lag length selected by AIC. $^{{***}}$, $^{{**}}$, $^{{*}}$ denote | |
| 238 | +significance at 1\%, 5\%, 10\%. | |
| 239 | +\end{{tablenotes}} | |
| 240 | +\end{{threeparttable}} | |
| 241 | +\end{{table}}""" | |
| 242 | + return _save_table(tex, "table5_granger") | |
| 243 | + | |
| 244 | + | |
| 245 | +def table_var_results(var_results: dict) -> str: | |
| 246 | + """Table 6: VAR estimation results.""" | |
| 247 | + tex = rf"""\begin{{table}}[htbp] | |
| 248 | +\centering | |
| 249 | +\begin{{threeparttable}} | |
| 250 | +\caption{{Vector Autoregression Estimates}} | |
| 251 | +\label{{tab:var}} | |
| 252 | +\begin{{tabular}}{{l {'S ' * min(len(var_results.get('coefficients', {}).keys()), 4)}}} | |
| 253 | +\toprule | |
| 254 | +""" | |
| 255 | + coefs = var_results.get("coefficients", {}) | |
| 256 | + if coefs: | |
| 257 | + cols = list(coefs.keys())[:4] | |
| 258 | + tex += " & ".join(["Variable"] + [f"{{{_esc(c)}}}" for c in cols]) + " \\\\\n\\midrule\n" | |
| 259 | + first_col = coefs[cols[0]] | |
| 260 | + if isinstance(first_col, pd.Series): | |
| 261 | + for var_name in first_col.index[:15]: | |
| 262 | + row_vals = [f"{_fmt(coefs[c].get(var_name, np.nan), 3)}" for c in cols] | |
| 263 | + tex += f" {_esc(str(var_name))} & " + " & ".join(row_vals) + " \\\\\n" | |
| 264 | + tex += rf"""\bottomrule | |
| 265 | +\end{{tabular}} | |
| 266 | +\begin{{tablenotes}} | |
| 267 | +\small | |
| 268 | +\item \textit{{Notes:}} VAR model estimates. Optimal lag length selected by AIC. | |
| 269 | +Standard errors in parentheses. $^{{***}}$, $^{{**}}$, $^{{*}}$ denote significance | |
| 270 | +at 1\%, 5\%, 10\%. | |
| 271 | +\end{{tablenotes}} | |
| 272 | +\end{{threeparttable}} | |
| 273 | +\end{{table}}""" | |
| 274 | + return _save_table(tex, "table6_var") | |
| 275 | + | |
| 276 | + | |
| 277 | +def table_garch(garch_results: dict[str, object]) -> str: | |
| 278 | + """Table 7: GARCH estimation results.""" | |
| 279 | + rows = [] | |
| 280 | + for name, result in garch_results.items(): | |
| 281 | + if hasattr(result, "params"): | |
| 282 | + for param_name, value in result.params.items(): | |
| 283 | + se = result.std_errors.get(param_name, np.nan) if hasattr(result, "std_errors") else np.nan | |
| 284 | + rows.append(f" {_esc(name)}: {_esc(str(param_name))} & {_fmt(value, 4)} & ({_fmt(se, 4)}) \\\\") | |
| 285 | + | |
| 286 | + body = "\n".join(rows) if rows else " \\multicolumn{3}{c}{No results} \\\\" | |
| 287 | + tex = rf"""\begin{{table}}[htbp] | |
| 288 | +\centering | |
| 289 | +\begin{{threeparttable}} | |
| 290 | +\caption{{GARCH Model Estimates}} | |
| 291 | +\label{{tab:garch}} | |
| 292 | +\begin{{tabular}}{{l S l}} | |
| 293 | +\toprule | |
| 294 | +Parameter & {{Estimate}} & (Std Err) \\ | |
| 295 | +\midrule | |
| 296 | +{body} | |
| 297 | +\bottomrule | |
| 298 | +\end{{tabular}} | |
| 299 | +\begin{{tablenotes}} | |
| 300 | +\small | |
| 301 | +\item \textit{{Notes:}} GARCH(1,1) model estimates for prediction market return volatility. | |
| 302 | +$^{{***}}$, $^{{**}}$, $^{{*}}$ denote significance at 1\%, 5\%, 10\%. | |
| 303 | +\end{{tablenotes}} | |
| 304 | +\end{{threeparttable}} | |
| 305 | +\end{{table}}""" | |
| 306 | + return _save_table(tex, "table7_garch") | |
| 307 | + | |
| 308 | + | |
| 309 | +def table_har_rv(har_result) -> str: | |
| 310 | + """Table 8: HAR-RV estimation results.""" | |
| 311 | + rows = [] | |
| 312 | + if hasattr(har_result, "coefficients"): | |
| 313 | + for name, val in har_result.coefficients.items(): | |
| 314 | + se = har_result.std_errors.get(name, np.nan) if hasattr(har_result, "std_errors") else np.nan | |
| 315 | + t = har_result.t_stats.get(name, np.nan) if hasattr(har_result, "t_stats") else np.nan | |
| 316 | + rows.append(f" {_esc(str(name))} & {_fmt(val, 4)} & ({_fmt(se, 4)}) & {_fmt(t, 2)} \\\\") | |
| 317 | + | |
| 318 | + body = "\n".join(rows) if rows else " \\multicolumn{4}{c}{No results} \\\\" | |
| 319 | + r2 = _fmt(har_result.r_squared, 3) if hasattr(har_result, "r_squared") else "" | |
| 320 | + adj_r2 = _fmt(har_result.adj_r_squared, 3) if hasattr(har_result, "adj_r_squared") else "" | |
| 321 | + | |
| 322 | + tex = rf"""\begin{{table}}[htbp] | |
| 323 | +\centering | |
| 324 | +\begin{{threeparttable}} | |
| 325 | +\caption{{HAR-RV Model Estimates}} | |
| 326 | +\label{{tab:har_rv}} | |
| 327 | +\begin{{tabular}}{{l S l S}} | |
| 328 | +\toprule | |
| 329 | +Variable & {{Coefficient}} & (Std Err) & {{$t$-stat}} \\ | |
| 330 | +\midrule | |
| 331 | +{body} | |
| 332 | +\midrule | |
| 333 | + $R^2$ & \multicolumn{{3}}{{c}}{{{r2}}} \\ | |
| 334 | + Adj.~$R^2$ & \multicolumn{{3}}{{c}}{{{adj_r2}}} \\ | |
| 335 | +\bottomrule | |
| 336 | +\end{{tabular}} | |
| 337 | +\begin{{tablenotes}} | |
| 338 | +\small | |
| 339 | +\item \textit{{Notes:}} HAR-RV model following \citet{{corsi2009simple}}. | |
| 340 | +RV$_{{t+1}}^{{(d)}} = c + \beta_d RV_t^{{(d)}} + \beta_w RV_t^{{(w)}} + \beta_m RV_t^{{(m)}} + \varepsilon_{{t+1}}$. | |
| 341 | +Newey--West standard errors with 5 lags. | |
| 342 | +\end{{tablenotes}} | |
| 343 | +\end{{threeparttable}} | |
| 344 | +\end{{table}}""" | |
| 345 | + return _save_table(tex, "table8_har_rv") | |
| 346 | + | |
| 347 | + | |
| 348 | +def table_liquidity(liquidity_stats: pd.DataFrame) -> str: | |
| 349 | + """Table 9: Liquidity regression results.""" | |
| 350 | + rows = [] | |
| 351 | + for _, row in liquidity_stats.iterrows(): | |
| 352 | + var_name = _esc(str(row.get("variable", ""))) | |
| 353 | + coef = row.get("coefficient", np.nan) | |
| 354 | + se = row.get("std_error", np.nan) | |
| 355 | + t = row.get("t_stat", np.nan) | |
| 356 | + p = row.get("p_value", np.nan) | |
| 357 | + stars = _stars(p) | |
| 358 | + rows.append( | |
| 359 | + f" {var_name} & ${_fmt(coef, 4)}{stars}$ & ({_fmt(se, 4)}) & {_fmt(t, 2)} \\\\" | |
| 360 | + ) | |
| 361 | + | |
| 362 | + body = "\n".join(rows) if rows else " \\multicolumn{4}{c}{No results} \\\\" | |
| 363 | + tex = rf"""\begin{{table}}[htbp] | |
| 364 | +\centering | |
| 365 | +\begin{{threeparttable}} | |
| 366 | +\caption{{Liquidity Determinants}} | |
| 367 | +\label{{tab:liquidity}} | |
| 368 | +\begin{{tabular}}{{l S l S}} | |
| 369 | +\toprule | |
| 370 | +Variable & {{Coefficient}} & (Std Err) & {{$t$-stat}} \\ | |
| 371 | +\midrule | |
| 372 | +{body} | |
| 373 | +\bottomrule | |
| 374 | +\end{{tabular}} | |
| 375 | +\begin{{tablenotes}} | |
| 376 | +\small | |
| 377 | +\item \textit{{Notes:}} OLS regression of bid--ask spread on volume, volatility, and event dummies. | |
| 378 | +Newey--West standard errors. $^{{***}}$, $^{{**}}$, $^{{*}}$ denote significance at 1\%, 5\%, 10\%. | |
| 379 | +\end{{tablenotes}} | |
| 380 | +\end{{threeparttable}} | |
| 381 | +\end{{table}}""" | |
| 382 | + return _save_table(tex, "table9_liquidity") | |
added
src/utils.py
+49 −0
@@ -0,0 +1,49 @@ | ||
| 1 | +from __future__ import annotations | |
| 2 | + | |
| 3 | +import json | |
| 4 | +import logging | |
| 5 | +from pathlib import Path | |
| 6 | +from typing import Any | |
| 7 | + | |
| 8 | +from src.config import ( | |
| 9 | + DATA_PROCESSED, | |
| 10 | + DATA_RAW_FRED, | |
| 11 | + DATA_RAW_POLY, | |
| 12 | + DATA_RAW_YAHOO, | |
| 13 | + FIGURES, | |
| 14 | + LOGS, | |
| 15 | + TABLES, | |
| 16 | +) | |
| 17 | + | |
| 18 | + | |
| 19 | +def setup_logging(name: str) -> logging.Logger: | |
| 20 | + LOGS.mkdir(parents=True, exist_ok=True) | |
| 21 | + logger = logging.getLogger(name) | |
| 22 | + logger.setLevel(logging.INFO) | |
| 23 | + if not logger.handlers: | |
| 24 | + fh = logging.FileHandler(LOGS / f"{name}.log") | |
| 25 | + fh.setFormatter(logging.Formatter("%(asctime)s %(levelname)s %(message)s")) | |
| 26 | + logger.addHandler(fh) | |
| 27 | + sh = logging.StreamHandler() | |
| 28 | + sh.setFormatter(logging.Formatter("%(levelname)s %(message)s")) | |
| 29 | + logger.addHandler(sh) | |
| 30 | + return logger | |
| 31 | + | |
| 32 | + | |
| 33 | +def ensure_dirs() -> None: | |
| 34 | + for d in [ | |
| 35 | + DATA_RAW_POLY, DATA_RAW_FRED, DATA_RAW_YAHOO, | |
| 36 | + DATA_PROCESSED, FIGURES, TABLES, LOGS, | |
| 37 | + ]: | |
| 38 | + d.mkdir(parents=True, exist_ok=True) | |
| 39 | + | |
| 40 | + | |
| 41 | +def save_json(data: Any, path: Path) -> None: | |
| 42 | + path.parent.mkdir(parents=True, exist_ok=True) | |
| 43 | + with open(path, "w") as f: | |
| 44 | + json.dump(data, f, indent=2, default=str) | |
| 45 | + | |
| 46 | + | |
| 47 | +def load_json(path: Path) -> Any: | |
| 48 | + with open(path) as f: | |
| 49 | + return json.load(f) | |
| 50 | ||