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