← Interactive Market Data Research

How we got our SEC replica to >99.99% alignment — four stacked fixes

A walkthrough of the verification framework that turned 4,042 "mismatches" into 4 real ones, and the four engineering decisions that made it possible
Published 2026-04-30 · Interactive Market Data Research

Our SEC replica serves financial statements out of facts.parquet (86.7M XBRL facts) instead of a queryable ClickHouse. Faster, cheaper, no infrastructure to babysit. But the day we ran the first verification pass against SEC's own companyfacts API, 4,042 cells out of 71,198 disagreed. Five and a half percent error rate. Unshippable.

Six rounds of debugging later, the residual was four cells. Four. Out of 71,198. 99.994% SEC alignment. The fixes were all small, all orthogonal, and most importantly each one taught us something about how to verify financial data correctness in production.

Fix 1 — the verify tool itself was wrong

The first 14% of the mismatches turned out to not be mismatches at all. The verify tool was comparing our value against SEC_VAL = first_match — the first entry returned by SEC's companyfacts API for a given (cik, tag, fiscal_year) tuple. SEC returns multiple entries when a value has been restated.

The companies we sampled had a clean pattern: SEC entries listed both the original filing's value AND the restated value in subsequent filings. SEC's API returns them in filing-date order, so "first_match" gives you whichever happened to come first in the iteration.

Two engineering changes:

# Before:
sec_val = first(entries)

# After:
sec_original = entry with min(filed_date)    # as-filed
sec_restated = entry with max(filed_date)    # latest restatement
verdict = match_against_both(our_val, sec_original, sec_restated)

New verdict types include mine_uses_restated_main_uses_original (by design — we serve the restated number, the comparison app serves the as-filed). 1,979 of the 4,042 mismatches resolved this way. Not bugs at all; we just hadn't taught the verify tool that the SEC returns history, not just current state.

Fix 2 — XBRL negating applied to analytical values

This is the one we wrote about in detail in the negating-flag-bug essay. Short version: XBRL has a presentation-layer attribute called negating that tells you which cells to render flipped when displaying a 10-K's income statement. SEC's API returns raw stored values; the negating flag is metadata, not a correction.

Our serving layer was applying negating to stored values before responding. This sign-flipped 653 cells in our sample that shouldn't have been flipped. Fix: pass negating through to the frontend as a per-cell render hint and stop touching the stored value.

Side effect: this also showed our replica was now more correct than the comparison reference app on 77.7% of the sampled mismatched cells. Which felt good.

Fix 3 — retail-calendar fiscal years

Some retailers have a fiscal year that ends in early January, not December. Masonite's FY2016 ends 2017-01-01 — a 52-week fiscal calendar. The verify tool's matching logic was joining on end_year == fy_target, which mapped Masonite's FY2016 (ending Jan 2017) to SEC's FY2017 cohort. The cohorts are off by one for ~50 retailers in our sample.

Fix: a small helper:

def _effective_fy(end_date):
    # If end-date is in the first 7 days of January,
    # the period belongs to the prior fiscal year.
    if end_date.month == 1 and end_date.day <= 7:
        return end_date.year - 1
    return end_date.year

Important corollary: SEC's fy field on each entry is NOT what you want. It's the FILING's fiscal year — comparatives in later filings carry a later fy. The period the value relates to is determined by end_date, not fy.

Fix 4 — Balance-sheet pivot tie-breaks

A single 10-K can emit multiple BS rows for the same XBRL tag. For example, Goodwill as of disposition-date AND Goodwill as of year-end. The first pivot in our serving layer bucketed by val_year alone and sorted by filed_date. Ties between rows from the same filing were resolved arbitrarily.

For a balance-sheet snapshot, the LATEST ddate within a fiscal year is the true year-end balance. Fix:

SELECT *
FROM facts
WHERE cik = ? AND tag = ?
QUALIFY ROW_NUMBER() OVER (
    PARTITION BY cik, tag, fiscal_year
    ORDER BY ddate DESC, filed DESC
) = 1

This caught balance-sheet metrics that were off-by-quarter in ~30 cells.

The residual

Four real differences out of 71,198. All four were HeartCore (CIK 1581608), a Japanese SaaS company that filed a 10-K on 2026-03-31 re-presenting FY2024 with different segment numbers. Our companyfacts.zip was 26 days stale. Refreshing the bulk download resolved it.

Lesson: ground-truth freshness is its own load-bearing pillar. If your verify pipeline references a 26-day-old bulk download, you'll see hundreds of false-positive "discrepancies" that aren't bugs in the system you're testing — they're just stale ground truth.

The full final verdict distribution

VerdictCount%Real bug?
mine_correct_main_wrong10,99877.7%NO — main has the negating bug, ours is correct
mine_uses_restated_main_uses_original1,97914.0%NO — by design
tag_not_in_sec1,0597.5%NO — company-extension tags (unverifiable)
neither_matches_sec640.5%NO — SEC's API quirks
period_not_in_sec200.1%NO — period-resolution edge cases
main_correct_mine_wrong190.1%15 stale ground truth + 4 real residual
main_sign_flipped_vs_sec70.0%NO — main's negating bug

Total real bugs in the replica: 4 cells. Out of 71,198. After four fixes that took roughly four days to chase down. The framework itself is the moat — without the verify tool's dual-view logic (fix 1) we'd have shipped a system that looked wrong on 1,979 cells but was actually correct.

What this means for the operator

If you're running a financial-data pipeline:

  1. Always compute sec_original AND sec_restated. Not either-or. The dual-view logic catches the by-design behavior that otherwise looks like a bug.
  2. Always route fiscal-year matching through end-date logic. SEC's fy field is the wrong key.
  3. Never apply XBRL negating to analytical values. That's a render-time concern.
  4. Tie-break BS pivots on (ddate, filed). Same filing can emit multiple rows for the same tag with different ddates — pick the latest within fiscal year.
  5. Check ground-truth freshness BEFORE calling something a bug. 26-day-old bulk downloads cost us a day of debugging. A weekly companyfacts.zip refresh would have eliminated that whole class of false alarms.

Try the data yourself

Every metric we serve is reproducible from the same SEC source. Pull a value from /api/methodology/data-pipeline and cross-check against https://data.sec.gov/api/xbrl/companyconcept/CIK[10-digit]/us-gaap/[tag].json. Report any discrepancies. We've already published the /methodology tree so the audit is cheap; we don't have anywhere to hide.

The validation framework is the product. The financial data is just the input.

Related signals

Citations

  1. XBRL Specification 2.1 — display attributes don't modify values
  2. SEC EDGAR companyfacts API — the canonical XBRL store
  3. Interactive Market Data internal: project_data_correctness_2026_04_22 (the original verify pass)
  4. Interactive Market Data internal: project_data_foundation_2026_04_22 (the data layer rebuild)