Trading Projects
Code and research tools built for trading competitions.
Unfinished Projects
Python Programs:
A Python research pipeline built for the 2026 Vanguard ETF Trading Challenge. It screens every active Vanguard ETF for pairs whose price ratio reverts, then backtests a long-only rotation between the two funds. Output is PDF reports; nothing connects to a broker.
Correlation measures whether two funds' returns move together day to day. Cointegration measures whether some fixed combination of their prices returns to a level — and which combination is part of the claim, not a detail. Neither implies the other, so two funds can track each other closely and still drift apart for good.
Cointegration is not a prerequisite for pairs trading. Gatev, Goetzmann and Rouwenhorst (2006) match pairs by distance between normalized prices, use no cointegration test, and report up to 11% annualized excess returns over 1962–2002. It is the route taken here, not the only one.
Method:
- Data: about 25 years of daily prices for 116 active Vanguard ETFs from Yahoo Finance, cached once a day.
- Ranking: the Pearson correlation of daily percent changes over the trailing 252 days is computed for every pair, and the 150 highest go to the test. Correlation is a way to cut the search space, not evidence of a tradable relationship; the two-stage design is standard, and Miao (2014) tests only pairs above 0.9, cutting 15,576 candidates to about 1,378. Returns rather than price levels, because a level correlation between two trending series is close to arbitrary (Granger and Newbold 1974). Note the windows differ: the filter sees one year, the test then uses the full shared history.
- Cointegration screen: Engle–Granger on price levels. The test is not symmetric, so it runs both ways and a pair passes only if the weaker p-value is below 0.05. Minimum 200 shared trading days.
- Signal: z-score of the ratio A/B against its 100-day rolling mean. Below −2 buy A, above +2 buy B.
- Backtest: long-only. Three exit rules on identical entries (z = 0, ±1, ±2), signalling pairs combined equally weighted and rebalanced daily against buy-and-hold.
- Caching: the day's download and the last screen are both cached, so a repeat run skips the downloads and the regressions.
Reports:
- Correlation report: the most correlated pairs by daily return, each with its rolling correlation over its full shared history, so a drifting relationship is visible rather than averaged away.
- Cointegration screen: every ranked pair tested, ordered by significance. Pairs that pass get their prices, statistics and signal history charted.
- Portfolio report: today's live signals and where each candidate sits relative to its trigger, then the backtest under each exit rule, the equity curve against buy-and-hold, and a page per pair.
Folder contents:
RunDaily.py: runs the full pipeline in order, each stage fed by the one before it; the usual entry point.Vanguard.py: the ETF universe of 116 tickers and the data layer (download, daily cache, column extraction, gap reporting).VanguardStats.py: aligned price/return tables and the correlation matrices, on levels and on returns.VanguardReporter.py: correlation report (most correlated pairs and their rolling correlation).VanguardPairsTrading.py: the cointegration screen, ratio z-scores and signal counts.VanguardPairsPortfolio.py: today's live signals, the backtest under each exit rule, and the combined portfolio.VanguardPairReport.py: a report for one pair on demand, e.g.python VanguardPairReport.py BIV VGIT. It refuses a pair that isn't cointegrated.VanguardPlots.py: rolling-correlation charts used by the reports.README.md: full documentation, configuration and runtime notes.
How to run:
Install with pip install pandas numpy yfinance matplotlib statsmodels scipy,
then run python RunDaily.py. Requires Python 3.10+ (built and tested on
3.14). A full run takes under a minute after the first download; screening many more than
the default 150 pairs is what makes it slower, since each pair costs two regressions.
Limitations:
- No costs, spreads, slippage or taxes, so every return is an upper bound.
- Multiple testing: many pairs screened at p < 0.05 yields false positives by chance, and the p-values are uncorrected. The set surviving a Benjamini–Hochberg (1995) correction is much smaller.
- The rolling window supplies the mean reversion. Subtracting a 100-day rolling mean removes any slow trend by construction, so the z-score looks mean-reverting whether or not the ratio is. Only the cointegration test, which runs on the undetrended series, separates signal from noise.
- The screen and the signal ask different questions. Engle–Granger works in price levels: is A − βB stationary for some fitted β? The signal trades A/B, which is stationary exactly when ln A − ln B is — unit weight on log prices. On trending series the two are near-incompatible. If A − βB = c + u with u stationary, then A/B = β + (c + u)/B, which converges to β as B grows rather than oscillating around it. Conversely if A/B = c + v with v stationary, then A − βB = (c − β)B + vB for every β, and vB has variance growing with B².
- Long-only: each position carries the market risk of the fund it holds, not the spread.
Notes:
- Built with help from Claude (AI), starting from pairs-trading code from coursework, for the competition.
- Verified Working: 9/27/2026