Merge pull request 'fix(sec): derive fiscal year end from the issuer's own 10-K' (#3) from fix/sec-fundamentals-parity-gaps into main
Reviewed-on: #3
This commit was merged in pull request #3.
This commit is contained in:
@@ -179,15 +179,19 @@ def parse_snapshots(
|
|||||||
) -> ParseResult:
|
) -> ParseResult:
|
||||||
"""Build snapshot rows for ``accessions`` (those with facts + filing meta).
|
"""Build snapshot rows for ``accessions`` (those with facts + filing meta).
|
||||||
|
|
||||||
``fiscal_year_end`` is the issuer's ``submissions.fiscalYearEnd`` (MMDD) and
|
``fiscal_year_end`` is the issuer's declared ``submissions.fiscalYearEnd``
|
||||||
is what makes period identity independent of SEC's unreliable fy/fp fields
|
(MMDD) and seeds period identity (see ``_period_identity``), making it
|
||||||
(see ``_period_identity``). Omitting it falls back to the old fy/fp behaviour.
|
independent of SEC's unreliable fy/fp fields. It is only a hint: the issuer's
|
||||||
|
own 10-K period ends override it (see ``resolve_fiscal_year_end``). With
|
||||||
|
neither available, the old fy/fp behaviour is used.
|
||||||
|
|
||||||
``skipped_filings`` = no row produced (missing facts/meta or no usable period
|
``skipped_filings`` = no row produced (missing facts/meta or no usable period
|
||||||
identity); ``field_issues`` = a row was produced but a field is null/ambiguous.
|
identity); ``field_issues`` = a row was produced but a field is null/ambiguous.
|
||||||
Callers must not use field issues as failed-row coverage.
|
Callers must not use field issues as failed-row coverage.
|
||||||
"""
|
"""
|
||||||
cik = f"{int(companyfacts['cik']):010d}"
|
cik = f"{int(companyfacts['cik']):010d}"
|
||||||
|
# The declared value is only a hint; the issuer's own 10-Ks are authoritative.
|
||||||
|
fiscal_year_end = resolve_fiscal_year_end(filings, fiscal_year_end)
|
||||||
by_accn = _index_by_accession(companyfacts)
|
by_accn = _index_by_accession(companyfacts)
|
||||||
result = ParseResult()
|
result = ParseResult()
|
||||||
for accn in accessions:
|
for accn in accessions:
|
||||||
@@ -291,6 +295,29 @@ def _parse_one(
|
|||||||
return row, ("ambiguous shares outstanding" if ambiguous else None)
|
return row, ("ambiguous shares outstanding" if ambiguous else None)
|
||||||
|
|
||||||
|
|
||||||
|
def resolve_fiscal_year_end(
|
||||||
|
filings: dict[str, FilingMeta], declared: str | None
|
||||||
|
) -> str | None:
|
||||||
|
"""The issuer's fiscal-year-end MMDD, preferring its own 10-K period ends.
|
||||||
|
|
||||||
|
``submissions.fiscalYearEnd`` is *not* reliable: Franklin Resources (BEN)
|
||||||
|
declares 1231 while every one of its 10-Ks ends 09-30. Trusting it put BEN's
|
||||||
|
fiscal Q1 (Dec) 0 days from the claimed year end — matching no quarter band —
|
||||||
|
and labelled its fiscal Q2 (Mar) as Q1, colliding two periods on one key and
|
||||||
|
destroying the quarter chain.
|
||||||
|
|
||||||
|
A 10-K's reportDate **is** the fiscal year end by definition, so it wins
|
||||||
|
whenever one is available; the declared value is only a fallback for an issuer
|
||||||
|
with no annual filing in the set. The most recent 10-K is used, so an issuer
|
||||||
|
that changed its year end is measured against its current calendar.
|
||||||
|
"""
|
||||||
|
annual = [m.report_date for m in filings.values() if m.form.startswith("10-K")]
|
||||||
|
if annual:
|
||||||
|
latest = max(annual)
|
||||||
|
return f"{latest.month:02d}{latest.day:02d}"
|
||||||
|
return declared
|
||||||
|
|
||||||
|
|
||||||
def _period_identity(
|
def _period_identity(
|
||||||
meta: FilingMeta, fiscal_year_end: str | None
|
meta: FilingMeta, fiscal_year_end: str | None
|
||||||
) -> tuple[int | None, str | None]:
|
) -> tuple[int | None, str | None]:
|
||||||
|
|||||||
@@ -494,3 +494,47 @@ def test_weighted_average_shares_falls_back_to_the_basic_and_diluted_concept():
|
|||||||
datetime(2026, 11, 1, 10, tzinfo=UTC), "10-Q")}
|
datetime(2026, 11, 1, 10, tzinfo=UTC), "10-Q")}
|
||||||
res = parse_snapshots(companyfacts, filings, {"X"}, fiscal_year_end="1231")
|
res = parse_snapshots(companyfacts, filings, {"X"}, fiscal_year_end="1231")
|
||||||
assert res.rows[0].weighted_avg_diluted_shares == 500_000
|
assert res.rows[0].weighted_avg_diluted_shares == 500_000
|
||||||
|
|
||||||
|
|
||||||
|
# -- fiscal year end resolution (submissions.fiscalYearEnd is unreliable) -----
|
||||||
|
|
||||||
|
from app.services.sec_facts_parser import resolve_fiscal_year_end # noqa: E402
|
||||||
|
|
||||||
|
|
||||||
|
def test_the_issuers_own_10k_overrides_a_wrong_declared_year_end():
|
||||||
|
# Franklin Resources declares 1231 while every 10-K ends 09-30. Trusting the
|
||||||
|
# declaration labelled its fiscal Q2 (Mar) as Q1, colliding with the real
|
||||||
|
# fiscal Q1 (Dec) and destroying the quarter chain.
|
||||||
|
filings = {
|
||||||
|
"K": _meta("2025-09-30", "10-K"),
|
||||||
|
"Q": _meta("2025-12-31"),
|
||||||
|
}
|
||||||
|
assert resolve_fiscal_year_end(filings, "1231") == "0930"
|
||||||
|
|
||||||
|
|
||||||
|
def test_declared_year_end_is_used_when_no_annual_filing_is_present():
|
||||||
|
assert resolve_fiscal_year_end({"Q": _meta("2026-03-31")}, "1231") == "1231"
|
||||||
|
assert resolve_fiscal_year_end({}, None) is None
|
||||||
|
|
||||||
|
|
||||||
|
def test_a_wrong_declared_year_end_no_longer_collides_two_periods():
|
||||||
|
"""End to end: BEN's Dec and Mar quarters must land on distinct keys."""
|
||||||
|
def _q(accn, start, end, val):
|
||||||
|
return _dur(start, end, val, accn, fp="Q1")
|
||||||
|
|
||||||
|
companyfacts = {
|
||||||
|
"cik": 38777,
|
||||||
|
"facts": {"us-gaap": {"Revenues": {"units": {"USD": [
|
||||||
|
_q("Q1", "2025-10-01", "2025-12-31", 2327), # fiscal Q1
|
||||||
|
_q("Q2", "2025-10-01", "2026-03-31", 4622), # fiscal Q2 YTD
|
||||||
|
]}}}},
|
||||||
|
}
|
||||||
|
filings = {
|
||||||
|
"K": _meta("2025-09-30", "10-K"),
|
||||||
|
"Q1": _meta("2025-12-31"),
|
||||||
|
"Q2": _meta("2026-03-31"),
|
||||||
|
}
|
||||||
|
res = parse_snapshots(companyfacts, filings, {"Q1", "Q2"}, fiscal_year_end="1231")
|
||||||
|
keys = {(r.fiscal_year, r.fiscal_period) for r in res.rows}
|
||||||
|
assert len(keys) == 2, f"periods collided on one key: {keys}"
|
||||||
|
assert keys == {(2026, "Q1"), (2026, "Q2")}
|
||||||
|
|||||||
Reference in New Issue
Block a user