Filing Change Signal
The Filing Change Signal measures how much an issuer's 10-K or 10-Q disclosure language moved year over year, section by section, and whether the meaning moved with the words. It operationalizes the Lazy Prices anomaly. To see coverage and the story behind it, browse the Filing Change Signal dataset.
Pulling filing change metrics
GET /v1/filings/{ticker}/changes returns a JSON array of FilingChangeRecord, newest first. Each record pairs a filing with the prior-year filing it was compared to and breaks the change down per section. Authenticate with the X-API-Key header. The endpoint is metered on every tier.
curl
curl https://api.vectorfinancials.com/v1/filings/AAPL/changes \
-H "X-API-Key: vf_sk_your_key_here" \
-G \
-d "form_type=10-K" \
-d "section=risk_factors" \
-d "limit=5"Python (requests)
import requests
resp = requests.get(
"https://api.vectorfinancials.com/v1/filings/AAPL/changes",
params={"form_type": "10-K", "section": "risk_factors", "limit": 5},
headers={"X-API-Key": "vf_sk_your_key_here"},
)
resp.raise_for_status()
for record in resp.json():
for sec in record["sections"]:
if sec["parse_status"] != "ok":
continue # honest null: nothing to score
lexical = 1 - sec["cosine"]
semantic = 1 - sec["cosine_embedding"]
print(
record["accession"], sec["section_id"],
f"lexical={lexical:.3f}", f"semantic={semantic:.3f}",
f"divergence={sec['lex_sem_divergence']:.3f}",
f"pctile={sec['change_pctile_universe']:.2f}",
"FORMAT-SWITCH" if sec["format_switch_suspected"] else "",
)Node (fetch)
const params = new URLSearchParams({
form_type: "10-K",
section: "risk_factors",
limit: "5",
});
const res = await fetch(
`https://api.vectorfinancials.com/v1/filings/AAPL/changes?${params}`,
{ headers: { "X-API-Key": "vf_sk_your_key_here" } },
);
const records = await res.json();
for (const record of records) {
for (const sec of record.sections) {
if (sec.parse_status !== "ok") continue; // honest null
const lexical = 1 - sec.cosine;
const semantic = 1 - sec.cosine_embedding;
console.log(
record.accession,
sec.section_id,
`lexical=${lexical.toFixed(3)}`,
`semantic=${semantic.toFixed(3)}`,
`divergence=${sec.lex_sem_divergence.toFixed(3)}`,
sec.format_switch_suspected ? "FORMAT-SWITCH" : "",
);
}
}Endpoint & parameters
Two routes serve the same record shape. Fetch a ticker's history, or pull a single filing by its EDGAR accession.
GET /v1/filings/{ticker}/changes
GET /v1/filings/changes/{accession}
| Param | Type | Meaning |
|---|---|---|
as_of | string (ISO 8601) | Point-in-time filter. Returns only records with knowledge_ts <= as_of, so percentiles match what was knowable then. Omit for the current view. |
form_type | 10-K | 10-Q | Restrict to annual or quarterly filings. Omit for both. |
section | risk_factors | mda | document | Restrict to one section grain. Omit to return all three per filing. |
limit | integer | Max records to return, newest first. |
FilingChangeRecord field reference
Every field on a record and on each entry in its sections array, its type, whether it can be null, and how to read it. Records are bitemporal and append-only.
| Field | Type | Nullable | Meaning & how to use |
|---|---|---|---|
ticker | string | no | Equity ticker symbol (e.g. AAPL). Echoes your request. |
cik | string | no | SEC Central Index Key for the issuer, zero-padded. |
accession | string | no | EDGAR accession number of the filing being scored. |
prior_accession | string | yes | Accession of the prior-year filing it was compared against. Null on a first observed filing with no prior-year match. |
filing_type | 10-K | 10-Q | no | Form type of this filing. |
comparison | string | no | Pairing key: "yoy_same_type" (10-K vs prior-year 10-K) or "yoy_same_quarter" (10-Q vs the same quarter a year earlier). |
effective_ts | string (ISO 8601) | no | Period of report — when the filing’s content is effective. The eff clock. |
knowledge_ts | string (ISO 8601) | no | EDGAR acceptance timestamp — the first instant the change was knowable. The as-of clock; bitemporal key. |
sections | array | no | Per-section breakdown. One entry per requested section grain (up to risk_factors, mda, document). |
sections[].section_id | risk_factors | mda | document | no | Which grain this entry scores. risk_factors = Item 1A; mda = Item 7; document = whole filing. |
sections[].parse_status | string | no | ok = scored. empty = issuer declared no material changes (honest null, not a zero). not_found = section incorporated by reference / absent. |
sections[].cosine | number (0 to 1) | yes | TF cosine similarity vs prior year, numbers kept (the CMN method). Lexical change = 1 − cosine. Null unless parse_status = ok. |
sections[].jaccard | number (0 to 1) | yes | Token-set Jaccard similarity vs prior year. Lexical change = 1 − jaccard. Null unless parse_status = ok. |
sections[].cosine_embedding | number (0 to 1) | yes | Cosine over 768-dim Gemini section embeddings. Semantic change = 1 − cosine_embedding. Null unless parse_status = ok. |
sections[].lex_sem_divergence | number | yes | (1 − cosine) − (1 − cosine_embedding). High = words moved more than meaning (reshuffle). Near zero with both high = real shift. |
sections[].format_switch_suspected | boolean | yes | True when lexical change is high but semantic change is low — a template/format swap. Down-weight these. |
sections[].change_pctile_universe | number (0 to 1) | yes | Cross-sectional percentile of the change across the universe, ranked as-of this filing’s knowledge_ts. No look-ahead. 0.71 = larger change than 71% of peers known at that time. |
sections[].word_count | integer | yes | Word count of this section in the current filing. Null unless parse_status = ok. |
sections[].prior_word_count | integer | yes | Word count of the matched prior-year section. A large delta is itself a tell. |
sections[].null_reason | string | yes | Why metrics are null, when applicable: no_prior (no prior-year filing to diff), empty_section (present but minimal, e.g. a 10-Q "no material changes" pointer), not_found (section absent or incorporated by reference), ambiguous, or prior_empty_section / prior_not_found / prior_ambiguous (the matched prior section is unusable). Null when parse_status = ok. |
Reading the results
In the explorer, each filing is diffed against its prior comparable filing: a 10-K against the prior-year 10-K, and a 10-Q against the same quarter a year earlier. That pairing is the yoy_same_type or yoy_same_quarter label on the card. The diff is broken down per section: Risk Factors (Item 1A), MD&A (Item 7), and the Whole document. Each row of the result card is one section.
The columns flip similarity into change, so higher always means “moved more.” The API returns similarity (1 = identical); the explorer shows 1 − similarity so the table reads in one direction. A small change is the norm. Filings are largely stable year over year, which is exactly why a big change is worth reading.
| Section | Lexical | Tokens | Semantic | Divergence | Pctile | Words | Read |
|---|---|---|---|---|---|---|---|
| Risk Factors | 0.341 | 0.195 | 0.194 | 0.147 | 92% | 560 (+449) | real disclosure shift |
| MD&A | 0.030 | 0.340 | 0.038 | -0.008 | 40% | 15,763 | — |
| Whole document | 0.400 | 0.450 | 0.020 | 0.380 | 85% | 12,995 (+1,906) | format / boilerplate change |
| Column | Formula | How to read it |
|---|---|---|
| Lexical | 1 − TF cosine | Lexical change with numbers kept, the Lazy Prices method. 0 = identical wording; higher = more of the text changed. |
| Tokens | 1 − Jaccard | Token-set change. How much of the vocabulary changed, ignoring frequency. A jump here with low lexical change = new terms added on top of retained text. |
| Semantic | 1 − cosine_embedding | Semantic change over 768-dim Gemini embeddings. Did the meaning move, or only the wording? |
| Divergence | lexical change − semantic change | High positive = words moved more than meaning (a reshuffle or format switch). Near zero with both elevated = a real shift. Shown signed. |
| Percentile | rank vs the as-of universe | Point-in-time cross-sectional percentile of this change, ranked only against filings knowable at this filing’s acceptance time. 81% = a bigger change than 81% of peers then. No look-ahead. |
| Words | count (Δ vs prior) | Word count of the section now, with the change vs the prior comparable filing in parentheses. |
The Read column
The last column is our plain-English read of the section, from the metrics. The badges mean:
- real disclosure shift
- Lexical and semantic change both moved. The words changed and so did the meaning, which is the case worth a human read. This is the only verdict that highlights its row. See the full Zillow case study.
- format / boilerplate change
- The
format_switch_suspectedflag fired: lots of lexical change, but the meaning held. A template or vendor reshuffle, not new disclosure. Down-weight these. - wording only
- The words moved but the meaning stayed flat. An edit to phrasing without a substantive change.
- no material changes declared
- The issuer said so. An honest null, not a zero score: there is nothing to diff because the filer declared the section unchanged.
- incorporated by reference
- The section points elsewhere, so there is nothing in the filing itself to diff.
Every row carries two clocks. The knowledge clock (knowledge_ts) is the EDGAR acceptance time, when the change became knowable. The effective clock (effective_ts) is the period of report. Screening on knowledge keeps a backtest point-in-time honest. See bitemporal data, Lazy Prices, and lexical-semantic divergence.
Methodology & bitemporality
The lexical side follows Cohen, Malloy & Nguyen's “Lazy Prices” (Journal of Finance, 2020): a term-frequency cosine over the section text with numbers retained, paired with a token-set Jaccard. Their finding is that firms which change their disclosure language tend to underperform, and that the language moves months ahead of the price. See the Lazy Prices glossary entry for the paper and a fuller summary. We add a semantic dimension the original paper did not have: cosine over 768-dim Gemini embeddings (gemini-embedding-2-preview), which is what makes the format-versus-substance distinction possible.
The same-quarter pairing for 10-Qs (rather than the prior quarter) is deliberate: it means a routine seasonal difference between, say, a Q3 and a Q4 is never mistaken for a real edit.
Every record is bitemporal on the two clocks defined under Reading the results. For a point-in-time backtest, pass as_of to filter on knowledge_ts, so no filing from the future leaks in. Tables are append-only; we never update a row, so history stays fixed underneath you.
Browse coverage and per-ticker pages on the Filing Change Signal dataset, or jump to the API reference for limits and delivery.