Skip to content

Worked example

Backtesting an LLM on filings without lookahead

Four mechanisms let a filings API hand you information from after your cut instant. A clock gets substituted for another. A gap in coverage goes unlabeled. An identity join runs against today’s ticker file. Or a backfill timestamp is stored as though it had been recorded at the time. Each has a measured size below. A fifth channel runs through the model itself, and it stays open.

Updated 2026-08-26. Every figure below was read off a running server.

Payload

The rows carry no filing text yet

A PIT row holds the form type, the company name, the accession number, the CIK, five clocks, a SHA-256 of the index bytes it was parsed from, and source_locator, which is the SEC URL for the document. The document itself is not on the row.

Every SEC title in this corpus is built the same way, as the company name from the daily index followed by the form type, so all 86 SIVB rows in the 10 March 2023 cut collapse to six distinct strings. Accession 0001193125-23-064680 on 8 March, the capital raise, and accession 0001193125-23-067777 on 10 March, the receivership, both read SVB FINANCIAL GROUP 8-K.

A model reading a cut therefore writes mostly from its own memory of the period. Our four-arm run over SVB week measured that: given only SVB FINANCIAL GROUP 8-K, the model produced a specific dollar figure for the capital raise and a description of the securities sale, neither of which was in the payload. With the issuer name withheld and every date shifted forward ten years, the same model returned a neutral view on every day of that week, receivership day included. The signal was coming from training data, and training data extends past your cut instant.

Filing text is being ingested now. Until it lands, fetch the document from source_locator yourself when the run depends on what a filing says, and treat a model’s narrative about a PIT row as unattributed to the row.

Taxonomy

The four ways a filings API leaks the future

The four are independent, and each has its own fix.

LeakWhat it looks likeSize of itWhat closes it
Clock substitutionYou filter on a publication or filing timestamp and treat the result as what was public. Or you read an acceptance receipt as dissemination.Stamping EDGAR’s Date Filed at the start of its day instead of the end buys 22h23m of lookahead on the SVB receivership 8-K. The same shift applies to every row in every daily index.Name visible_by per query. available_at is never imputed, and acceptance_at is readable but not selectable as a cut clock.
Silent holeThe vendor never ingested a day, the API returns an empty list for it, and you record zero filings.A partition is one source on one day, and a certificate records what we read on it. 99 of our 303 SEC partitions carry a missing certificate instead of a row count, and every day outside the ten months we hold is missing too. Asking for SIVB as of 2015-06-15 returns count null with sec.edgar/2015-06-15 in coverage.missing; a vendor answering the same call with [] would have you recording a quiet day.A missing partition returns HTTP 409 with count as JSON null and the day named in coverage.missing, so it cannot be read as an absence of filings.
Current-universe joinYou join historical rows against a ticker file downloaded today. Symbols that have since been renamed or delisted drop out or point somewhere new.Ticker FB resolves to CIK 1326801 up to 2022-06-09 and META to the same CIK from that date, so asking for FB at 2023-03-10 is a hole. SIVB stops resolving after 2023-03-28. A join on today’s file loses both.Half-open ticker-to-CIK intervals resolved at the query instant. A ticker no interval places there is a 409 keyed identity.ticker/TICKER@YYYY-MM-DD.
Lane mixA first-seen timestamp recorded during a 2026 backfill gets stored as though it were 2020 knowledge.Every SEC row we hold carries availability_basis: unknown, so there is no evidenced availability to mistake a backfill for.A lane says how a row got its timestamp. certified_pit and forward_first_seen are separate lanes, and a page never mixes them. Backfilled rows receive no available_at.

Fifth

The leak PIT does not close

The four above are retrieval leaks, in that they govern which rows reach your model. The fifth runs through the model’s own weights: it was trained on text from after your cut instant, and cutting the retrieval side does not remove that. A prompt that fixes the date to 9 March 2023 is an instruction to the model, and the weights carry that week either way.

That channel matters more here than it usually would. The rows carry no filing text yet, so the model’s own memory is the only thing in the run carrying detail. Our control arm withholds the issuer name and shifts every date forward ten years, which is the cheapest way to measure how much of a result depends on that memory. If your numbers survive the shift, the payload was doing the work.

Procedure

Seven rules and the call each one maps to

#RuleThe call
1Name the clock rather than accepting a default you did not choose.visible_by=published_at for index replay, or available_at for a strict cut.
2Cut at T. The comparison is inclusive and in UTC, and a row whose named clock is null falls outside.GET /v1/news?ticker=…&as_of=…&visible_by=…, or query_as_of(known_at=…) in Python.
3Stop when a partition is missing rather than filling it with zeros.HTTP 409, status coverage_missing, count JSON null. See coverage certificate.
4Resolve identity at T, not against the listing as it stands today.GET /v1/mapping/ticker/{ticker}?as_of=…
5Ask what became knowable between two instants when that is the question.GET /v1/news/diff?as_of_start=…&as_of_end=…
6Keep the lanes apart.certified_pit and forward_first_seen never share a page of results.
7Read parquet for anything at scale; REST is for exploring.GET /v1/files on the Power plan returns 415 partitions, 178 MiB, each with its own SHA-256.

Sample

Three calls that show the mechanism

The playground needs no account, and every response it returns carries sample: true.

before, after, and the coverage behind both 0 → 1

$ 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 returns count 0 with both sources certified complete for 9 March. The second returns the 8-K on the default clock. The same instant asked with visible_by=available_at returns HTTP 409, because that clock is null on SEC rows and the strict cut refuses to guess.

Limits

Five limits

  • PIT closes the retrieval channel. A model’s own memory of the period stays open, and with no filing text on the rows, that memory carries most of the signal.
  • status: ok with an empty coverage.missing says the days behind the cut were covered, and says nothing about what the model did with them.
  • The public sample serves three fixed examples from pinned fixtures, so treat it as a demo rather than a small paid plan.
  • Every certified_pit row is parsed from a stored publisher document, so nothing generated is ever served back as a certified row.
  • The certified backfill ends 2023-03-31, and the live tape picks up at 2026-08-26, so 2024 and 2025 are outside the periods you can test.

Check it

Run the cut before designing the run

Two instants and the diff between them, with the coverage behind each answer visible in the response.

The full policy is on clocks, coverage, and lookahead bias. Plans and what the corpus covers.