sec_filings: fetch from EDGAR directly to fix stale data #2912
peterhxk
started this conversation in
Development
Replies: 1 comment 1 reply
-
|
@ranaroussi I'm not sure who I should reach out to regarding this, I noticed that the discussion board is quite inactive, should I directly implement this and open a PR for review? |
Beta Was this translation helpful? Give feedback.
1 reply
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Uh oh!
There was an error while loading. Please reload this page.
-
Proposal: EDGAR backend for
sec_filingsto fix incomplete filing coverageProblem
Ticker.get_sec_filings()sources filings from Yahoo'squoteSummaryAPI (secFilingsmodule). The dates Yahoo reports are accurate for the filings it carries, but the list itself is badly incomplete in two ways.1. Ownership and insider filings are missing entirely. Forms 3, 4, and 144 and Schedules 13D/G never appear, for any ticker I tested. Insider transaction data is one of the most common reasons to want SEC filings programmatically, and it is simply not available through yfinance today.
2. Core filings are silently dropped when several land on the same day. Yahoo's history is deep (multi-year, back to 2021 for some tickers I checked), but it appears to keep at most one filing per date. Every same-day collision I found follows this pattern, four days across two tickers:
Across all raw Yahoo output I inspected, no date ever appears twice. Because earnings press-release 8-Ks routinely coincide with the 10-Q or 10-K, this behavior systematically drops earnings releases, and as the MSFT case shows, it can drop the 10-Q itself. Which filing survives the collision does not appear to follow any sensible priority.
Measured over the last 180 days (run on 2026-07-21 with the script attached below):
get_sec_filings()* MSFT's missing 10-Q (0001193125-26-027207, filed 2026-01-28) collided with the earnings 8-K and an IRANNOTICE; Yahoo kept only the IRANNOTICE. Details in problem 2 above.
There is also a related note already in the codebase:
_fetch_calendar()inscrapers/quote.pysays "secFilings return too old data, so not requesting it for now." In my tests the dates on covered filings were accurate, so the current problem is coverage rather than staleness, but it suggests this module's quality has been a known concern.Proposal
Keep the existing interface and add an opt-in backend that queries SEC EDGAR directly:
EDGAR's
data.sec.gov/submissions/endpoint is free and keyless, includes every filing (insider forms included), and posts filings within minutes of SEC acceptance. It also returns data in columnar form, so this pairs naturally with the DataFrame conversion that already exists as commented out "Experimental" code in_fetch_sec_filings(): for example apd.DataFramereturn for the EDGAR path, or anas_dict=flag matching the convention used byget_major_holders()and others.How it works
Two requests, no auth:
https://www.sec.gov/files/company_tickers.json, fetched once and cached, reusing the existingcache.pylayer the same way the timezone cache does.https://data.sec.gov/submissions/CIK{cik:010d}.json.Prototype:
This yields every recent filing (10-K, 10-Q, 8-K, S-1, Form 4, and so on) with form type, report date, accession number, and a direct document URL. A
form=filter would let users grab, say, only insider filings in one line.Anticipated concerns (and how I'd address them)
"yfinance is a Yahoo library, a second data source is scope creep." Fair, and I'd understand a no on these grounds. The counterargument: this adds no new API surface, it fixes a measured deficiency behind an existing method, and it is opt-in only. Yahoo remains the default. If the maintainers prefer, I'm equally happy to ship this as a small companion package instead. I'd just rather ask first, since the coverage gap above affects the flagship forms and not only exotic ones.
SEC fair-access rules. EDGAR requires a descriptive
User-Agentwith contact info and caps clients at 10 requests per second; violators get IP banned. The implementation would (a) let users set their contact string via config, refusing EDGAR requests without one, and (b) rate limit well below the cap. This proposal makes zero additional requests to Yahoo, so it cannot affect yfinance's standing with its primary upstream.Older filings. The
submissionsendpoint inlines roughly the most recent 1,000 filings; older ones live in paginated supplement files referenced in the same JSON. v1 would cover the recent set (which is already vastly more than Yahoo returns) and document the limit, with pagination as a follow-up if wanted.Ticker edge cases. The CIK map covers SEC registrants only. Foreign issuers without US filings and most non-equity tickers would return an empty result with a clear message rather than an error.
What I'm offering
If there's appetite for this, I'll open a PR with: the CIK mapping and caching, the EDGAR fetcher with rate limiting and mandatory User-Agent config, form type filtering, tests, and docs. If the consensus is "out of scope," I'll build it as a companion package and would appreciate at most a pointer in the docs.
Would love feedback on: (1) in scope or not, (2)
source="edgar"kwarg versus a separate method likeget_edgar_filings(), (3) dict versus DataFrame for the new path's return type.Reproduction script
```python import datetime as dt from collections import Counterimport requests
import yfinance as yf
CONTACT = "Your Name your@email.com" # SEC fair-access requirement
WINDOW_DAYS = 180
TICKERS = ["AAPL", "MSFT", "TSLA", "ETSY", "CAVA", "RDDT"]
EDGAR_HEADERS = {"User-Agent": CONTACT}
CUTOFF = dt.date.today() - dt.timedelta(days=WINDOW_DAYS)
def cik_map():
r = requests.get("https://www.sec.gov/files/company_tickers.json",
headers=EDGAR_HEADERS, timeout=30)
r.raise_for_status()
return {v["ticker"]: int(v["cik_str"]) for v in r.json().values()}
def edgar_recent(cik):
url = f"https://data.sec.gov/submissions/CIK{cik:010d}.json"
r = requests.get(url, headers=EDGAR_HEADERS, timeout=30)
r.raise_for_status()
recent = r.json()["filings"]["recent"]
return sorted(((dt.date.fromisoformat(d), f)
for f, d in zip(recent["form"], recent["filingDate"])
if dt.date.fromisoformat(d) >= CUTOFF), reverse=True)
def yahoo_recent(ticker):
return sorted(((f["date"], f.get("type", "?"))
for f in yf.Ticker(ticker).get_sec_filings()
if f["date"] >= CUTOFF), reverse=True)
def summarize(name, rows):
if not rows:
return f"{name}: nothing in window"
forms = Counter(f for _, f in rows)
return (f"{name}: latest = {rows[0][1]} on {rows[0][0]} | "
f"{len(rows)} filings in {WINDOW_DAYS}d | "
+ ", ".join(f"{k}x{v}" for k, v in sorted(forms.items())))
ciks = cik_map()
for t in TICKERS:
print(f"\n=== {t} ===")
print(summarize("Yahoo", yahoo_recent(t)))
print(summarize("EDGAR", edgar_recent(ciks[t])))
Beta Was this translation helpful? Give feedback.
All reactions