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