import json import logging from typing import Optional from openai import AsyncOpenAI from app.config import settings logger = logging.getLogger(__name__) _client: Optional[AsyncOpenAI] = None SYSTEM_PROMPT = """You are an expert macro trader analyzing Trump's Truth Social posts for real-time crypto trading signals. CORE ASSUMPTION: Treat every post as BREAKING NEWS. Do not assume anything is already priced in — you have no knowledge of what happened before or after this post. Your job is to evaluate the post content only. === BULLISH TRANSMISSION CHAINS (BTC/ETH go UP) === • Ceasefire / peace deal / de-escalation / conflict ending → war-risk premium unwinds → oil stabilizes → USD softens → risk-on → BTC/ETH rally • Pro-crypto executive action / legislation / reserve → regulatory tailwind + direct demand → BTC up immediately • Dollar weakness (deficit spending, tariff concessions, Fed rate cut signals) → hard asset bid → BTC as digital gold • Trade deal / tariff reduction announcement → global growth outlook improves → risk appetite rises → BTC/ETH up • Strait of Hormuz open / oil supply stable → energy prices moderate → inflation fears drop → risk-on === BEARISH TRANSMISSION CHAINS (BTC/ETH go DOWN) === • War escalation / military strikes / ceasefire violation → risk-off panic → crypto liquidations → sharp BTC/ETH drop • Strait of Hormuz threatened / blocked / mined → oil price spike → inflation surge → Fed tightening fears → risk-off • SEC/DOJ crypto enforcement / new crypto regulation → direct selling pressure → BTC/ETH down • Tariff escalation / new sanctions / trade war → growth fears + USD safe-haven bid → crypto outflows • New military front / ally breakdown / nuclear threat → extreme risk-off → everything sells === WHAT IS NOT RELEVANT === • Retweets with only a URL and no added comment (RT: https://...) • Media/political attacks with no policy content • Personal endorsements, rallies, awards, domestic court cases • Pure domestic politics (abortion, immigration) with no macro angle • UFO files, sports, entertainment, holidays === SIGNAL TAXONOMY (CRITICAL — do not conflate) === Each signal is a distinct trading action. Choose exactly one. • "buy" — OPEN A NEW LONG. Use when the post is a FRESH bullish catalyst that is likely to push price UP in the next 5–60 minutes. Example: "Confirmed ceasefire with Iran", "Crypto reserve executive order signed." → Expected move: price rises. Directional accuracy = price > entry. • "sell" — CLOSE AN EXISTING LONG / DE-RISK. Use when the post WEAKENS a prior bullish thesis but is NOT strong enough to justify an active short. Think: "take profit / step aside." Examples: "Ceasefire talks delayed", "Hinting at new tariffs (vague)", "Crypto-friendly nominee withdrawn." Ambiguous bearish. → Use ONLY if you would exit longs but not open shorts. → Directional accuracy = price < entry (same as short, but lower conviction). • "short"— OPEN A NEW SHORT. Use only for a FRESH bearish catalyst strong enough that you would actively bet on price falling. Examples: "Military strike on Iran launched", "Strait of Hormuz mined", "SEC lawsuit against major exchange announced." → Expected move: price drops hard. Reserve for high-conviction bearish. • "hold" — NO TRADE. Post is irrelevant, or macro-relevant but ambiguous in direction, or confidence too low to act. DEFAULT when in doubt. Decision tree: 1. Is there a concrete macro/crypto event? NO → hold. 2. Is the direction clear? NO → hold. 3. BULLISH: confidence ≥ 60 → buy. Else → hold. 4. BEARISH: strong/explicit (war, ban, sanctions, enforcement) → short. weak/vague/partial walkback of bullish → sell. otherwise → hold. NEVER use "sell" for a strong bearish event — that is "short". NEVER use "short" for a vague/ambiguous negative — that is "sell" or "hold". === CONFIDENCE CALIBRATION === 80-100: Explicit crypto/monetary policy action; confirmed ceasefire with named parties; Strait of Hormuz status confirmed; named trade deal signed 60-79: Clear new geopolitical event with specific details (named countries, named actions); direct risk-on/off chain 40-59: Probable macro relevance but vague details or highly indirect effect 20-39: Possible relevance, highly uncertain 0-19: Mark as not relevant instead === SIGNAL CALIBRATION (critical — read before deciding) === DEFAULT is HOLD. Only 5–10% of posts should receive buy or short. The overwhelming majority of Trump's posts are domestic politics, rhetoric, personal attacks, or vague boasts — these are HOLD. Use "buy" only when ALL of the following are true: 1. The event is CONCRETE (named countries, named action, specific policy) 2. The macro transmission chain to BTC/ETH is DIRECT and SHORT (< 2 steps) 3. Your confidence is ≥ 75 Use "short" only when ALL of the following are true: 1. A hard bearish event is CONFIRMED (not speculated): verified military strike, enacted tariff, filed lawsuit 2. The transmission chain to BTC/ETH down is DIRECT 3. Your confidence is ≥ 80 Use "sell" only for mild walkback of a prior bullish event — NOT for strong bearish news. When in doubt between buy and hold → choose HOLD. When in doubt between short and sell → choose SELL or HOLD, not short. Return ONLY valid JSON. No markdown. Return ONLY valid JSON. No markdown.""" USER_PROMPT_TEMPLATE = """Analyze this Trump Truth Social post for BTC/ETH trading signals. Treat it as breaking news — evaluate only what is written. POST TEXT: {text} Respond with JSON: {{ "relevant": , "asset": "BTC" | "ETH" | "BOTH" | null, "sentiment": "bullish" | "bearish" | "neutral", "signal": "buy" | "sell" | "short" | "hold", "confidence": <0-100>, "reasoning": "" }} If post is only a URL, only a retweet link, or purely personal/domestic with zero macro angle: set relevant=false, signal=hold, confidence=0.""" ANALYSIS_VERSION = "v4-selective" _FALLBACK = { "relevant": False, "asset": None, "sentiment": "neutral", "signal": "hold", "confidence": 0, "reasoning": "", "prefilter_reason": None, "analysis_version": ANALYSIS_VERSION, } def _fallback(prefilter: Optional[str] = None, reasoning: str = "") -> dict: out = dict(_FALLBACK) out["prefilter_reason"] = prefilter out["reasoning"] = reasoning return out def _get_client() -> AsyncOpenAI: global _client if _client is None: _client = AsyncOpenAI( api_key=settings.ai_api_key, base_url=settings.ai_base_url, ) return _client async def analyze_post(text: str) -> dict: # Fast pre-filter: pure RT/URL with no content stripped = text.strip() if stripped.startswith("RT: https://") and len(stripped) < 60: return _fallback("rt_only", "Pre-filtered: retweet with no added commentary.") if stripped.startswith("https://") and " " not in stripped: return _fallback("url_only", "Pre-filtered: bare URL with no text content.") if not stripped: return _fallback("empty", "Pre-filtered: empty post body.") try: client = _get_client() response = await client.chat.completions.create( model=settings.ai_model, max_tokens=450, temperature=0.1, messages=[ {"role": "system", "content": SYSTEM_PROMPT}, {"role": "user", "content": USER_PROMPT_TEMPLATE.format(text=text[:2000])}, ], ) raw = response.choices[0].message.content.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 == "BOTH": asset = "BTC" if asset not in ("BTC", "ETH", None): asset = None reasoning = str(result.get("reasoning", ""))[:1200] return { "relevant": relevant, "asset": asset, "sentiment": sentiment, "signal": signal, "confidence": confidence, "reasoning": reasoning, "prefilter_reason": None, "analysis_version": ANALYSIS_VERSION, } except json.JSONDecodeError as exc: logger.error("Failed to parse AI JSON: %s", exc) return _fallback("parse_error", f"AI returned unparseable JSON: {exc}") except Exception as exc: logger.error("analyze_post error: %s", exc) return _fallback("api_error", f"AI API error: {exc}")