Worked example
Why a forward-only arena cannot backtest
A live arena scores events after they happen, so nothing it scores could have been in a training set. The same design fixes its window: it starts the day the arena started. Ask how an agent would have handled March 2023 and there is no run to point at. A replay answers that in the time the query takes, and this page walks through one we measured, where moving to the reachable clock takes the strategy from +5.13% to −2.02%.
Updated 2026-08-26. Every figure below comes from our own ablation run; the file it is read from is /data/ablation.json.
The run
One basket, one strategy, three clocks
88 tradable 8-K filings from 12 issuers, 2022-11-01 to 2023-03-31, scanned out of 287,929 corpus rows and held over 105 sessions. The strategy, the basket and the price series are identical across the three arms. The only input that changes is which clock decides when a filing was knowable.
| Arm | The clock it trusts | Total return | Max drawdown |
|---|---|---|---|
leaky | The partition date at 00:00:00Z. Enters at the open of the filing’s own index day. | +5.13% | −13.83% |
dump | published_at at 23:59:59Z on the index day. Enters the next open. | −2.02% | −17.58% |
pit | acceptance_at where the row carries a receipt, published_at otherwise. Enters the open after the receipt. | +0.18% | −16.91% |
The first arm buys filings before EDGAR has them. On 47 of the 64
filings that carry an acceptance receipt it entered a mean of 6.05
hours before acceptance, and at most 7.88 hours before, so no account
could have placed those trades. Switching to the clock that was
reachable takes the same 88 filings from +5.13% to −2.02%, and
the gap between the naive arm and the receipt arm is 4.95 points,
recorded in the file as lookahead_inflation.
The third row rests on 17 filings. pit and
dump enter on different sessions for only 17 of the 88,
so only those 17 can differ at all. Across them the per-filing difference averages
+0.95% with a standard deviation of 2.92%, and 9 of the 17 go the
pit arm’s way. That is roughly 1.3 standard errors
from zero, so the run sizes the effect and leaves its sign open. The
2.20 points describes this basket in this window and nothing wider.
The basket rule, the exclusions and the per-event table are on
methods.
The counterfactual
What the same question costs forward-only
A forward-only board cannot run the table above, because the window is in the past and the board’s window starts at its own launch. The nearest thing it can do is wait for 88 comparable events to arrive. Our five contiguous months produced exactly that count across a twelve-issuer basket, so the same sample costs roughly five months of calendar, plus the time to notice that the sample is still small.
The gold-winning entrants in CLEF 2026 task 3 report that one submission’s return moved from +2.0% to +13.51% between two snapshots of the same live window, with rank moving between 1st and 13th, and their own conclusion is that “live agent rankings are unstable”. Replay returns the 88-event result the same day, and returns it again on a re-run. The forward route costs about five months of calendar and ends at a ranking its own authors call provisional.
A live window happens once, so a reader cannot re-run it, and model stochasticity moves the number even for the operator running it twice (AlphaForgeBench, arXiv 2602.18481). Of 19 primary LLM-trading studies surveyed in arXiv 2605.19337, two report time-consistent splits and none reaches reproducibility. The three contamination controls are compared side by side on the other page.
The change
The walk-forward loop, with one line replaced
The filter on the left reads a date column your dataframe already holds, so every row you loaded is in scope at every t. The call on the right asks the server what was knowable at t, and answers 409 when it cannot say.
Procedure
Six steps and the call each one maps to
| # | Step | The call |
|---|---|---|
| 1 | Start from the certified window. Five contiguous SEC months are in today. | GET /v1/coverage?source=sec.edgar, or coverage month by month. |
| 2 | Name the clock on every query. The comparison is inclusive and in UTC. | visible_by=published_at for index replay, available_at for a strict cut. |
| 3 | Cut at every t in the loop, so each iteration fetches its own rows. | GET /v1/news?ticker=…&as_of=…&visible_by=…, or fetch_all_as_of(known_at=…) in Python. |
| 4 | Stop on a day nobody read. A 409 marks the day unread; a 200 with count 0 marks it read and empty. | HTTP 409 with status coverage_missing and count JSON null. See coverage certificate. |
| 5 | Resolve tickers at t. SIVB stops resolving after 2023-03-28, and today’s listing file drops it. | GET /v1/mapping/ticker/{ticker}?as_of=… |
| 6 | Measure your own timing error by reading the receipt clock off the rows you traded. | acceptance_at is on the row. It is not a visible_by option, because a receipt is not a dissemination record. |
Step 6 is what produced the 47-of-64 figure above. The measurement is
three lines over rows you already have: take the entry timestamp your
join implied, subtract acceptance_at, and count the
filings where the difference is positive. Any backtest over EDGAR can
run that check against its own trades, and a vendor without the
receipt clock cannot.
Still open
What replay does not fix
Cutting the retrieval side bounds what reaches the model. It does
nothing about the model having read the period during training, and
with no filing text on our rows yet, that memory is carrying most of
the narrative. Our four-arm run over SVB week measured it: given only
the string SVB FINANCIAL GROUP 8-K, the model produced a
dollar figure for the capital raise and a description of the
securities sale, neither of which appeared in the payload.
The control arm is how you size that channel. Withhold the issuer name and move every date forward ten years, then run the same prompt. In our run the same model returned a neutral view on every day of that week, receivership day included, which puts the signal in the weights. If your numbers survive the shift, the payload was doing the work. This is also the one place a post-cutoff forward window genuinely beats replay, and the two controls compose: use replay for the history and a post-cutoff window for the weights.
Limits
Five limits on the run above
- The certified backfill ends 2023-03-31 and the tape restarts 2026-08-26, so 2024 and 2025 cannot be replayed here.
- 64 of the 88 filings carry an acceptance receipt and 24 fall back to the day-end bound, so the
pitcurve is partly thedumpcurve until the backfill stamps more rows. - SIVB and WBA are excluded because the price source returns no bars for them, which removes SVB’s own filings from all three curves.
- Returns come from one basket over one five-month window with a five-session hold, so they size a timing error over that sample.
- PIT rows carry filing index metadata rather than filing text, so fetch the document from
source_locatorwhen the run depends on what a filing says.
Check it
Two instants, and the coverage behind both
The playground needs no account and watermarks every response sample: true.
$ curl -s "$PIT/v1/sample/news?example=svb&as_of=2023-03-09T20:00:00Z" $ curl -s "$PIT/v1/sample/news?example=svb&as_of=2023-03-10T23:59:59Z" $ curl -s "$PIT/v1/sample/coverage?example=svb"
The first answers count 0 with both sources certified
complete for 9 March. The second returns the receivership 8-K,
accession 0001193125-23-067777, whose acceptance_at is
2023-03-10T22:23:03Z. The same instant asked with
visible_by=available_at answers 409, because that clock
is null on SEC rows and we do not fill it in from a receipt.
Nearby