SEC 10-K filing deltas · S&P 500

Do year-over-year SEC filing changes predict returns?

A point-in-time, no-lookahead event study across the S&P 500, with costs included, out-of-sample, and against an honest null. Try the two grounded tools first, then see what the study found.

Try it: the grounded narrator

Search any of the 480 S&P 500 companies, pick one of its 10-K filings, and a server-side model (GLM 5.2, free via NVIDIA) writes a short memo from the study's pre-computed numbers only. It never computes a figure and never gives advice. It can narrate only real, measured filing events, so it can't be pushed into predicting or inventing anything.

Loading companies…

Ask about the filing changes

Ask a plain-English question about a company's most recent 10-K risk-factor changes. The model answers only from the actual added, removed, and reworded sentences the study diffs out of that filing, with the source cited. If the answer isn't in those changes, it says so. Descriptive only: it won't predict, value, or advise.

What we found
New here? Read the plain-English version (about a minute)

The question. Every US public company files a yearly report with the SEC (a "10-K") that includes a "Risk Factors" section listing what could go wrong for the business. Companies reword that section a little every year. We test whether large changes in that language predict how the stock moves over the following weeks. There is a known finding (nicknamed "Lazy Prices") that the changes companies do make tend to carry real news, often bad news, that the market is slow to price in.

How we keep it honest. Four rules do the heavy lifting, and they are the real point of the project:

  • Abnormal returns, not raw. If a stock rose 3% but the whole market rose 3% that week, it did nothing special. We subtract what it would have done anyway, to isolate the part that might be tied to the filing.
  • No lookahead. We only allow trading from the first market open after a filing becomes public, never before it existed. This is the most common way people accidentally produce fake results.
  • Costs included. An edge that disappears once you pay to buy and sell is not a real edge.
  • Look-once holdout. We test on data we did not tune on, exactly once, so we are not just memorizing noise.

What we found. The effect is real but weak, and it does not survive trading costs. That honest "no" is the valuable result: a system you can trust to tell you when something does not hold up matters more than a flashy signal that falls apart in practice.

How it's built (system design)

Free public data goes in one end; a calibrated verdict and two grounded tools come out the other. The research pipeline runs offline and is fully reproducible from the repo; the serving layer runs on Vercel.

SEC EDGAR10-K filings + exact public timestamps (the event clock). Rate-limited, cached.
Yahoo FinanceDaily adjusted prices: each stock, the market benchmark, sector ETFs.
1

Event clock no-lookahead gate

Each filing's acceptance instant maps to the first market open strictly after it. No feature may use data from after that instant, enforced in code and checked by a pre-event placebo window.

eventtime.py · 18 tests
2

Extract and diff the text

Pull Item 1A (Risk Factors) out of the filing HTML, then sentence-diff it against last year's: added, removed, reworded. The diff grounds everything downstream; a model can label a change it is shown, but it can never invent one.

ingest/sections.py · delta/diff.py (rapidfuzz reconciliation)
3

Point-in-time feature panel

One row per filing event: net added risk language, document similarity, tone deltas. Every feature is computable from information available at the event instant, by construction.

delta/panel.py · delta/tone.py (Loughran-McDonald)
4

Event study

Abnormal returns against three expected-return models (market, sector, market-model beta), CAR windows of 1, 5, and 21 days, plus the placebo window and momentum and beta controls.

study/abnormal.py · study/car.py
5

Honest test

Cross-sections with filing-date-clustered errors, an in-sample/holdout split examined once, and a net-of-cost long-short spread. Output: the verdict at the top of this page.

study/crosssection.py · study/portfolio.py · study/costs.py

Serving layer. The pipeline's outputs are baked into the deployment as fixed, server-side data. The browser only ever sends an event ID or a question:

A

This page

A single static file on Vercel. The company search reads a small pre-built index; no framework, no build step.

docs/index.html · panel_index.json
B

/api/narrate

Looks up one real event's pre-computed numbers server-side and turns them into a cited memo.

api/narrate.py · _panel.py (4,697 events)
C

/api/ask

Answers a question strictly from the real added, removed, and reworded sentences of one filing.

api/ask.py · _passages.py (480 companies)
D

GLM 5.2 grounded

Free via NVIDIA's endpoint. The model restates and cites what the pipeline computed. It never computes a figure, never predicts, never advises. Unknown IDs are rejected before any model call; requests are rate-limited and cached.

integrate.api.nvidia.com · z-ai/glm-5.2
The calibrated answer: no, the effect is weak and does not survive as a tradeable signal. On the full universe, point-in-time and net of costs, more added risk-factor language shows the documented "Lazy Prices" direction only marginally on the holdout (CAR[0,+21] p = 0.03), flips to the wrong sign in-sample, and produces a net-of-cost long-short spread of just +0.07% that is statistically insignificant (p = 0.51). That is a legitimate, credible result. The value is the harness that says so honestly, not a signal that doesn't hold up.
4,705
filing events (480 names)
+0.07%
net-of-cost spread (p = 0.51)
0.03
holdout p-value, best window
clean
placebo / no lookahead

