fix: complete v5 routing — API exposure, rescore persistence, leverage cap, key backup doc
Three plumbing fixes + one ops doc that close the gaps from the audit. scripts/rescore_v5.py Was overwriting only signal/conf/reasoning/sentiment/relevant/ prefilter_reason/analysis_version. Now also persists target_asset, category, expected_move_pct — without these the bot can't route rescored posts correctly (would silently fall back to BTC). app/schemas.py + app/api/posts.py TrumpPost response model didn't expose target_asset/category/ expected_move_pct, so the frontend had no way to display "this signal will trade SOL". Added the three fields + mapping in _post_to_schema(). Pre-v5 posts return null. No frontend changes yet — display work is a follow-up. app/services/hyperliquid.py HL caps max leverage per asset (BTC/ETH 50×, SOL 20×, memes 3-5×). set_leverage() always tried to push self._leverage — if user set 30× and bot routed to TRUMP, HL rejected the order and the trade silently dropped. Added _get_max_leverage() (queries meta()'s maxLeverage field) and _clip_leverage() that caps to HL's max. set_leverage now returns the effective leverage so callers can use it for notional sizing if needed. deploy/ENCRYPTION_KEY_BACKUP.md Documented mandatory backup procedure for the symmetric key that encrypts every user's HL API key. Lost key = all users' bots dead with no recovery. Includes rotation procedure + quarterly test step + things-not-to-do list. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -57,6 +57,10 @@ def _post_to_schema(post: Post) -> TrumpPost:
|
||||
analysis_version=post.analysis_version,
|
||||
relevant=post.relevant,
|
||||
price_impact=price_impact,
|
||||
# v5 routing fields — null for pre-v5 posts
|
||||
target_asset=post.target_asset,
|
||||
category=post.category,
|
||||
expected_move_pct=post.expected_move_pct,
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -30,6 +30,13 @@ class TrumpPost(BaseModel):
|
||||
analysis_version: Optional[str] = None
|
||||
relevant: bool
|
||||
price_impact: Optional[PriceImpact] = None
|
||||
# v5 asset routing — what the bot will actually trade if signal != hold.
|
||||
# target_asset = any HL perp ticker (BTC/ETH/SOL/TRUMP/...). category
|
||||
# buckets the post's catalyst type. expected_move_pct = AI's own 1h-move
|
||||
# estimate on target_asset. All three are null on pre-v5 posts.
|
||||
target_asset: Optional[str] = None
|
||||
category: Optional[str] = None
|
||||
expected_move_pct: Optional[float] = None
|
||||
|
||||
model_config = {"from_attributes": True}
|
||||
|
||||
|
||||
@@ -74,6 +74,32 @@ class HyperliquidTrader:
|
||||
pass
|
||||
return SZ_DECIMALS_FALLBACK.get(coin, 4)
|
||||
|
||||
async def _get_max_leverage(self, coin: str) -> int:
|
||||
"""Hyperliquid caps max leverage per asset (BTC/ETH 50×, SOL 20×,
|
||||
memes typically 3-5×). Querying meta() returns each asset's
|
||||
`maxLeverage`. We cache nothing — meta() is fast and HL can change
|
||||
tiers. Returns a conservative 3 if lookup fails (memes default)."""
|
||||
try:
|
||||
meta = await self._run(self._info.meta)
|
||||
for u in meta.get("universe", []):
|
||||
if u.get("name") == coin:
|
||||
ml = int(u.get("maxLeverage", 3))
|
||||
return max(1, ml)
|
||||
except Exception as exc:
|
||||
logger.warning("_get_max_leverage failed for %s: %s", coin, exc)
|
||||
return 3 # safe fallback for unknown / illiquid coin
|
||||
|
||||
async def _clip_leverage(self, coin: str, requested: int) -> int:
|
||||
"""Cap user's requested leverage at the asset's HL max. Without
|
||||
this, opening a 30× position on a meme (which caps at 3×) gets
|
||||
rejected by HL and the trade is silently dropped."""
|
||||
max_lev = await self._get_max_leverage(coin)
|
||||
if requested > max_lev:
|
||||
logger.info("Clipped leverage for %s: %d× → %d× (HL max)",
|
||||
coin, requested, max_lev)
|
||||
return max_lev
|
||||
return requested
|
||||
|
||||
async def _mid_price(self, coin: str) -> float:
|
||||
mids = await self._run(self._info.all_mids)
|
||||
price = float(mids.get(coin, 0))
|
||||
@@ -113,18 +139,23 @@ class HyperliquidTrader:
|
||||
logger.error("get_open_positions error: %s", exc)
|
||||
raise
|
||||
|
||||
async def set_leverage(self, coin: str) -> None:
|
||||
"""Set isolated leverage for the given coin."""
|
||||
async def set_leverage(self, coin: str) -> int:
|
||||
"""Set isolated leverage for the given coin, clipped to the asset's
|
||||
HL maximum. Returns the actual leverage used (may be less than
|
||||
self._leverage if asset capped). Caller should prefer this return
|
||||
value when computing notional / position size."""
|
||||
effective = await self._clip_leverage(coin, self._leverage)
|
||||
try:
|
||||
await self._run(
|
||||
self._exchange.update_leverage,
|
||||
self._leverage,
|
||||
effective,
|
||||
coin,
|
||||
False, # is_cross=False → isolated margin
|
||||
)
|
||||
logger.info("Set leverage %dx for %s (isolated)", self._leverage, coin)
|
||||
logger.info("Set leverage %dx for %s (isolated)", effective, coin)
|
||||
except Exception as exc:
|
||||
logger.warning("set_leverage error (non-fatal): %s", exc)
|
||||
return effective
|
||||
|
||||
async def open_position(
|
||||
self,
|
||||
|
||||
Reference in New Issue
Block a user