Skip to content

Latest commit

 

History

3 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Systematic Trading Research

tests

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.


Why this repo exists

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:

  1. Register the hypothesis and the pass/fail bar before running the test.
  2. Freeze the registration on a remote: prereg.py refuses 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.
  3. Deflate for the number of things you tried. n_trials is recorded in the registration and carried into the significance test.
  4. 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.


1 · The 70/30 core allocation

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?

The result worth reading

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.


2 · The swing sleeve

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.py carries a truncation-invariance canary that catches it.
  • Slot sizing is a share of equity, not of cash. cash / max_positions looks identical to equity / max_positions and 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 = 16 rather 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=0 versus 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.


3 · The analysis layer

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."


Running it

pip install -r requirements.txt
python monthly_signal.py --budget 10000
python -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).


What is deliberately not here

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.

License

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.

About

Retail-scale systematic portfolio research: a 70/30 equity+trend allocation, a swing sleeve, and a pre-registration harness that refuses to grade a hypothesis until its bar is frozen. All five graded registrations failed, and are published with their verdicts.

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages