Skip to content

[BugFix] openbb-sec: Resolve per-share facts tagged only with share-class dimensions - #7660

Closed
liamthuynh wants to merge 1 commit into
OpenBB-finance:feature/v5-secfrom
liamthuynh:codex/sec-dimensioned-per-share-facts
Closed

liamthuynh wants to merge 1 commit into
OpenBB-finance:feature/v5-secfrom
liamthuynh:codex/sec-dimensioned-per-share-facts

Conversation

@liamthuynh

@liamthuynh liamthuynh commented Sep 6, 2026 •

Copy link
Copy Markdown

Pull Request the OpenBB Platform

Description

Fixes #7645.

get_standardized_financials returned None for basic_eps, diluted_eps, weighted_ave_basic_shares_os and weighted_ave_diluted_shares_os for issuers that tag those concepts only with share-class dimensions. The SEC companyfacts API carries undimensioned facts only, so the fields were empty even though every filing contains the values.

Adds a fallback in the standardizer: for periods companyfacts does not cover, the filing's extracted instance is parsed with the existing XBRLParser.parse_instance and the class-dimensioned values are merged in before resolve_company_facts runs.

Share classes are discovered per filing — from the cover page (dei:EntityCommonStockSharesOutstanding), with tickers mapped via dei:TradingSymbol — and matched on the member, not the axis. That matters: U-Haul tags its voting class on StatementClassOfStockAxis in some quarters and StatementEquityComponentsAxis in others, and keying on the axis silently drops it.

Per the issue's semantics, basic counts are summed across classes while a senior class's diluted count is taken as-converted and never summed (COKE FY2025 emits 83,807,000, not the ~94,000,000 a naive sum gives).

Safeguards, each found against live SEC data and covered by a test:

  • Fail-closed sums — a cross-class sum is emitted only when the members present cover the whole class set, so a partially tagged period degrades to None, never to an understated total.
  • Restatements win — filings merge newest-first. COKE's 10-for-1 split previously made fiscal_years=[2023] and fiscal_years=None return values 10x apart for the same year.
  • Share-basis guard — periods where the filing and companyfacts disagree on the share count are skipped, so a post-split value is never paired with a stale pre-split one (U-Haul FY2021–22).
  • U-Haul basic EPS uses IncomeLossFromContinuingOperationsPerBasicShare. The note's Undistributed + Distributed pair cannot be summed — in a loss period the undistributed component is a positive magnitude with a negated presentation label, which overstated the result (0.28 vs a filed 0.18).
  • Amendments no longer supersede the filing carrying the statements; reported facts always outrank synthesized ones.

The fallback is income-statement-only, so get_standardized_financials gained a statement hint and balance sheet / cash flow requests skip the extra SEC requests. No new dependencies.

Scope: covers the three issuers in the issue. The discovery pieces are already issuer-agnostic, so generalizing is a natural follow-up PR. I'll raise two questions on the issue first — whether synthesized values should carry a distinguishable source, and whether weighted_ave_basic_shares_os should be the cross-class sum or the requested class, since COKE and U-Haul use opposite conventions in their own tagging.

How has this been tested?

openbb_platform/providers/sec/tests/test_dimensional_facts.py — 20 tests, no network. All pre-commit hooks pass.

pytest openbb_platform/providers/sec/tests/ -q
243 passed

The issue's reproduction now returns basic_eps 6.82 / diluted_eps 6.81 (was None), and every value the issue quotes reproduces: COKE 2.39 with 66,564,000 basic shares (56,517,000 + 10,047,000); BRK-A 17,868/quarter; BRK-B 11.91/quarter; U-Haul's two-class method at 196,077,880.

Populated per-share values, fallback disabled → enabled:

Issuer Annual Quarterly
COKE 36 → 60 116 → 154
BRK-A / BRK-B 7 → 19 47 → 68
UHAL / UHAL-B 8 → 21 17 → 36

Regression check across 10 configurations (5 symbols × annual/quarterly, all three statements): no SEC-reported value changes and no row is lost — only the intended new rows appear. Single-class issuers (AAPL, MSFT, JNJ) are a no-op with zero extra HTTP requests.

  • Ensure all unit and integration tests pass.
  • Ensure the command(s) execute with the expected output — Python interface.

Checklist

  • I have performed a self-review of my own code.
  • I have commented my code, particularly in hard-to-understand areas.
  • I ensure that I am following the CONTRIBUTING guidelines.
  • (If applicable) I have updated tests.

@CLAassistant

CLAassistant commented Sep 6, 2026 •

Copy link
Copy Markdown

CLA assistant check
All committers have signed the CLA.

@piiq

piiq commented Sep 7, 2026

Copy link
Copy Markdown
Member

Thanks for the PR @liamthuynh

Can you check if the issue persists in the feature/v5-sec branch and if so re-target your PR to stack on top of #7525

There is a v5 release in the works and it would be best if the bugfix is available there.

…-class dimensions

companyfacts carries undimensioned facts only, so `get_standardized_financials`
returned None for `basic_eps`, `diluted_eps`, `weighted_ave_basic_shares_os` and
`weighted_ave_diluted_shares_os` for issuers that tag them per share class —
COKE since FY2019, Berkshire entirely, U-Haul's two-class method.

Falls back to parsing the filing's extracted instance with
`XBRLParser.parse_instance` and merging the class-dimensioned values before
standardization. Share classes and the ticker-to-class map are read from each
filing's cover page rather than hardcoded, because issuers move the same member
between `StatementClassOfStockAxis` and `StatementEquityComponentsAxis` from one
filing to the next.

Basic counts are summed across classes; a senior class's diluted count is already
as-converted and is not. Three cases cost real accuracy and are handled
explicitly: sums are fail-closed, so a period whose members don't cover the full
class set yields None rather than an understated total; filings merge
newest-first so a restatement is never overwritten by the figures it restates
(COKE's 10-for-1 split); and a period is skipped when the filing and companyfacts
disagree on the share count, which would otherwise pair a post-split EPS with a
pre-split denominator.

Fetches through `openbb_sec.utils.cache.cached_request` — `aiohttp-client-cache`
is not a dependency on this branch — and is income-statement-only, so balance
sheet and cash flow requests skip the extra SEC calls.

Refs OpenBB-finance#7645
@liamthuynh
liamthuynh changed the base branch from develop to feature/v5-sec September 7, 2026 17:15
@liamthuynh
liamthuynh force-pushed the codex/sec-dimensioned-per-share-facts branch from dcbac86 to 2de0720 Compare September 7, 2026 17:21
@liamthuynh

liamthuynh commented Sep 7, 2026 •

Copy link
Copy Markdown
Author

My pleasure @piiq !

Re-targeted this PR onto feature/v5-sec. One port change: v5 drops aiohttp-client-cache, so the fallback now fetches via openbb_sec.utils.cache.cached_request, which also puts it behind the extension's rate limiter. Tests pass, and the repro returns 6.82 / 6.81 for COKE FY2025 on v5.

The bug is live on develop too, happy to open a backport once this lands.

@deeleeramone
deeleeramone deleted the branch OpenBB-finance:feature/v5-sec September 24, 2026 15:47
@liamthuynh
liamthuynh deleted the codex/sec-dimensioned-per-share-facts branch September 24, 2026 15:51
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.

SEC standardized financials: per-share fields empty for issuers that file only class-dimensioned facts (COKE, BRK-B, UHAL)

4 participants