Compare commits
2
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4135946cb3 | ||
|
|
3d42ca7241 |
@@ -179,15 +179,19 @@ def parse_snapshots(
|
||||
) -> ParseResult:
|
||||
"""Build snapshot rows for ``accessions`` (those with facts + filing meta).
|
||||
|
||||
``fiscal_year_end`` is the issuer's ``submissions.fiscalYearEnd`` (MMDD) and
|
||||
is what makes period identity independent of SEC's unreliable fy/fp fields
|
||||
(see ``_period_identity``). Omitting it falls back to the old fy/fp behaviour.
|
||||
``fiscal_year_end`` is the issuer's declared ``submissions.fiscalYearEnd``
|
||||
(MMDD) and seeds period identity (see ``_period_identity``), making it
|
||||
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
|
||||
identity); ``field_issues`` = a row was produced but a field is null/ambiguous.
|
||||
Callers must not use field issues as failed-row coverage.
|
||||
"""
|
||||
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)
|
||||
result = ParseResult()
|
||||
for accn in accessions:
|
||||
@@ -291,6 +295,29 @@ def _parse_one(
|
||||
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(
|
||||
meta: FilingMeta, fiscal_year_end: 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")}
|
||||
res = parse_snapshots(companyfacts, filings, {"X"}, fiscal_year_end="1231")
|
||||
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