fix(entity): revenue was the ASC-606 slice for insurers and banks - #1167
Merged
Conversation
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
This was referenced Aug 30, 2026
Merged
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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:
MetLife tags both for FY2025:
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:
IncludingAssessedTaxoverExcluding, and gross over net.ProfitLossexceedsNetIncomeLossby 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 itsRevenuestag 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 inedgar/entity/utils.pybecause it cannot live in either module:entity_factsimportsenhanced_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'sRevenuesfigure to the contract-revenue tag.Verification
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_trendshas 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