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
+31 -2
View File
@@ -31,9 +31,10 @@ from __future__ import annotations
import asyncio
import logging
from collections import OrderedDict
from dataclasses import dataclass
from datetime import datetime, timezone
from typing import Dict, Optional
from typing import Optional
from sqlalchemy import select, update
from sqlalchemy.ext.asyncio import AsyncSession
@@ -66,14 +67,28 @@ ADOPTED_CATEGORY = "btc_bottom_reversal_long"
# (each row would race to reduce the position). Process-local; with the
# atomic duplicate check inside the lock, multi-process deploys are still
# safe (losers would just see "already_adopted" on the second attempt).
_adopt_locks: Dict[str, asyncio.Lock] = {}
#
# Capacity-capped at _ADOPT_LOCK_MAX (matches _WALLET_LOCK_MAX in bot_engine)
# to prevent unbounded growth if many unique wallets pass through. LRU
# eviction: evicting an in-flight lock is safe — the holder already holds a
# reference; new adopt for that wallet just gets a fresh lock.
_ADOPT_LOCK_MAX = 512
_adopt_locks: OrderedDict[str, asyncio.Lock] = OrderedDict()
def _wallet_adopt_lock(wallet: str) -> asyncio.Lock:
lock = _adopt_locks.get(wallet)
if lock is None:
if len(_adopt_locks) >= _ADOPT_LOCK_MAX:
# Evict LRU entry. The evicted lock object stays alive as long as
# any coroutine holds a reference to it — eviction only means it
# won't be reused, not that it's freed mid-acquire.
_adopt_locks.popitem(last=False)
lock = asyncio.Lock()
_adopt_locks[wallet] = lock
else:
# Promote to MRU position.
_adopt_locks.move_to_end(wallet)
return lock
@@ -311,6 +326,20 @@ async def _adopt_locked(wallet_l: str, asset_u: str, mode_n: str) -> AdoptionRes
lev_info = target.get("leverage") or {}
leverage = int(lev_info.get("value") or sub.leverage or 2)
# BUG-09 guard: sys2_protective_stop_pct clamps leverage to SYS2_MAX_LEVERAGE
# when computing the stop distance. If the actual position is above that cap
# the computed stop is WIDER than the real liquidation band — the user could
# get HL-liquidated before our stop fires. Reject up front with a clear
# error so the user knows to reduce leverage on HL before adopting.
if leverage > SYS2_MAX_LEVERAGE:
raise AdoptionError(
"leverage_too_high",
f"{asset_u} position is at {leverage}× leverage. "
f"The bot's System-2 strategy supports up to {SYS2_MAX_LEVERAGE}×. "
f"Reduce leverage on Hyperliquid to ≤{SYS2_MAX_LEVERAGE}× first, "
"then try /adopt again.",
)
# Build the System-2 exit profile against the ACTUAL HL leverage so the
# protective stop is always inside the real liquidation line. Identical
# math to bot_engine's old sys2 open path; reused so behaviour stays