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:
+208
-22
@@ -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,
|
||||||
|
|||||||
Reference in New Issue
Block a user