Return by amount of change: noisy, and it flips

Mean 5-day abnormal return (CAR[0,+5]) by net-added-risk quintile, Q1 (fewest changes) to Q5 (most). If "Lazy Prices" held cleanly, Q5 would sit well below Q1. It doesn't. The pattern is non-monotonic, and the in-sample and holdout periods disagree.

In-sample (pre-2022) Holdout (2022+)
Values in %. Bars grow from the zero line; below-zero bars point down.
Data table
QuintileIn-sample %Holdout %

Every signal, on the holdout: none significant

Cross-sectional coefficient on CAR[0,+5] per 1 SD of each signal, controlling for momentum and beta, with filing-date-clustered standard errors. Blue is the Lazy-Prices direction; red is against it. All p-values are above 0.05, including the transparent Loughran-McDonald tone deltas, a cross-check that the null isn't an artifact of one signal.

Coefficient in % per 1 SD. Labels show p-values.
Data table
SignalCoef (% / SD)p

Signal #2: measuring meaning change, not counting sentences

Motivated by Didisheim, Kelly, Pourmohammadi and Tian's "The Inefficient Pricing of News" (NBER w35093): represent text as an embedding vector and treat only the part not explained by firm characteristics as news. Here, each Item 1A is embedded locally (bge-large, chunked and mean-pooled) and the signal is the cosine distance between consecutive years, plus a residualized version that strips what sector and section size predict, fit on prior years only so nothing leaks backward.

Test (in-sample, 2,529 events)emb_deltaresidualized
Placebo [-5,-1] (want insignificant)p=0.34, cleanp=0.82, clean
CAR[0,+5] coef per 1 SD-0.10%, p=0.56-0.10%, p=0.63
CAR[0,+21] coef per 1 SD+0.30%, p=0.19 (wrong sign)+0.40%, p=0.13 (wrong sign)
Long-short spread, net of costs-0.43% (p=0.47)

Same verdict as the mechanical count: nothing significant, the sign flips across horizons, and the spread is negative after costs. That is informative. The paper's version works on monthly news across thousands of names; at one filing per company per year there are too few independent bets for a faint text effect to clear costs. In-sample only, stated plainly: the holdout was spent on the original signal, and judging this one honestly would need a fresh out-of-time window, looked at once.

Why this is the credible outcome

A 30-name subsample showed a strong, significant holdout effect. The full 500 washes it out, a textbook demonstration of why out-of-sample, full-universe, net-of-cost discipline exists. The methodology is the point:

Not claimed: net_added is a blunt mechanical count. A diff-grounded LLM classifier sharpens it into "substantive new risks vs boilerplate"; a small demonstration is below, and whether the sharpened signal survives the harness is the documented next step.

The LLM sharpening, demonstrated

net_added treats every added sentence equally. A diff-grounded classifier (Claude Opus, shown only the changed sentences, never the full filing) labels each one: genuinely new risk, expansion, rewording, or boilerplate. Four sample runs, one model call each:

Company10-KAdded (mechanical)Substantive (LLM)Brand-new risks
AAPL2025513516
BA202631249
NVDA2026655743
JPM2026795518

Example labels, straight from the runs:

boilerplate"The risks and uncertainties described below are not exhaustive and should not be..." (AAPL 2025)
new risk"Beginning in the second quarter of 2025, new tariffs were announced on imports to the U.S." (AAPL 2025)
new risk"For example, the 777X program, which launched in 2013 and is currently expecting first..." (BA 2026)

Roughly a quarter to a third of mechanically-added sentences are noise the classifier filters out, and the share varies by company (NVDA 88% substantive, JPM 70%).

Browse it yourself. The same classifier was then run once over every company's latest 10-K (473 companies, 13,208 added sentences) using Claude Sonnet 5 through the batch API, and the labels were saved as static files. Pick a company to see exactly what the LLM decided, sentence by sentence; hover a label for its one-line reason. Nothing runs live here: you are reading a cached, one-time run. Lists are capped at the first 40 added sentences per filing. On the four Opus-labeled names above, Sonnet's substantive share agrees closely (NVDA 88% in both runs); the line between "new risk" and "expanded" is more model-sensitive, which is exactly why the sharpened signal counts both.

Loading the label index…

Honest framing: these labels cover each company's latest 10-K, so you can inspect what the classifier does. The study's headline numbers still use the mechanical count; scoring n_substantive_added across all 4,705 historical events under the same harness (holdout, net of costs) is the documented next step.