From 1454ad311296141f2ef1adeef37f837724304a44 Mon Sep 17 00:00:00 2001 From: tinkle-community Date: Wed, 14 Jan 2026 12:34:45 +0800 Subject: [PATCH] feat(grid): enforce max drawdown limit with emergency exit CRITICAL: Add drawdown protection - New checkMaxDrawdown() function tracks peak equity - emergencyExit() closes all positions and cancels orders - Auto-pause grid when MaxDrawdownPct exceeded - Protect capital from excessive losses --- trader/auto_trader_grid.go | 93 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 93 insertions(+) diff --git a/trader/auto_trader_grid.go b/trader/auto_trader_grid.go index 93243d5b..7a9ba8f7 100644 --- a/trader/auto_trader_grid.go +++ b/trader/auto_trader_grid.go @@ -104,6 +104,93 @@ func (at *AutoTrader) checkBreakout() (BreakoutType, float64) { return BreakoutNone, 0 } +// checkMaxDrawdown checks if current drawdown exceeds maximum allowed +// Returns: (exceeded bool, currentDrawdown float64) +func (at *AutoTrader) checkMaxDrawdown() (bool, float64) { + gridConfig := at.config.StrategyConfig.GridConfig + if gridConfig.MaxDrawdownPct <= 0 { + return false, 0 + } + + // Get current equity + balance, err := at.trader.GetBalance() + if err != nil { + return false, 0 + } + + currentEquity := 0.0 + if equity, ok := balance["total_equity"].(float64); ok { + currentEquity = equity + } else if total, ok := balance["totalWalletBalance"].(float64); ok { + if unrealized, ok := balance["totalUnrealizedProfit"].(float64); ok { + currentEquity = total + unrealized + } + } + + if currentEquity <= 0 { + return false, 0 + } + + // Update peak equity + at.gridState.mu.Lock() + if currentEquity > at.gridState.PeakEquity { + at.gridState.PeakEquity = currentEquity + } + peakEquity := at.gridState.PeakEquity + at.gridState.mu.Unlock() + + if peakEquity <= 0 { + return false, 0 + } + + // Calculate current drawdown + drawdown := (peakEquity - currentEquity) / peakEquity * 100 + + // Update max drawdown tracking + at.gridState.mu.Lock() + if drawdown > at.gridState.MaxDrawdown { + at.gridState.MaxDrawdown = drawdown + } + at.gridState.mu.Unlock() + + return drawdown >= gridConfig.MaxDrawdownPct, drawdown +} + +// emergencyExit closes all positions and cancels all orders +func (at *AutoTrader) emergencyExit(reason string) error { + gridConfig := at.config.StrategyConfig.GridConfig + + logger.Errorf("[Grid] EMERGENCY EXIT: %s", reason) + + // Cancel all orders + if err := at.cancelAllGridOrders(); err != nil { + logger.Errorf("[Grid] Failed to cancel orders in emergency: %v", err) + } + + // Close all positions + positions, err := at.trader.GetPositions() + if err == nil { + for _, pos := range positions { + if sym, ok := pos["symbol"].(string); ok && sym == gridConfig.Symbol { + if size, ok := pos["positionAmt"].(float64); ok && size != 0 { + if size > 0 { + at.trader.CloseLong(gridConfig.Symbol, size) + } else { + at.trader.CloseShort(gridConfig.Symbol, -size) + } + } + } + } + } + + // Pause grid + at.gridState.mu.Lock() + at.gridState.IsPaused = true + at.gridState.mu.Unlock() + + return nil +} + // handleBreakout handles price breakout from grid range func (at *AutoTrader) handleBreakout(breakoutType BreakoutType, breakoutPct float64) error { logger.Warnf("[Grid] BREAKOUT DETECTED: %s, %.2f%% beyond boundary", breakoutType, breakoutPct) @@ -283,6 +370,12 @@ func (at *AutoTrader) RunGridCycle() error { } } + // CRITICAL: Check max drawdown + exceeded, drawdown := at.checkMaxDrawdown() + if exceeded { + return at.emergencyExit(fmt.Sprintf("max drawdown exceeded: %.2f%%", drawdown)) + } + // Check if grid is paused at.gridState.mu.RLock() isPaused := at.gridState.IsPaused