"""Deterministic text 'reads' for the fundamentals panel (pure, one rule set). The tape reads and the header sentence use identical outputs — no LLM, no new composite score. Thresholds are tunable named constants, not scattered literals (plan: ±2pp growth, ±1pp margins, ±1% dilution, 60/40 peer bands, ≥3 periods). Consumers pass metric series (value + dated history, from ``fundamentals_derivation``) and peer percentiles; these functions return short strings or None (render "—", no read). """ from __future__ import annotations from statistics import mean from typing import Any MIN_PERIODS = 3 GROWTH_ACCEL_PP = 2.0 MARGIN_MOVE_PP = 1.0 SHARE_DILUTION_PCT = 1.0 PEER_FAVORABLE = 60 PEER_ADVERSE = 40 def _latest_run(history: list[Any]) -> list[float]: """The consecutive non-null values ending at the latest point (oldest->newest). A null latest, or an internal gap, truncates the run — so a read never reflects a period whose displayed value is n/a.""" run: list[float] = [] for p in reversed(history): if p.value is None: break run.append(p.value) run.reverse() return run def growth_read(history: list[Any]) -> str | None: """Change in a YoY-growth series: latest − prior. Needs >= 3 consecutive non-null values ending at the latest point.""" vals = _latest_run(history) if len(vals) < MIN_PERIODS: return None delta = vals[-1] - vals[-2] if delta >= GROWTH_ACCEL_PP: return "accelerating" if delta <= -GROWTH_ACCEL_PP: return "decelerating" return "steady" def margin_read(history: list[Any]) -> str | None: """Latest margin vs the mean of prior periods (pp). Needs >= 3 consecutive non-null values ending at the latest point.""" vals = _latest_run(history) if len(vals) < MIN_PERIODS: return None delta = vals[-1] - mean(vals[:-1]) if delta >= MARGIN_MOVE_PP: return "improving" if delta <= -MARGIN_MOVE_PP: return "deteriorating" return "stable" def share_count_read(value: float | None) -> str | None: """Share-count YoY %: >+1% dilution, <-1% buying back, else flat.""" if value is None: return None if value > SHARE_DILUTION_PCT: return f"{value:.1f}% dilution" if value < -SHARE_DILUTION_PCT: return "buying back" return "flat" def peer_read(metric_key: str, favorable_percentile: int | None) -> str | None: """Peer-relative read for a metric, polarity already baked into the percentile (higher = more favorable).""" if favorable_percentile is None: return None if favorable_percentile >= PEER_FAVORABLE: return _FAVORABLE.get(metric_key, "above peers") if favorable_percentile <= PEER_ADVERSE: return _ADVERSE.get(metric_key, "below peers") return "in line" _FAVORABLE = { "pe": "attractively valued", "fcf_yield": "above peers", "net_debt_to_ebitda": "conservative leverage", } _ADVERSE = { "pe": "priced above peers", "fcf_yield": "below peers", "net_debt_to_ebitda": "elevated leverage", } def header_sentence( growth: str | None, margin: str | None, valuation: str | None ) -> str: """Join the growth / margin / peer-valuation reads with ' · ', omitting segments with no read. Segment sources are fixed by the caller (growth = revenue-growth read, margin = operating-margin read, valuation = P/E peer read falling back to FCF yield).""" parts = [] if growth: parts.append(f"growth {growth}") if margin: parts.append(f"margins {margin}") if valuation: parts.append(f"valuation {valuation}") return " · ".join(parts)