Why Your SEC Filing Data Is Wrong (And How to Check)
The first filing that broke my parser was American Airlines. The code returned total equity as a positive number. The actual figure is negative, and has been for years.
Nothing errored. No exception, no warning, no null. The parser found a tag
called StockholdersEquity, read a plausible dollar figure out of
it, and handed it back. It was a real number from a real filing. It was just
the wrong one: a subsidiary's equity rather than the consolidated deficit that
belongs on the face of the balance sheet.
That is the failure mode worth understanding, because it is silent. Bad SEC data almost never arrives as a crash or an obviously broken figure. It arrives as a number that looks exactly like the number you wanted.
Why one filing contains the same figure many times
XBRL lets a filer attach dimensions to a fact. The same concept, tagged repeatedly, each instance qualified: this figure but for the consumer segment, this one for the investment bank, this one for a named subsidiary, this one for a geography.
That is a good design. A bank's consumer arm and its investment arm are genuinely different businesses, and flattening them into a single line would throw away information somebody needs.
The problem is how the consolidated figure is distinguished from the rest. It is not flagged. It is not first. It does not carry a label saying "consolidated". It is the instance with no dimensions attached. The number you want is defined by an absence.
JPMorgan reports Assets twenty-three times in a single filing.
Twenty-two of those are dimensioned. One is not, and that one is the bank. If
your code takes the first Assets fact it finds, it gets whichever
one the document happens to list first, and nothing tells it that a choice was
made at all.
Why "take the first tag" fails more than it looks like it should
The reason first-match survives casual testing is that it is right for
simple filers. A single-segment company reports Assets once, and
the first match is the only match. Test against a handful of mid-cap
industrials and the approach looks fine.
It breaks on exactly the filings that matter most. Banks, insurers, conglomerates, REITs with joint ventures, anything with a captive finance arm, anything post-acquisition that still reports the acquired entity separately. The companies with the most segments are also the companies people most want data on, so the error rate on the filings you care about is higher than the error rate across the filer universe.
I am not going to put a percentage on it. Any figure I could quote would depend on which companies were in the sample, which quarters, and how I counted a partial match, and a number with those caveats stripped off becomes a marketing claim rather than a measurement. The failure mode is the point: picking by position picks a segment, and picking a segment is silent.
A five minute test you can run on your own data
You do not have to take my word for any of this. Pick a company you already hold figures for and ask SEC directly. The companyfacts endpoint returns every XBRL fact a filer has ever reported, with dimensions intact:
# JPMorgan. The CIK is zero-padded to ten digits.
curl -H "User-Agent: You you@example.com" \
https://data.sec.gov/api/xbrl/companyfacts/CIK0000019617.json \
-o jpm.json
import json
from collections import Counter
facts = json.load(open("jpm.json"))
units = facts["facts"]["us-gaap"]["Assets"]["units"]["USD"]
# Group by period end. Anything with more than one entry for a single
# period is the same concept reported at several levels at once.
per_period = Counter(f["end"] for f in units)
for end, n in sorted(per_period.items())[-4:]:
print(end, n, "reported values")
for f in units:
if f["end"] == end:
print(" ", f"{f['val']:>20,}", f.get("frame", "(dimensioned)"))
Run it and you will see several figures for one date. Then check which one your current data source gave you. If it matches the largest, you are probably fine on that filing. If it matches something else, you have found the bug, and you have found it on one company out of however many you are carrying.
The check that actually resolves it
Heuristics do not fix this. "Take the largest" fails on a company whose parent is smaller than a consolidated subsidiary line. "Take the one without dimensions" is closer but depends on every filer tagging cleanly, which they do not.
Arithmetic fixes it. A balance sheet balances, and that gives you a test rather than a guess:
Assets = Liabilities + Equity
Pull every candidate for assets, every candidate for liabilities, every candidate for equity, and find the combination that reconciles. Consolidated figures balance against each other. A segment's assets do not balance against the whole company's liabilities. The arithmetic identifies the right set without needing to know anything about the company.
Three adjustments make it hold in the real world.
Noncontrolling interests belong in equity. When a parent
consolidates a subsidiary it does not wholly own, the outside shareholders'
stake sits in equity as NCI. StockholdersEquity excludes it;
StockholdersEquityIncludingPortionAttributableToNoncontrollingInterest
includes it. Use the wrong one and the identity misses by exactly the minority
stake, which reads as a reconciliation failure when the filing is fine.
Mezzanine is neither, and it is not optional for SPACs. Redeemable preferred and redeemable NCI sit between liabilities and equity on the face of the sheet. A SPAC with shares subject to possible redemption carries most of its balance sheet there. Ignore the mezzanine line and the identity fails on every one of them.
The tolerance is a fraction, not a constant. Filers round. Half a percent of assets absorbs that at any size. A fixed dollar tolerance either rejects every large bank or accepts anything at a small cap.
When it still does not balance
Sometimes no combination reconciles. The tempting move is to return the closest match, and it is the wrong one. A figure that is wrong by a segment is worse than a missing figure, because a gap gets investigated and a plausible number does not.
So I do not hide the failures, I name them. A filing that does not reconcile is drawn on the site with a red warning saying so, rather than quietly adjusted until it agrees. If a company filed something that does not add up, that is a fact about the company and it should reach you as one. The same goes for missing components: 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.
This is also why I publish the method and not a rate. The methodology page shows what is checked, how, and what is currently failing, with live counts read off the database rather than a figure written down once. A single accuracy percentage is the easiest thing in the world to quote and the hardest to verify, and I would rather hand you something you can check.
What to do with this
Run the companyfacts test above against three companies you have data for. Pick a bank, a REIT and something with a recent acquisition, because those are where it breaks. If the figures agree, your source is doing the reconciliation and you can stop worrying about it. If they do not, you now know which direction the error goes.
If you would rather not maintain that yourself, BalanceProof runs the check above over every filing before storing anything, across 6,222 companies and 1.6M data points. You can look up any company on the site with no key and no account. The comparison with Intrinio covers how that differs from a general-purpose financial data feed, and pricing has the tiers.
And if you find a figure that disagrees with the filing, tell me. That is the bug report I actually want.