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
+30 -94
View File
@@ -2,7 +2,7 @@
from __future__ import annotations
from dataclasses import dataclass, replace
from dataclasses import dataclass
from datetime import date, datetime, timezone
import pytest
@@ -38,9 +38,7 @@ _ENDS = { # period_end per (fy, quarter index 0..3)
}
def _year(
fy, discretes: dict[str, list[float]], instants: dict[str, list] | None = None
):
def _year(fy, discretes: dict[str, list[float]], instants: dict[str, list] | None = None):
"""Build 4 snapshot rows (Q1,Q2,Q3,FY) with YTD-cumulative flow fields from the
given per-quarter discrete values; instants set as-is per quarter."""
rows = []
@@ -57,38 +55,22 @@ def _year(
def _two_years():
rev25 = [100, 110, 120, 130]
rev26 = [110, 121, 132, 143] # +10% each quarter YoY
rows = _year(
2025,
{
"revenue": rev25,
"operating_income": [x * 0.2 for x in rev25],
"diluted_eps": [1.0, 1.1, 1.2, 1.3],
"cfo": [x * 0.25 for x in rev25],
"capex": [x * 0.05 for x in rev25],
"depreciation_amortization": [x * 0.05 for x in rev25],
},
instants={
"shares_outstanding": [1000, 1000, 1000, 1000],
"cash_and_st_investments": [40] * 4,
"total_debt": [140] * 4,
},
)
rows += _year(
2026,
{
"revenue": rev26,
"operating_income": [x * 0.2 for x in rev26],
"diluted_eps": [1.1, 1.21, 1.32, 1.43],
"cfo": [x * 0.25 for x in rev26],
"capex": [x * 0.05 for x in rev26],
"depreciation_amortization": [x * 0.05 for x in rev26],
},
instants={
"shares_outstanding": [900, 900, 900, 900],
"cash_and_st_investments": [50] * 4,
"total_debt": [150] * 4,
},
)
rows = _year(2025, {
"revenue": rev25,
"operating_income": [x * 0.2 for x in rev25],
"diluted_eps": [1.0, 1.1, 1.2, 1.3],
"cfo": [x * 0.25 for x in rev25],
"capex": [x * 0.05 for x in rev25],
"depreciation_amortization": [x * 0.05 for x in rev25],
}, instants={"shares_outstanding": [1000, 1000, 1000, 1000], "cash_and_st_investments": [40] * 4, "total_debt": [140] * 4})
rows += _year(2026, {
"revenue": rev26,
"operating_income": [x * 0.2 for x in rev26],
"diluted_eps": [1.1, 1.21, 1.32, 1.43],
"cfo": [x * 0.25 for x in rev26],
"capex": [x * 0.05 for x in rev26],
"depreciation_amortization": [x * 0.05 for x in rev26],
}, instants={"shares_outstanding": [900, 900, 900, 900], "cash_and_st_investments": [50] * 4, "total_debt": [150] * 4})
return rows
@@ -115,11 +97,9 @@ def test_net_debt_leverage_and_share_dilution():
# net debt = total_debt - cash = 150 - 50 = 100 (latest instant)
assert d.metrics["net_debt"].value == pytest.approx(100.0)
# EBITDA TTM = TTM operating_income + TTM D&A; net_debt/ebitda
op_ttm = 506 * 0.2 # 101.2
da_ttm = 506 * 0.05 # 25.3
assert d.metrics["net_debt_to_ebitda"].value == pytest.approx(
100.0 / (op_ttm + da_ttm), rel=1e-6
)
op_ttm = 506 * 0.2 # 101.2
da_ttm = 506 * 0.05 # 25.3
assert d.metrics["net_debt_to_ebitda"].value == pytest.approx(100.0 / (op_ttm + da_ttm), rel=1e-6)
# shares 900 vs 1000 a year earlier -> -10% (buyback)
assert d.metrics["share_count_change_yoy"].value == pytest.approx(-10.0, abs=1e-6)
@@ -150,9 +130,7 @@ def test_net_debt_requires_both_components():
r.total_debt = None
d = fd.derive(rows)
assert d.metrics["net_debt"].value is None
assert (
d.metrics["net_debt_to_ebitda"].value is None
) # net debt null -> leverage null
assert d.metrics["net_debt_to_ebitda"].value is None # net debt null -> leverage null
def test_leverage_null_when_ebitda_nonpositive():
@@ -162,23 +140,15 @@ def test_leverage_null_when_ebitda_nonpositive():
r.depreciation_amortization = 1
d = fd.derive(rows)
assert d.metrics["net_debt"].value == pytest.approx(100.0) # net debt still valid
assert d.metrics["net_debt_to_ebitda"].value is None # but leverage nulled
assert d.metrics["net_debt_to_ebitda"].value is None # but leverage nulled
def test_tape_stops_at_a_gap():
rows = [
r
for r in _two_years()
if not (r.fiscal_year == 2026 and r.fiscal_period == "Q1")
]
rows = [r for r in _two_years() if not (r.fiscal_year == 2026 and r.fiscal_period == "Q1")]
d = fd.derive(rows)
hist = d.metrics["operating_margin"].history
# consecutive suffix ending at FY2026: Q2, Q3, FY (not compressed across the Q1 gap)
assert [p.period_end for p in hist] == [
date(2026, 3, 31),
date(2026, 6, 30),
date(2026, 9, 30),
]
assert [p.period_end for p in hist] == [date(2026, 3, 31), date(2026, 6, 30), date(2026, 9, 30)]
def test_yoy_growth_null_when_prior_nonpositive():
@@ -190,48 +160,14 @@ def test_yoy_growth_null_when_prior_nonpositive():
assert d.metrics["eps_growth_yoy"].value is None # loss->profit is not a %
def test_derive_as_of_excludes_future_amendment():
rows = _two_years()
original = next(
row for row in rows if row.fiscal_year == 2026 and row.fiscal_period == "FY"
)
amendment = replace(
original,
accepted_at=datetime(2027, 1, 1, tzinfo=UTC),
revenue=999999,
)
before = fd.derive_as_of([*rows, amendment], datetime(2026, 12, 31, tzinfo=UTC))
after = fd.derive_as_of([*rows, amendment], datetime(2027, 1, 2, tzinfo=UTC))
assert before.metrics["revenue_growth_yoy"].value == pytest.approx(10.0)
assert after.metrics["revenue_growth_yoy"].value != pytest.approx(10.0)
def test_derive_as_of_treats_sqlite_naive_acceptance_as_utc():
rows = _two_years()
rows[0].accepted_at = datetime(2025, 1, 1)
result = fd.derive_as_of(rows, datetime(2027, 1, 1, tzinfo=UTC))
assert result.latest_period_end == date(2026, 9, 30)
def test_amendment_selection_newest_accepted_wins():
rows = _two_years()
# an amendment to FY2026 FY restates revenue YTD higher, accepted later
amended = Snap(
2026,
"FY",
date(2026, 9, 30),
date(2026, 11, 1),
datetime(2027, 1, 1, tzinfo=UTC),
revenue=999999,
operating_income=100,
diluted_eps=1.43,
cfo=100,
capex=10,
depreciation_amortization=25,
shares_outstanding=900,
cash_and_st_investments=50,
total_debt=150,
)
amended = Snap(2026, "FY", date(2026, 9, 30), date(2026, 11, 1),
datetime(2027, 1, 1, tzinfo=UTC), revenue=999999,
operating_income=100, diluted_eps=1.43, cfo=100, capex=10,
depreciation_amortization=25, shares_outstanding=900,
cash_and_st_investments=50, total_debt=150)
d = fd.derive(rows + [amended])
# Q4 revenue discrete now uses the amended YTD(FY)=999999 minus YTD(Q3)=363
# so TTM/growth reflects the amendment, proving newest accepted_at won.