The line we added to keep answers independent made them biased

We removed a statistical dependence from our question bank. The removal was itself a sampling rule, and it turned a coin flip into 63%. We misdiagnosed it twice.

In short

  • Removing overlapping outcome windows is not housekeeping. Qualifying events arrive in runs, so a skip-forward rule keeps the first of each run, and that selection moved a naive rule from 49.3% to 63.0%.
  • The obvious suspect was innocent. The magnitude filter everyone points at - us included - is worth half a percentage point applied on its own.
  • We published two wrong explanations before measuring the right one. The first was inherited from an unmeasured sentence; the second we inferred from a file size instead of reading the generator.
  • The consequence for anyone reading our numbers: a baseline measured inside a question bank describes the question bank. Ours were presented as though they described the market, and that was our error to fix.

Someone put our methodology page through an unsympathetic review this week and came back with a specific accusation: our question bank decides which moments become questions by looking at what happened next. A trader standing at 10:35 cannot know that the next fifteen minutes will produce a move of 1–3× ATR. A SwipeTA player, implicitly, can — because if the chart is in front of them, it already passed that test.

That is correct, and we will come back to what it means. But checking it turned up something we did not expect, which is that we had been wrong about our own numbers for two weeks, and our first attempt to fix it was wrong too.

One thing to rule out before anything else, because the title invites the wrong reading: no answer in the bank was ever computed incorrectly. The labels are right; every question's outcome is what price actually did. What went wrong is upstream of that — which moments survived into the bank at all. This is a story about sample composition, not about arithmetic.

The sentence that started it#

We publish a figure that gets quoted more than anything else we have measured: fading the last 5-minute candle is right 60.67% of the time. And this site used to defend it like this:

It is not noise, and it is not an artifact of our selection — it is there in the raw candidate pool before any filtering.

That sentence was written from intuition. Nobody measured it. Eight days later we shipped a horizon sweep that scanned 140,319 unfiltered decision points and found the same rule sitting at 50.0% — and nobody reconciled the two, because the new script was checked against the article it was written for and not against the article it happened to falsify.

So the first thing to say plainly: the rule is a coin. Across 366,606 unfiltered decision points on the exact seven symbols and date range the 60.67% came from, it is right 49.78% of the time.

The obvious suspect is innocent#

The natural next move is to blame the magnitude filter. It is the step that conditions on the future, it is the one an outside reviewer would point at, and it is the one we pointed at ourselves in a second article.

Apply the 1–3× ATR band to every eligible bar and the rule scores 49.31%.

Half a point. The filter that looks like the whole story is worth almost nothing on its own.

Where the thirteen points actually are#

One naive rule, scored after each stage of building the question bank
Every eligible bar 49.78%
+ keep 1–3× ATR moves 49.31%
+ skip forward after each pick 63%
+ one per symbol per session 59.3%
+ balance 50/50, cut to 1,200 59.75%

Dashed: 50% — a coin. Bars run the full 0–70% scale.

How often "fade the last candle" is correct, measured again after each construction step, on the seven symbols and the 2023-01-01 to 2026-06-30 range the published 60.67% was computed on. The band that looks like the obvious culprit costs half a point. The thirteen points arrive at the next line, which is not a filter at all: after accepting a decision point the generator skips forward one horizon so outcome windows stay disjoint, and qualifying bars arrive in runs, so that rule keeps the first bar of each run. The reconstruction ends at 59.75% against a published 60.67%, and at 6,103 pool rows against 6,083. SwipeTA fade-rule decomposition, measured 2026-08-17; script and output at research/fade_rule_decomposition.py.

The jump happens at the next line of the generator, and that line is not a filter. It is this:

after accepting a decision point, skip forward one horizon before accepting another

Its purpose is written in the code beside it — outcome windows must not overlap, or two questions in the bank are decided by the same future bars. That reasoning is correct, and we would still enforce non-overlapping outcome windows. What we would not do again is assume this implementation of it is neutral: taking the first member of each run is one way to enforce disjointness, and picking a random member, or staggering the offset, are others that do not select on position. It is the same instinct behind keeping answers disjoint everywhere else in the pipeline.

Here is what nobody checked. Qualifying bars do not arrive one at a time. Neighbouring decision points share most of an outcome window, so when one bar's next fifteen minutes lands in the band, its neighbours usually do too. They come in runs.

