DEV Community

ssapable
ssapable

Posted on

Public minute bars passed every sanity check and were still wrong: 233 vs 293 fills

Last week I published a result about my partner's futures trading: her hand exits beat every fixed take-profit/stop bracket I could fit. It was out of sample, it had a sanity check, and it was wrong. Not because of a bug. Because of the bars.

This is what the check looked like, what it couldn't see, and the version of the table I now trust.

The check that passed

Her NinjaTrader export has one row per trade with entry time and fill price, on the PC's clock. The 1-minute bars I had at first were public (Yahoo Finance), in UTC. To replay her entries on bars you need two things: the clock offset between the two, and some confidence that each fill actually happened inside the bar you're about to use.

The tool searches offsets in 30-minute steps across a day in each direction, and for every offset counts how many entries have a fill price between that bar's low and high. The offset with the highest share wins:

def align(trades, bars):
    best = (pd.Timedelta(0), -1.0)
    for half_hours in range(-28, 29):
        off = pd.Timedelta(minutes=30 * half_hours)
        m = _fill_bars(trades, bars, off)          # one row per entry: its bar at this offset
        inside = ((m["entry_price"] >= m["low"]) & (m["entry_price"] <= m["high"])).sum() / len(m)
        if inside > best[1]:
            best = (off, float(inside))
    return best
Enter fullscreen mode Exit fullscreen mode

On her September export the answer was an offset of 9 hours (Korea), and every entry that had a bar sat inside it. The output said:

233 of 293 entries have bars that match their fills (clock offset 0 days 09:00:00).
Enter fullscreen mode Exit fullscreen mode

Every bar that existed was the right bar. I read that line as "the data is good" and moved on.

What it couldn't see

The 60 missing entries were not random. The public feed started on September 3, so her first days were gone, and it only carried the front-month contract, so anything she traded on another expiry had no bars at all. The check only scores entries that have a bar. It says nothing about the ones that don't, and it can't, because there is nothing to compare them against.

Then she exported her own 1-minute bars from NinjaTrader. All 293 entries had a bar, and the same alignment code found the same offset.

The same test, both ways

The test: fit a bracket (target and stop as multiples of her usual move) on her older 70% of trades, score it on the newest 30% it never saw, and compare with what her hand exits made on those same trades.

Bars Entries with bars Holdout trades Best in-sample bracket Bracket, out of sample Her exits, same trades
Public (Yahoo) 233 of 293 70 target 3x / stop 3x −$75.54 +$176.46
Her own (NinjaTrader) 293 of 293 88 target 3x / stop 3x +$444.86 +$271.36

Same code, same bracket, opposite sign. On public bars her exits looked like the strongest part of her record, and I wrote that up. On her own bars a wide symmetric bracket would have made about $170 more than her hands this month.

How much of that is noise? Her trades cluster by day, so I resampled the 88 holdout trades by day 1,000 times: the bracket came out ahead in 66% of resamples. Better than a coin flip, not by much. The honest summary is "open question, next month's export decides", and that is what the report says now.

It wasn't only the bracket

The rules table moved too. She trades with a rule about the 15-minute 200 EMA. Checked at the moment of each click, no look-ahead:

Bars Followed Trades Win rate Net
Public yes 60 60.0% +$26.54
Public no 96 52.1% −$83.95
Her own yes 82 67.1% +$450.08
Her own no 161 57.1% +$520.31

The direction of the gap survived (with the trend wins more). The size of everything else didn't. On public bars breaking the rule looked like a losing habit. On her own bars it was 161 trades that made money, with a ten-point lower win rate. Those lead to different conversations.

What I do differently now

  1. Coverage is a result, not a precondition. The replay prints "N of M entries have bars" before any table, and the next change is to list which entries are missing and why (date range, contract month). A check that only scores what exists needs a second line about what doesn't.
  2. The trader's own bars, or nothing. Public minute data for futures is fine for charts and bad for replaying someone's fills: wrong start date, front month only, and no way to tell from the inside.
  3. Report the sign flip. I put a correction on the first post rather than quietly updating the number. If a reader made a decision on the old table, they deserve to know it moved.
  4. Fill-inside-bar is necessary, not sufficient. It catches the wrong offset and the wrong contract. It cannot catch absence.

The code is open source and runs on your machine; the replay prints the coverage line and the bracket table from a plain NinjaTrader export plus your own bars:

GitHub logo ssap-pa / tilt-check

Pre-trade check for futures traders: TabPFN learns from your own NinjaTrader history, Gemma explains it locally. Never places orders.

tilt-check

A pre-trade check for futures traders that learns from your own NinjaTrader history.

TabPFN (open weights) reads your past trades. Gemma (open weights, through Ollama) tells you in plain language what your own numbers say about the trade you're about to take. Everything runs on your machine. It never places an order.

A real check replayed on my partner's history: the groups this trade falls into, an honest model check, and a note from Gemma running locally

I built it for my partner, who trades micro futures (MNQ, MES, MGC) on a prop-firm account and kept asking the same question after a bad session: is this one of my good trades or one of my bad ones?

Want this run on your own export and written up? Your Trading History, Audited: a written report within 48 hours, late means a full refund. The sample is one real account.

What it said about her account

Her numbers, shared with her permission. One account, Sep 1 to Oct 2, 2026:

  • 293 entries over 23…




If you'd rather I run it on your export and write it up, the details are here: https://ssap-pa.github.io/tilt-check/

Top comments (1)

Collapse
 
arhancanli profile image
Arhan Canli •

One thing in the bracket table is worth splitting before putting the whole sign flip on the bars: the two holdouts aren't the same trades. The newest 30% of 233 entries and the newest 30% of 293 are different sets, 70 and 88 trades cut at different points, so the move from -$75.54 to +$444.86 mixes two causes: different bars and different trades. Replaying the 3x/3x bracket on only the trades both holdouts share, once on each set of bars, separates them. If those trades flip sign too, the public bars were wrong even where they existed; if they don't, the flip came from which trades got in. With the bracket ahead in 66% of day-resamples, knowing which it was decides what next month's export can actually settle.