Eight ways SEC filing data lied to me, each with the bug report that found it.
pip install edgar-trapsNo dependencies. No network calls. Every function takes data you already fetched, so it drops into whatever client you have.
I run a research site that values around 900 companies from their own filings. Everything it publishes comes from EDGAR, and for a year I have been finding out, one incident at a time, that reading EDGAR correctly is not the same as parsing it correctly.
None of the bugs below were parsing errors. Not one of them raised. Every single one produced a plausible number from valid data, which is exactly why they survived review and were found weeks later by accident — usually while I was looking at something else entirely.
This package is those eight, with the damage each one did. The number is the argument. A trap you cannot picture costing anything is a trap you will reintroduce.
A Form 4 names two parties: the issuer whose shares moved, and the reporting
owner who moved them. I queried by issuer CIK and never read <issuer>.
But a CIK's submissions feed contains both the filings made about it and the filings it made as a reporter of someone else's stock. Own more than 10% of another public company and you are an insider of that company; every trade you make in it lands in your own feed.
So a large oil major's page showed $276.6 million of insider selling that never happened. It was real selling — of a completely different company's shares, by the oil major, in a stake it held. The number looked entirely reasonable for a company that size. Nothing flagged it, because nothing was malformed.
from edgar_traps import form4_is_about_issuer
form4_is_about_issuer(filing, issuer_cik="0000034088") # False if it's someone else'sIt is the institutional "see attached": my holdings are reported on somebody else's filing. It contains no positions.
Count it as a report and a fund looks current while its last real portfolio is quarters old. I had a well-known manager showing a fresh filing date over stale holdings for two quarters. The tracker said they had reported, and they had — they just had not reported anything.
from edgar_traps import is_position_report
is_position_report("13F-HR") # True
is_position_report("13F-NT") # False13F filings name holdings in free text, so joining them to tickers means
normalising both sides. I replaced punctuation with a space, which is the
reflex, and it turns Moody's into moody s. That matches nothing.
31 companies and $51.2 billion of reported positions went unmatched, including Berkshire Hathaway's fifth-largest holding, which showed up on an otherwise complete page as a blank ticker. I found it because a CSV export looked one row short.
Apostrophes get deleted. Everything else becomes a separator.
from edgar_traps import normalise_issuer_name
normalise_issuer_name("Moody's Corporation") # 'moodys'
normalise_issuer_name("MOODYS CORP") # 'moodys'The history stays behind. EDGAR's ticker-to-CIK map follows the reorganisation immediately, so an ingest keyed on "the CIK for this ticker" starts reading an entity incorporated last quarter and finds almost nothing — correctly, about a company that has been filing for decades.
My sweep reported 0 filings in the window for a company with 301 of them. It was right about the CIK it was given. The bridge is in the submissions document; follow it.
from edgar_traps import candidate_ciks
candidate_ciks(submissions) # ['0000034088', '0000034089'] — newest firstdata.sec.gov serves the JSON APIs. www.sec.gov/Archives serves the documents.
Different origins, different storage.
You meet the JSON APIs first, so you pin Host: data.sec.gov once and reuse the
client. Every document fetch then arrives at the archive with the wrong host and
comes back as a storage-layer 404 with a NoSuchKey body.
That reads as "this filing does not exist". Meanwhile the same URL opens perfectly in a browser, so it looks like a network block rather than your own header. I lost a day to it and very nearly told a colleague our server had been cut off from the SEC.
from edgar_traps import archive_url, host_header_for
archive_url("0001067983", "0001067983-26-000012", "form4.xml")
# unpadded CIK, stripped accession — either one wrong is a 404 that looks like a
# missing filingSearch EDGAR for there is substantial doubt about and you get two completely
different sentences back:
management has concluded that there is substantial doubt about the Company's ability to continue as a going concern
the Company is required to evaluate whether there is substantial doubt about its ability to continue as a going concern
The second is the accounting standard quoted back at you. Over a 30-day window across every US filer, I read the first twenty hits by hand: two were the standard's own wording. A 10% false-positive rate on the claim "this company disclosed going-concern doubt" — which is defamatory when wrong, on a page with a permanent date on it.
The phrase had been validated. Against large filers, where it returns zero false positives, because big issuers' counsel does not quote the standard back at you. Small issuers' counsel does, and small issuers are the entire population you are searching. The validation set was never the population.
from edgar_traps import states_affirmatively
states_affirmatively(document_html, "there is substantial doubt about")Judge every occurrence. A filing routinely carries both — the standard in the accounting policies, the conclusion in the notes — so stopping at the first match clears a company that plainly said it.
Form 144 carries aggregateMarketValue as free-entry text. Two I found in one
universe: $25.24 billion for 4,000 shares, and $11.7 billion for 860.
Real filings, typed by real people, perfectly well-formed.
The check is arithmetic rather than a size threshold: divide by the share count and ask whether the implied price could exist.
from edgar_traps import form144_value_is_plausible
form144_value_is_plausible(25_240_000_000, shares=4_000, reference_price=60.0)
# False — implies $6.3m per shareOne flag matters. A London-listed issuer files this form in dollars while its shares quote in pence, so the implied price sits ~87x from the local quote for entirely correct reasons. I ran the check across that gap and it rejected 47 correct values before I noticed they were all listed in one place.
Form 144 announces an intent to sell. The sale, if it happens, appears later on a Form 4. They are different documents about different events, and conflating them is how "insider plans to sell" becomes "insider sold" in a headline.
I was going to write that most announced sales never happen. Then I measured it: across 11,741 notices whose 90-day window had closed, 85% were followed by a real sale from the same person. The notice is a good predictor. The article I had in mind did not survive contact with the data, which is a cheaper thing to learn from a docstring than from a correction.
from edgar_traps import notice_was_executed
notice_was_executed(notice_date, seller_name, form4_sales, window_days=90)Yes, that is nine. The eighth was a bonus and this one is the worst.
A foreign private issuer can file with the SEC in one currency and trade in another. A UK spirits group reports in dollars while its London line quotes in pence. Divide one by the other and your P/E is roughly 150x from reality, with no field anywhere marked wrong.
The nastier version is same-currency: pounds against pence is exactly 100x, and the currency code looks right at a glance.
from edgar_traps import price_matches_fundamentals, minor_unit_factor
price_matches_fundamentals("USD", "GBp") # False
minor_unit_factor("GBP", "GBp") # 100.0
minor_unit_factor("USD", "GBp") # None — that's a real FX rate, not a constantThree things I keep relearning.
A plausible number is the dangerous kind. Every bug here produced output that looked fine. The ones that crash get fixed the same afternoon.
Validate against the population you will run on. The going-concern phrase was checked against mega-caps and it was clean, and that told me nothing about the micro-caps it would actually be pointed at.
Repetition is a stronger signal than magnitude. Twice now I have published a derived constant as if it were a measurement, and both times the tell was the same value appearing on companies that had nothing to do with each other — not the value being wrong-looking.
pip install -e ".[dev]"
pytestEach test is the bug report. The docstrings say what the mistake cost.
MIT. The filings are public record; what is here is the list of ways I misread them.