A skip-forward rule applied to a run keeps the first bar and discards the rest.

So the rule does exactly what it was designed to do — no two questions share outcome bars — while also, silently, changing what kind of moment ends up in the bank. Not a random point inside a qualifying move, but its onset. Run the identical scan with that one rule switched off and the figure falls from 63.00% back to 49.31%.

We have not established why the onset of a qualifying run should favour a contrarian call. Having now been wrong about this twice in one day, we would rather leave it open than guess a third time.

The general form#

This is the part worth taking away, because it has nothing to do with candles.

De-duplication is sampling. Any time you remove overlapping observations — one trade per day per symbol, one signal per event, one row per user session, drop near-duplicates before training — you are not tidying the data. You are choosing which member of each cluster survives, and clusters are rarely arranged so that the choice is neutral.

What made it invisible here is that the step had a correct stated purpose. Nobody audits a line whose comment explains a real statistical problem it really does solve. The filter above it got scrutinised for years, in public, by us and by outside reviewers, because it obviously conditions on the future. The line below it was housekeeping.

The obvious selection step was worth half a point. The invisible one was worth thirteen.

The warm-up mistake described below is the same theme in a different costume: requiring fourteen bars of history before asking a question is not a neutral technical detail, it is a rule that deletes the opening period. In a data pipeline, preprocessing is rarely just preprocessing.

We got it wrong twice, in public, on the same day#

This piece would be dishonest without the process that produced it, so:

First wrong answer, inherited. The original sentence claimed the rate was not a selection artifact. It was written before the measurement existed, and it survived because our lint gate checks that numbers have sources — and "this is not a selection effect" contains no number.

Second wrong answer, ours, this morning. We shipped a correction blaming the per-session cap: the bank keeps one setup per symbol per session, which is a real selection step and sounds like a good culprit. We inferred that from the pool size — 6,083 rows against about 6,160 symbol-sessions — instead of opening the generator. When we did open it, the rule turned out to keep the day's most decisive setup, and reconstructing that stage properly moves the rate down, from 63.0% to 59.3%. The stage we blamed was working in the opposite direction.

There was a second defect in that attempt worth naming, because it is a trap anyone reconstructing a pipeline will meet. Our scan started at bar 14 to let a 14-bar ATR warm up. The generator starts at bar 5 and computes ATR from whatever history exists. A warm-up requirement silently deleted the entire opening period, which is 19% of the published bank and, as we measured last week, the part of the session that behaves least like the rest.

Three attempts, two of them published. The measurement that settled it took about forty seconds to run.

What we changed#

And the accusation itself#

It stands, and it is the right frame for reading anything we publish about our own bank.

Every question in SwipeTA passed a test applied after the fact. That makes the bank good at asking "given a moment where something happened, which way?" — and silent on "was this a moment worth acting on at all?", which is a large part of the job. A baseline measured inside the bank describes the bank. We presented one as though it described the tape, and that was ours to fix rather than theirs to catch.

The review was right about the conditioning and wrong about what produced the thirteen-point jump — wrong in exactly the place we were, by assuming the conditioning happens where the conditioning is visible.

SwipeTA is a training game and a simulation: no real money, no broker, and it does not provide investment advice. The parameters behind our measurements are on the methodology page.

Sources#

  • SwipeTA fade-rule decomposition, measured 2026-08-17: 366,606 decision points across MSFT, AMD, SPY, QQQ, TSLA, AAPL and NVDA over 2023-01-01 to 2026-06-30 — the universe and range the published 60.67% was computed on. Scores the same naive rule after each construction stage, and isolates the spacing rule by running identical scans with it on and off. Script and output: research/fade_rule_decomposition.py and research/out/fade_rule_decomposition.json in the site repository.
  • SwipeTA horizon sweep: the unfiltered rate for the same rule across 140,319 decision points and 22 symbols, 2022-03-07 to 2026-06-30, at five horizons. Script and output: research/horizon_sweep.py and results/horizon_sweep.json in the public research repository https://github.com/BOHARRY/swipeta-research (MIT / CC BY 4.0).
  • SwipeTA question bank generator: the spacing rule, the per-session cap, the balancing step and the point at which the baseline statistic is computed are read from services/quizbank/src/quizbank/generate_intraday.py. Read 2026-08-17.
  • https://www.swipeta.net/methodology