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:
objectOne 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:
opinion (AnalystOpinion | None)
current_price (Decimal)
- Return type:
HealthScoreResult | None