e5166ed668
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>
41 lines
986 B
Python
41 lines
986 B
Python
"""Pydantic schemas for sentiment endpoints."""
|
|
|
|
from __future__ import annotations
|
|
|
|
from datetime import datetime
|
|
from typing import Literal
|
|
|
|
from pydantic import BaseModel, Field
|
|
|
|
|
|
class CitationItem(BaseModel):
|
|
"""A single citation from the sentiment analysis."""
|
|
|
|
url: str
|
|
title: str
|
|
|
|
|
|
class SentimentScoreResult(BaseModel):
|
|
"""A single sentiment score record."""
|
|
|
|
id: int
|
|
classification: Literal["bullish", "bearish", "neutral"]
|
|
confidence: int = Field(ge=0, le=100)
|
|
source: str
|
|
timestamp: datetime
|
|
reasoning: str = ""
|
|
citations: list[CitationItem] = []
|
|
recommendation: Literal["buy", "hold", "avoid"] | None = None
|
|
|
|
|
|
class SentimentResponse(BaseModel):
|
|
"""Envelope-ready sentiment response."""
|
|
|
|
symbol: str
|
|
scores: list[SentimentScoreResult]
|
|
count: int
|
|
dimension_score: float | None = Field(
|
|
None, ge=0, le=100, description="Time-decay weighted sentiment dimension score"
|
|
)
|
|
lookback_hours: float
|