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:
@@ -156,11 +156,27 @@ def compute_ema(
|
||||
}
|
||||
|
||||
|
||||
def _rsi_to_score(rsi: float) -> float:
|
||||
"""Map RSI to a 'healthy momentum' score, penalizing extremes.
|
||||
|
||||
Raw RSI as a score treats 90 as maximally bullish, but RSI 90 is extreme
|
||||
overbought (exhaustion/reversal risk), not a green light. This peaks around
|
||||
RSI 60 (healthy uptrend) and falls off toward both ends, with the overbought
|
||||
side penalized harder than oversold (oversold can mean-revert upward).
|
||||
"""
|
||||
peak = 60.0
|
||||
if rsi <= peak:
|
||||
score = 90.0 - (peak - rsi) * 0.9 # 60→90, 30→63, 0→36
|
||||
else:
|
||||
score = 90.0 - (rsi - peak) * 1.6 # 60→90, 80→58, 90→42, 100→26
|
||||
return max(0.0, min(100.0, score))
|
||||
|
||||
|
||||
def compute_rsi(
|
||||
closes: list[float],
|
||||
period: int = 14,
|
||||
) -> dict[str, Any]:
|
||||
"""Compute RSI. Score = RSI value (already 0-100)."""
|
||||
"""Compute RSI. Score is a peaked mapping (see _rsi_to_score), not raw RSI."""
|
||||
n = len(closes)
|
||||
if n < period + 1:
|
||||
raise ValidationError(
|
||||
@@ -184,12 +200,10 @@ def compute_rsi(
|
||||
rs = avg_gain / avg_loss
|
||||
rsi = 100.0 - 100.0 / (1.0 + rs)
|
||||
|
||||
score = max(0.0, min(100.0, rsi))
|
||||
|
||||
return {
|
||||
"rsi": round(rsi, 4),
|
||||
"period": period,
|
||||
"score": round(score, 4),
|
||||
"score": round(_rsi_to_score(rsi), 4),
|
||||
}
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user