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.
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.
- Why this exists
- Architecture
- Installation / running locally
- Quickstart
- How the diffing works
- Data handling & privacy
- Design principles
- API overview
- Tech stack
- Deployment
- Disclaimer
- License
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.
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"]
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
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.
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 8420Open 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.
- 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.
- Click a company card to open its detail page.
- 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.
- 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. - 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).
- If you've entered an API key, a plain-English AI summary of the change appears in its own panel below the summary strip.
- Toggle light/dark theme from the circular button in the top-right at any time.
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.
- 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 tolocalStorage,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 tohttps://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.
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.
| 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.
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
sqlite3with 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-librarydifflibfor 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.
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.
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).
MIT — see LICENSE.