finllm-foundry / src /data /edgar_ingest.py
finpy1789's picture
Upload folder using huggingface_hub
0418f40 verified
Raw
History Blame Contribute Delete
5.09 kB
"""Ingest SEC EDGAR filings (10-K, 10-Q, 8-K, ...) or arbitrary document URLs
(FOMC minutes, FCA handbook pages, IFRS/Basel texts) into training-ready JSONL.
Outputs plain-text chunks with metadata:
{"text": ..., "company": ..., "form": ..., "date": ..., "source_url": ...}
These chunks are the raw material for (a) continued pretraining, or (b) synthetic
instruction generation (turning chunks into Q&A pairs with a teacher model) —
step (b) is a separate pass; this script only does clean ingestion.
SEC requires a descriptive User-Agent with contact email on all requests.
Usage:
python -m src.data.edgar_ingest --tickers AAPL MSFT JPM --forms 10-K 10-Q --out data/edgar
python -m src.data.edgar_ingest --url-list urls.txt --out data/regulatory
"""
import argparse
import json
import pathlib
import re
import time
import requests
from bs4 import BeautifulSoup
USER_AGENT = "FinLLM-Foundry research (finpy07@gmail.com)"
HEADERS = {"User-Agent": USER_AGENT, "Accept-Encoding": "gzip, deflate"}
RATE_LIMIT_S = 0.15 # SEC allows max 10 req/s; stay well under
def _get(url):
time.sleep(RATE_LIMIT_S)
r = requests.get(url, headers=HEADERS, timeout=60)
r.raise_for_status()
return r
def ticker_to_cik():
data = _get("https://www.sec.gov/files/company_tickers.json").json()
return {v["ticker"].upper(): str(v["cik_str"]).zfill(10) for v in data.values()}
def list_filings(cik, forms, limit):
subs = _get(f"https://data.sec.gov/submissions/CIK{cik}.json").json()
recent = subs["filings"]["recent"]
out = []
for form, acc, doc, date in zip(
recent["form"], recent["accessionNumber"], recent["primaryDocument"], recent["filingDate"]
):
if form in forms:
acc_nodash = acc.replace("-", "")
url = f"https://www.sec.gov/Archives/edgar/data/{int(cik)}/{acc_nodash}/{doc}"
out.append({"form": form, "date": date, "url": url})
if len(out) >= limit:
break
return out
def html_to_text(html):
soup = BeautifulSoup(html, "html.parser")
for tag in soup(["script", "style"]):
tag.decompose()
text = soup.get_text(separator="\n")
text = re.sub(r"\n{3,}", "\n\n", text)
text = re.sub(r"[ \t]{2,}", " ", text)
return text.strip()
def chunk(text, target_chars=4000, overlap=400):
"""Paragraph-aware chunking; ~4000 chars ≈ 1000 tokens."""
paras = [p.strip() for p in text.split("\n\n") if len(p.strip()) > 80]
chunks, buf = [], ""
for p in paras:
if len(buf) + len(p) > target_chars and buf:
chunks.append(buf.strip())
buf = buf[-overlap:] + "\n\n" + p
else:
buf += "\n\n" + p
if len(buf.strip()) > 500:
chunks.append(buf.strip())
return chunks
def main():
ap = argparse.ArgumentParser()
ap.add_argument("--tickers", nargs="*", default=[])
ap.add_argument("--forms", nargs="*", default=["10-K", "10-Q"])
ap.add_argument("--per-company", type=int, default=4, help="filings per company")
ap.add_argument("--url-list", default=None, help="text file, one document URL per line")
ap.add_argument("--out", default="data/edgar")
args = ap.parse_args()
outdir = pathlib.Path(args.out)
outdir.mkdir(parents=True, exist_ok=True)
outfile = outdir / "chunks.jsonl"
n_chunks = 0
with open(outfile, "w") as f:
if args.tickers:
ciks = ticker_to_cik()
for t in args.tickers:
cik = ciks.get(t.upper())
if not cik:
print(f"[skip] unknown ticker {t}")
continue
for filing in list_filings(cik, set(args.forms), args.per_company):
try:
text = html_to_text(_get(filing["url"]).text)
except Exception as e: # noqa: BLE001
print(f"[skip] {filing['url']}: {e}")
continue
for c in chunk(text):
f.write(json.dumps({"text": c, "company": t.upper(), **filing,
"source_url": filing["url"]}) + "\n")
n_chunks += 1
print(f"[ok] {t} {filing['form']} {filing['date']}")
if args.url_list:
for url in pathlib.Path(args.url_list).read_text().splitlines():
url = url.strip()
if not url or url.startswith("#"):
continue
try:
text = html_to_text(_get(url).text)
except Exception as e: # noqa: BLE001
print(f"[skip] {url}: {e}")
continue
for c in chunk(text):
f.write(json.dumps({"text": c, "company": None, "form": "doc",
"date": None, "source_url": url}) + "\n")
n_chunks += 1
print(f"[ok] {url}")
print(f"Wrote {n_chunks} chunks to {outfile}")
if __name__ == "__main__":
main()