fix: pre-launch hardening — HYPE price feed, KOL wallet cleanup, Telegram Trump alert, rate limiting, brittle test

Batch of the pre-launch audit campaign (BUG-01…14 plus three new features):

Pricing / TP-SL protection
- Add app/services/hl_price_feed.py: supplemental HL allMids poller for
  HL-native assets (HYPE, PURR) not listed on Binance. Pumps price_store +
  tp_sl_monitor.on_price_tick so bot trades on these assets keep full
  stop-loss / take-profit / trailing protection instead of max-hold only.
- Wire feed into main.py lifespan (startup task + graceful shutdown cancel).

Telegram
- Add format_trump_mention + PATH B in _dispatch: crypto-relevant Trump
  posts with no directional signal (relevant=True, signal=hold) now alert
  the public channel only (no per-subscriber noise).
- Rate limiter (slowapi) on the API; assorted bot/digest fixes.

KOL on-chain
- seed_kol_wallets.py: KOL_FEEDS coverage cross-check; reversibly deactivate
  orphaned wallets (handle not in KOL_FEEDS → can never produce divergence)
  so the scanner stops burning cycles on them.

Tests / misc
- Fix brittle test_macro_ahr999_uses_same_formula_as_scanner: mock now uses
  realistic ms timestamps so the in-progress-day drop fires, matching the
  fetcher's bar count (was 0.3179 vs 0.3178 off-by-one).
- Refresh stale notify_signal comment in truth_social.py.

Frontend reduce-action type fix lives in the sibling repo.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
k
2026-05-29 11:57:19 +08:00
parent 6471e44aac
commit d6c802ef26
40 changed files with 1833 additions and 209 deletions
+38 -7
View File
@@ -12,6 +12,7 @@ Both are read from config (hl_account_address, hl_api_private_key).
import asyncio
import logging
import time
from functools import partial
from typing import Optional
@@ -28,6 +29,17 @@ HL_TESTNET_URL = "https://api.hyperliquid-testnet.xyz"
# Fetched dynamically via meta() but cached here as fallback
SZ_DECIMALS_FALLBACK = {"BTC": 5, "ETH": 4}
# Module-level cache: coin → (szDecimals, expiry_timestamp)
_SZ_DECIMALS_CACHE: dict[str, tuple[int, float]] = {}
_SZ_DECIMALS_TTL = 300 # 5 minutes
# Max-leverage cache: coin → (maxLeverage, expiry_timestamp).
# meta() is a full-universe call (~1 KB JSON), cheap but wasteful on every
# trade open when the leverage caps change at most once a month. 5-min TTL
# matches _SZ_DECIMALS_TTL so a single meta() response can refresh both.
_MAX_LEV_CACHE: dict[str, tuple[int, float]] = {}
_MAX_LEV_TTL = 300 # 5 minutes
class HyperliquidTrader:
def __init__(
@@ -65,11 +77,18 @@ class HyperliquidTrader:
return await loop.run_in_executor(None, partial(fn, *args, **kwargs))
async def _get_sz_decimals(self, coin: str) -> int:
now = time.monotonic()
cached = _SZ_DECIMALS_CACHE.get(coin)
if cached is not None and now < cached[1]:
return cached[0]
try:
meta = await self._run(self._info.meta)
for u in meta.get("universe", []):
if u.get("name") == coin:
return int(u.get("szDecimals", SZ_DECIMALS_FALLBACK.get(coin, 4)))
name = u.get("name")
val = int(u.get("szDecimals", SZ_DECIMALS_FALLBACK.get(name, 4)))
_SZ_DECIMALS_CACHE[name] = (val, now + _SZ_DECIMALS_TTL)
if coin in _SZ_DECIMALS_CACHE:
return _SZ_DECIMALS_CACHE[coin][0]
except Exception:
pass
return SZ_DECIMALS_FALLBACK.get(coin, 4)
@@ -77,14 +96,26 @@ class HyperliquidTrader:
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)."""
`maxLeverage`. Returns a conservative 3 if lookup fails (memes default).
Results are cached for _MAX_LEV_TTL seconds (same TTL as szDecimals).
meta() returns the full universe in one call so we populate all coins
on a cache miss to amortise the cost across concurrent opens.
"""
import time as _time
now = _time.time()
cached = _MAX_LEV_CACHE.get(coin)
if cached is not None and cached[1] > now:
return cached[0]
try:
meta = await self._run(self._info.meta)
expiry = now + _MAX_LEV_TTL
for u in meta.get("universe", []):
if u.get("name") == coin:
ml = int(u.get("maxLeverage", 3))
return max(1, ml)
name = u.get("name")
if name:
_MAX_LEV_CACHE[name] = (max(1, int(u.get("maxLeverage", 3))), expiry)
if coin in _MAX_LEV_CACHE:
return _MAX_LEV_CACHE[coin][0]
except Exception as exc:
logger.warning("_get_max_leverage failed for %s: %s", coin, exc)
return 3 # safe fallback for unknown / illiquid coin