Files
nofx/trader/auto_trader_risk_test.go
tinkle-community 5cd62e3c3a 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.
2026-07-24 13:39:40 +09:00

28 lines
920 B
Go

package trader
import "testing"
func TestDrawdownCloseArmsOnPriceBasisOnly(t *testing.T) {
cases := []struct {
name string
pricePnLPct float64
drawdownPct float64
shouldClose bool
}{
// +0.5% price move (what +5% margin at 10x used to arm on) must NOT
// arm the monitor, no matter how large the relative drawdown is.
{"tiny price gain big drawdown", 0.5, 60.0, false},
// Armed only from a real +5% price move, and still needs the 40% giveback.
{"real gain small drawdown", 6.0, 20.0, false},
{"real gain big drawdown", 6.0, 45.0, true},
{"at threshold not armed", 5.0, 45.0, false},
{"loss never triggers", -3.0, 80.0, false},
}
for _, c := range cases {
if got := shouldDrawdownClose(c.pricePnLPct, c.drawdownPct); got != c.shouldClose {
t.Fatalf("%s: shouldDrawdownClose(%.1f, %.1f) = %v, want %v",
c.name, c.pricePnLPct, c.drawdownPct, got, c.shouldClose)
}
}
}