Files
trumpsignal-backend/app/services/analysis.py
T
k 747158b5ed feat(ai): v5 extreme-alpha prompt — checklist gate + drop sell signal
Why
  v4 was firing buy/short on 13% of posts, but only 9% of those had a
  ≥1% move within the hour. Median move on 'actionable' was 0.298% vs
  0.258% on 'hold' — a 1.15× signal-to-noise ratio (random would be 1.0).
  The model was confabulating transmission chains to please the user
  rather than holding when uncertain.

  Separately: 'sell' meant 'close longs / de-risk' in the prompt but
  was traded as 'open short' by bot_engine.py, producing systematically
  negative results on sell signals (27% win rate vs 57% on real shorts).

What changed
  • analysis.py rewritten as v5-extreme-alpha:
    - Asymmetric error costs framing (false positive = -$30, FN = $0)
    - 7-item checklist that MUST all pass before buy/short
    - Only 4 named transmission paths (a/b/c/d); anything else = HOLD
    - 5 positive + 5 negative few-shot examples
    - UTC hour injected with liquidity context (Asia thin → stricter)
    - Adversarial steelman self-check before final output
    - confidence < 80 + checklist failure both force-collapse to HOLD
      in code, regardless of what the model returns (defense-in-depth)
    - 'sell' removed from output schema entirely
  • bot_engine.py: stop trading 'sell' signals (treat as hold)
  • Case-insensitive normalization on checklist values so model
    returning 'None'/'True' (capitalized) doesn't slip through

Expected impact (to validate over next 2-3 weeks of new posts)
  • actionable rate: 13% → 2-4%
  • signal/hold MFE ratio: 1.15× → 3-5×
  • ≥1% hit rate among actionable: 9% → 40-60%

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-08 15:00:44 +08:00

397 lines
19 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""
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
from app.config import settings
logger = logging.getLogger(__name__)
_client: Optional[AsyncOpenAI] = None
ANALYSIS_VERSION = "v5-extreme-alpha"
# ────────────────────────────────────────────────────────────────────
# 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.
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.
═══════════════════════════════════════════════════════════════════════
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)
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.
═══════════════════════════════════════════════════════════════════════
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:
(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.
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.
═══════════════════════════════════════════════════════════════════════
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.
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.
═══════════════════════════════════════════════════════════════════════
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.
═══════════════════════════════════════════════════════════════════════
EXAMPLES — calibrate against these
═══════════════════════════════════════════════════════════════════════
[REAL CATALYST — should fire]
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.
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.
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.
[NOT A CATALYST — must be HOLD]
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).
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).
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": "<the specific entity/number/date OR 'none'>",
"novelty": "<'new' | 'repeated' | 'unsure'>",
"transmission": "<'a' | 'b' | 'c' | 'd' | 'none'>",
"execution": "<'<24h' | 'future' | 'none'>",
"domestic": <true|false>,
"is_rt": <true|false>,
"surprise": "<'headline' | 'noise'>"
},
"relevant": <true|false>,
"asset": "BTC" | "ETH" | null,
"sentiment": "bullish" | "bearish" | "neutral",
"signal": "buy" | "short" | "hold",
"confidence": <0-100>,
"reasoning": "<EITHER the ≤15-word transmission chain (if buy/short),
OR which checklist item failed (if hold). One sentence.>"
}
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}
Run the 7-item checklist out loud in the JSON `checklist` field, then
decide. Default to HOLD. Only buy/short if every gate passes."""
# ────────────────────────────────────────────────────────────────────
# Implementation
# ────────────────────────────────────────────────────────────────────
_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
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:
"""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.")
# 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=600,
temperature=0.1,
messages=[
{"role": "system", "content": SYSTEM_PROMPT},
{"role": "user", "content": user_prompt},
],
)
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")
# 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) or 0)
confidence = max(0, min(100, confidence))
# 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":
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}")