Spaces:
Running on Zero
Running on Zero
| """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() | |