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: object

One 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: object

One 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:
Return type:

list[RankStability]