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>
90 lines
2.6 KiB
Python
90 lines
2.6 KiB
Python
"""Provider protocols and lightweight data transfer objects.
|
|
|
|
Protocols define the interface for external data providers.
|
|
DTOs are simple dataclasses — NOT SQLAlchemy models — used to
|
|
transfer data between providers and the service layer.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
from dataclasses import dataclass, field
|
|
from datetime import date, datetime
|
|
from typing import Protocol
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Data Transfer Objects
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
@dataclass(frozen=True, slots=True)
|
|
class OHLCVData:
|
|
"""Lightweight OHLCV record returned by market data providers."""
|
|
|
|
ticker: str
|
|
date: date
|
|
open: float
|
|
high: float
|
|
low: float
|
|
close: float
|
|
volume: int
|
|
|
|
|
|
@dataclass(frozen=True, slots=True)
|
|
class SentimentData:
|
|
"""Sentiment analysis result returned by sentiment providers."""
|
|
|
|
ticker: str
|
|
classification: str # "bullish" | "bearish" | "neutral"
|
|
confidence: int # 0-100
|
|
source: str
|
|
timestamp: datetime
|
|
reasoning: str = ""
|
|
citations: list[dict[str, str]] = field(default_factory=list) # [{"url": ..., "title": ...}]
|
|
recommendation: str | None = None # "buy" | "hold" | "avoid" — actionable LLM view
|
|
|
|
|
|
@dataclass(frozen=True, slots=True)
|
|
class FundamentalData:
|
|
"""Fundamental metrics returned by fundamental providers."""
|
|
|
|
ticker: str
|
|
pe_ratio: float | None
|
|
revenue_growth: float | None
|
|
earnings_surprise: float | None
|
|
market_cap: float | None
|
|
fetched_at: datetime
|
|
next_earnings_date: date | None = None
|
|
unavailable_fields: dict[str, str] = field(default_factory=dict)
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Provider Protocols
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
class MarketDataProvider(Protocol):
|
|
"""Protocol for OHLCV market data providers."""
|
|
|
|
async def fetch_ohlcv(
|
|
self, ticker: str, start_date: date, end_date: date
|
|
) -> list[OHLCVData]:
|
|
"""Fetch OHLCV data for a ticker in a date range."""
|
|
...
|
|
|
|
|
|
class SentimentProvider(Protocol):
|
|
"""Protocol for sentiment analysis providers."""
|
|
|
|
async def fetch_sentiment(self, ticker: str) -> SentimentData:
|
|
"""Fetch current sentiment analysis for a ticker."""
|
|
...
|
|
|
|
|
|
class FundamentalProvider(Protocol):
|
|
"""Protocol for fundamental data providers."""
|
|
|
|
async def fetch_fundamentals(self, ticker: str) -> FundamentalData:
|
|
"""Fetch fundamental data for a ticker."""
|
|
...
|