sentiment: LLM buy/hold/avoid + full analysis, and search-budget scoping
Richer LLM output (same grounded call, ~no extra cost): - All providers now also return a recommendation (buy/hold/avoid) and a thorough reasoning paragraph; Gemini now actually captures reasoning + grounding citations (it was dropping them). Stored on sentiment_scores (migration 008), exposed in the API; display-only — NOT fed into the composite/EV. - Ticker Sentiment panel shows an "LLM view" badge and a "Full analysis & sources" expander with the complete reasoning + citations. Search-budget scoping (Gemini grounding free tier = 5000/mo): - collect_sentiment now targets only watchlist + open paper trades + top-N by composite, skips tickers refreshed within sentiment_fresh_hours (72h), and caps per run (sentiment_max_per_run). Once the relevant set is fresh, runs spend 0 searches until it ages out — bounding monthly usage well under the free tier. - Widened sentiment lookback to 7d (scoring + display) so sparser collection still feeds the dimension score. Deploy: alembic upgrade (sentiment_scores.recommendation). Switch provider to Gemini Flash in Admin for the cost win (grounded, cheapest). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -12,6 +12,12 @@ const classificationColors: Record<string, string> = {
|
||||
neutral: 'text-gray-300',
|
||||
};
|
||||
|
||||
const recommendationStyle: Record<string, string> = {
|
||||
buy: 'bg-emerald-500/15 text-emerald-300 border-emerald-500/30',
|
||||
hold: 'bg-amber-500/15 text-amber-300 border-amber-500/30',
|
||||
avoid: 'bg-red-500/15 text-red-300 border-red-500/30',
|
||||
};
|
||||
|
||||
export function SentimentPanel({ data }: SentimentPanelProps) {
|
||||
const [expanded, setExpanded] = useState<boolean>(false);
|
||||
const latest = data.scores[0];
|
||||
@@ -21,6 +27,14 @@ export function SentimentPanel({ data }: SentimentPanelProps) {
|
||||
<h3 className="mb-3 text-xs font-medium uppercase tracking-widest text-gray-500">Sentiment</h3>
|
||||
{latest ? (
|
||||
<>
|
||||
{latest.recommendation && (
|
||||
<div className="mb-3 flex items-center justify-between rounded-lg border bg-white/[0.02] px-3 py-2">
|
||||
<span className="text-xs uppercase tracking-wider text-gray-500">LLM view</span>
|
||||
<span className={`rounded-md border px-2.5 py-1 text-sm font-semibold uppercase tracking-wide ${recommendationStyle[latest.recommendation] ?? 'text-gray-300'}`}>
|
||||
{latest.recommendation}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
<div className="space-y-2.5 text-sm">
|
||||
<div className="flex justify-between">
|
||||
<span className="text-gray-400">Classification</span>
|
||||
@@ -45,12 +59,12 @@ export function SentimentPanel({ data }: SentimentPanelProps) {
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setExpanded((prev) => !prev)}
|
||||
className="mt-3 flex w-full items-center justify-center gap-1 text-xs text-gray-500 hover:text-gray-300 transition-colors"
|
||||
className="mt-3 flex w-full items-center justify-center gap-1.5 rounded-md border border-white/[0.08] py-1.5 text-xs text-gray-400 hover:bg-white/[0.04] hover:text-gray-200 transition-colors"
|
||||
aria-expanded={expanded}
|
||||
aria-label={expanded ? 'Collapse details' : 'Expand details'}
|
||||
>
|
||||
{expanded ? 'Hide' : 'Full analysis & sources'}
|
||||
<svg
|
||||
className={`h-4 w-4 transition-transform ${expanded ? 'rotate-180' : ''}`}
|
||||
className={`h-3.5 w-3.5 transition-transform ${expanded ? 'rotate-180' : ''}`}
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
|
||||
@@ -319,6 +319,7 @@ export interface SentimentScore {
|
||||
timestamp: string;
|
||||
reasoning: string;
|
||||
citations: CitationItem[];
|
||||
recommendation: 'buy' | 'hold' | 'avoid' | null;
|
||||
}
|
||||
|
||||
export interface SentimentResponse {
|
||||
|
||||
Reference in New Issue
Block a user