A research codebase for a retail-scale systematic portfolio: a 70/30 equity + managed-futures allocation, a swing-trading sleeve, and, the part that matters most, the statistical machinery that decides whether either of them is actually worth funding.
51 modules · 14 test suites · ~24k lines of Python · 8 pre-registered hypotheses, 5 graded.
All five graded so far returned FAIL. They are published here in full, with their pre-registrations and their verdicts intact. That is the point of the repository.
Research only. Nothing here places, routes, or previews an order. No broker credentials, no account identifiers, and no personal position data are present in this repository or its history.
Most published backtests are a selection artifact. You try twenty things, one of them prints a Sharpe of 1.4, and you publish that one. The number is real and it means nothing, because the search that produced it is invisible.
This project is built around making that search visible and pricing it:
- Register the hypothesis and the pass/fail bar before running the test.
- Freeze the registration on a remote:
prereg.pyrefuses to grade a bar that is not committed and pushed, because a bar still sitting in your working tree can be amended after you have seen the result. - Deflate for the number of things you tried.
n_trialsis recorded in the registration and carried into the significance test. - Publish the failures. All five graded registrations failed. They are in
reports/*_grade.md.
The most useful artifact in here is not a strategy. It is
reports/swing_blend_2026_08_grade.md, which
fails its own bar by 0.0057 Sharpe and then argues, against its author's interest, that
the criterion it nearly passed is the least robust of the three, because two free
construction choices each move the result by more than the margin.
monthly_signal.py: the live allocation rule.
70% EQUITY sleeve 50% RSP (equal-weight S&P 500)
50% MTUM (momentum), scaled down when momentum volatility is high
30% TREND sleeve trend-follow SPY / TLT / GLD / DBC / IEF
hold each only while it is above its 200-day moving average, else cash
Re-run on the first of the month; it prints target weights and dollar amounts. It places no orders.
The supporting studies each ask whether one specific complication earns its keep:
| Module | Question |
|---|---|
diversify_test.py |
Does the 70/30 split beat buy-and-hold on Sharpe, drawdown and consistency? |
skfolio_cv.py |
Does mean-variance optimisation beat fixed weights out-of-sample? |
rebalance_cadence_test.py |
Does rebalance frequency matter? (registered → FAIL, 0/2) |
risk_parity_test.py |
Does risk parity beat the fixed split? |
leverage_test.py · leverage_realistic_test.py |
Does leverage survive realistic financing costs? |
trend_ensemble_lab.py · trend_plus_test.py |
Does an ensemble of trend rules beat a single 200-day MA? |
stack_test.py |
Return stacking: is the capital-efficiency claim real? |
skfolio_cv.py cross-validates portfolio optimisers against the fixed 70/30 split. Its
first run put 99.8% into a cash proxy with a reported Sharpe of 19.8, in all seven
folds.
That was not a finding. It was a bug in how the problem was posed: raw-return max-Sharpe always collapses into the lowest-volatility asset, and a cash proxy has near-zero volatility with positive drift. It is worth being explicit about why it was caught, cross-validation printed the same degenerate answer seven times. A single backtest would have printed one seductive Sharpe of 19.8 and nothing to contradict it.
Corrected, optimising the three risk assets on returns in excess of cash, every method lands within 0.54pp CAGR and 0.12 Sharpe of every other out-of-sample, while max-Sharpe demands you believe a VTI weight that swings the full 0→100% between adjacent quarters. The conclusion is that the optimisation buys nothing and costs trust: a flat 30% that never moves is the better estimator, precisely because it does not move.
A 16-cell slate of swing families, pullback, breakout, Donchian channel, volatility compression, backtested over 2011, 2026 and graded twice.
Core engine. swing_lib.py · swing_lab.py ·
swing_blend_lab.py · paper_swing.py
Three modelling decisions in swing_lib.py are load-bearing, and each is the kind of
thing that fails quietly rather than loudly:
- T+1 settlement. This is a cash account: proceeds from a day-t sale cannot fund a day-t buy. Modelled with a pending-cash bucket, not left as a comment.
- One-bar execution delay. Signals compute on the close of day t and fill at the
open of day t+1, entries and exits alike. A backtest that fills on the signal bar
looks fine and is wrong;
test_swing_lib.pycarries a truncation-invariance canary that catches it. - Slot sizing is a share of equity, not of cash.
cash / max_positionslooks identical toequity / max_positionsand is wrong, it pays 500/375/281/211 into a 4-slot $2,000 sleeve and strands ~32% in the cash rate forever. It survived a full 16-cell slate undetected, because a flat price marks cash and stock to the same number.
Both gradings failed, and the second failure is the more interesting one.
swing_slate_2026_08 graded the cells standalone (FAIL, 1/3). That criterion was
structurally blind to a low-beta diversifier, a sleeve can lose on its own and still
improve a portfolio it is blended into. So swing_blend_2026_08 re-graded all sixteen
cells as 50/50 blends against the benchmark (FAIL, 0/3).
The registration for that retry is worth reading in full
(reports/prereg/swing_blend_2026_08.json)
because of what it refuses to do:
- It keeps
n_trials = 16rather than 1. The winning cell was selected from sixteen, so deflation still has to price sixteen, testing it alone and claiming one trial would be a fiction. - It sets the significance bar at the conventional t ≥ 2.0 while knowing the prior estimate was 1.56. Setting it at 1.5 to clear a known 1.56 is goalpost-fitting, "and it is exactly what a registration exists to prevent."
- It pins the risk-free convention up front, because the previous registration did not,
and
rf=0versus realised cash had given opposite verdicts on two of three criteria. - It states plainly that it is confirmatory work on data already seen, and "must never be quoted as" out-of-sample evidence.
Per power_budget.py, the detectable floor at N=16 over 15 years is a
0.61 Sharpe, so the null result is uninformative below true Sharpe ≈ 0.6, and the
write-up says so rather than claiming the strategies are dead.
This is the part that generalises beyond trading.
prereg.py: pre-registration with a freeze gate.
Registers a named hypothesis, its criteria, and n_trials to a JSON record stamped with
the git HEAD. grade() raises NotFrozen and exits non-zero if the registration is not
committed and pushed. You cannot grade against a bar you could still edit.
stats_lib.py: multiple-testing machinery.
Hansen's SPA, White's Reality Check, StepM, the Model Confidence Set, and bootstrap Sharpe
confidence intervals (via arch). Written because pre-registration is the retail-scale
substitute for a deflated Sharpe, and the two work better together.
power_budget.py: what a test can detect.
Computes the minimum true Sharpe a given (N trials, window length) design can resolve.
Run before registering, so a test that cannot answer its question is never run.
panel_freeze.py · build_frozen_panel.py: reproducibility.
Freezes the price panel to a content hash so a re-run cannot silently pick up revised
vendor data.
universe.py · crsp_universe.py: survivorship bias.
Point-in-time S&P 500 membership from a vendored, offline
dataset (MIT), so a screen and a backtest can never
disagree about what the index contained on a given day. Pulling this at runtime would make
every historical result depend on whatever upstream looked like that morning.
robustness.py · rolling_test.py · breaks.py
Parameter-neighbourhood sweeps, rolling-window stability, and change-point detection
(ruptures). Three different ways of asking whether a result is a regime artifact.
placer_lib.py: an order rail that cannot place an order.
Included as a design exercise in fail-closed engineering. submit_order raises
NotImplementedError by construction; there is no API client and no credential path in
the module at all. Around that core it still implements a kill switch that treats an
unreadable halt file as engaged, snapshot staleness limits, future-clock-skew refusal,
and a settled-cash model that will not spend unsettled proceeds. test_placer_lib.py
asserts the module performs no environment lookup, "there is no secret to look up."
pip install -r requirements.txt
python monthly_signal.py --budget 10000python -c "import glob,subprocess,sys; sys.exit(max(subprocess.call([sys.executable,t]) for t in sorted(glob.glob('test_*.py'))))"Tests are plain executable scripts and need no test runner. Each prints PASS/FAIL
per assertion and exits non-zero on failure. Several checks skip on non-Windows hosts or
on a clean checkout (where live-state files are absent by design) and say so explicitly
rather than passing vacuously.
Requires Python 3.9+, numpy, pandas, yfinance; plus arch (bootstrap tests),
ruptures (change-point detection) and pyxirr (money-weighted return).
The private operational half of this system, broker synchronisation, the daily brief, portfolio state, the trade journal and the double-entry ledger, is not published. Those files carry account identifiers and live positions and have no research value.
Some modules therefore reference state files (portfolio.csv, config.json) that a clean
checkout will not contain. The research paths do not need them. Where a test exercises one,
it constructs a synthetic fixture.
MIT. See LICENSE. Vendored index-membership data is MIT from fja05680/sp500.
This is not investment advice. Every graded strategy in this repository failed its own pre-registered bar.