trader.marketdata.analysts module¶
Analyst consensus for a symbol.
The cheapest real signal available about whether the market’s professional readers like a name: 0.05-0.23s per symbol measured on 2026-08-03, against a 400-day bar fetch plus a backtest for the alternative. That is why discovery consults this before doing anything expensive. (Issue #27, 2026-08-19, added a second yfinance surface — .info, for the mean rating and price targets — alongside .recommendations, so this call now costs two round trips, not one; not re-measured, which is why trader scan-universe rate-limits itself sequentially rather than assuming this is still as cheap as before.)
Absent coverage is None, never zeros. ETFs have no analysts at all — SPY returns nothing — and an all-zero opinion would answer “is it net negative?” with a confident no for entirely the wrong reason.
- class trader.marketdata.analysts.AnalystOpinion(strong_buy, buy, hold, sell, strong_sell, recommendation_mean=None, recommendation_mean_is_derived=False, target_mean_price=None, target_high_price=None)[source]¶
Bases:
objectHow many analysts hold each view, for the current period.
The four fields below strong_sell are issue #27 additions and default to “unknown” (None/False) so every pre-existing call site (AnalystOpinion(strong_buy=.., buy=.., …)) keeps working unchanged.
- Parameters:
strong_buy (int)
buy (int)
hold (int)
sell (int)
strong_sell (int)
recommendation_mean (float | None)
recommendation_mean_is_derived (bool)
target_mean_price (Decimal | None)
target_high_price (Decimal | None)
- strong_buy: int¶
- buy: int¶
- hold: int¶
- sell: int¶
- strong_sell: int¶
- recommendation_mean: float | None¶
1 (strong buy) .. 5 (strong sell), Yahoo’s own consensus scale. A rating, not money — float, not Decimal.
- recommendation_mean_is_derived: bool¶
True when yfinance omitted recommendationMean and this was computed from the five counts instead — never silently indistinguishable from a Yahoo-reported value.
- target_mean_price: Decimal | None¶
These two are money — a dollar price target — so Decimal.
- target_high_price: Decimal | None¶
- property is_net_negative: bool¶
Whether the bears strictly outnumber the bulls.
hold is deliberately excluded: a wall of holds is indecision, not disapproval, and treating it as negative would reject every quiet large-cap. Strictly greater, so a tie passes — discovery’s job is to remove the clearly disliked, not to arbitrate close calls the model is better placed to read.
- class trader.marketdata.analysts.AnalystProvider(*args, **kwargs)[source]¶
Bases:
ProtocolCurrent analyst consensus, or None when nobody covers the symbol.
- get_opinion(symbol)[source]¶
Raises MarketDataError if the lookup itself fails.
- Parameters:
symbol (str)
- Return type:
AnalystOpinion | None
- class trader.marketdata.analysts.YFinanceAnalystProvider(ticker_factory=None)[source]¶
Bases:
objectAnalyst opinions from yfinance. No pandas type escapes.
Caches a successful answer per symbol for this instance’s lifetime (issue #100) — “no coverage” for an ETF is a structural fact, not something that changes cycle to cycle, so re-deriving it fresh from yfinance every ~15 minutes, forever, was pure waste (and, for SPY specifically, a guaranteed 404 every time — issue #98’s reordering below removed one of two redundant calls but, corrected by #100, cannot remove the other: .recommendations alone already triggers it). Deliberately no TTL: a symbol that gains real coverage later stays cached as “none” until the process restarts, an accepted tradeoff — see issue #100. Never cached on an exception, so a transient fetch failure is retried fresh next call rather than permanently misremembered.
- Parameters:
ticker_factory (Callable[[str], object] | None)
- get_opinion(symbol)[source]¶
- Parameters:
symbol (str)
- Return type:
AnalystOpinion | None