fix(sec): compose total debt across the styles filers actually tag
total_debt read LongTermDebt, else LongTermDebtNoncurrent/Current, plus one of ShortTermBorrowings/CommercialPaper. That misses two whole tagging styles, and it feeds net_debt -> net_debt_to_ebitda -> the categorical leverage read, so the misses were not absences but confident wrong answers: Coca-Cola scored on 0.25bn of commercial paper against ~39bn of debt, Verizon on 21.78bn of current maturities against ~165bn, AT&T and Exxon produced no value at all against 134bn and 33bn tagged. Measured over 19 large caps and 14 REITs, 11 were wrong or absent and the rest are unchanged. Each concept's span is now respected. LongTermDebt already includes current maturities (Apple tags all three: 71.34 + 11.01 = 82.30), so only true short-term borrowing is added. LongTermDebtAndCapitalLeaseObligations — what KO, HD, T, XOM and CVX tag, and nothing read before — is noncurrent and takes a current complement, and DebtCurrent *is* that whole complement rather than an addition to it. The REIT branch needed disambiguating: NotesPayable is not the same line across issuers. MAA tags NotesPayable 5.66bn = UnsecuredDebt 5.30bn + SecuredDebt 0.36bn exactly, so there it is the total and adding the secured side double-counts; EQR tags it alongside a larger SecuredDebt, where it is only the unsecured component. UnsecuredDebt's presence separates them. A component alone is no longer reported as a total. Chevron tags full debt only in its 10-K, so its 10-Q carried 0.40bn of short-term borrowing; Boston Properties tags SecuredDebt 4.28bn against ~15bn real. net_debt needs both sides and yields nothing when either is missing, so None costs a leverage read where the fragment produced a confidently wrong one. Snapshots are immutable, so this corrects new filings only; stored history needs scripts/reparse_fundamentals.py, which cannot complete until the EQR/931182 collision is retired. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016Lo99z3jWqu9X9ueBU3Z3D
This commit is contained in:
@@ -113,8 +113,41 @@ _WEIGHTED_AVG_SHARE_CONCEPTS = [
|
||||
# us-gaap instant (balance-sheet) concepts, at end == reportDate.
|
||||
_CASH = ["CashAndCashEquivalentsAtCarryingValue"]
|
||||
_ST_INVESTMENTS = ["ShortTermInvestments", "MarketableSecuritiesCurrent"] # pick one
|
||||
# Debt is tagged in four mutually exclusive styles across large filers, and
|
||||
# composing a total means knowing which span each concept covers (measured
|
||||
# 2026-08 over a 20-issuer sample; the counts below are from it).
|
||||
#
|
||||
# ``LongTermDebt`` already spans current + noncurrent maturities — Apple tags all
|
||||
# three and 71.34bn + 11.01bn = 82.30bn confirms it — so its complement is only
|
||||
# genuinely short-term borrowing.
|
||||
_LONG_TERM_DEBT_AGG = ["LongTermDebt"]
|
||||
_LONG_TERM_DEBT_PARTS = ["LongTermDebtNoncurrent", "LongTermDebtCurrent"]
|
||||
# Noncurrent-only balance-sheet lines, needing a current complement added.
|
||||
# ``LongTermDebtAndCapitalLeaseObligations`` is what KO, HD, T, XOM and CVX tag
|
||||
# and nothing read it before: AT&T reported no total_debt at all against 134bn
|
||||
# tagged, and Coca-Cola reported 0.25bn of commercial paper against 39bn.
|
||||
_LONG_TERM_DEBT_NONCURRENT = [
|
||||
"LongTermDebtNoncurrent",
|
||||
"LongTermDebtAndCapitalLeaseObligations",
|
||||
]
|
||||
_LONG_TERM_DEBT_CURRENT = ["LongTermDebtCurrent"]
|
||||
# REITs that tag no aggregate at all, carrying a secured and an unsecured side
|
||||
# instead. Both sides are required, because ``NotesPayable`` does not mean the
|
||||
# same thing across issuers (measured 2026-08 over 14 REITs):
|
||||
# - MAA tags NotesPayable 5.66bn = UnsecuredDebt 5.30bn + SecuredDebt 0.36bn
|
||||
# exactly, so there it IS the total and adding SecuredDebt double-counts.
|
||||
# - EQR/VMRK tags NotesPayable alongside a *larger* SecuredDebt (5.38bn vs
|
||||
# 6.38bn in 2013), so there it is only the unsecured component.
|
||||
# ``UnsecuredDebt`` is what separates them: where it is tagged it is the
|
||||
# unambiguous unsecured side and NotesPayable is ignored; where it is absent,
|
||||
# NotesPayable is that side. Requiring both sides is also what keeps this branch
|
||||
# from inventing a total out of a fragment — Boston Properties tags SecuredDebt
|
||||
# 4.28bn and nothing else against ~15bn of real debt, and Regency tags an
|
||||
# UnsecuredDebt of 0.03bn that is a credit-line draw, not its 5bn of notes.
|
||||
_SECURED_DEBT = ["SecuredDebt"]
|
||||
_UNSECURED_DEBT = ["UnsecuredDebt", "NotesPayable"] # first present wins
|
||||
# ``DebtCurrent`` spans short-term borrowing AND current maturities, so it is the
|
||||
# whole current complement where present and must never be added alongside them.
|
||||
_ALL_CURRENT_DEBT = ["DebtCurrent"]
|
||||
_SHORT_TERM_DEBT = ["ShortTermBorrowings", "CommercialPaper"] # pick one
|
||||
|
||||
|
||||
@@ -459,15 +492,35 @@ def _compose_cash(facts: list[Fact], report_date: date) -> float | None:
|
||||
|
||||
|
||||
def _compose_debt(facts: list[Fact], report_date: date) -> float | None:
|
||||
long_term = _select_instant(facts, _LONG_TERM_DEBT_AGG, report_date)
|
||||
if long_term is None:
|
||||
nc = _select_instant(facts, ["LongTermDebtNoncurrent"], report_date)
|
||||
cur = _select_instant(facts, ["LongTermDebtCurrent"], report_date)
|
||||
long_term = None if nc is None and cur is None else (nc or 0.0) + (cur or 0.0)
|
||||
short_term = _select_instant(facts, _SHORT_TERM_DEBT, report_date)
|
||||
if long_term is None and short_term is None:
|
||||
return None
|
||||
return (long_term or 0.0) + (short_term or 0.0)
|
||||
"""Total debt at ``report_date``, or None when no long-term component is found.
|
||||
|
||||
**A short-term component alone is never a total.** Chevron tags its full debt
|
||||
only in the 10-K, so its 10-Q carries ``ShortTermBorrowings`` of 0.40bn and
|
||||
nothing else; returning that as total debt reads as a near-unlevered issuer
|
||||
carrying 50bn. Since ``_net_debt`` needs both sides and yields nothing when
|
||||
either is missing, None costs a leverage read while the partial value
|
||||
produces a confidently wrong one.
|
||||
"""
|
||||
# An aggregate spanning current + noncurrent: only true short-term is missing.
|
||||
total = _select_instant(facts, _LONG_TERM_DEBT_AGG, report_date)
|
||||
if total is not None:
|
||||
return total + (_select_instant(facts, _SHORT_TERM_DEBT, report_date) or 0.0)
|
||||
|
||||
noncurrent = _select_instant(facts, _LONG_TERM_DEBT_NONCURRENT, report_date)
|
||||
if noncurrent is None:
|
||||
secured = _select_instant(facts, _SECURED_DEBT, report_date)
|
||||
unsecured = _select_instant(facts, _UNSECURED_DEBT, report_date)
|
||||
if secured is None or unsecured is None:
|
||||
return None # one side of a REIT's debt is not its total
|
||||
noncurrent = secured + unsecured
|
||||
|
||||
current = _select_instant(facts, _ALL_CURRENT_DEBT, report_date)
|
||||
if current is None:
|
||||
current = (
|
||||
(_select_instant(facts, _LONG_TERM_DEBT_CURRENT, report_date) or 0.0)
|
||||
+ (_select_instant(facts, _SHORT_TERM_DEBT, report_date) or 0.0)
|
||||
)
|
||||
return noncurrent + current
|
||||
|
||||
|
||||
def _select_shares(
|
||||
|
||||
Reference in New Issue
Block a user