From 8fd9da3c8d7271be7df8ed107e21dd00b4114856 Mon Sep 17 00:00:00 2001 From: k Date: Fri, 8 May 2026 15:25:27 +0800 Subject: [PATCH] =?UTF-8?q?feat(ai):=20asset=20routing=20=E2=80=94=20categ?= =?UTF-8?q?orize=20post=20=E2=86=92=20trade=20the=20right=20perp?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Why Trading BTC on every actionable signal leaves alpha on the table. The same Trump post moves different assets very differently: • "Strategic Reserve including SOL" → BTC +8%, SOL +33% • "Tokenize Treasuries on ETH" → ETH +5%, BTC +2% • "$TRUMP coin is GREATEST" → TRUMP +50%+, BTC ~0% Picking the wrong asset is silent alpha leak. What changed • SYSTEM_PROMPT: new ASSET ROUTING section with 6 named categories (direct_named / crypto_policy / macro_risk_on / macro_risk_off / defi_thematic / meme_named) + asset-pick rules per category. • Few-shot examples now show category + target_asset + expected_move. • Output JSON adds: category, target_asset, target_chain, expected_move_pct. • Python normalization: - HL_PERPS whitelist (current Hyperliquid perp universe). - CHAIN_FALLBACK map: meme not on HL → trade chain native (e.g. $FLOKI on Solana → SOL; ETH-chain meme not on HL → ETH). - Safe default → BTC if everything else fails. - expected_move_pct < 0.8 → coerce to hold (not worth fees). - Legacy `asset` field kept aligned to BTC/ETH for the existing price_impact tracker; alts leave it null until tracker is upgraded. Note Bot routing (bot_engine.py) NOT yet updated — it still trades whatever `price_impact_asset` is, which is BTC/ETH only. The new fields are emitted and stored in DB but consumed downstream in a follow-up. This commit is "AI says the right thing" — making the bot ACT on it is the next step. Co-Authored-By: Claude Opus 4.7 --- app/services/analysis.py | 230 +++++++++++++++++++++++++++++++++++---- 1 file changed, 208 insertions(+), 22 deletions(-) diff --git a/app/services/analysis.py b/app/services/analysis.py index 7693324..13b8e35 100644 --- a/app/services/analysis.py +++ b/app/services/analysis.py @@ -116,6 +116,67 @@ CONFIDENCE CALIBRATION (read before assigning a number) below 80 produces zero downstream action. Do NOT pad to 70-79 as a "soft signal" — that wastes your output. Just emit HOLD. +═══════════════════════════════════════════════════════════════════════ +ASSET ROUTING — which coin to actually trade +═══════════════════════════════════════════════════════════════════════ +Different posts move different assets DIFFERENTLY. Trading BTC on every +signal leaves alpha on the table. Classify the post into a CATEGORY, +then pick the asset that this category moves MOST. + +CATEGORIES (pick exactly one): + + "direct_named" — post explicitly names a specific coin/token + (Bitcoin, Solana, XRP, $TRUMP, Dogecoin, Ethereum, + SUI, AVAX, etc.). The named asset reacts most. + Example move: post says "SOL in reserve" → + BTC +8%, SOL +33%. Trade SOL. + + "crypto_policy" — generic crypto policy without a specific coin + (SEC enforcement, executive order on crypto, + "Strategic Reserve" without naming). BTC is the + cleanest beta vehicle. Trade BTC. + + "macro_risk_on" — risk appetite rises (ceasefire, deal signed, + rate cut signal, tariff withdrawal). All risk + assets up; alts amplify BTC's move. Trade SOL + for amplification, BTC if HL liquidity concern. + + "macro_risk_off" — risk appetite falls (war escalation, tariff + enacted, sanctions, banking shock). All risk + assets down; alts amplify. Trade SOL short for + amplification, BTC short for liquidity. + + "defi_thematic" — tokenization, DeFi, on-chain settlement, staking, + yield products. ETH is the primary beneficiary. + Trade ETH. + + "meme_named" — Trump explicitly references a meme coin ($TRUMP, + $WIF, $PEPE, $DOGE, etc.). Trade THAT meme. If + not on Hyperliquid, trade its CHAIN'S native + token (Solana memes → SOL, ETH memes → ETH, + BNB memes → BNB). + +TARGET ASSET (the actual perp to trade): + Pick from this Hyperliquid universe, in priority order based on + liquidity and the category above: + BTC, ETH, SOL, AVAX, ARB, OP, DOGE, SHIB, PEPE, WIF, TRUMP, + SUI, APT, INJ, ATOM, ADA, MATIC, FARTCOIN + + Fallback rules if your preferred asset isn't on HL: + • Solana-based meme not on HL → SOL + • ETH-chain meme not on HL → ETH + • BNB-chain meme not on HL → BNB (if perp exists, else BTC) + • Unknown chain or ambiguous → BTC (always-safe default) + +EXPECTED MOVE (% in 1h, side-adjusted toward the signal direction): + Realistic ranges based on past Trump-event reactions: + BTC: 0.5 – 2% for strong direct catalyst, max ~5% + ETH: 0.7 – 3% slightly amplified + SOL: 1.5 – 5% ~3× BTC's beta + Memes: 3 – 15% wildly variable, asymmetric upside + If your honest expected move is < 0.8% on the chosen asset, the trade + isn't worth fees + slippage → DOWNGRADE TO HOLD. + ═══════════════════════════════════════════════════════════════════════ EXAMPLES — calibrate against these ═══════════════════════════════════════════════════════════════════════ @@ -124,21 +185,43 @@ EXAMPLES — calibrate against these 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. +ANSWER: signal=buy, conf=95, category=direct_named, target_asset=SOL, + expected_move=4 +WHY: (a) DIRECT_CRYPTO. Smaller-cap named coin (SOL) outperforms BTC + dramatically on direct mention — historical precedent shows + +30% range. Trade SOL not BTC for amplification. 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. +ANSWER: signal=short, conf=88, category=macro_risk_off, target_asset=SOL, + expected_move=3 +WHY: (b) USD_RATES. Risk-off. SOL has higher beta than BTC, so short + SOL captures 1.5-2× the move with same conviction. 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. +ANSWER: signal=buy, conf=85, category=macro_risk_on, target_asset=SOL, + expected_move=2.5 +WHY: (c) GEOPOLITICAL. Risk-on unwind. SOL amplifies BTC's beta. + +POST: "Just signed Executive Order: Tokenization of US Treasury bills + on Ethereum-based protocols. Effective Q1." +ANSWER: signal=buy, conf=82, category=defi_thematic, target_asset=ETH, + expected_move=3 +WHY: (d) REGULATORY. ETH primary beneficiary of on-chain tokenization + narrative. BTC reaction muted compared to ETH on this theme. + +POST: "$TRUMP coin is the GREATEST. Buying NOW. America First!" +ANSWER: signal=buy, conf=82, category=meme_named, target_asset=TRUMP, + expected_move=15 +WHY: meme_named. Direct meme reference. TRUMP perp exists on HL. + Asymmetric upside, expect 10-30% range historically. + +POST: "Big things coming for $WIF holders. Solana season is back!" +ANSWER: signal=buy, conf=80, category=meme_named, target_asset=SOL, + expected_move=4 +WHY: meme_named WIF, but if WIF perp not on HL, route to chain native + SOL. Captures the Solana-meme rotation effect. [NOT A CATALYST — must be HOLD] @@ -190,13 +273,24 @@ OUTPUT FORMAT (strict JSON, no markdown) "is_rt": , "surprise": "<'headline' | 'noise'>" }, - "relevant": , - "asset": "BTC" | "ETH" | null, - "sentiment": "bullish" | "bearish" | "neutral", - "signal": "buy" | "short" | "hold", - "confidence": <0-100>, - "reasoning": "" + "relevant": , + "asset": "BTC" | "ETH" | null, + ← legacy field for price_impact tracking; set to BTC if + target_asset is not BTC/ETH. Used only for backtest tables. + "category": "direct_named" | "crypto_policy" | "macro_risk_on" | + "macro_risk_off" | "defi_thematic" | "meme_named" | "none", + "target_asset": "" + | null ← null if signal is hold + "target_chain": "" | null + "expected_move_pct": <0-50, your honest 1h move estimate on target_asset + in the signal direction>, + "sentiment": "bullish" | "bearish" | "neutral", + "signal": "buy" | "short" | "hold", + "confidence": <0-100>, + "reasoning": "" } HARD CONSTRAINTS on the JSON: @@ -205,6 +299,8 @@ HARD CONSTRAINTS on the JSON: • 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. + • If signal != "hold", target_asset MUST be a real ticker. + • If expected_move_pct < 0.8, signal MUST be "hold" (not worth fees). • Never emit "sell" — that signal type does not exist anymore. """ @@ -228,6 +324,10 @@ decide. Default to HOLD. Only buy/short if every gate passes.""" _FALLBACK = { "relevant": False, "asset": None, + "category": "none", + "target_asset": None, + "target_chain": None, + "expected_move_pct": 0.0, "sentiment": "neutral", "signal": "hold", "confidence": 0, @@ -236,6 +336,35 @@ _FALLBACK = { "analysis_version": ANALYSIS_VERSION, } +# Hyperliquid perp universe (as of v5). Used to validate target_asset and +# fall back to BTC when the model picks something we can't actually trade. +# Update this list when HL adds new perps. +HL_PERPS = { + "BTC", "ETH", "SOL", "AVAX", "ARB", "OP", "DOGE", "SHIB", "PEPE", + "WIF", "TRUMP", "SUI", "APT", "INJ", "ATOM", "ADA", "MATIC", + "FARTCOIN", "BNB", "LINK", "LTC", "XRP", +} + +# Chain → its native token's HL ticker. Used when a meme isn't on HL — +# we route to the chain's native instead. +CHAIN_FALLBACK = { + "SOL": "SOL", + "SOLANA": "SOL", + "ETH": "ETH", + "ETHEREUM":"ETH", + "BNB": "BNB", + "BSC": "BNB", + "AVAX": "AVAX", + "BASE": "ETH", # Base is ETH L2 + "ARB": "ARB", + "OP": "OP", +} + +VALID_CATEGORIES = { + "direct_named", "crypto_policy", "macro_risk_on", "macro_risk_off", + "defi_thematic", "meme_named", "none", +} + def _fallback(prefilter: Optional[str] = None, reasoning: str = "") -> dict: out = dict(_FALLBACK) @@ -369,17 +498,74 @@ async def analyze_post(text: str) -> dict: 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 + # ── Asset routing — pick the actual perp to trade ───────────── + # `asset` (BTC/ETH legacy) is kept for backward-compat with the + # price_impact tracker. `target_asset` is the new field that the + # bot router should consume. + asset_raw = (result.get("asset") or "").upper().strip() or None + if asset_raw == "BOTH": + asset_raw = "BTC" + if asset_raw not in ("BTC", "ETH", None): + asset_raw = None + + category = (result.get("category") or "none").strip().lower() + if category not in VALID_CATEGORIES: + category = "none" + + target_raw = (result.get("target_asset") or "").upper().strip() or None + target_chain = (result.get("target_chain") or "").upper().strip() or None + + # Resolve target_asset to a real HL perp. + # 1. If model picked a real HL ticker → use it. + # 2. If not on HL but chain is given → fall back to chain native. + # 3. Otherwise → safe default BTC (only if we have a signal at all). + target_asset: Optional[str] = None + if signal in ("buy", "short"): + if target_raw and target_raw in HL_PERPS: + target_asset = target_raw + elif target_chain and CHAIN_FALLBACK.get(target_chain) in HL_PERPS: + target_asset = CHAIN_FALLBACK[target_chain] + logger.info("Routed %s (not on HL) → chain fallback %s", + target_raw, target_asset) + else: + target_asset = "BTC" + logger.info("Routed %s/%s → safe default BTC", + target_raw, target_chain) + + # expected_move_pct gate: if model itself says <0.8%, kill the trade + try: + expected_move = float(result.get("expected_move_pct") or 0) + except (TypeError, ValueError): + expected_move = 0.0 + expected_move = max(0.0, min(50.0, expected_move)) + if signal in ("buy", "short") and expected_move < 0.8: + logger.info("Coerced to hold: expected_move=%.2f < 0.8%%", expected_move) + signal = "hold" + confidence = 0 + target_asset = None + + # Keep legacy `asset` field aligned with target_asset when possible + # so existing price_impact_monitor (which only tracks BTC/ETH) still + # sees something meaningful for those two assets. For SOL/MEME, set + # asset=None — the new router will decide what to do. + if target_asset in ("BTC", "ETH"): + asset_raw = target_asset + elif target_asset is None: + asset_raw = None # hold case + else: + # Trade routed to alt — leave legacy asset null so the existing + # BTC/ETH-only price_impact tracker doesn't get confused. + asset_raw = None reasoning = str(result.get("reasoning", ""))[:1200] return { "relevant": relevant, - "asset": asset, + "asset": asset_raw, + "category": category, + "target_asset": target_asset, + "target_chain": target_chain, + "expected_move_pct": round(expected_move, 2), "sentiment": sentiment, "signal": signal, "confidence": confidence,