Skip to content

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

9 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

SEC Filing Risk-Factor Diff Tracker — see exactly what changed in a company's disclosed risk factors between filings

Python 3.9+ FastAPI SQLite SEC EDGAR 10-K BYOK optional LLM layer License: MIT

A public, browsable dashboard that shows exactly what changed in a company's SEC risk-factor disclosures between consecutive filings — a GitHub pull-request diff, applied to prose.

Live demo →

Hosted on Render's free tier — the first request after a period of inactivity may take 30-60 seconds to wake the instance up. That's expected free-tier cold-start behavior, not a bug.


The SEC Filing Risk-Factor Diff Tracker pulls the "Risk Factors" (Item 1A) section out of consecutive 10-K filings for a fixed universe of well-known public companies, directly from SEC EDGAR, and runs a deterministic paragraph-level diff between each consecutive pair. Every risk-factor paragraph a company has ever disclosed across its ingested filing history is classified as unchanged, newly added, or removed relative to the prior filing, and rendered with the same visual grammar as a code review: additions highlighted with a + prefix, removals struck through with a prefix, unchanged text in neutral grey — never color alone, so the diff stays legible for colorblind readers. A timeline control lets you pick any two ingested periods (not just consecutive ones), a summary strip gives an at-a-glance sentence count of what changed before you read a word of the prose itself, a section-jump sidebar lets you skip straight to a specific risk topic instead of scrolling a 40-paragraph document, and an analyst-metrics panel surfaces three deterministic, citation-backed disclosure-analysis figures (textual similarity, Fog readability, section length) alongside the qualitative diff.

