fix: exit-guard thresholds were margin-basis, tightening with leverage

Live decomposition of the 2026-07-23 drawdown (equity 62.6 -> 31.1 in one
day, 43 fills, avg hold <1h despite a 4h min hold): both exit guards
compared thresholds meant as price moves against leverage-multiplied
margin PnL%, so raising leverage 5x -> 10x silently halved every trigger
distance:

- throttle bypass: -5% 'stop' unlocked AI closes at a -0.5% price wiggle
  (ETH -0.5%/44min, AAVE -0.9%/45min, MU -2.4%/14min all sailed through)
- drawdown monitor: 'profit > 5%' armed at +0.5% price, then market-closed
  winners on a 40% giveback (SNDK +0.6%/35min, NVDA +0.3%/37min) — every
  winner strangled at breakeven while losers realized fast

Fix: evaluate both guards on price-basis PnL (margin PnL% / leverage).
Throttle thresholds keep their documented price meaning at any leverage;
the drawdown monitor now arms only after a real +5% price move. Peak-PnL
cache stays margin-basis for prompt display consistency.

Replay simulator updated to the same price-basis semantics.
This commit is contained in:
tinkle-community
2026-07-24 13:39:40 +09:00
parent ed3bebf287
commit 5cd62e3c3a
5 changed files with 124 additions and 24 deletions

View File

@@ -1,8 +1,9 @@
"""Replay recorded AI decisions under alternative risk/throttle parameters.
Mirrors the live semantics of trader/auto_trader_throttle.go:
- throttle thresholds compare MARGIN-based PnL% (leverage-multiplied price move),
because auto_trader_loop.go computes UnrealizedPnLPct = pnl / margin
- throttle thresholds compare PRICE-basis PnL% (leverage-independent),
matching the 2026-07-24 fix that converted the live thresholds from
margin basis to price basis
- stop-loss / take-profit are exchange trigger orders -> intra-candle fills
- opens are gated by confidence, per-cycle/per-hour caps, re-entry cooldown,
max positions and available margin
@@ -38,9 +39,9 @@ class Params:
max_positions: int = 2
leverage: float = 10.0
ratio: float = 5.0 # per-position notional = ratio x equity
sl_bypass: float = -5.0 # margin-PnL% allowing early AI close
sl_bypass: float = -5.0 # price-PnL% allowing early AI close
tp_bypass: float = 12.0
noise_floor: float = -4.0 # margin-PnL% band blocking flat closes
noise_floor: float = -4.0 # price-PnL% band blocking flat closes
noise_ceiling: float = 6.0
sl_mult: float = 1.0 # scale AI stop distance from entry
tp_mult: float = 1.0
@@ -138,11 +139,11 @@ class Simulator:
i = bisect.bisect_right(times, ts) - 1
return self.candles[symbol][i][4] if i >= 0 else None
def margin_pnl_pct(self, pos, price, leverage):
def price_pnl_pct(self, pos, price):
move = (price - pos.entry) / pos.entry
if pos.side == "short":
move = -move
return move * leverage * 100.0
return move * 100.0
def run(self, p: Params, start=None, end=None):
equity = self.start_equity
@@ -223,7 +224,7 @@ class Simulator:
price = self.price_at(sym, cycle_id, ts) or pos.last_price
if not price:
continue
pnl_pct = self.margin_pnl_pct(pos, price, p.leverage)
pnl_pct = self.price_pnl_pct(pos, price)
held = ts - pos.entry_ts
if held >= min_hold:
allowed = (held >= noise_hold