If you trade sector ETFs on a rotation strategy and want to know whether an M-Signal-driven allocation actually beats SPY buy-and-hold, this backtest answers that with a real, tunable exit mechanic — not a single fixed-day snapshot.
Built for systematic ETF traders, sector-rotation strategists, and quant researchers evaluating momentum-signal-driven allocation against a buy-and-hold benchmark. On each M signal, it commits alloc% of portfolio value to that ticker, funded by selling SPY, entering at T+1 open. Positions exit via a tiered profit-target mechanic: a minimum-hold floor, then evenly-spaced % tiers off the entry price, each tier selling a fixed unit-count block (100 / tier count) of the original position size, with the final tier always closing whatever remains. A Broad-market-exit mode force-liquidates every open position the moment a broad-market M signal (SPY/QQQ/DIA/IWM) fires, regardless of tier progress. A No-distinction mode widens the tradeable universe to include QQQ/DIA/IWM alongside the sector ETFs.
Exit timing is path-dependent on the full daily price series, so — unlike the old fixed-exit-day design — it doesn't reduce to a precomputable (allocation, exit-day) grid. The dashboard ships the daily OHLC + entry-signal data and recomputes the equity curve client-side in the browser on demand (the Run button), for whatever min-hold / tier-count / allocation you set on the sliders.
- Modes: Standard, Broad-market-exit, No-distinction
- Metrics: CAGR, Volatility, Beta, Max Drawdown, Sharpe
- Charts: equity curve, transaction log, skip log, time-slice breakdown
- Try the live interactive dashboard
| File | Description |
|---|---|
05_portfolio_rotation.py |
Source — writes daily OHLC/signal data and the report shell; simulate_v7() is the Python reference implementation used by tests/parity/ to keep the browser's JS port honest |
v7.1/report.html |
Latest interactive report (open via GitHub Pages — data files must be fetched over HTTP, not opened from disk) |
CHANGELOG.md |
Full version history |
_archive/screenshots/ |
Screenshots of all versions |
Each version outputs a folder (vX.Y/report.html + vX.Y/data/*.json). From v7.0 onward
the simulation itself runs client-side, so data/ only holds the daily price series and
entry signals (prices.daily.json, signals.v7.json) — no precomputed grid. See the v7.0
CHANGELOG entry for why the grid was dropped.
v7.1 is a docs/cleanup/responsive-fix pass on top of v7.0 — no simulation logic changed
(confirmed via the 54/54 tests/parity/ harness); see the v7.1 CHANGELOG entry.
See CHANGELOG.md or git log for the full evolution.
- Author: Joseph Solomon · LinkedIn
- General enquiries: [email protected]
- Bugs and discussion: open an issue