Why Offline ML Evaluation Fails
Eleven documented failures of look-ahead leakage, selection bias and offline/live mismatch that survived the validation applied to them; the prefix test, the audits and the replay that expose each class; and the evaluation policy and causality gate that now stand before any number is quoted.
A model that fired about twice a day in offline evaluation fired zero times in four days of live operation. The model weights were the same. The data fields matched. The difference was that offline evaluation supplied the whole session, while live operation supplied only the observations available at the decision; when the offline session was truncated to each decision, none of the model's 25 apparent decisions survived. Walk-forward validation had not detected it, because the leak was inside each evaluated session. That case is one of eleven documented failures on one platform in ten months, of look-ahead leakage, selection bias and offline/live evaluation mismatch, each of which survived the validation procedures originally applied to its experiment. This is the record of the eleven, the tests that expose each class, the evaluation policy that now stands before any number is quoted, and the causality gate that automates part of it.
Key findings
- None of the eleven documented failures originated in a model's forward pass. The convolutions were causal and the attention masks were square; the problems arose in upstream data preparation, population selection, label generation, reference data, or the offline evaluation itself (a census at the wrong cadence, fills from prices the decision never saw).
- In the documented probe, the leaking action score increased as more future observations were supplied. On one session it was 0.001 with the tape cut at the decision, 0.011 with an hour of future, and 0.899 with the whole day; even an additional hour of observations did not reproduce the full-session score.
- Test the continuous quantity, never the thresholded decision. The harness's own self-test passed a whole-session-mean scorer when its output was thresholded and failed it when scored continuously.
- A scorer that never commits has no verdict, not a pass. The harness refuses to certify a checkpoint that acts on nothing in the scanned sessions.
- Prefix tests expose computations that depend on future observations within a sequence. Reference-data audits, population checks and live-path replay are needed for the failures truncation cannot reveal, and six of the eleven were of those kinds.
- The evaluation policy that followed is five checks, with the audits and the replay as separate boundaries beside them; the causality gate is the one command that automates the truncation, mutation and live-probe part of it. The gate is mandatory before a production checkpoint changes; the evaluation policy requires it before any research number from a model or census is quoted, recommended or deployed. 81 of the platform's 154 test files are tagged for the gate, a count of tests rather than a measure of coverage.
1. The claim
Ordinary offline evaluation can reproduce a look-ahead leak without detecting it. Walk-forward validation moves the training window forward in time; it does not change the evaluation's view of each session, which is always the whole day, so a feature computed from the whole day scores in research exactly as it will not in production. Interleaved halves and multiple seeds test stability, not causality. Two claims should be kept apart. The observed one: these eleven problems survived the validation procedures originally applied to their experiments, which were not the same procedure in every case. The general one: conventional walk-forward metrics, seed checks and ordinary batch scoring do not by themselves establish causal correctness or serving parity. Prefix tests expose computations that depend on future observations within a sequence: hand the scorer the session as it stood at the decision and ask whether the decision changes. Historical reference-data audits, population checks and live-path replay are needed for the failures that truncation cannot reveal, because in those cases the future is already inside the input before anything is truncated, or the mismatch is between two execution paths rather than inside one.
"Causal by construction" is the phrase under which all eleven leaks shipped, and the phrase was true of the parts anyone thought to check. The sequence models used causal convolutions and square subsequent masks, and the mutation tests found no future dependence in the fine features on the tested cases. The leaks lived between the components whose causal properties had been checked. Verifying the model architecture alone did not establish that the surrounding data and evaluation pipelines were causal.
2. The eleven cases: leakage, selection bias and evaluation mismatch
Eleven recorded cases, not eleven independent root causes and not mutually exclusive mechanisms: the fifth row is downstream contamination of the fourth, and the tenth row holds two reference-data problems found a week apart. By the class column, four cases involved future-dependent computations, one involved labels contaminated by an earlier leak, two involved offline/live execution mismatches, two involved outcome-selected populations, and two involved historically unavailable reference data.
| Found | What leaked | Kind | Class | Caught by | What it had done to the number |
|---|---|---|---|---|---|
| 2026-08-16 | A census that read indicators from buckets not yet closed at decision time | Indicator computed on a window not yet closed | Computation leak, in the census | Live trades diverging from the census | The live configuration was negative in both years under causal evaluation; the confidence model had anti-selected out of sample |
| 2026-08-23 | An exit evaluated five seconds after the decision, on five-second bars | Exit timing | Offline/live mismatch | A one-second replay | The exit's apparent premium disappeared |
| 2026-08-24 | A grid model with two features that read forward: a membership flag and a fill boundary | Feature look-ahead | Computation leak | "Are you sure?", then a truncation audit | The confirmation was retracted; the ranking survived, the trading edge did not |
| 2026-09-14 | A codebook centred on the whole-session mean | Post-hoc normalisation over the full session | Computation leak | Code review, then the truncation probe | The action score moved from 0.001 to 0.899 between the prefix and the full day; 0 of 25 firing columns survived truncation |
| 2026-09-14 | Ride labels produced by that model | Labels inherit the leak of the model that made them | Downstream contamination of the case above | Followed from the case above | Every number built on those labels was retracted |
| 2026-09-18 | A census that stepped over bar indices on sparse five-second bars, evaluating once per 50 to 125 seconds, and filled resting limits from earlier lows | Offline cadence and fill timing differ from serving | Offline/live mismatch | Replaying the live class at the live cadence | A 59% win rate became 34%; a loser classifier had learned the late fires |
| 2026-09-22 | A previous close backed out of the same day's 09:30 open and a gap captured months later, in 56 to 59% of corpus sessions | Reference data rebuilt from a future snapshot | Reference data not point-in-time | Comparing the anchor with the provider's true prior close | Off by a median 25%; the research fires fell from 81 sessions to 4 on the true anchor, and the four lost |
| 2026-09-22 | A research universe of end-of-day gainers, picked after the day | Population selected on the outcome | Selection on the outcome | Splitting results by data source | A positive per-trade mean became negative on the full population |
| 2026-09-22 | A collector that kept only turns whose rising run lasted at least six more buckets | Population filtered on a future outcome | Selection on the outcome | Audit of the collectors | The excluded turns were 43% of what the live lane traded and lost four times as much as the kept ones |
| 2026-09-22, 09-29 | A float captured a median 331 days after the day; an issuer country from a current profile, where "unknown" predicted the outcome | Reference data not point-in-time; survivorship in a lookup | Reference data not point-in-time; two problems found a week apart, one case | A capture-date audit; the unknown flag predicting the label | The country channel was the whole of one model's apparent crash avoidance |
| 2026-09-22 | A setup that indexed the previous five minutes as position minus 301, which at 04:05 is index −1: the day's last close | Off-by-one into the future | Computation leak | The truncation test itself | 656 of 7,121 setups had been selected by the 10:00 price |
One more belongs beside them. The harness's self-test, on the day the harness was written, passed a deliberately leaking scorer when its output was thresholded and failed it when scored continuously; the rule that followed is in section 5.
3. One case end to end: the score that needed the whole day
A sequence model had been trained to decide, every ten seconds, whether to act. Its research census showed it firing about twice a day with a strong record, and when it went live it fired zero times in four days. Everything ordinary was ruled out: the model and its forward pass produced the census's values when fed a cached session, the session fields matched, the previous close and float matched, the tape density matched, and the live flow gate was open on the columns where the census fired its best trades.
What was left was the shape of the two evaluations. Live scores a growing prefix of the session; the census scores the whole day at once. Truncating each of the census's firing columns to the session as it stood at that column, 0 of 25 survived, and the action score fell from between 0.52 and 0.99 to between 0.000 and 0.42. Feeding the model progressively more of the future beyond the decision column raised the value on every session probed, which is the figure above: on one session 0.001 with no future, 0.011 with an hour of it, 0.899 with the whole day, so even an hour of observations did not reproduce the full-session score. The partial-column explanation was dead at ten seconds, because at that point the decision column was complete and the score was still near zero.
A stage-by-stage mutation found where the future entered. The fine features did not move when every bar after the column was corrupted; the session grid moved; the codebook's token moved; the belief and action heads moved. The leak was upstream of the convolution stack, in the grid and the codebook: the code stream had been centred by the mean over all 540 tokens of the session, a history marker at a column could exist or not depending on tape after it, and that marker read the first print of the next column, one second of future sitting on the candle boundary. Only the third was subtle. The first two had been invisible because everything around them was correctly causal, so nobody had checked the parts in between.
The blast radius was every result built on that grid: the codebook, the model's belief and action outputs, the labels its belief had produced, the pyramid censuses and a second lane's gate. Live firing zero had been correct behaviour throughout. Four days later causality became a hard requirement in the operating manual, with the four leaks that caused it written at the top of the section, and a single command that must pass before a production checkpoint changes.
4. Why conventional offline evaluation missed them
| In this platform | In any ML org |
|---|---|
| A previous close rebuilt from the future open | A feature joined from a reference table snapshotted after the event |
| End-of-day gainers used as the research universe | Training and evaluation population selected on the label |
| A census stepping bar indices on sparse bars | Offline evaluation at a different cadence from serving |
| Fills taken from lows before the decision | Offline reward computed from state the policy never saw |
| Labels produced by a leaking model | Pseudo-labels and distillation inherit the teacher's leak |
| A codebook centred on the whole-session mean | Normalisation statistics computed over the evaluation set |
| A float captured a year late; a country from a current profile | Reference data that is not point-in-time; survivorship in a lookup |
| A thresholded test passing where the continuous one fails | Metric granularity hiding the defect |
Each row is reproduced, not detected, by ordinary batch scoring, because in every row the evaluation and the defect agree with each other. Future-dependent computations can be exposed by comparing full-session outputs against prefix-only outputs at appropriately selected decision points, which is what the harness does. The reference-data and population rows do not: the future is already inside the input, so the same wrong value comes back at every cutoff, and they need a capture-date audit or a population check instead. The cadence and fill rows are mismatches between two execution paths, and only a replay through the serving path shows them.
5. The harness
The harness takes any scorer that maps a session, a dictionary of per-bar arrays plus scalars, to one decision value per ten-second column. It discovers, per scorer, the columns where that scorer actually commits, so that a retrained checkpoint cannot quietly measure nothing, and it judges only columns whose prefix holds enough bars to be scored at all. Then it runs two checks:
- Truncation. The decision at column j computed on the session cut at j must equal the decision at j computed with the whole day.
- Mutation. Corrupt every bar after a column, prices shuffled and volumes rescaled, rebuild, and require everything at and before that column to be bit-identical to the clean build. Mutation perturbs future observations while preserving the sequence structure; it complements truncation by detecting value-dependent future reads that a truncation-specific branch in the code could otherwise mask. A read of the sequence's length, by contrast, is caught by truncation and not by mutation, since mutation leaves the length alone.
| Check | What it primarily detects |
|---|---|
| Truncation | Dependence on future values, on the sequence's length, or on whether a future row exists |
| Mutation | Dependence on future values when the full sequence shape remains intact |
| Membership or drop cut | Dependence on entities that were selected using future information |
| Historical availability audit | Reference values that were not available at decision time |
| Live-path replay | Differences between offline and serving execution semantics |
Two rules make the checks mean something. First, score the continuous quantity the decision thresholds, the probability, the logit, the predicted return, never the 0/1 decision. Second, no verdict is not a pass: a scorer that never commits in the scanned sessions is uncertified, and the gate exits with a code of its own for that case rather than a green one.
The causality gate is one command with three stages: the mutation tests, every test in the suite tagged causality, and the acceptance probe on the live configuration through the live loader, which checks that each decision the deployed checkpoint actually commits to is unchanged when the session is truncated to that instant. Exit code zero means the gate passed. Anything else means do not quote and do not deploy. The gate is enforced before the production checkpoints change; before a research result is reported it is required by the evaluation policy, a procedural rule rather than a technical one, which is the distinction this article is about. Every new lane, model, feature, label or census ships with a truncation test tagged for it before its first number is quoted.
Why the continuous score, not the decision
Imagine a decision bar of 0.5. A leaking score changes from 0.10 with the prefix to 0.40 with the whole day. Both values are below the bar, so a test that compares only the decision sees no difference and passes; a test that compares the score sees 0.10 against 0.40 and fails. The harness's own self-test found exactly this on the day it was written: a scorer that centred on the whole-session mean passed when its output was thresholded and failed when scored continuously. The codebook leak above shows a much larger discrepancy, from 0.001 to 0.899. A binary-decision comparison would detect that one at a bar of 0.5, but continuous-score testing remains necessary because smaller leaks change the score without changing the decision. The rule is to test the quantity the decision is made from, even when the system's output is binary.
6. The evaluation policy that followed
Before any number from a model or census is quoted, recommended or deployed:
- Truncation. The decision at column j on the session cut at j equals the decision on the whole day. A scorer that never commits has no verdict, not a pass.
- Live-path replay. Score real recent days through the serving class, never a research loader.
- Gate parity. A census applies the live gate the way live does, per column, with that lane's thresholds. Latching at the first opening had overstated one lane's trades twofold; another lane's thresholds had made 80% of its census births unreachable.
- Labels are only as causal as what made them. A model-managed label inherits that model's look-ahead.
- Artifacts carry their causality. A manifest sits beside every codebook and grid cache, and loaders refuse a mismatch.
The five checks are not one boundary. They enforce model-output causality (check 1), offline/live execution parity (checks 2 and 3), label lineage (check 4) and artifact integrity (check 5), and the causality gate command automates the first of these and the replay probe; the others are enforced by the loaders, the census code and review. "Evaluation policy" below means the whole set; "causality gate" means only what the command runs. Two classes of failure sit upstream of anything the five checks can cut, and they got rules of their own after the September audits. A number joined from a reference table must have been knowable on the day: check capture dates, not column names. And a "pre-known constant" derived from same-day data is not pre-known. Per-second truncation of a model's inputs cannot see either; the upstream builders are tested by swapping the anchor for the true value on the same sessions and watching the decisions. In October the same discipline moved into the platform's feature store, which certifies every feature group by truncation, mutation and a membership cut before it is written; that is its own article, and this one is the record of the failures that made it necessary.
7. What it changed
The September audits replayed every premarket lane on the full population with the true previous close and each lane's own live gate. Every one that had looked profitable in research lost on honest inputs, and the platform's research archive records 32 threads closed or refuted, each with its numbers, checked before any new proposal. That is a negative result and it is the point: a system built to be able to say no said no. Live firing zero had been the system working. The lesson for the platform was not a better model but a harness any scorer must pass, a replay through the serving class, and a store that refuses rather than warns.
The purpose of an evaluation system is not to produce convincing numbers. It is to reject numbers that cannot survive the conditions under which the system must operate.
Appendix: manifest
| Item | Value |
|---|---|
| The cases | The operating manual's causality section, which opens with the first four leaks; the audit documents of 2026-09-22 and 09-23 on the corpus anchor, the conviction lanes, the episode lane and the premarket edge map; the memory notes on each case, dated as above |
| The harness | tests/causality_harness.py: truncation_check, mutation_check, discover_cases, a NO-VERDICT outcome; the self-test of 2026-09-14 |
| The gate | scripts/tapelm/causality_gate.sh: mutation tests, every test marked causality, the acceptance probe on the live configuration; exit 0 passes, exit 2 is no verdict |
| The probe | scripts/tapelm/conviction_causality_probe.py, 2026-09-10: 0 of 25 firing columns survive truncation; the recovery table in the figure is its output on five sessions, labelled A to E here |
| Tagged tests | 81 of 154 test files are tagged for the gate as of 2026-10-11; a count of tests, not a measure of how many leakage paths are covered |
| Population | The September audits ran on the full-population store as frozen on 2026-08-11: 904 trading days from 2023-01-03, about 2,000 names a day; the edge map on 31,531 mover symbol-days in that window. The feature-store article's 944 days are the same store extended to 2026-10-07 |
| Not claimed | Any return, win rate or trading result as evidence of edge; the effect sizes above are quoted to show the size of each phantom, not a result |
Scope
One platform, one domain, one author, and eleven documented failure cases of several related classes; there is no claim that these are all the failures the platform has had, only all the ones it found and recorded. The cases were built by AI coding agents under the platform's standards and found by the same agents once the questions and then the gates existed, which the article on that program covers. The mechanism is general: ordinary batch scoring of any sequential decision system holds the whole sequence, and the classes in section 4 appear in recommendation, ranking and forecasting pipelines in the same forms. The harness and the policy are offered as the smallest set of checks that caught every case here, not as a proof that nothing remains.
