feat(fundamentals): A4 — pure peer comparison + deterministic reads
Completes the pure read-time core. fundamentals_peers.py: median + polarity-aware favorable percentile + peer_count for a subject within its SIC group (CIK-deduped by the caller); returns None below MIN_PEERS=5 so the caller omits the industry object. Absolute net_debt is intentionally NOT peer-eligible (size-dependent) — leverage compares via net_debt_to_ebitda. HIGHER_IS_BETTER polarity map + two_digit_sic() grouping key. fundamentals_reads.py: one shared deterministic rule set (no LLM): growth_read (+-2pp), margin_read (latest vs mean-of-prior, +-1pp), share_count_read (+-1%), peer_read (60/40 bands, polarity-aware phrasing per metric), header_sentence (growth · margins · valuation, omitting empty). Tunable named constants; >=3 periods required for a series read. Tests: 8 peer + 5 reads, anchored on the boundary cases (exactly +2.0pp, exactly 60th percentile, exactly +1.0pp margin). 13 passed. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,50 @@
|
||||
"""Tests for pure peer statistics."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from app.services import fundamentals_peers as pr
|
||||
|
||||
|
||||
def test_peer_stat_higher_is_better_percentile_and_median():
|
||||
s = pr.peer_stat(3, [1, 2, 3, 4, 5], higher_is_better=True)
|
||||
assert s.median == 3
|
||||
assert s.favorable_percentile == 60 # beats/ties 3 of 5
|
||||
assert s.peer_count == 5
|
||||
|
||||
|
||||
def test_peer_stat_lower_is_better_flips_direction():
|
||||
s = pr.peer_stat(3, [1, 2, 3, 4, 5], higher_is_better=False)
|
||||
assert s.favorable_percentile == 60 # 3 of 5 are >= 3
|
||||
|
||||
|
||||
def test_peer_stat_top_and_bottom():
|
||||
assert pr.peer_stat(5, [1, 2, 3, 4, 5], higher_is_better=True).favorable_percentile == 100
|
||||
assert pr.peer_stat(1, [1, 2, 3, 4, 5], higher_is_better=True).favorable_percentile == 20
|
||||
|
||||
|
||||
def test_peer_stat_requires_min_valid_peers():
|
||||
assert pr.peer_stat(3, [1, 2, 3, None], higher_is_better=True) is None # 3 valid < 5
|
||||
assert pr.peer_stat(None, [1, 2, 3, 4, 5], higher_is_better=True) is None # null subject
|
||||
|
||||
|
||||
def test_peer_stat_excludes_nulls_from_group():
|
||||
s = pr.peer_stat(3, [1, 2, 3, 4, 5, None, None], higher_is_better=True)
|
||||
assert s.peer_count == 5 # nulls dropped
|
||||
|
||||
|
||||
def test_net_debt_is_not_peer_eligible():
|
||||
assert pr.peer_stat_for("net_debt", 100, [10, 20, 30, 40, 50]) is None # size-dependent
|
||||
assert pr.peer_stat_for("net_debt_to_ebitda", 1.0, [1, 2, 3, 4, 5]) is not None
|
||||
|
||||
|
||||
def test_peer_stat_for_uses_polarity():
|
||||
# pe is lower-is-better: a low pe beats most peers
|
||||
s = pr.peer_stat_for("pe", 10, [10, 20, 30, 40, 50])
|
||||
assert s.favorable_percentile == 100
|
||||
|
||||
|
||||
def test_two_digit_sic():
|
||||
assert pr.two_digit_sic("7372") == "73"
|
||||
assert pr.two_digit_sic("3571") == "35"
|
||||
assert pr.two_digit_sic(None) is None
|
||||
assert pr.two_digit_sic("x") is None
|
||||
@@ -0,0 +1,53 @@
|
||||
"""Tests for deterministic text reads, incl. threshold boundaries."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from types import SimpleNamespace
|
||||
|
||||
from app.services import fundamentals_reads as rd
|
||||
|
||||
|
||||
def _hist(*values):
|
||||
return [SimpleNamespace(value=v, period_end=None) for v in values]
|
||||
|
||||
|
||||
def test_growth_read_boundaries():
|
||||
assert rd.growth_read(_hist(5, 6, 8)) == "accelerating" # +2.0 exactly (>=)
|
||||
assert rd.growth_read(_hist(5, 6, 7.9)) == "steady" # +1.9 < 2.0
|
||||
assert rd.growth_read(_hist(10, 9, 7)) == "decelerating" # -2.0 exactly
|
||||
assert rd.growth_read(_hist(5, 6)) is None # < 3 periods
|
||||
|
||||
|
||||
def test_margin_read_vs_mean_of_prior():
|
||||
# prior mean = (19+20)/2 = 19.5; latest 21 -> +1.5 -> improving
|
||||
assert rd.margin_read(_hist(19, 20, 21)) == "improving"
|
||||
# latest exactly +1.0 over prior mean -> improving
|
||||
assert rd.margin_read(_hist(20, 20, 21)) == "improving"
|
||||
# within band
|
||||
assert rd.margin_read(_hist(20, 20, 20.5)) == "stable"
|
||||
assert rd.margin_read(_hist(21, 20)) is None # < 3 periods
|
||||
|
||||
|
||||
def test_share_count_read():
|
||||
assert rd.share_count_read(1.8) == "1.8% dilution"
|
||||
assert rd.share_count_read(-2.0) == "buying back"
|
||||
assert rd.share_count_read(1.0) == "flat" # boundary: not > 1.0
|
||||
assert rd.share_count_read(None) is None
|
||||
|
||||
|
||||
def test_peer_read_bands_and_polarity_phrasing():
|
||||
assert rd.peer_read("operating_margin", 60) == "above peers" # boundary favorable
|
||||
assert rd.peer_read("operating_margin", 40) == "below peers" # boundary adverse
|
||||
assert rd.peer_read("operating_margin", 50) == "in line"
|
||||
assert rd.peer_read("pe", 65) == "attractively valued"
|
||||
assert rd.peer_read("pe", 30) == "priced above peers"
|
||||
assert rd.peer_read("net_debt_to_ebitda", 20) == "elevated leverage"
|
||||
assert rd.peer_read("pe", None) is None
|
||||
|
||||
|
||||
def test_header_sentence_omits_missing_segments():
|
||||
assert rd.header_sentence("accelerating", "stable", "priced above peers") == (
|
||||
"growth accelerating · margins stable · valuation priced above peers"
|
||||
)
|
||||
assert rd.header_sentence(None, "improving", None) == "margins improving"
|
||||
assert rd.header_sentence(None, None, None) == ""
|
||||
Reference in New Issue
Block a user