"""News & Sentiment Agent. Two modes: - **LLM mode** (Phase 2): when ANTHROPIC_API_KEY or OPENAI_API_KEY is set, an LLM estimates sentiment from the symbol's recent price context. - **Mock mode** (default): deterministic sentiment derived from recent price action, seeded per symbol per day. Both return the identical AgentResult shape, and any LLM failure silently falls back to mock — the pipeline never breaks on a vendor outage. """ import random from datetime import date from .base import Agent, AgentResult from ..services.llm import llm_available, score_sentiment class SentimentAgent(Agent): name = "sentiment" def run(self, context: dict) -> AgentResult: symbol = context["symbol"] closes = [b["close"] for b in context["history"]] change_5d = (closes[-1] / closes[-6] - 1) if len(closes) > 6 else 0.0 change_20d = (closes[-1] / closes[-21] - 1) if len(closes) > 21 else 0.0 llm_result = None if llm_available(): price_context = ( f"last close {closes[-1]:.2f}, 5-day change {change_5d * 100:+.1f}%, " f"20-day change {change_20d * 100:+.1f}%" ) llm_result = score_sentiment(symbol, price_context) if llm_result: raw = llm_result["sentiment"] label = llm_result["label"] source = "llm" note = f"LLM sentiment {label} ({raw:+.2f}): {llm_result['reason']}" else: rng = random.Random(f"{symbol}:{date.today().isoformat()}:news") raw = max(-1.0, min(1.0, change_5d * 8 + rng.uniform(-0.15, 0.15))) label = "positive" if raw > 0.15 else "negative" if raw < -0.15 else "neutral" source = "mock" note = f"Sentiment {label} ({raw:+.2f}) — mock model (set an LLM API key for live analysis)" score = round((raw + 1) / 2 * 100, 1) # -1..1 → 0..100 direction = "CALL" if raw > 0.15 else "PUT" if raw < -0.15 else None return AgentResult( agent=self.name, score=score, direction=direction, data={"sentiment": round(raw, 3), "label": label, "source": source}, notes=[note], )