A modular, educational quantitative finance project that builds an ETF rotation strategy from scratch — starting with a rules-based momentum approach and designed from day one to be extended with machine learning.
This project is built in two phases:
| Phase | Status | Description |
|---|---|---|
| Phase 1 | 🔨 In progress | Rules-based momentum strategy with inverse-volatility weighting |
| Phase 2 | 📋 Planned | ML model replaces the momentum signal (return prediction / ranking) |
The codebase is intentionally structured so that Phase 2 requires no changes to portfolio construction or backtesting — only the signal module is swapped.
Universe: 13 liquid ETFs across equities, bonds, commodities, and sectors
Signal: 12-1 momentum (12-month return, skip last month to avoid short-term reversal)
Selection: Top N ETFs by momentum score at each monthly rebalance
Weighting: Inverse volatility (allocate more to less volatile assets)
Benchmark: SPY (S&P 500)
| Ticker | Description | Asset Class |
|---|---|---|
| SPY | S&P 500 | US Equity / Benchmark |
| QQQ | Nasdaq 100 | US Equity / Growth |
| IWM | Russell 2000 | US Equity / Small Cap |
| EFA | MSCI EAFE | International Developed |
| EEM | MSCI Emerging Markets | International Emerging |
| TLT | 20+ Year US Treasuries | Long Bond |
| IEF | 7–10 Year US Treasuries | Intermediate Bond |
| GLD | Gold | Commodity / Hedge |
| DBC | Diversified Commodities | Commodity |
| XLK | Technology Sector | US Sector |
| XLF | Financials Sector | US Sector |
| XLE | Energy Sector | US Sector |
| XLV | Healthcare Sector | US Sector |
quant-etf-rotation/
│
├── configs/
│ ├── base.yaml # Universe, dates, rebalance frequency
│ └── strategy_v1.yaml # Momentum params, top N, weighting method
│
├── data/
│ ├── raw/ # yfinance downloads (auto-cached, git-ignored)
│ └── processed/ # Cleaned, aligned price data
│
├── notebooks/
│ ├── 01_download_data.ipynb
│ ├── 02_feature_engineering.ipynb
│ ├── 03_strategy_backtest.ipynb
│ └── 04_results_analysis.ipynb
│
├── src/
│ ├── data_loader.py # Data download and caching (yfinance)
│ ├── features.py # Momentum, volatility, and derived features
│ ├── signals.py # Rules-based ETF ranking and selection
│ ├── portfolio.py # Weighting and position sizing
│ ├── backtest.py # Core backtesting engine (reusable)
│ ├── metrics.py # Sharpe, drawdown, CAGR, benchmark comparison
│ ├── plots.py # All visualisations
│ └── utils.py # Shared helpers
│
├── models/ # Phase 2: ML signal models (empty for now)
│
├── reports/
│ ├── figures/ # Saved plots
│ ├── trades/ # Rebalance logs and position history
│ └── summary.md # Performance summary table
│
└── tests/
├── test_features.py
├── test_portfolio.py
└── test_backtest.py
git clone https://github.com/philzghub/ETF-Rotation-Backtester.git
cd ETF-Rotation-Backtester
pip install -r requirements.txtpython src/data_loader.pyOr run notebooks/01_download_data.ipynb for an interactive walkthrough.
python main.pyEach module has a single responsibility. backtest.py doesn't know how signals are generated. portfolio.py doesn't know how ETFs are ranked. This separation is what allows the ML swap in Phase 2.
Both the rules-based (Phase 1) and ML-based (Phase 2) signal modules expose the same interface:
def get_signal(prices, config, date) -> pd.Series:
# Returns a score per ticker — higher = more attractive
...portfolio.py and backtest.py consume this Series without caring how it was produced.
The backtester is designed to be strict about lookahead. At each rebalance date, only data available before that date is used to generate signals. This is the most common source of inflated backtest results in amateur quant projects.
Results are always compared against buy-and-hold SPY. Outperformance is only meaningful when stated net of transaction costs and with realistic assumptions.
- Monthly rebalancing on the last trading day of each month
- No transaction costs in the first version (noted explicitly in results)
- No leverage
- Prices used are adjusted close (accounts for dividends and splits)
- Data source: Yahoo Finance via
yfinance
- Project scaffold and config
- Data download and caching module
- Feature engineering (momentum, volatility)
- Rules-based signal and ETF selection
- Portfolio construction (inverse volatility weighting)
- Backtesting engine
- Performance metrics and benchmark comparison
- Visualisations and results notebooks
- Phase 2: ML-based signal (return prediction / ranking)
If you're working through this project to learn quant finance, these are worth reading alongside the code:
- Quantitative Momentum — Wesley Gray & Jack Vogel
- Advances in Financial Machine Learning — Marcos López de Prado (for Phase 2)
- AQR's research library: aqr.com/insights
- Investopedia's explanation of momentum investing
- Universe selection bias: the 13 ETFs were chosen knowing they survived and remained liquid through 2007-2026. This is standard practice in fixed-universe backtests but is an acknowledged limitation.
- Execution timing: rebalancing assumes end-of-month close prices. Real execution would occur at next-day open. Flagged for V2.
- Risk-free rate: Sharpe and related ratios use rf=0. Flagged for V2.
This project is for educational purposes only. Nothing here constitutes financial advice. Past backtest performance does not guarantee future results.