feat: add fundamentals parity reporting

This commit is contained in:
2026-07-23 21:17:58 +02:00
parent 361cfd7883
commit ce8c60d957
26 changed files with 1331 additions and 8 deletions
+41
View File
@@ -27,6 +27,11 @@ _FP_TO_Q = {"Q1": 1, "Q2": 2, "Q3": 3, "FY": 4}
_Q_TO_FP = {1: "Q1", 2: "Q2", 3: "Q3", 4: "FY"}
_PREV_FP = {"Q2": "Q1", "Q3": "Q2", "FY": "Q3"}
TAPE_LEN = 4 # quarter-tape length
SPLIT_SUSPECT_SHARE_CHANGE_PCT = 25.0
SPLIT_SENSITIVE_CAVEAT = (
"Not comparable: share count changed at least 25%; possible split or "
"corporate action."
)
# Duration (flow) fields differenced from YTD into discrete quarters + summed to TTM.
_FLOW_FIELDS = (
@@ -47,6 +52,7 @@ class MetricSeries:
history: list[MetricPoint] = field(default_factory=list) # oldest -> newest, <= TAPE_LEN
period_end: date | None = None
filed_date: date | None = None
caveat: str | None = None
@dataclass
@@ -96,6 +102,7 @@ def derive(snapshots: Iterable[Any]) -> DerivedFundamentals:
"net_debt_to_ebitda": _leverage_series(selected, discrete, tape),
"share_count_change_yoy": _share_change_series(selected, tape),
}
_guard_split_sensitive_metrics(result.metrics)
for series in result.metrics.values():
series.period_end = latest_row.period_end
series.filed_date = latest_row.filed_date
@@ -248,6 +255,40 @@ def _share_change_series(selected, tape) -> MetricSeries:
return _series(pts)
def _guard_split_sensitive_metrics(metrics: dict[str, MetricSeries]) -> None:
"""Suppress historical comparisons likely distorted by a corporate action.
Company Facts has no point-in-time split factors. A large YoY share-count
move can therefore make both the point-in-time share comparison and
per-share EPS growth non-comparable. Keep the raw facts in snapshots, but
expose nulls plus an explicit caveat in the user-facing derived series.
"""
shares = metrics.get("share_count_change_yoy")
eps = metrics.get("eps_growth_yoy")
if shares is None or eps is None:
return
suspect_periods = {
point.period_end
for point in shares.history
if point.value is not None
and abs(point.value) >= SPLIT_SUSPECT_SHARE_CHANGE_PCT
}
if not suspect_periods:
return
for series in (shares, eps):
latest_guarded = bool(
series.history and series.history[-1].period_end in suspect_periods
)
for point in series.history:
if point.period_end in suspect_periods:
point.value = None
series.value = series.history[-1].value if series.history else None
if latest_guarded:
series.caveat = SPLIT_SENSITIVE_CAVEAT
def _net_debt(row: Any) -> float | None:
if row is None:
return None