feat: sentiment as a signed adjustment to the composite, not averaged in
Going from no sentiment to a bullish read used to be able to *lower* the composite:
sentiment was blended into the weighted average as an absolute level, so a bullish
75 diluted a ticker already scoring 78. That's backwards for a directional signal.
Now the non-sentiment dimensions form a re-normalized weighted-average base, and
sentiment is applied as a signed adjustment around neutral (50):
composite = clamp(base + MAX_ADJ * (sentiment - 50) / 50)
MAX_ADJ = sentiment weight * 100 (default weight 0.10 → ±10)
Neutral leaves the base unchanged, bullish adds and bearish subtracts (scaled by
confidence, since a 50%-confidence call maps to 50 → no effect), and no sentiment
never penalises. Default sentiment weight 0.15 → 0.10; the weight now means "max ±
points." Composite breakdown exposes base_score/sentiment_score/sentiment_adjustment,
and the ScoreCard shows "Base 78 · sentiment +5.0" plus the per-dimension adjustment.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -76,8 +76,14 @@ export function ScoreCard({ compositeScore, dimensions, compositeBreakdown, show
|
||||
{compositeScore !== null ? Math.round(compositeScore) : '—'}
|
||||
</p>
|
||||
{compositeBreakdown && (
|
||||
<p className="mt-1 text-[10px] text-gray-500 leading-snug max-w-[200px]" data-testid="renorm-explanation">
|
||||
Weighted average of available dimensions with re-normalized weights.
|
||||
<p className="mt-1 text-[10px] text-gray-500 leading-snug max-w-[220px]" data-testid="renorm-explanation">
|
||||
{compositeBreakdown.sentiment_adjustment != null &&
|
||||
compositeBreakdown.base_score != null &&
|
||||
Math.abs(compositeBreakdown.sentiment_adjustment) >= 0.05
|
||||
? `Base ${Math.round(compositeBreakdown.base_score)} · sentiment ${
|
||||
compositeBreakdown.sentiment_adjustment >= 0 ? '+' : '−'
|
||||
}${Math.abs(compositeBreakdown.sentiment_adjustment).toFixed(1)}`
|
||||
: 'Weighted base of the other dimensions; sentiment adjusts it up or down.'}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
@@ -107,11 +113,26 @@ export function ScoreCard({ compositeScore, dimensions, compositeBreakdown, show
|
||||
{d.dimension}
|
||||
</span>
|
||||
<div className="flex items-center gap-2">
|
||||
{weight != null && (
|
||||
{d.dimension === 'sentiment' && compositeBreakdown?.sentiment_adjustment != null ? (
|
||||
<span
|
||||
className={`text-[10px] tabular-nums ${
|
||||
compositeBreakdown.sentiment_adjustment > 0.05
|
||||
? 'text-emerald-400/80'
|
||||
: compositeBreakdown.sentiment_adjustment < -0.05
|
||||
? 'text-red-400/80'
|
||||
: 'text-gray-500'
|
||||
}`}
|
||||
data-testid="weight-sentiment"
|
||||
title="Points sentiment adds to or subtracts from the base composite"
|
||||
>
|
||||
{compositeBreakdown.sentiment_adjustment >= 0 ? '+' : '−'}
|
||||
{Math.abs(compositeBreakdown.sentiment_adjustment).toFixed(1)}
|
||||
</span>
|
||||
) : weight != null ? (
|
||||
<span className="text-[10px] text-gray-500 tabular-nums" data-testid={`weight-${d.dimension}`}>
|
||||
{Math.round(weight * 100)}%
|
||||
</span>
|
||||
)}
|
||||
) : null}
|
||||
<div className="h-1.5 w-20 rounded-full bg-white/[0.06] overflow-hidden">
|
||||
<div
|
||||
className={`h-1.5 rounded-full bg-gradient-to-r ${barGradient(d.score)} transition-all duration-500`}
|
||||
|
||||
@@ -82,6 +82,10 @@ export interface CompositeBreakdown {
|
||||
missing_dimensions: string[];
|
||||
renormalized_weights: Record<string, number>;
|
||||
formula: string;
|
||||
base_score?: number | null;
|
||||
sentiment_score?: number | null;
|
||||
sentiment_adjustment?: number | null;
|
||||
max_sentiment_adjustment?: number | null;
|
||||
}
|
||||
|
||||
export interface ScoreResponse {
|
||||
|
||||
Reference in New Issue
Block a user