This is a standalone project built to demonstrate four things: SEC EDGAR filing ingestion and section-level text extraction (locating a specific, inconsistently-formatted legal section reliably across dozens of different filers' HTML conventions); deterministic text-diffing and version-comparison techniques applied to prose rather than code; temporal/timeline UI design for browsing a company's disclosure history; and responsible BYOK (bring-your-own-key) API key handling for the one optional AI feature in the app.

Educational tool for exploring changes in public company risk factor disclosures. Not investment advice. Diffs are generated by automated text comparison and may contain extraction errors; always verify against the original filing. See the Disclaimer at the bottom, which is also shown unmissably in the running app itself.

Contents

Why this exists

Equity research desks and credit risk teams read risk-factor sections comparatively, not in isolation — the signal usually isn't in what's disclosed, it's in what changed since last year. A new paragraph about supply-chain concentration, a quietly dropped disclosure about a customer concentration, a suddenly much longer discussion of litigation exposure: these are early, management-hasn't-said-it-outright signals that a company's risk profile is shifting, long before it shows up in earnings. Reading two 40-page Item 1A sections side by side to find these changes by eye is exactly the kind of mechanical comparison a computer should do instead — this project takes that specific analyst technique and makes it a one-click, browsable comparison across a real company universe, with the underlying diff logic fully deterministic and inspectable.

Architecture

Two separate paths, matching the split in this project's other BYOK tools: the ingestion and diffing pipeline runs entirely offline with no LLM anywhere in it, and the optional AI narration layer runs entirely client-side, bypassing this app's own backend completely.

1. Offline ingestion + diffing — run once, populates the dataset:

flowchart LR
    A["SEC EDGAR<br/>submissions + filing HTML"] --> B["ingest.py<br/>fetch 10-Ks per company"]
    B --> C["extract.py<br/>isolate Item 1A text"]
    C --> D["diffing.py<br/>paragraph-level difflib"]
    D --> E[("SQLite<br/>tracker.db")]
    E --> F["FastAPI<br/>read-only JSON API"]
    F --> G["Frontend<br/>timeline + diff view"]
Loading

2. The optional BYOK AI summary layer — entirely client-side:

flowchart LR
    K["Your Anthropic API key<br/>(typed into the browser)"] -->|"held in a JS variable,<br/>never persisted"| H["Browser fetch()"]
    G2["Frontend<br/>(already-rendered diff)"] --> H
    H -->|"direct HTTPS request"| A2["api.anthropic.com"]
    A2 --> H
    H --> R["Plain-English summary<br/>rendered in the diff view"]

    style H fill:#e8f0fc,stroke:#2a78d6
Loading

Notice what's not in the second diagram: this project's own FastAPI server. There is no passthrough endpoint or proxy — the request goes straight from your browser to Anthropic.

Installation / running locally

Requires Python 3.9+. No Docker, no build step for the frontend, no separate database server, no automated test suite (by design — this project puts that time into UI polish instead).

git clone https://github.com/divyaanshkumar24/SEC-Filing-Risk-Factor-Diff-Tracker.git
cd SEC-Filing-Risk-Factor-Diff-Tracker

python3 -m venv .venv
source .venv/bin/activate          # Windows: .venv\Scripts\activate
pip install -r requirements.txt

# Populate the SQLite database from SEC EDGAR (takes a few minutes — this is
# a polite, rate-limited batch job that identifies itself to SEC per their
# fair-use guidance; it is NOT part of the live app's request path).
python -m backend.app.ingest

# Run the app — one process serves both the JSON API and the static frontend.
uvicorn backend.app.main:app --reload --port 8420

Open http://localhost:8420 in a browser. That's the whole setup — no environment variables required to get a fully working, no-key experience.

To turn on the optional AI summary layer: click the "AI summaries: off" button in the top-right of the running app and paste in your own Anthropic API key. No restart, no config file — the key lives only in that browser tab for that session. See Data handling & privacy.

Re-running the ingestion: python -m backend.app.ingest --reset drops and recreates the database from scratch, re-fetching current filings. This project intentionally does not auto-refresh on a schedule — re-run it whenever you want an updated snapshot.

Quickstart

  1. Open the homepage — a "biggest recent changes" strip highlights the companies with the largest sentence-level change in their latest comparison, below which is a searchable, sector-filterable, sortable (A–Z or biggest change) grid of the whole company universe. Each card shows ticker, name, sector, and a "+N / −M" badge for its most recent filing-over-filing change.
  2. Click a company card to open its detail page.
  3. Use the two period dropdowns (or click a dot on the timeline) to pick which two filings to compare — any two, not just consecutive ones.
  4. Read the diff: added paragraphs highlighted in blue with a +, removed paragraphs in amber with a and a strikethrough, unchanged paragraphs in neutral grey. The summary strip above it gives the sentence-level added/removed counts and links to both original filings on SEC EDGAR.
  5. Use the Jump to section sidebar to skip directly to a specific risk topic (e.g. "Macroeconomic and Industry Risks") instead of scrolling the whole document — it highlights your current section as you scroll, and the Analyst metrics panel above it gives you textual similarity, Fog readability, and section length for this comparison (see How the diffing works).
  6. If you've entered an API key, a plain-English AI summary of the change appears in its own panel below the summary strip.
  7. Toggle light/dark theme from the circular button in the top-right at any time.

How the diffing works

Extraction (backend/app/extract.py): each 10-K's primary HTML document is rendered to plain text with paragraph breaks preserved (block-level HTML tags become line breaks before stripping markup, so the diff has paragraph boundaries to work with). The Item 1A section is then isolated with a two-tier heuristic — first, look for a heading that occupies an entire line by itself ("Item 1A. Risk Factors.") since a table-of-contents entry for the same item is a different standalone line, and pair it with the nearest following end-of-section heading ("Item 1B. Unresolved Staff Comments.") that's far enough away to be real prose rather than an adjacent TOC entry. Filers that run the heading directly into the first sentence of body text ("RISK FACTORS. The following discussion...") are caught by a fallback that looks for that pattern in ALL CAPS specifically, since real section headers are stylized that way while inline cross-references to "Item 1A" elsewhere in the document are not. If neither heuristic produces a section within a plausible length range, that filing is skipped entirely — no garbled or truncated text is ever stored.

Normalization: whitespace is collapsed, and short header-like lines (a bolded risk-topic title that lost its styling when HTML was stripped) are merged into the paragraph that follows them, so the diff doesn't awkwardly split a title from its own body text.

Diffing (backend/app/diffing.py): Python's difflib.SequenceMatcher runs over the two filings' paragraph lists — the prose analogue of comparing line-by-line in a code diff. Each opcode is classified: equal paragraphs render unchanged, insert paragraphs render added, delete paragraphs render removed, and a replace (an edited paragraph) renders as a removed old paragraph immediately followed by an added new one — exactly how a GitHub PR shows an edited line as delete-then-insert, rather than as an opaque "changed" block. Sentence-level added/ removed counts for the summary strip are computed with a lightweight sentence splitter applied only to the added/removed paragraphs, giving a more granular magnitude signal than a raw paragraph count.

This is deliberately not a complex NLP model — a fixed universe of well-formatted legal filings is exactly the case where deterministic text alignment gets you a correct, reproducible answer without the opacity of a learned model.

Analyst metrics (backend/app/diffing.py): three deterministic text-analysis figures, computed once at ingestion time alongside the diff itself, drawn from published equity-research and disclosure-analysis methodology — no LLM involved in any of them:

Metric What it measures Methodology
Textual similarity Cosine similarity of the two filings' word-frequency vectors, as a percentage The standard year-over-year 10-K "document similarity" measure in academic finance research — Cohen, Malloy & Nguyen (2020, "Lazy Prices," Journal of Finance) use it as a predictor of future stock returns; Brown & Tucker (2011, Journal of Accounting Research) apply it to MD&A modifications. A low score means the language was substantially rewritten, not lightly edited.
Fog readability Gunning Fog index (0.4 × ((words ÷ sentences) + 100 × (complex words ÷ words))) for each filing, plus the change A standard disclosure-complexity measure in accounting literature — Li (2008, Journal of Accounting and Economics) links harder-to-read 10-Ks to lower and less persistent earnings. Complex-word and sentence counts use a syllable-counting heuristic, standard practice for automated Fog calculators.
Item 1A length Word count of the section, and % change vs. the prior filing A simple, practitioner-level volume signal — a risk-factor section growing sharply is itself often worth noticing, independent of what specifically changed.

These are simplified, deterministic implementations inspired by the cited methodology — not a reimplementation of any paper's full model — consistent with how the rest of this project's diff logic favors transparent, reproducible arithmetic over a black box.

Data handling & privacy

  • The optional AI summary layer is bring-your-own-key. You paste your own Anthropic API key into the running app; it is held in a single in-memory JavaScript module variable (frontend/js/state.js) for that browser tab's session only — never written to localStorage, sessionStorage, a cookie, a database, a log file, or disk anywhere. Reloading the page clears it.
  • This app's own backend never sees your key. The summary call (frontend/js/llm.js) goes directly from your browser to https://api.anthropic.com — there is no server-side proxy or passthrough endpoint in this project at all, by design.
  • Everything else works with no key at all. The company universe, every filing, every precomputed diff, and every summary statistic is served from the local SQLite database — the AI layer only adds an optional paragraph on top of data you can already see and read yourself.
  • No user accounts, no tracking, no saved preferences. There is nothing to opt out of because nothing is collected.

Design principles

Substantive change over noise. Whitespace, boilerplate formatting, and page-break artifacts are normalized out before diffing so what you see reflects a real change in disclosed language, not a formatting difference.

Graceful degradation without a key. No API key → the app is still fully functional, just without the AI paragraph. A filing whose Item 1A section can't be reliably isolated → that filing is skipped, not shown as broken or garbled text.

Readable prose over dense data. Typography is a first-class design concern here: a serif reading font, comfortable line-height, and constrained line length for the diff body, because this is a reading-heavy tool being judged on how comfortably you can read a 40-paragraph prose diff.

Diffs describe language, not implications. Every added/removed paragraph is presented as exactly what it is — a change in disclosed text — never as a prediction, a rating, or an accusation. The AI summary layer is explicitly instructed the same way.

Accessible by more than color. Every added/removed paragraph carries a +/ prefix and (for removals) a strikethrough in addition to its background color, and the diff palette uses blue/amber rather than red/green so it stays legible for the most common forms of color vision deficiency.

Metrics inform, never conclude. The analyst-metrics panel presents its three figures with their methodology cited in the UI itself; none of them roll up into a single score or a verdict — a low similarity score or a rising Fog index is a prompt to go read the diff, not a conclusion in itself.

API overview

Method Endpoint Description
GET /api/health Liveness check + whether the database has been populated
GET /api/meta Dataset "data as of" date and ingested company count
GET /api/companies List every company with >=2 ingested filings, with a latest-change summary badge
GET /api/companies/{ticker} Company detail: all ingested filing periods with dates and source URLs
GET /api/companies/{ticker}/diff?from_id=&to_id= Precomputed paragraph-level diff (each chunk flagged heading: true/false for the section nav) + summary + analyst_metrics between two periods

This backend never accepts or forwards an Anthropic API key — see Data handling & privacy. The AI summary call happens entirely in the browser, outside this API surface.

Tech stack

Python FastAPI SQLite SEC EDGAR 10-K Python difflib Vanilla JS Anthropic API BYOK

text-diffing · sec-edgar · risk-factor-analysis · document-versioning · byok · fastapi · sqlite · open-source

  • Backend: Python + FastAPI, serving pre-computed data from SQLite over a small read-only JSON API. No ORM — plain sqlite3 with hand-written, parameterized queries.
  • Ingestion: a standalone script (backend/app/ingest.py) that pulls SEC EDGAR's submissions API and each filing's primary HTML document, run offline — never in the live request path.
  • Extraction & diffing: dependency-light Python (backend/app/extract.py, backend/app/diffing.py) using BeautifulSoup for HTML-to-text and the standard-library difflib for comparison — no NLP model involved.
  • Frontend: vanilla HTML/CSS/JS with ES modules, no build step, no framework — a hash-based router between the browse grid and company detail views, with a serif reading typeface and a colorblind-conscious diff palette.
  • Database: SQLite — a single file, no server process, trivially inspectable with any SQLite client.

Deployment

Live at https://sec-filing-risk-factor-diff-tracker.onrender.com/, deployed on Render's free tier. See DEPLOYMENT.md for the full reasoning: why a long-lived-process host (Render/Railway/Fly.io) is the natural fit for this single-process, read-only-SQLite architecture over serverless, the Render vs. Railway cost comparison, and the exact deploy steps.

Disclaimer

Educational tool for exploring changes in public company risk factor disclosures. Not investment advice. Diffs are generated by automated text comparison and may contain extraction errors; always verify against the original filing. AI-generated summaries, when enabled, are not guaranteed to be accurate.

This disclaimer is also shown, unmissably, in the running app itself (footer on every page).

License

MIT — see LICENSE.

About

See exactly what changed in a company's SEC risk-factor disclosures between filings

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages