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:
2026-07-22 20:25:48 +02:00
co-authored by Claude Opus 4.8
parent 256880899b
commit 2038b84b72
4 changed files with 292 additions and 0 deletions
+50
View File
@@ -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