mirror of
https://github.com/NoFxAiOS/nofx.git
synced 2026-07-28 14:32:45 +08:00
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:
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user