trader.discovery.rank_persistence module¶
A rank-persistence / stability screen over analyst_consensus_history (issue #39).
discover_symbols()’s analyst-scan source, and trader scan-universe directly, already populate this append-only table on every run. A single point-in-time reading — today’s strong_buy count, or today’s recommendation_mean — says nothing about whether that reading is durable. This ranks every symbol present in a given historical fetch (fetched_at) against its peers from that same fetch, then reports the mean and standard deviation of each symbol’s rank across every fetch it appeared in. A low mean rank and a low rank standard deviation together identify a name that is consistently well-regarded, not merely well-regarded today — closing the gap docs/ideas.md’s “Analyst consensus as a non-price input” entry flagged: the delta is stored but was “never computed or surfaced anywhere.”
Computed entirely over data already fetched — no new provider, no live lookup. ConsensusHistoryPoint is a small, duck-typed row shape rather than the SQLAlchemy AnalystConsensusHistory model, so this module — like the rest of discovery/ — never imports persistence/.
- trader.discovery.rank_persistence.METRIC_RECOMMENDATION_MEAN = 'recommendation_mean'¶
Rank by recommendation_mean, lowest first (Yahoo’s 1=strong-buy .. 5=strong-sell scale, so a low mean is the favourable end).
- trader.discovery.rank_persistence.METRIC_STRONG_BUY = 'strong_buy'¶
Rank by strong_buy count, highest first (rank 1 = most strong-buys).
- class trader.discovery.rank_persistence.ConsensusHistoryPoint(symbol, fetched_at, strong_buy, recommendation_mean)[source]¶
Bases:
objectOne analyst_consensus_history row, reduced to what ranking needs.
- Parameters:
symbol (str)
fetched_at (datetime)
strong_buy (int)
recommendation_mean (float | None)
- symbol: str¶
- fetched_at: datetime¶
- strong_buy: int¶
- recommendation_mean: float | None¶
- class trader.discovery.rank_persistence.RankStability(symbol, mean_rank, stddev_rank, appearances)[source]¶
Bases:
objectOne symbol’s rank mean/stddev across every fetch it appeared in.
stddev_rank is None when the symbol appeared in exactly one fetch — a standard deviation is undefined from a single point, and reporting it as 0 would read as “proven stable” for a name that has simply never been re-scanned. The same “unknown is not zero” discipline this codebase already applies to missing analyst coverage and the fill-poll settle guard.
- Parameters:
symbol (str)
mean_rank (Decimal)
stddev_rank (Decimal | None)
appearances (int)
- symbol: str¶
- mean_rank: Decimal¶
- stddev_rank: Decimal | None¶
- appearances: int¶
- trader.discovery.rank_persistence.compute_rank_stability(points, *, metric='strong_buy')[source]¶
Rank every symbol within each fetched_at snapshot, then reduce each symbol’s ranks across snapshots to a mean and standard deviation.
Rank 1 is “most favoured” for either metric. A symbol missing the ranked metric in a given snapshot (a None recommendation_mean, most often) is excluded from that snapshot’s ranking only — it neither claims a rank nor drags one down, the same “absence is not zero” shape as everything else this predicate touches.
Sorted with the most consistently well-regarded symbol first: ascending mean rank, then ascending stddev rank, with a None stddev (a single-appearance symbol — unproven consistency, not proven stability) sorted after every symbol with a measured one.
Raises ValueError for an unsupported metric — a typo here must not silently rank by the wrong column.
- Parameters:
points (Sequence[ConsensusHistoryPoint])
metric (str)
- Return type:
list[RankStability]