Local tool + research · private repo · audit dated Jul 2026
I built the validation harness that killed my own strategy.
Swing Screener started as a CLI that scans a watchlist for the setups I trade. Then I built a backtest engine on top of it. Then I spent a week trying to destroy my own headline result — and largely succeeded. This page is about that second part, because it is the more useful engineering story.
129
symbols, 11 sectors, 10 years
audit: universe expansion
4,933
trades in the broad re-test
walk-forward, no lookahead
150
parameter cells swept
sweep bit-exact vs repo path
0
strategy lines changed to get there
no strategy source modified
The Problem
A backtest that confirms you is worth nothing.
My walk-forward on the anchored-VWAP-reclaim setup produced a headline expectancy that looked like a real edge — from eight correlated large-cap tech names I picked with hindsight, over one arbitrary window alignment. That is exactly the kind of number people put in a bio and then trade on. Before putting money behind it, the honest move is to build the test designed to kill it, not the one designed to confirm it. The headline did not survive, so it has been retired rather than quoted.
What It Does
Three setups, scanned on free daily bars.
Point it at a watchlist and it prints one row per symbol: which setups triggered and the levels that define them. Data is free yfinance daily bars, so there is no API key and no market-data bill.
Anchored-VWAP reclaim
VWAP tracks the average cost basis traders paid. Anchored to the recent swing low it becomes a support line measured from that event. The detector fires when price has closed below the anchored VWAP for at least two consecutive bars and the latest close pushes back above it. Reports the anchored VWAP level.
VCP — volatility contraction
Minervini's base pattern: successively tighter price ranges as weak hands are shaken out, confirmed by a volume dry-up against the baseline. The screener splits the recent tail into consecutive windows, checks each range is tighter than the last, and reports the pivot — the high of the final window.
Gap / opening range
The latest bar's open against the prior close gives the gap percentage; anything beyond the threshold flags a continuation candidate. The prior bar's high and low become opening-range-breakout reference levels for the next session — ORB Hi and ORB Lo.
What the backtest engine guarantees
The backtest subcommand replays the same detectors bar by bar. At each bar the detectors see only data up to that bar — no lookahead. When a setup fires it opens a trade using that setup's own entry and stop levels, targets a configurable R multiple, and walks forward to the stop, the target, or the max-hold timeout. Nothing is decided with information the trade would not have had.
What it outputs
A Rich-rendered terminal table of triggered setups with close, anchored VWAP, VCP pivot, gap % and ORB levels — optionally filtered to symbols that actually triggered. The backtest prints hit rate, average R multiple and expectancy per setup. Either mode can save a dated markdown report.
What I Built
Three layers: screener, backtester, audit.
The screener
A terminal CLI over free yfinance data. Scans a watchlist for three setups — anchored-VWAP reclaim, Minervini-style volatility contraction with volume dry-up, and gap/opening-range levels — and prints a table of what triggered with the key levels. Saves a markdown report for the day.
The backtester
Replays the same detectors bar-by-bar with no lookahead: at each bar it only sees data up to that bar. When a setup fires it opens a trade using that setup's own entry and stop levels, targets a configurable R multiple, and walks forward to stop, target, or timeout. Reports hit rate, average R and expectancy per setup.
The audit harness
Universe expansion, survivorship bucketing, a 150-cell parameter sweep, regime splits, correlation-adjusted significance, block bootstraps, and — the one that mattered — signal-free control benchmarks. Six scripts, all reproducible from the repo.
The bit I'm proudest of. The repo's own backtest loop is O(n²) with pandas overhead — far too slow for a 150-cell sweep across 129 symbols. So I wrote a prefix-sum re-implementation and then asserted it bit-exact against the real repo code path across 160 windows and 16 symbols, zero mismatches. Speed came from a proof of equivalence, not from quietly changing the strategy under the test. No strategy source was modified during the audit.
What The Audit Found
The headline number did not survive.
VERDICT: MARGINAL — do not size up.
Reproduced verbatim from my own report. The recommendation to myself: cap risk at 0.25% of equity, run a 60-trade forward test with a −10R kill switch, and do not scale until forward expectancy clears +0.10R. Trade it small or not at all.
1. It wasn't reproducible
The 126-bar window grid start is arbitrary. Sliding it across 21 alignments on the same eight names gives a median of +0.004R, ranging −0.045R to +0.287R. The original headline sat near the top of that distribution — a coin flip that landed well. The centre of the distribution is indistinguishable from zero.
2. It's mostly long drift
Bucketing symbols by their own 10-year buy-and-hold return, expectancy is a clean monotonic function of it: +0.159R on winners, +0.038R (t = 1.22, not significant) on names that actually went down. That is the signature of harvesting market drift, not of a pattern edge.
3. The signal carries no information
The killer control: entries taken when price is above the anchored VWAP earn +0.100R; entries taken when price is below it earn +0.098R. Identical. The setup's entire premise — that reclaiming the anchored cost basis means something — tests as pure noise.
4. What's left, costs eat
Risk-matched random entries using the identical stop and target already capture +0.086R — long-term market drift. So the signal's own incremental contribution is +0.030R, 95% CI [−0.014, +0.077] — crossing zero, so it does not clear significance. Round-trip costs of 5–10 bps run 0.018–0.036R and would mostly or entirely consume it.
What did survive, in fairness. The strategy is not fake and it is not curve-fit. Expanded to a 129-symbol, 10-year universe it still returns +0.117R over 4,933 trades with a block-bootstrap 95% CI of [+0.057, +0.174] and a correlation-adjusted t of +3.4. All 150 parameter cells came back positive with a smooth, monotone surface — and the shipped parameterisation sits mid-pack rather than on the peak, which is the strongest evidence against curve-fitting. The raw expectancy is real. It just isn't the signal's.
Stated plainly: this is not a validated edge.
The only defensible summary is the one above — a positive raw expectancy on a broad universe, almost all of which a random entry with the same risk geometry would have collected anyway, and a signal-attributable remainder whose confidence interval crosses zero and sits inside transaction costs. Anything stronger than that is not supported by the data on disk.
Install & Quickstart
Python, uv, no API keys.
The repository is private today, so there is no public clone URL — access is by request rather than a link. Once you have a copy, the whole setup is one dependency sync: uv sync from the project root creates the virtualenv and installs everything. There is nothing to configure, no key to obtain and no account to make.
screener
uv sync # create .venv, install deps
uv run swing-screener # default watchlist
uv run swing-screener --symbols SPY,NVDA,AMD # tickers inline
uv run swing-screener --config watchlist.example.txt
uv run swing-screener --triggered-only # hide symbols with no setup
uv run swing-screener --period 1y # longer history window
uv run swing-screener --report # save a dated markdown report
backtest
uv run swing-screener backtest # default watchlist, 2y
uv run swing-screener backtest --symbols SPY,NVDA,AMD --target-r 3 --max-hold 15
uv run swing-screener backtest --period 5y --report
Every flag above is the real CLI surface, not an illustration. Defaults: --period 6mo for the screener and 2y for the backtest, --target-r 2.0, --max-hold 20 bars. A watchlist file is one ticker per line and accepts # comments. The audit scripts that produced the findings above live alongside the package and are reproducible from the repo.
The Stack
Free data, no black boxes.
Python
uv
pandas · numpy
yfinance
Rich (terminal UI)
Walk-forward backtest
Moving-block bootstrap
ICC / design-effect adjustment
Risk-matched control benchmarks
pytest (no-network fixtures)
Indicator math — anchored VWAP, VCP contraction and volume dry-up, gap arithmetic — is unit-tested against small synthetic OHLCV fixtures with no network calls, so the tests are deterministic and fast.
Honest Status
A local tool and a research artifact.
What it is
A CLI I run on my own watchlist, plus a no-lookahead backtest engine, a validation harness and a full written audit. All of it works, and the audit is reproducible from scripts in the repo. MIT licensed, 125 passing tests.
Not a signals service
It does not send alerts, run on a server, or tell you what to buy. It prints levels; levels are references, not recommendations. It places no orders, connects to no broker and has no idea your account exists.
No return promises
Not a profitable trading system, and I am not claiming one. Every figure on this page is backtested or walk-forward simulated — none of it is live P&L. The audit above is the reason to treat the setup as unproven rather than promising.
Paper only, downstream
The execution engine that would trade this setup exists and is tested, but it stays in paper mode precisely because of what this audit found — and it refuses to connect a live broker until a real paper track record exists.
Why this is on my portfolio at all. Anyone can produce a backtest with a good number in it. The transferable skill is building the adversarial test that finds out whether the number means anything — controls, correlation adjustment, survivorship checks, bit-exactness proofs — and then writing down the answer when it goes against you. Educational tooling, not financial advice.
The method is written up separately, without the project attached to it: how walk-forward testing works, the ways a backtest overfits, how to read a backtest report, and the anchored-VWAP setup itself.
FAQ
Common questions.
What is an anchored VWAP reclaim?
VWAP weights price by volume, so it tracks the average cost basis traders actually paid. Anchoring it to a meaningful bar, here the recent swing low, turns it into a dynamic support line measured from that event. A reclaim fires when price has traded below the anchored VWAP for at least two consecutive bars and the latest close pushes back above it.
Does Swing Screener give me buy signals?
No. It reports which setups triggered and prints the associated levels, and those levels are references rather than recommendations. It places no orders, connects to no broker and knows nothing about your account. Nothing it outputs is financial advice.
Is the backtest walk-forward?
Yes. The backtest replays the same detectors bar by bar, and at every bar the detectors see only the data available up to that bar, so there is no lookahead. When a setup fires it opens a trade using that setup's own entry and stop levels, targets a configurable R multiple, and walks forward until the stop, the target or the max-hold timeout is reached.
What does expectancy in R mean?
R is one unit of risk, the distance from the entry to the stop. A trade that reaches a 2R target earns twice what it risked and a trade that is stopped out loses 1R. Expectancy is the mean R across every trade, so an expectancy of +0.10R means the average trade returned a tenth of what it put at risk.
Can I run it on my own watchlist?
Yes. Pass tickers inline with the symbols flag, or point the config flag at a watchlist file containing one ticker per line. Price data comes from free yfinance daily bars, so there is no API key to obtain and no paid market data subscription to buy.
Did the strategy actually hold up?
Not as a validated edge. Across a 129-symbol, 10-year universe the setup returned +0.117R over 4,933 trades with a block-bootstrap 95% confidence interval of [+0.057, +0.174]. Risk-matched random entries using the identical stop and target already captured +0.086R of that, so the signal's own incremental contribution is +0.030R with a 95% confidence interval of [−0.014, +0.077], which does not clear significance.
Is it free and open source?
It is MIT licensed, with no paid tier, no account and no telemetry. The repository is private at the moment rather than published, so access is by request instead of a public clone URL.