docs: record fundamentals research decision and clean up

This commit is contained in:
2026-07-23 17:50:33 +02:00
parent dba7ea739b
commit 361cfd7883
21 changed files with 102 additions and 209972 deletions
+16 -66
View File
@@ -20,7 +20,7 @@ Rules:
from __future__ import annotations
from dataclasses import dataclass, field
from datetime import date, datetime, timezone
from datetime import date
from typing import Any, Iterable
_FP_TO_Q = {"Q1": 1, "Q2": 2, "Q3": 3, "FY": 4}
@@ -30,12 +30,7 @@ TAPE_LEN = 4 # quarter-tape length
# Duration (flow) fields differenced from YTD into discrete quarters + summed to TTM.
_FLOW_FIELDS = (
"revenue",
"net_income",
"operating_income",
"diluted_eps",
"cfo",
"capex",
"revenue", "net_income", "operating_income", "diluted_eps", "cfo", "capex",
"depreciation_amortization",
)
@@ -49,9 +44,7 @@ class MetricPoint:
@dataclass
class MetricSeries:
value: float | None = None
history: list[MetricPoint] = field(
default_factory=list
) # oldest -> newest, <= TAPE_LEN
history: list[MetricPoint] = field(default_factory=list) # oldest -> newest, <= TAPE_LEN
period_end: date | None = None
filed_date: date | None = None
@@ -89,9 +82,7 @@ def derive(snapshots: Iterable[Any]) -> DerivedFundamentals:
result.ttm_diluted_eps = _ttm(discrete["diluted_eps"], *latest)
ttm_cfo = _ttm(discrete["cfo"], *latest)
ttm_capex = _ttm(discrete["capex"], *latest)
result.ttm_fcf = (
None if ttm_cfo is None or ttm_capex is None else ttm_cfo - ttm_capex
)
result.ttm_fcf = None if ttm_cfo is None or ttm_capex is None else ttm_cfo - ttm_capex
# tape = the CONSECUTIVE run of up to TAPE_LEN quarters ending at the latest,
# stopping at a gap — so trend text never compares non-adjacent periods.
@@ -99,9 +90,7 @@ def derive(snapshots: Iterable[Any]) -> DerivedFundamentals:
result.metrics = {
"revenue_growth_yoy": _yoy_growth_series(discrete["revenue"], selected, tape),
"eps_growth_yoy": _yoy_growth_series(discrete["diluted_eps"], selected, tape),
"operating_margin": _margin_series(
discrete["operating_income"], discrete["revenue"], selected, tape
),
"operating_margin": _margin_series(discrete["operating_income"], discrete["revenue"], selected, tape),
"fcf_margin": _fcf_margin_series(discrete, selected, tape),
"net_debt": _instant_series(selected, tape, _net_debt),
"net_debt_to_ebitda": _leverage_series(selected, discrete, tape),
@@ -113,27 +102,8 @@ def derive(snapshots: Iterable[Any]) -> DerivedFundamentals:
return result
def derive_as_of(snapshots: Iterable[Any], as_of: datetime) -> DerivedFundamentals:
"""Derive using only SEC filings accepted by the historical cutoff."""
cutoff = _utc_datetime(as_of)
visible = (
row
for row in snapshots
if (accepted := getattr(row, "accepted_at", None)) is not None
and _utc_datetime(accepted) <= cutoff
)
return derive(visible)
def _utc_datetime(value: datetime) -> datetime:
if value.tzinfo is None:
return value.replace(tzinfo=timezone.utc)
return value.astimezone(timezone.utc)
# -- period selection --------------------------------------------------------
def _select_latest_per_period(snapshots: Iterable[Any]) -> dict[tuple[int, str], Any]:
best: dict[tuple[int, str], Any] = {}
for row in snapshots:
@@ -156,9 +126,7 @@ def _ordered_quarters(selected: dict[tuple[int, str], Any]) -> list[tuple[int, i
return sorted((fy, _FP_TO_Q[fp]) for (fy, fp) in selected)
def _consecutive_suffix(
quarters: list[tuple[int, int]], n: int
) -> list[tuple[int, int]]:
def _consecutive_suffix(quarters: list[tuple[int, int]], n: int) -> list[tuple[int, int]]:
"""The run of up to n quarters ending at the latest, walking back only through
adjacent periods (stop at the first gap). Returned oldest -> newest."""
if not quarters:
@@ -178,10 +146,7 @@ def _consecutive_suffix(
# -- discrete + TTM ----------------------------------------------------------
def _discrete_quarters(
selected: dict[tuple[int, str], Any], field_name: str
) -> dict[tuple[int, int], float]:
def _discrete_quarters(selected: dict[tuple[int, str], Any], field_name: str) -> dict[tuple[int, int], float]:
out: dict[tuple[int, int], float] = {}
for (fy, fp), row in selected.items():
val = _discrete_value(selected, fy, fp, field_name)
@@ -224,7 +189,6 @@ def _pct_change(cur: float | None, prior: float | None) -> float | None:
# -- per-metric series (value at latest + tape history) ----------------------
def _period_end(selected, fy: int, q: int) -> date | None:
row = selected.get((fy, _Q_TO_FP[q]))
return row.period_end if row is not None else None
@@ -232,7 +196,7 @@ def _period_end(selected, fy: int, q: int) -> date | None:
def _yoy_growth_series(dq, selected, tape) -> MetricSeries:
pts = []
for fy, q in tape:
for (fy, q) in tape:
cur, prior = _ttm(dq, fy, q), _ttm(dq, fy - 1, q)
pts.append(MetricPoint(_period_end(selected, fy, q), _pct_change(cur, prior)))
return _series(pts)
@@ -240,7 +204,7 @@ def _yoy_growth_series(dq, selected, tape) -> MetricSeries:
def _margin_series(num_dq, den_dq, selected, tape) -> MetricSeries:
pts = []
for fy, q in tape:
for (fy, q) in tape:
num, den = _ttm(num_dq, fy, q), _ttm(den_dq, fy, q)
val = None if num is None or not den else num / den * 100.0
pts.append(MetricPoint(_period_end(selected, fy, q), val))
@@ -249,38 +213,24 @@ def _margin_series(num_dq, den_dq, selected, tape) -> MetricSeries:
def _fcf_margin_series(discrete, selected, tape) -> MetricSeries:
pts = []
for fy, q in tape:
cfo, capex, rev = (
_ttm(discrete["cfo"], fy, q),
_ttm(discrete["capex"], fy, q),
_ttm(discrete["revenue"], fy, q),
)
val = (
None
if cfo is None or capex is None or not rev
else (cfo - capex) / rev * 100.0
)
for (fy, q) in tape:
cfo, capex, rev = _ttm(discrete["cfo"], fy, q), _ttm(discrete["capex"], fy, q), _ttm(discrete["revenue"], fy, q)
val = None if cfo is None or capex is None or not rev else (cfo - capex) / rev * 100.0
pts.append(MetricPoint(_period_end(selected, fy, q), val))
return _series(pts)
def _instant_series(selected, tape, fn) -> MetricSeries:
pts = [
MetricPoint(_period_end(selected, fy, q), fn(selected.get((fy, _Q_TO_FP[q]))))
for (fy, q) in tape
]
pts = [MetricPoint(_period_end(selected, fy, q), fn(selected.get((fy, _Q_TO_FP[q])))) for (fy, q) in tape]
return _series(pts)
def _leverage_series(selected, discrete, tape) -> MetricSeries:
pts = []
for fy, q in tape:
for (fy, q) in tape:
row = selected.get((fy, _Q_TO_FP[q]))
nd = _net_debt(row)
op, da = (
_ttm(discrete["operating_income"], fy, q),
_ttm(discrete["depreciation_amortization"], fy, q),
)
op, da = _ttm(discrete["operating_income"], fy, q), _ttm(discrete["depreciation_amortization"], fy, q)
ebitda = None if op is None or da is None else op + da
# Null when EBITDA <= 0: a negative denominator would flip polarity and a
# "lower is better" read would rank a distressed issuer as favorable.
@@ -291,7 +241,7 @@ def _leverage_series(selected, discrete, tape) -> MetricSeries:
def _share_change_series(selected, tape) -> MetricSeries:
pts = []
for fy, q in tape:
for (fy, q) in tape:
cur = _shares(selected.get((fy, _Q_TO_FP[q])))
prior = _shares(selected.get((fy - 1, _Q_TO_FP[q])))
pts.append(MetricPoint(_period_end(selected, fy, q), _pct_change(cur, prior)))