import json import logging import anthropic from app.config import settings logger = logging.getLogger(__name__) from typing import Optional _client: Optional[anthropic.AsyncAnthropic] = None SYSTEM_PROMPT = ( "You are a crypto trading signal analyst. Analyze Donald Trump's social media posts " "and determine their likely impact on cryptocurrency markets. " "Return only valid JSON with no additional text or markdown." ) USER_PROMPT_TEMPLATE = """Analyze this Trump post for cryptocurrency trading signals. Post: {text} Respond with JSON only: {{ "relevant": , "asset": "BTC" | "ETH" | null, "sentiment": "bullish" | "bearish" | "neutral", "signal": "buy" | "sell" | "short" | "hold", "confidence": <0-100>, "reasoning": "<1-2 sentences explaining the signal and why>" }} Signal rules: - "buy": bullish crypto/macro signal → expect price rise → go long - "sell": bearish signal for existing longs → exit position - "short": strong bearish signal → expect price drop → go short - "hold": relevant but unclear direction, or neutral macro Bullish examples: pro-crypto policy, BTC reserve, anti-regulation, dollar weakness, tariff deal, market optimism Bearish examples: SEC crackdowns, war escalation, economic sanctions, crypto ban threats, crisis Not relevant: personal attacks, sports, endorsements, unrelated politics Be strict on relevance — most posts are NOT relevant to crypto.""" _FALLBACK = { "relevant": False, "asset": None, "sentiment": "neutral", "signal": "hold", "confidence": 0, "reasoning": "", } def _get_client() -> anthropic.AsyncAnthropic: global _client if _client is None: _client = anthropic.AsyncAnthropic(api_key=settings.anthropic_api_key) return _client async def analyze_post(text: str) -> dict: """Run Claude signal analysis on a Trump post. Returns dict with keys: relevant, asset, sentiment, signal, confidence, reasoning. Falls back to neutral hold on any error. """ try: client = _get_client() message = await client.messages.create( model="claude-haiku-4-5-20251001", max_tokens=300, system=SYSTEM_PROMPT, messages=[ { "role": "user", "content": USER_PROMPT_TEMPLATE.format(text=text[:2000]), } ], ) raw = message.content[0].text.strip() if raw.startswith("```"): lines = raw.split("\n") raw = "\n".join(lines[1:-1] if lines[-1].strip() == "```" else lines[1:]) result = json.loads(raw) sentiment = result.get("sentiment", "neutral") if sentiment not in ("bullish", "bearish", "neutral"): sentiment = "neutral" signal = result.get("signal", "hold") if signal not in ("buy", "sell", "short", "hold"): signal = "hold" confidence = int(result.get("confidence", 0)) confidence = max(0, min(100, confidence)) relevant = bool(result.get("relevant", False)) asset = result.get("asset") if asset not in ("BTC", "ETH", None): asset = None reasoning = str(result.get("reasoning", ""))[:500] return { "relevant": relevant, "asset": asset, "sentiment": sentiment, "signal": signal, "confidence": confidence, "reasoning": reasoning, } except anthropic.APIError as exc: logger.error("Anthropic API error: %s", exc) return dict(_FALLBACK) except json.JSONDecodeError as exc: logger.error("Failed to parse Claude JSON response: %s", exc) return dict(_FALLBACK) except Exception as exc: logger.error("Unexpected error in analyze_post: %s", exc) return dict(_FALLBACK)