fix(trading): close five risk gaps surfaced by pre-launch audit

1. hyperliquid.open_position now returns effective_leverage. HL clips
   the requested leverage to each asset's max (e.g. memes capped at 3×)
   and silently applied the lower value; we were discarding that. For
   System-2 this meant sys2_protective_stop_pct was computed against the
   REQUESTED leverage, so the "inside-liquidation" full-close rung was
   actually well inside an 8.5% stop while the real liquidation sat ~33%
   away — strategy intent broken. bot_engine now reads the effective
   value back, recomputes the protective stop + derisk ladder against
   it, and freezes the correct values into BotTrade.eff_* / .leverage.

2. circuit_breaker.clear_trip now clears BOTH systems by default. The
   sys2 column was never reachable from any reset path — a sys2 trip
   forced users to wait the full 24h lockout even after explicit
   human re-arm.

3. /api/user/.../manual-window and /auto-trade re-arm endpoints now
   clear sys1 AND sys2 CB on the same human ack. Symmetry with
   check_and_trip / is_tripped which already supported both systems.

4. telegram.py deep-link map: btc_bottom_reversal + funding_reversal
   alerts now point at /en/macro instead of the now-404 /en/btc.

5. (Already landed: _sys2_mode UnboundLocalError fix in bot_engine —
   restated here for the audit trail.)

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
k
2026-05-26 01:05:06 +08:00
parent 4442e97f28
commit fc735f251a
5 changed files with 113 additions and 34 deletions
+28 -11
View File
@@ -182,20 +182,37 @@ def is_tripped(sub_dict: dict, system: str = "sys1") -> tuple[bool, str]:
# ─── Manual reset (called from /manual-window endpoint) ─────────────────────
async def clear_trip(wallet: str, db: AsyncSession) -> bool:
"""Clear the trip state. Returns True if a trip was actually cleared.
async def clear_trip(
wallet: str, db: AsyncSession, system: Optional[str] = None,
) -> bool:
"""Clear circuit-breaker trip state. Returns True if anything cleared.
Called when the user explicitly re-arms manual_window — that's the
human-in-the-loop unblock. Just letting LOCKOUT_HOURS expire works too,
but most users will hit the button.
`system` selects which breaker to clear:
None → clear BOTH sys1 and sys2 (default for human-ack endpoints)
"sys1"/"sys2" → clear that one only
Critical: there are TWO independent breakers (Trump book / reversal book).
The earlier implementation only cleared sys1; a sys2 trip therefore had
no manual reset path and forced users to wait 24h. When the human
re-arms via manual-window / auto-trade, they're acknowledging risk
across the wallet — clear both.
"""
sub = (await db.execute(
select(Subscription).where(Subscription.wallet_address == wallet)
)).scalar_one_or_none()
if sub is None or sub.circuit_breaker_tripped_at is None:
if sub is None:
return False
sub.circuit_breaker_tripped_at = None
sub.circuit_breaker_reason = None
await db.commit()
logger.info("CB cleared for %s", wallet)
return True
targets = (system,) if system in ("sys1", "sys2") else ("sys1", "sys2")
cleared_any = False
for s in targets:
col_at, col_reason = _CB_COLS[s]
if getattr(sub, col_at) is not None:
setattr(sub, col_at, None)
setattr(sub, col_reason, None)
cleared_any = True
logger.info("CB[%s] cleared for %s", s, wallet)
if cleared_any:
await db.commit()
return cleared_any