747158b5ed
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>
397 lines
19 KiB
Python
397 lines
19 KiB
Python
"""
|
||
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}")
|