Why SEC XBRL Data Is Wrong 1 Out of 5 Times (And How to Fix It)
I pulled JPMorgan's 10-Q expecting one number for total assets. I got twenty-three.
Not twenty-three values — twenty-three tags, all of them
called Assets, all of them in the same filing, all of them
technically correct. One for the consolidated bank. One for the investment
bank. One for consumer banking. One for each subsidiary that has to be broken
out. And exactly one of them is the number you actually want.
If your code does what mine did the first time — take the first
Assets fact and move on — you get a segment. Sometimes a small
one. And nothing anywhere tells you that you got the wrong one, because you
didn't. You got a right one.
This is not a JPMorgan problem
XBRL lets a filer attach dimensions to a fact: this figure, but for this segment, this geography, this legal entity. That is a genuinely good idea. A bank's consumer arm and its investment arm are different businesses and flattening them into one line would lose real information.
The problem is that the consolidated figure — the one on the face of the balance sheet, the one everybody means by "total assets" — is stored the same way as all the others. It is just the one with no dimensions on it. So the correct figure is defined by an absence, and an absence is the easiest thing in the world for a parser to fail to notice.
I went and measured how often this bites. Across the companies I had loaded at the time, taking a naive first-match approach agreed with the consolidated figure roughly four times in five. The fifth is wrong. Not slightly wrong — wrong by whatever the largest segment happens to be, which for a big bank can be most of the balance sheet.
That number is not a swipe at anyone. It is what you get from the obvious implementation, and the obvious implementation is what almost everybody ships, because nothing about it looks broken. Your parser runs clean. Your JSON has a number in it. Your backtest returns a Sharpe ratio. Everything is fine right up until somebody asks you to tie a figure back to the filing.
The naive version
Here is roughly what most extractors do. It reads fine and it is wrong one time in five:
import requests
FACTS = "https://data.sec.gov/api/xbrl/companyconcept/CIK0000019617/us-gaap/Assets.json"
facts = requests.get(FACTS, headers={"User-Agent": "you you@example.com"}).json()
# Take the most recent 10-Q figure. What could go wrong.
quarterly = [f for f in facts["units"]["USD"] if f.get("form") == "10-Q"]
total_assets = sorted(quarterly, key=lambda f: f["end"])[-1]["val"]
print(total_assets) # a number. which one? nobody knows
There is no error here to catch. There is no exception, no null, no warning. The API gave you facts and you picked one. The bug is entirely in the selection, and selection bugs are invisible.
The fix is 500 years old
You do not need a heuristic for this, and you definitely do not need a model. You need the identity that has defined a balance sheet since Pacioli wrote it down in 1494:
Assets = Liabilities + Equity
That equation is not a guideline. It is what makes the document a balance sheet. And it gives you something better than a guess: a test. Pull every candidate for assets, every candidate for liabilities, every candidate for equity — then find the combination that actually balances. The consolidated figures balance against each other. A segment's assets do not balance against the whole company's liabilities. The arithmetic tells you which set is the real one.
# Pick the candidate set that satisfies A = L + E.
#
# `assets` and friends are EVERY fact reported for that concept in one
# filing, dimensioned ones included. Exactly one combination balances,
# and it is the consolidated one.
def reconcile(assets, liabilities, equity, tolerance=0.005):
best = None
for a in assets:
for lia in liabilities:
for eq in equity:
if a <= 0:
continue
gap = abs(a - (lia + eq)) / a
if gap <= tolerance and (best is None or gap < best[0]):
best = (gap, a, lia, eq)
if best is None:
return None # it does not balance: say so, do not guess
_, a, lia, eq = best
return {"total_assets": a, "total_liabilities": lia, "total_equity": eq}
Two details matter more than the loop.
The tolerance is small and it is a fraction, not a constant. Filers round. Half a percent of assets absorbs that; a fixed dollar tolerance either fails every large bank or passes anything at a small one.
When nothing balances, return nothing. This is the part that is tempting to skip. If no combination reconciles, the honest answer is that this filing does not balance — not the closest match. A number that is wrong by a factor is worse than no number, because you will never audit the one you were given.
What I built
I'm 17. I started this because I wanted balance sheet data for something else entirely and I could not find a source I trusted enough to build on. Every free one I tried disagreed with the filing somewhere, and the paid ones wanted enterprise money to tell me what the SEC publishes for free.
So To Scale is the reconciler, running over every filing, with the result behind an API. Every figure is checked against A = L + E before it is stored. Across 6,221 companies and 1.6M data points that reconciles to the accounting identity — against the silent failure you get from taking the first tag.
The exceptions are not rounding. Those are filings that genuinely do not balance, and the site draws them with a red warning saying so rather than quietly adjusting the numbers until they agree. If a company filed something that does not add up, that is a fact about the company, and it should reach you as one.
Every response is as-reported. Nothing is estimated, nothing is forward-filled, nothing is smoothed. If a filer does not break out receivables, you get a labelled remainder rather than a zero — because a zero is a claim and "they did not say" is not.
Try it against a filing you already know
The free tier needs no card. Pick a company where you know the answer, call it, and check the number against the 10-Q yourself — that is the only test that means anything:
curl -H "X-API-Key: YOUR_KEY" https://toscale.pro/api/company/JPM
You can also look up any company on the site with no key and no account at all — the drawings are free and always will be. Get a key from the dashboard, read the API reference, or see what the tiers cost on the pricing page. If you would rather have the whole thing as one file, the full dataset is a one-time download.
And if you find a figure that disagrees with the filing, tell me. That is the one bug report I actually want.