trader.discovery.health_score module

A composite analyst “health score” (0-100), reimplemented in Decimal from ../stock-screener’s calculate_health_score (issue #39).

Scored purely from data this app already fetches and stores: AnalystOpinion (the five recommendation counts plus, since issue #27, recommendation_mean/ target_mean_price) and a current price the caller already holds — no new provider, no new fetch policy. calculate_health_score is a pure function; it does not reach for a price or an opinion itself.

A scoring/screening tool, not a trading signal by itself: nothing here is wired into discovery/filters.py’s gates, LlmStrategy, or any other order-affecting path.

class trader.discovery.health_score.HealthScoreResult(score, tier, upside_pct, total_analysts, is_buy_candidate, is_review_flag)[source]

Bases: object

One symbol’s composite health score, at one point in time.

score is rounded to PERCENT_QUANTUM (2dp) for display, but every comparison below — the tier bucket and the two screen flags — is decided from the exact, unrounded value first, so a score that lands squarely on a boundary is never misclassified by its own rounding.

upside_pct is None when target_mean_price is absent (or the current price is not strictly positive) — a missing target is not the same claim as zero upside, so the target-upside tiers contribute no bonus/penalty rather than reading None as 0.

Parameters:
  • score (Decimal)

  • tier (str)

  • upside_pct (Decimal | None)

  • total_analysts (int)

  • is_buy_candidate (bool)

  • is_review_flag (bool)

score: Decimal
tier: str
upside_pct: Decimal | None
total_analysts: int
is_buy_candidate: bool
is_review_flag: bool
trader.discovery.health_score.calculate_health_score(opinion, current_price)[source]

The 0-100 composite score, or None when there is no coverage.

opinion is None — no analysts cover the symbol, ETFs mainly — is a distinct claim from an AnalystOpinion whose five counts are all zero (which can legitimately reach here from a persisted row). Handled explicitly rather than by falling through arithmetic that would treat a missing opinion as zero counts, the same discipline opinion_gate already applies in discovery/filters.py (CLAUDE.md, “Missing analyst coverage passes, and is not the same as zero”).

Parameters:
Return type:

HealthScoreResult | None