diff --git a/app/services/analysis.py b/app/services/analysis.py index 22a1c6e..7693324 100644 --- a/app/services/analysis.py +++ b/app/services/analysis.py @@ -1,5 +1,29 @@ +""" +Trump Truth Social → BTC/ETH signal scorer. + +v5-extreme-alpha: rewritten for high-precision, low-recall trading. + +Design notes (read before tweaking): + - Default is HOLD. The cost of a false positive (-$30 with 20× leverage) is + much higher than a missed signal (0). Prompt is explicitly biased that way. + - Output schema is buy / short / hold ONLY. "sell" was previously emitted + to mean "close longs" and silently mistraded as "open short" by the bot. + Removed entirely. + - 4 named transmission paths only. If the AI cannot fit the post into one + of (a/b/c/d), the answer is HOLD — no fancy multi-step storytelling. + - Confidence < 80 is forced to HOLD inside the prompt. Padding to 70 to + "play it safe with a soft signal" produces the same downstream outcome + as just outputting hold, so we don't let the AI hedge. + - Few-shot examples are concrete posts that did or didn't move BTC, drawn + from the press cards on the landing page (Mar 2025 reserve announcement, + repeated rhetoric, geopolitical noise, etc.). + - The current UTC hour is injected so the AI can apply a tighter filter + during Asia thin-liquidity hours (UTC 03-09). +""" + import json import logging +from datetime import datetime, timezone from typing import Optional from openai import AsyncOpenAI @@ -9,127 +33,198 @@ 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. +ANALYSIS_VERSION = "v5-extreme-alpha" -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 +# ──────────────────────────────────────────────────────────────────── +# SYSTEM PROMPT +# ──────────────────────────────────────────────────────────────────── +SYSTEM_PROMPT = """\ +You are the news-analyst desk at a quant crypto fund. You read every Trump +Truth Social post and decide whether it justifies an IMMEDIATE trade. -=== 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 +Your job is NOT to find "interesting" posts. Your job is to find posts where +a sophisticated trader would aggressively click the trade button within 30 +seconds of seeing them. Everything else is HOLD. -=== 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 +═══════════════════════════════════════════════════════════════════════ +ASYMMETRIC ERROR COSTS — internalize this before everything else +═══════════════════════════════════════════════════════════════════════ + • False positive (you say buy/short, no real move): -$30 of real money lost + • False negative (you say hold, real catalyst): $0 missed (we didn't + trade — that's fine) -=== SIGNAL TAXONOMY (CRITICAL — do not conflate) === -Each signal is a distinct trading action. Choose exactly one. +You are punished for false positives. You are NOT punished for false +negatives. **DEFAULT IS ALWAYS HOLD.** When in doubt, HOLD. When the +catalyst is "interesting but not specific", HOLD. When you would not +personally bet your salary on the call, HOLD. -• "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. +═══════════════════════════════════════════════════════════════════════ +THE ONLY 4 TRANSMISSION PATHS +═══════════════════════════════════════════════════════════════════════ +A post can only justify a signal if you can write a chain ≤ 15 words +using ONE of these paths: -• "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). + (a) DIRECT_CRYPTO — post explicitly names BTC / ETH / crypto / SEC / CFTC + / Strategic Reserve / a specific coin policy. + (b) USD_RATES — concrete tariff order, Fed rate signal, debt deal, + USD-strengthening or weakening fiscal action. + (c) GEOPOLITICAL — confirmed military strike, ceasefire signed/broken + with NAMED parties, Strait of Hormuz status, sanctions + enacted (not threatened, not "considering", ENACTED). + (d) REGULATORY — SEC/DOJ enforcement filed/settled against a named + crypto entity, executive order signed. -• "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. +If your transmission chain requires path (e) "well, this might cause +markets to get nervous about uncertainty" → REJECT, that is HOLD. +If you find yourself writing "→ ... → ... → ... → BTC" with 4+ steps +→ REJECT, that is confabulation, output HOLD. -• "hold" — NO TRADE. Post is irrelevant, or macro-relevant but ambiguous in - direction, or confidence too low to act. DEFAULT when in doubt. +═══════════════════════════════════════════════════════════════════════ +CHECKLIST (must answer ALL 7 before allowing buy/short) +═══════════════════════════════════════════════════════════════════════ + 1. SPECIFICITY: Does the post name a specific entity, number, date, + or order? (Vague rhetoric → no.) + 2. NEW INFO: Is this NEW information, not something Trump has said + in the past 30 days? (Repeats are priced in.) + 3. TRANSMISSION: Can you write the chain in ≤ 15 words using exactly + ONE of (a/b/c/d)? + 4. EXECUTION ≤ 24h: Is the action effective today/tomorrow, OR is it + just "I will" / "considering" / "thinking about"? + (Future intent ≠ tradeable. HOLD on future-tense.) + 5. NOT DOMESTIC: Is this US-only domestic politics (elections, courts, + opponents, monuments, agency disputes, immigration, + medical claims)? If yes → HOLD regardless. + 6. NOT RT: Does the post start with "RT @" or "RT:" or is it just + a URL with no commentary? If yes → HOLD. + 7. SURPRISE TEST: Would a Bloomberg desk lead with this as a HEADLINE? + If your honest answer is "this is just Trump being + Trump" → HOLD. -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. +A single FAIL on any item → HOLD. There are no exceptions, no "well +mostly yes". Asymmetric costs (see top) make it cheap to over-reject. -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 (read before assigning a number) +═══════════════════════════════════════════════════════════════════════ + 90-100 — "Bet the desk." All 7 checklist items pass STRONGLY. Concrete + order/EO/sanction taking effect <24h on a major crypto-relevant + entity. Should fire ≤ 1×/week historically. + 80-89 — "I'd trade this." All 7 checklist items pass. Catalyst is + specific and not yet priced in. Should fire ≤ 1×/2 weeks. + ≤ 79 — HOLD. The trading bot's minimum threshold is 80, so anything + below 80 produces zero downstream action. Do NOT pad to 70-79 + as a "soft signal" — that wastes your output. Just emit 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 +═══════════════════════════════════════════════════════════════════════ +EXAMPLES — calibrate against these +═══════════════════════════════════════════════════════════════════════ -=== 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. +[REAL CATALYST — should fire] -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 +POST: "Today I am announcing a Strategic Crypto Reserve including Bitcoin, + Solana, XRP, Ethereum. Effective immediately by Executive Order." +ANSWER: signal=buy, conf=95 +WHY: (a) DIRECT_CRYPTO. Specific entities, immediate execution, concrete + EO. All 7 checklist items pass strongly. This is the textbook hit. -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 +POST: "Effective midnight Eastern time, 25% tariffs on all Chinese imports. + Signed today. No exceptions." +ANSWER: signal=short, conf=88 +WHY: (b) USD_RATES. Concrete order, immediate execution, named country and + percentage. Strong USD bid + risk-off → BTC down. -Use "sell" only for mild walkback of a prior bullish event — NOT for strong bearish news. +POST: "Just spoke with Israel and Iran. Ceasefire signed and effective in + 6 hours. All hostilities to cease." +ANSWER: signal=buy, conf=85 +WHY: (c) GEOPOLITICAL. Named parties, signed (not "discussing"), hard + deadline. Risk-on unwind → BTC up. -When in doubt between buy and hold → choose HOLD. -When in doubt between short and sell → choose SELL or HOLD, not short. +[NOT A CATALYST — must be HOLD] -Return ONLY valid JSON. No markdown. +POST: "BITCOIN is the FUTURE of money. America LEADS the world in crypto!" +ANSWER: signal=hold, conf=0 +WHY: Pure rhetoric, no specific action. Trump has said variations of this + 30+ times. Priced in. Fails checklist #1 (specificity) and #2 (newness). -Return ONLY valid JSON. No markdown.""" +POST: "I am thinking very seriously about new tariffs on China. We will see." +ANSWER: signal=hold, conf=0 +WHY: Future-tense, "thinking about". Fails checklist #4 (execution ≤ 24h). -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: "Iran tankers approaching US coasts. Texas Louisiana Alaska. NOT GOOD!" +ANSWER: signal=hold, conf=0 +WHY: Vague threat description. No specific action announced. Trump + frequently makes such claims without follow-through. Fails #1 and #4. + +POST: "Crooked Hillary will go to JAIL for what she's done. SAD!" +ANSWER: signal=hold, conf=0 +WHY: Domestic politics. Fails #5. + +POST: "RT @realDonaldTrump: The wall is being built faster than ever!" +ANSWER: signal=hold, conf=0 +WHY: Retweet. Fails #6. + +POST: "The Fed must CUT rates NOW. They are STUPID and WEAK!" +ANSWER: signal=hold, conf=0 +WHY: Rhetoric / Fed criticism, no concrete fiscal action. Trump complains + about Fed weekly. Priced in. Fails #1 and #2. + +═══════════════════════════════════════════════════════════════════════ +ADVERSARIAL SELF-CHECK (before final output) +═══════════════════════════════════════════════════════════════════════ +Before you commit to buy/short, ask yourself: would a contrarian quant +who has read 10,000 Trump posts actually click TRADE on this? Steelman +the case for HOLD in one sentence. If the steelman is convincing, +DOWNGRADE TO HOLD. + +═══════════════════════════════════════════════════════════════════════ +OUTPUT FORMAT (strict JSON, no markdown) +═══════════════════════════════════════════════════════════════════════ +{ + "checklist": { + "specificity": "", + "novelty": "<'new' | 'repeated' | 'unsure'>", + "transmission": "<'a' | 'b' | 'c' | 'd' | 'none'>", + "execution": "<'<24h' | 'future' | 'none'>", + "domestic": , + "is_rt": , + "surprise": "<'headline' | 'noise'>" + }, + "relevant": , + "asset": "BTC" | "ETH" | null, + "sentiment": "bullish" | "bearish" | "neutral", + "signal": "buy" | "short" | "hold", + "confidence": <0-100>, + "reasoning": "" +} + +HARD CONSTRAINTS on the JSON: + • If signal == "buy" or "short", then confidence MUST be ≥ 80. + Lower than 80? Change signal to "hold" and confidence to 0. + • If transmission == "none" OR domestic == true OR is_rt == true OR + execution == "future" OR execution == "none", then signal MUST be + "hold" and confidence MUST be 0. + • Never emit "sell" — that signal type does not exist anymore. +""" + + +USER_PROMPT_TEMPLATE = """\ +Trump just posted on Truth Social. Score it for an immediate BTC/ETH +trade per your system rules. + +Current UTC hour: {hour} ({liquidity_note}) 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": "" -}} +Run the 7-item checklist out loud in the JSON `checklist` field, then +decide. Default to HOLD. Only buy/short if every gate passes.""" -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" +# ──────────────────────────────────────────────────────────────────── +# Implementation +# ──────────────────────────────────────────────────────────────────── _FALLBACK = { "relevant": False, "asset": None, @@ -159,47 +254,120 @@ def _get_client() -> AsyncOpenAI: return _client +def _liquidity_note(hour: int) -> str: + """Inject session context so AI can apply tighter filters during + Asia thin-liquidity hours when crypto reactions are smaller.""" + if 3 <= hour < 9: + return "Asia overnight, thin liquidity — be EXTRA strict, only the cleanest signals fire" + if 13 <= hour < 21: + return "US session, deepest liquidity — normal strictness applies" + return "Off-peak hours, moderate liquidity" + + async def analyze_post(text: str) -> dict: - # Fast pre-filter: pure RT/URL with no content + """Score a Trump post and return signal + structured reasoning. + + Returns the canonical dict shape expected by the rest of the codebase: + relevant, asset, sentiment, signal (buy/short/hold), confidence, + reasoning, prefilter_reason, analysis_version. + """ + # ── Fast pre-filter (no AI call) ──────────────────────────────── stripped = text.strip() + if not stripped: + return _fallback("empty", "Pre-filtered: empty post body.") 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.") + # New v5 prefilter: strict RT-prefix, even with extra text. Pure retweets + # of self / others rarely contain new tradeable info. + low = stripped.lower() + if low.startswith("rt @") and len(stripped) < 200: + return _fallback("short_rt", "Pre-filtered: short retweet, unlikely to contain new catalyst.") + + # ── Full AI scoring ───────────────────────────────────────────── + hour = datetime.now(timezone.utc).hour + user_prompt = USER_PROMPT_TEMPLATE.format( + hour=hour, + liquidity_note=_liquidity_note(hour), + text=text[:2000], + ) try: client = _get_client() response = await client.chat.completions.create( model=settings.ai_model, - max_tokens=450, + max_tokens=600, temperature=0.1, messages=[ {"role": "system", "content": SYSTEM_PROMPT}, - {"role": "user", "content": USER_PROMPT_TEMPLATE.format(text=text[:2000])}, + {"role": "user", "content": user_prompt}, ], ) - raw = response.choices[0].message.content.strip() + raw = (response.choices[0].message.content or "").strip() + # Strip ```json fences if the model adds them despite instructions if raw.startswith("```"): lines = raw.split("\n") raw = "\n".join(lines[1:-1] if lines[-1].strip() == "```" else lines[1:]) result = json.loads(raw) + # ── Normalize + enforce hard constraints ───────────────────── 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"): + # v5: drop "sell" entirely. If the model still emits it (during the + # transition before retraining), coerce to hold. + if signal == "sell": + signal = "hold" + if signal not in ("buy", "short", "hold"): signal = "hold" - confidence = int(result.get("confidence", 0)) + confidence = int(result.get("confidence", 0) or 0) confidence = max(0, min(100, confidence)) - relevant = bool(result.get("relevant", False)) + # Hard floor: signal != hold MUST have confidence ≥ 80. Otherwise + # the bot's downstream subscribers (whose min_confidence defaults + # to 60-70) would still fire on weak signals. Force-collapse here. + if signal in ("buy", "short") and confidence < 80: + signal = "hold" + confidence = 0 + + # Checklist gate — if any of these failed, the model should already + # have set hold, but enforce it as a defense-in-depth check. + # Normalize values: model occasionally returns "None"/"Future"/"True" + # (capitalized) or string "true" instead of JSON true. Lowercase + # everything before comparing. + checklist = result.get("checklist") or {} + + def _norm(v): + return v.strip().lower() if isinstance(v, str) else v + + def _is_truthy(v): + v = _norm(v) + return v is True or v in ("true", "yes", "1") + + transmission = _norm(checklist.get("transmission")) + execution = _norm(checklist.get("execution")) + + bad_checks = ( + transmission in (None, "none", "") + or _is_truthy(checklist.get("domestic")) + or _is_truthy(checklist.get("is_rt")) + or execution in (None, "none", "future", "") + ) + if signal in ("buy", "short") and bad_checks: + logger.info( + "Coerced signal to hold due to failed checklist gate: %s", + checklist, + ) + signal = "hold" + confidence = 0 + + relevant = bool(result.get("relevant", False)) or signal in ("buy", "short") asset = result.get("asset") if asset == "BOTH": diff --git a/app/services/bot_engine.py b/app/services/bot_engine.py index 029bf85..bdde71c 100644 --- a/app/services/bot_engine.py +++ b/app/services/bot_engine.py @@ -70,12 +70,14 @@ async def process_post(post: Post, db: AsyncSession) -> None: """ if not post.relevant: return - if post.signal not in ('buy', 'short', 'sell'): + # v5: "sell" is no longer emitted by the AI. Old DB rows might still have + # it; treat as hold (do not trade) — see analysis.py docstring for why + # the previous "sell→short" treatment was a semantic bug. + if post.signal not in ('buy', 'short'): logger.info("Post %d skipped: signal=%s is not actionable", post.id, post.signal) return asset = post.price_impact_asset or 'BTC' - # "sell" is treated as a short signal (bearish directional trade) side = 'long' if post.signal == 'buy' else 'short' logger.info("Signal: post=%d asset=%s side=%s confidence=%d", post.id, asset, side, post.ai_confidence)