fix(mcp): edgar_trends reported the revenue slice the getters had stopped reporting - #1168
Merged
Conversation
…1138) `edgar_trends(concepts=["revenue"])` did not return revenue. What it returned depended on `periods`, was wrong for some companies at every value, and never signalled anything was off. Two defects composed. `EntityFacts.time_series` passes `exact=":" in concept` to `FactQuery.by_concept`, and no value in CONCEPT_MAP contained a colon, so every lookup took the substring branch and matched every concept CONTAINING the name. "Revenue" pulled in CostOfRevenue, DeferredRevenue and 12 others for NVIDIA; "Assets" and "Liabilities" matched their Current, Other and Intangible variants. Then time_series sorted by filing date and truncated to `periods * 3` rows BEFORE the tool filtered to fiscal_period == 'FY', so the correct annual fact could be cut from the window entirely and the value-descending dedup picked the largest surviving wrong concept. Apple read as revenue falling from $383B to $7.7B. NVIDIA's FY2026 came back as cost of revenue -- and its `periods=8` result was correct when the issue was filed, turning wrong once a 10-Q landed on 2026-08-26. The answer tracked filing cadence, not the request. Concepts are now fully qualified so matching is exact, the period-type filter runs before the row limit, and each entry lists its variants in priority order so a company that migrates tags mid-series no longer leaves a hole. All eight mapped concepts were affected. Not the same defect as #1149: that one is the exact-name priority loop in _get_standardized_concept_value, with no fuzzy matching and no truncation. Revenue for insurers still shows the ASC-606 slice here until this path routes through that selector (edgartools-fdye); tracked separately. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: 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
…nto fix/trends-consolidated-total
Found while verifying the concept fix. `fiscal_period` does not separate annual facts from quarterly ones: companyfacts labels quarterly facts FY as well. General Mills' 90-day Q3 and its 370-day fiscal year are both fiscal_period == 'FY' with fiscal_year == 2026, so filtering on that alone let a quarter into the annual series, where it rendered under the same year label as the real annual figure: GIS periods=2 -> 2026=18,424,600,000 2026=4,436,700,000 Two rows reading 2026, one of them a quarter. The period type is now decided by the reporting window (duration_days), which time_series returns for exactly this purpose. Instants carry no duration -- a balance-sheet concept is a point in time -- and belong in either series, so nulls are kept; verified that total_assets still resolves in both annual and quarterly modes. The window bounds are wide enough for 52/53-week fiscal calendars and for the short stub reported when a company changes its fiscal year end. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LN2NaNcXEuv5YvcKFntcaZ
…nto fix/trends-consolidated-total
…pped reporting edgar_trends does its own concept selection, so it did not inherit the consolidated-total cross-check added to the standardized getters, and the two surfaces disagreed about the same company and year: MET trends: 2025=2,436,000,000 the ASC-606 slice MET getter: 2025=77,084,000,000 consolidated Revenues MetLife tags both for FY2025 and the variant list ranks the contract tag first. That list orders names; it cannot tell that one name measures a part of what another measures. The cross-check reuses is_consolidated_total_over rather than adding a third copy of the threshold -- two copies drifting apart is what edgartools-fdye objected to in the first place. It is opt-in per concept family and on only for revenue, for the same reason as in the getters: ProfitLoss exceeds NetIncomeLoss by the noncontrolling interest, and preferring it would change whose earnings are reported. GIS remains the control and is unchanged: its correct value is the lower-ranked concept in the newer period, so the cross-check must not fire for it. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LN2NaNcXEuv5YvcKFntcaZ
…ed-total # Conflicts: # edgar/ai/mcp/tools/trends.py
CodeFactor flagged the two zip() calls for the missing strict= parameter. The per-period largest value is a groupby, and the swap is a comprehension over the chosen rows, so neither zip nor the intermediate replacements dict is needed. Same behaviour, three fewer moving parts. Lint count on this file is back to main's three pre-existing findings. 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.
The follow-up promised in #1164 and #1167. Depends on both — until they merge, this PR's diff contains their commits too; it shrinks to just the last commit once they land.
The disagreement
edgar_trendsdoes its own concept selection, so it did not inherit the consolidated-total cross-check that #1167 adds to the standardized getters. Two surfaces then answered differently about the same company and year:MetLife tags both for FY2025, and the variant list ranks the contract tag first. That list orders names; it cannot tell that one name measures a part of what another measures.
The fix
Revenue trends now take the same cross-check, reusing
is_consolidated_total_overrather than adding a third copy of the threshold — two copies drifting apart is whatedgartools-fdyeobjected to in the first place. Opt-in per concept family and on only for revenue, for the same reason as in the getters:ProfitLossexceedsNetIncomeLossby the noncontrolling interest, and preferring it would change whose earnings are reported.After, on live data:
GIS remains the control: its correct value is the lower-ranked concept in the newer period, so the cross-check must not fire for it.
A separate defect found while verifying this
Pushed to #1164, where it belongs, but worth flagging since it was invisible until the two surfaces were compared:
fiscal_perioddoes not separate annual facts from quarterly ones. Companyfacts labels quarterly factsFYas well — General Mills' 90-day Q3 and its 370-day fiscal year are bothfiscal_period == "FY"withfiscal_year == 2026— so a quarter entered the annual series and rendered under the same year label as the real annual figure:Two rows reading 2026, one of them a quarter. The period type is now decided by the reporting window (
duration_days), whichtime_seriesreturns for exactly this purpose. Instants carry no duration — a balance-sheet concept is a point in time — so they are kept in both modes; verifiedtotal_assetsstill resolves annually and quarterly.Verification
8 tests here; 5 fail before and pass after, the other 3 being the "leaves correct series alone" controls that must pass on both sides. 30 pass across this file plus #1164's and #1167's, and 676 across the MCP/entity/statement selection.
🤖 Generated with Claude Code
https://claude.ai/code/session_01LN2NaNcXEuv5YvcKFntcaZ