Fix scoring/recommendation correctness and calibration
Triggered by CNC showing "LONG (High Confidence)" with SHORT reasoning and no long setup. - A: recommendation action + reasoning are ticker-level and identical on both setups; reasoning always matches the shown action - B: recommended_action only picks a direction with a tradeable setup; strong bias with no setup (e.g. price at ATH) → NEUTRAL with an explanatory reason instead of a fake LONG_HIGH - C: confidence is a directional-agreement model — opposing signals push it below 50 (SHORT on a 92-technical/99-momentum stock ~0%, not 55%) - D: fundamental score requires >=2 real metrics (market-cap-only no longer yields a high score) - E: RSI score peaks at healthy momentum (~60) and penalizes overbought/oversold extremes instead of treating RSI 90 as maximal - F: fundamentals chain merges fields across providers (FMP market cap + Finnhub P/E) instead of stopping at the first with any field - NEUTRAL label: "No Clear Setup" (covers untradeable-bias case) Scores recompute on next scan/scoring run; C and E shift score distributions intentionally. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -202,8 +202,18 @@ class AlphaVantageFundamentalProvider:
|
||||
)
|
||||
|
||||
|
||||
_FUNDAMENTAL_FIELDS = ("pe_ratio", "revenue_growth", "earnings_surprise", "market_cap")
|
||||
|
||||
|
||||
class ChainedFundamentalProvider:
|
||||
"""Try multiple fundamental providers in order until one succeeds."""
|
||||
"""Merge fundamentals across providers, filling gaps from later sources.
|
||||
|
||||
A single provider rarely covers everything on free tiers — FMP's free plan,
|
||||
for example, returns only market cap (the ratios/growth/earnings endpoints
|
||||
402). Rather than stop at the first provider with *any* field, we take each
|
||||
field from the first provider that supplies it, so FMP's market cap is
|
||||
combined with Finnhub's P/E and earnings surprise.
|
||||
"""
|
||||
|
||||
def __init__(self, providers: list[tuple[str, FundamentalProvider]]) -> None:
|
||||
if not providers:
|
||||
@@ -211,37 +221,48 @@ class ChainedFundamentalProvider:
|
||||
self._providers = providers
|
||||
|
||||
async def fetch_fundamentals(self, ticker: str) -> FundamentalData:
|
||||
merged: dict[str, float | None] = {f: None for f in _FUNDAMENTAL_FIELDS}
|
||||
field_source: dict[str, str] = {}
|
||||
errors: list[str] = []
|
||||
|
||||
for provider_name, provider in self._providers:
|
||||
if all(merged[f] is not None for f in _FUNDAMENTAL_FIELDS):
|
||||
break
|
||||
try:
|
||||
data = await provider.fetch_fundamentals(ticker)
|
||||
|
||||
has_any_metric = any(
|
||||
value is not None
|
||||
for value in (data.pe_ratio, data.revenue_growth, data.earnings_surprise, data.market_cap)
|
||||
)
|
||||
if not has_any_metric:
|
||||
errors.append(f"{provider_name}: no usable metrics returned")
|
||||
continue
|
||||
|
||||
unavailable = dict(data.unavailable_fields)
|
||||
unavailable["provider"] = provider_name
|
||||
|
||||
return FundamentalData(
|
||||
ticker=data.ticker,
|
||||
pe_ratio=data.pe_ratio,
|
||||
revenue_growth=data.revenue_growth,
|
||||
earnings_surprise=data.earnings_surprise,
|
||||
market_cap=data.market_cap,
|
||||
fetched_at=data.fetched_at,
|
||||
unavailable_fields=unavailable,
|
||||
)
|
||||
except Exception as exc:
|
||||
errors.append(f"{provider_name}: {type(exc).__name__}: {exc}")
|
||||
continue
|
||||
|
||||
attempts = "; ".join(errors[:6]) if errors else "no provider attempts"
|
||||
raise ProviderError(f"All fundamentals providers failed for {ticker}. Attempts: {attempts}")
|
||||
for field in _FUNDAMENTAL_FIELDS:
|
||||
if merged[field] is None:
|
||||
value = getattr(data, field)
|
||||
if value is not None:
|
||||
merged[field] = value
|
||||
field_source[field] = provider_name
|
||||
|
||||
if all(merged[f] is None for f in _FUNDAMENTAL_FIELDS):
|
||||
attempts = "; ".join(errors[:6]) if errors else "no usable metrics from any provider"
|
||||
raise ProviderError(f"All fundamentals providers failed for {ticker}. Attempts: {attempts}")
|
||||
|
||||
unavailable: dict[str, str] = {
|
||||
field: "not available from any configured provider"
|
||||
for field in _FUNDAMENTAL_FIELDS
|
||||
if merged[field] is None
|
||||
}
|
||||
# Record which provider supplied each field for transparency.
|
||||
for field, src in field_source.items():
|
||||
unavailable[f"source_{field}"] = src
|
||||
|
||||
return FundamentalData(
|
||||
ticker=ticker,
|
||||
pe_ratio=merged["pe_ratio"],
|
||||
revenue_growth=merged["revenue_growth"],
|
||||
earnings_surprise=merged["earnings_surprise"],
|
||||
market_cap=merged["market_cap"],
|
||||
fetched_at=datetime.now(timezone.utc),
|
||||
unavailable_fields=unavailable,
|
||||
)
|
||||
|
||||
|
||||
def build_fundamental_provider_chain() -> FundamentalProvider:
|
||||
|
||||
Reference in New Issue
Block a user