feat(ai): asset routing — categorize post → trade the right perp

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 <noreply@anthropic.com>
This commit is contained in:
k
2026-05-08 15:25:27 +08:00
parent 5bfc07c7e1
commit 8fd9da3c8d
+208 -22
View File
@@ -116,6 +116,67 @@ CONFIDENCE CALIBRATION (read before assigning a number)
below 80 produces zero downstream action. Do NOT pad to 70-79 below 80 produces zero downstream action. Do NOT pad to 70-79
as a "soft signal" — that wastes your output. Just emit HOLD. 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 EXAMPLES — calibrate against these
═══════════════════════════════════════════════════════════════════════ ═══════════════════════════════════════════════════════════════════════
@@ -124,21 +185,43 @@ EXAMPLES — calibrate against these
POST: "Today I am announcing a Strategic Crypto Reserve including Bitcoin, POST: "Today I am announcing a Strategic Crypto Reserve including Bitcoin,
Solana, XRP, Ethereum. Effective immediately by Executive Order." Solana, XRP, Ethereum. Effective immediately by Executive Order."
ANSWER: signal=buy, conf=95 ANSWER: signal=buy, conf=95, category=direct_named, target_asset=SOL,
WHY: (a) DIRECT_CRYPTO. Specific entities, immediate execution, concrete expected_move=4
EO. All 7 checklist items pass strongly. This is the textbook hit. 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. POST: "Effective midnight Eastern time, 25% tariffs on all Chinese imports.
Signed today. No exceptions." Signed today. No exceptions."
ANSWER: signal=short, conf=88 ANSWER: signal=short, conf=88, category=macro_risk_off, target_asset=SOL,
WHY: (b) USD_RATES. Concrete order, immediate execution, named country and expected_move=3
percentage. Strong USD bid + risk-off → BTC down. 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 POST: "Just spoke with Israel and Iran. Ceasefire signed and effective in
6 hours. All hostilities to cease." 6 hours. All hostilities to cease."
ANSWER: signal=buy, conf=85 ANSWER: signal=buy, conf=85, category=macro_risk_on, target_asset=SOL,
WHY: (c) GEOPOLITICAL. Named parties, signed (not "discussing"), hard expected_move=2.5
deadline. Risk-on unwind → BTC up. 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] [NOT A CATALYST — must be HOLD]
@@ -190,13 +273,24 @@ OUTPUT FORMAT (strict JSON, no markdown)
"is_rt": <true|false>, "is_rt": <true|false>,
"surprise": "<'headline' | 'noise'>" "surprise": "<'headline' | 'noise'>"
}, },
"relevant": <true|false>, "relevant": <true|false>,
"asset": "BTC" | "ETH" | null, "asset": "BTC" | "ETH" | null,
"sentiment": "bullish" | "bearish" | "neutral", ← legacy field for price_impact tracking; set to BTC if
"signal": "buy" | "short" | "hold", target_asset is not BTC/ETH. Used only for backtest tables.
"confidence": <0-100>, "category": "direct_named" | "crypto_policy" | "macro_risk_on" |
"reasoning": "<EITHER the ≤15-word transmission chain (if buy/short), "macro_risk_off" | "defi_thematic" | "meme_named" | "none",
OR which checklist item failed (if hold). One sentence.>" "target_asset": "<HL ticker for the actual trade — e.g. BTC, SOL, TRUMP>"
| null ← null if signal is hold
"target_chain": "<chain native token if target_asset is a meme — e.g.
SOL for $WIF; ETH for $PEPE>" | 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": "<EITHER the ≤15-word transmission chain + asset
choice rationale (if buy/short), OR which checklist
item failed (if hold). One sentence.>"
} }
HARD CONSTRAINTS on the JSON: 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 • If transmission == "none" OR domestic == true OR is_rt == true OR
execution == "future" OR execution == "none", then signal MUST be execution == "future" OR execution == "none", then signal MUST be
"hold" and confidence MUST be 0. "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. • 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 = { _FALLBACK = {
"relevant": False, "relevant": False,
"asset": None, "asset": None,
"category": "none",
"target_asset": None,
"target_chain": None,
"expected_move_pct": 0.0,
"sentiment": "neutral", "sentiment": "neutral",
"signal": "hold", "signal": "hold",
"confidence": 0, "confidence": 0,
@@ -236,6 +336,35 @@ _FALLBACK = {
"analysis_version": ANALYSIS_VERSION, "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: def _fallback(prefilter: Optional[str] = None, reasoning: str = "") -> dict:
out = dict(_FALLBACK) 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") relevant = bool(result.get("relevant", False)) or signal in ("buy", "short")
asset = result.get("asset") # ── Asset routing — pick the actual perp to trade ─────────────
if asset == "BOTH": # `asset` (BTC/ETH legacy) is kept for backward-compat with the
asset = "BTC" # price_impact tracker. `target_asset` is the new field that the
if asset not in ("BTC", "ETH", None): # bot router should consume.
asset = None 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] reasoning = str(result.get("reasoning", ""))[:1200]
return { return {
"relevant": relevant, "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, "sentiment": sentiment,
"signal": signal, "signal": signal,
"confidence": confidence, "confidence": confidence,