Skip to content

fix(entity): revenue was the ASC-606 slice for insurers and banks - #1167

Merged
dgunning merged 3 commits into
mainfrom
fix/fdye-consolidated-total
Aug 30, 2026
Merged

fix(entity): revenue was the ASC-606 slice for insurers and banks#1167
dgunning merged 3 commits into
mainfrom
fix/fdye-consolidated-total

Conversation

@dgunning

Copy link
Copy Markdown
Owner

Builds on #1151 by @biautomator, now merged. Closes internal bead edgartools-fdye.

What #1151 settles, and what it can't

#1151 ranks standardized-concept candidates by recency, which settles which year to answer from — that is what made NVIDIA answer FY2022 forever.

It cannot settle which of two concepts tagged for that same year is the consolidated total. Among same-recency candidates the variant list decides, and that list is an order over names, not over what those names measure. Verified by running their branch directly:

NVDA  215,938,000,000   correct (#1151)
GIS    18,424,600,000   correct (control, unchanged)
MET     2,436,000,000   still the ASC-606 slice

MetLife tags both for FY2025:

RevenueFromContractWithCustomerExcludingAssessedTax    2,436,000,000
Revenues                                             77,084,000,000

and the contract tag is ranked first — so revenue came back 32x understated, and the income statement showed $2.4B of revenue above $6.1B of operating income, which cannot be true. Exposure is sector-wide: insurers and banks, where ASC-606 contract revenue is a sliver of consolidated revenue, and the wrong number still looks like money.

The rule

A same-period candidate that dwarfs the ranked pick is treated as the total the pick is a slice of. Two constraints shaped it:

  • Magnitude is never the ranking, only a cross-check on it. Taking the largest outright would prefer IncludingAssessedTax over Excluding, and gross over net.
  • It is opt-in per concept family, and on only for revenue. Net income's variants are not slices of one another — ProfitLoss exceeds NetIncomeLoss by the noncontrolling interest, and preferring it would quietly change whose earnings are reported. A test asserts the other getters have not opted in.

The cross-check is restricted to the chosen fact's own period, so a large figure from an older year can never stand in for this year's total.

General Mills is the control and is unchanged at 18,424,600,000: its correct value is the lower-ranked concept in the newer period, and its Revenues tag is a small slice of a different year — so neither "newest wins" nor "largest wins" is safe on its own. It is the case that rules out both naive repairs.

The second copy

The statement builder holds its own copy of the revenue priority list and had drifted from the getter — MetLife's income statement disagreed with get_revenue() for the same company and year. Both now share one threshold, which lives in edgar/entity/utils.py because it cannot live in either module: entity_facts imports enhanced_statement. The revenue row is also named after the concept its value came from, rather than the highest-priority concept merely present, which had been attributing MetLife's Revenues figure to the contract-revenue tag.

Verification

MET  2,436,000,000  ->  77,084,000,000
NVDA and GIS unchanged
MET income statement: Total Revenue row now 77,084,000,000, keyed to `Revenues`

11 regression tests. 10 fail against #1151 alone and pass after; the eleventh is the GIS control, which is green on both sides by design. Helper imports are inside the tests that need them so the behavioural tests fail on behaviour rather than erroring at collection.

690 entity/facts/statement tests pass, including @biautomator's test_issue_1149_stale_tag_revenue.py.

Known gap, tracked separately

MCP edgar_trends has its own concept selection and does not share this cross-check, so a revenue trend for an insurer still shows the slice. Adding a third copy of the threshold is exactly what this bead objects to, so there is a follow-up to route that path through the shared selector instead.

🤖 Generated with Claude Code

https://claude.ai/code/session_01LN2NaNcXEuv5YvcKFntcaZ

Builds on #1151 by @biautomator, which ranks standardized-concept candidates by
recency. That settles which YEAR to answer from. It cannot settle which of two
concepts tagged for that same year is the consolidated total: among same-recency
candidates the variant list decides, and that list orders names, not what the
names measure.

MetLife tags both for FY2025 --
  RevenueFromContractWithCustomerExcludingAssessedTax   2,436,000,000
  Revenues                                            77,084,000,000
-- and the contract tag is ranked first, so get_revenue() returned the slice, a
32x understatement. The income statement showed $2.4B of revenue above $6.1B of
operating income, which cannot be true and also disagreed with get_revenue() for
the same company and year.

A same-period candidate that dwarfs the ranked pick is now treated as the total
the pick is a slice of. Magnitude is only ever a cross-check on the ranked pick,
never the ranking itself: taking the largest outright would prefer
IncludingAssessedTax over Excluding, and gross over net. It is opt-in per
concept family and off everywhere else -- net income's variants are not slices
of one another, and preferring ProfitLoss over NetIncomeLoss would change whose
earnings are reported.

General Mills is the control and is unchanged at 18,424,600,000: its correct
value is the lower-ranked concept in the newer period, and its Revenues tag is a
small slice of a different year, so neither "newest wins" nor "largest wins" is
safe on its own.

The statement builder holds a second copy of the priority list and had drifted
from the getter. Both now share one threshold in edgar/entity/utils.py -- it
cannot live in either module, since entity_facts imports enhanced_statement --
and the revenue row is named after the concept its value came from rather than
the highest-priority concept merely present.

Closes edgartools-fdye.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LN2NaNcXEuv5YvcKFntcaZ
@dgunning
dgunning merged commit 2cc682e into main Aug 30, 2026
11 checks passed
@dgunning
dgunning deleted the fix/fdye-consolidated-total branch August 30, 2026 17:16
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant