From 60b97399e4c9a93abf6797cec10e6e1f5978beac Mon Sep 17 00:00:00 2001 From: shinchan-zhai Date: Mon, 23 Mar 2026 12:29:21 +0800 Subject: [PATCH] fix: consistent safe type helpers in auto_trader, log emergency exit errors - Replace all raw pos["key"].(type) assertions with posFloat64/posString/posInt64 helpers across auto_trader_decision, auto_trader_grid, auto_trader_grid_orders, auto_trader_grid_regime, auto_trader_loop, auto_trader_orders, auto_trader_risk - Add posInt64 helper for int64 extraction (createdTime etc) - Fix emergencyExit: log CloseLong/CloseShort errors instead of silently dropping - Fix emergencyExit: log GetPositions error on failure - Upgrade closeAllPositions log level from Infof to Warnf for close failures - Zero raw type assertions remaining in auto_trader_* files --- trader/auto_trader_decision.go | 4 ++-- trader/auto_trader_grid.go | 33 ++++++++++++++++++++----------- trader/auto_trader_grid_orders.go | 26 +++++++++++------------- trader/auto_trader_grid_regime.go | 6 +++--- trader/auto_trader_loop.go | 4 ++-- trader/auto_trader_orders.go | 20 ++++++++----------- trader/auto_trader_risk.go | 12 +++++------ trader/helpers.go | 24 ++++++++++++++++++++++ 8 files changed, 78 insertions(+), 51 deletions(-) diff --git a/trader/auto_trader_decision.go b/trader/auto_trader_decision.go index c4cf837f..7a485850 100644 --- a/trader/auto_trader_decision.go +++ b/trader/auto_trader_decision.go @@ -142,7 +142,7 @@ func (at *AutoTrader) GetAccountInfo() (map[string]interface{}, error) { totalUnrealizedPnLCalculated += unrealizedPnl leverage := 10 - if lev, ok := pos["leverage"].(float64); ok { + if lev := posFloat64(pos, "leverage"); lev > 0 { leverage = int(lev) } marginUsed := (quantity * markPrice) / float64(leverage) @@ -211,7 +211,7 @@ func (at *AutoTrader) GetPositions() ([]map[string]interface{}, error) { liquidationPrice := posFloat64(pos, "liquidationPrice") leverage := 10 - if lev, ok := pos["leverage"].(float64); ok { + if lev := posFloat64(pos, "leverage"); lev > 0 { leverage = int(lev) } diff --git a/trader/auto_trader_grid.go b/trader/auto_trader_grid.go index 6151dbbb..8d96829a 100644 --- a/trader/auto_trader_grid.go +++ b/trader/auto_trader_grid.go @@ -231,16 +231,26 @@ func (at *AutoTrader) emergencyExit(reason string) error { 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) - } - } + sym := posString(pos, "symbol") + if sym != gridConfig.Symbol { + continue + } + size := posFloat64(pos, "positionAmt") + if size == 0 { + continue + } + var closeErr error + if size > 0 { + _, closeErr = at.trader.CloseLong(gridConfig.Symbol, size) + } else { + _, closeErr = at.trader.CloseShort(gridConfig.Symbol, -size) + } + if closeErr != nil { + logger.Errorf("[Grid] EMERGENCY: failed to close position (size=%.6f): %v", size, closeErr) } } + } else { + logger.Errorf("[Grid] EMERGENCY: failed to get positions: %v", err) } // Pause grid @@ -504,10 +514,9 @@ func (at *AutoTrader) buildGridContext() (*kernel.GridContext, error) { 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 { - ctx.CurrentPosition = size - } + if posString(pos, "symbol") == gridConfig.Symbol { + ctx.CurrentPosition = posFloat64(pos, "positionAmt") + break } } } diff --git a/trader/auto_trader_grid_orders.go b/trader/auto_trader_grid_orders.go index c19dc6d0..78a400a6 100644 --- a/trader/auto_trader_grid_orders.go +++ b/trader/auto_trader_grid_orders.go @@ -26,13 +26,12 @@ func (at *AutoTrader) checkTotalPositionLimit(symbol string, additionalValue flo positions, err := at.trader.GetPositions() if err == nil { for _, pos := range positions { - if sym, ok := pos["symbol"].(string); ok && sym == symbol { - if size, ok := pos["positionAmt"].(float64); ok { - if price, ok := pos["markPrice"].(float64); ok { - currentPositionValue = math.Abs(size) * price - } else if entryPrice, ok := pos["entryPrice"].(float64); ok { - currentPositionValue = math.Abs(size) * entryPrice - } + if posString(pos, "symbol") == symbol { + size := posFloat64(pos, "positionAmt") + if price := posFloat64(pos, "markPrice"); price > 0 { + currentPositionValue = math.Abs(size) * price + } else if entryPrice := posFloat64(pos, "entryPrice"); entryPrice > 0 { + currentPositionValue = math.Abs(size) * entryPrice } } } @@ -267,10 +266,9 @@ func (at *AutoTrader) syncGridState() { logger.Warnf("[Grid] Failed to get positions for state sync: %v", err) } else { for _, pos := range positions { - if sym, ok := pos["symbol"].(string); ok && sym == gridConfig.Symbol { - if size, ok := pos["positionAmt"].(float64); ok { - currentPositionSize = size - } + if posString(pos, "symbol") == gridConfig.Symbol { + currentPositionSize = posFloat64(pos, "positionAmt") + break } } } @@ -333,12 +331,12 @@ func (at *AutoTrader) closeAllPositions() error { } for _, pos := range positions { - symbol, _ := pos["symbol"].(string) + symbol := posString(pos, "symbol") if symbol != gridConfig.Symbol { continue } - size, _ := pos["positionAmt"].(float64) + size := posFloat64(pos, "positionAmt") if size == 0 { continue } @@ -349,7 +347,7 @@ func (at *AutoTrader) closeAllPositions() error { _, err = at.trader.CloseShort(symbol, -size) } if err != nil { - logger.Infof("Failed to close position: %v", err) + logger.Warnf("[Grid] Failed to close position (size=%.6f): %v", size, err) } } diff --git a/trader/auto_trader_grid_regime.go b/trader/auto_trader_grid_regime.go index 0999ff1f..ae255f5e 100644 --- a/trader/auto_trader_grid_regime.go +++ b/trader/auto_trader_grid_regime.go @@ -245,9 +245,9 @@ func (at *AutoTrader) GetGridRiskInfo() *GridRiskInfo { var currentPositionValue float64 var currentPositionSize float64 for _, pos := range positions { - if sym, _ := pos["symbol"].(string); sym == gridConfig.Symbol { - size, _ := pos["positionAmt"].(float64) - entry, _ := pos["entryPrice"].(float64) + if posString(pos, "symbol") == gridConfig.Symbol { + size := posFloat64(pos, "positionAmt") + entry := posFloat64(pos, "entryPrice") currentPositionValue = math.Abs(size * entry) currentPositionSize = size break diff --git a/trader/auto_trader_loop.go b/trader/auto_trader_loop.go index cbf96435..2190ffc4 100644 --- a/trader/auto_trader_loop.go +++ b/trader/auto_trader_loop.go @@ -350,7 +350,7 @@ func (at *AutoTrader) buildTradingContext() (*kernel.Context, error) { // Calculate margin used (estimated) leverage := 10 // Default value, should actually be fetched from position info - if lev, ok := pos["leverage"].(float64); ok { + if lev := posFloat64(pos, "leverage"); lev > 0 { leverage = int(lev) } marginUsed := (quantity * markPrice) / float64(leverage) @@ -374,7 +374,7 @@ func (at *AutoTrader) buildTradingContext() (*kernel.Context, error) { } // Priority 2: Get from exchange API (Bybit: createdTime, OKX: createdTime) if updateTime == 0 { - if createdTime, ok := pos["createdTime"].(int64); ok && createdTime > 0 { + if createdTime := posInt64(pos, "createdTime"); createdTime > 0 { updateTime = createdTime } } diff --git a/trader/auto_trader_orders.go b/trader/auto_trader_orders.go index edfc1995..de1e6ae7 100644 --- a/trader/auto_trader_orders.go +++ b/trader/auto_trader_orders.go @@ -45,7 +45,7 @@ func (at *AutoTrader) executeOpenLongWithRecord(decision *kernel.Decision, actio // Check if there's already a position in the same symbol and direction for _, pos := range positions { - if pos["symbol"] == decision.Symbol && pos["side"] == "long" { + if posString(pos, "symbol") == decision.Symbol && posString(pos, "side") == "long" { return fmt.Errorf("❌ %s already has long position, close it first", decision.Symbol) } } @@ -162,7 +162,7 @@ func (at *AutoTrader) executeOpenShortWithRecord(decision *kernel.Decision, acti // Check if there's already a position in the same symbol and direction for _, pos := range positions { - if pos["symbol"] == decision.Symbol && pos["side"] == "short" { + if posString(pos, "symbol") == decision.Symbol && posString(pos, "side") == "short" { return fmt.Errorf("❌ %s already has short position, close it first", decision.Symbol) } } @@ -294,11 +294,9 @@ func (at *AutoTrader) executeCloseLongWithRecord(decision *kernel.Decision, acti positions, err := at.trader.GetPositions() if err == nil { for _, pos := range positions { - if pos["symbol"] == decision.Symbol && pos["side"] == "long" { - if ep, ok := pos["entryPrice"].(float64); ok { - entryPrice = ep - } - if amt, ok := pos["positionAmt"].(float64); ok && amt > 0 { + if posString(pos, "symbol") == decision.Symbol && posString(pos, "side") == "long" { + entryPrice = posFloat64(pos, "entryPrice") + if amt := posFloat64(pos, "positionAmt"); amt > 0 { quantity = amt } break @@ -358,11 +356,9 @@ func (at *AutoTrader) executeCloseShortWithRecord(decision *kernel.Decision, act positions, err := at.trader.GetPositions() if err == nil { for _, pos := range positions { - if pos["symbol"] == decision.Symbol && pos["side"] == "short" { - if ep, ok := pos["entryPrice"].(float64); ok { - entryPrice = ep - } - if amt, ok := pos["positionAmt"].(float64); ok { + if posString(pos, "symbol") == decision.Symbol && posString(pos, "side") == "short" { + entryPrice = posFloat64(pos, "entryPrice") + if amt := posFloat64(pos, "positionAmt"); amt != 0 { quantity = -amt // positionAmt is negative for short } break diff --git a/trader/auto_trader_risk.go b/trader/auto_trader_risk.go index 1ed575c4..890efebb 100644 --- a/trader/auto_trader_risk.go +++ b/trader/auto_trader_risk.go @@ -41,11 +41,11 @@ func (at *AutoTrader) checkPositionDrawdown() { } for _, pos := range positions { - symbol, _ := pos["symbol"].(string) - side, _ := pos["side"].(string) - entryPrice, _ := pos["entryPrice"].(float64) - markPrice, _ := pos["markPrice"].(float64) - quantity, _ := pos["positionAmt"].(float64) + symbol := posString(pos, "symbol") + side := posString(pos, "side") + entryPrice := posFloat64(pos, "entryPrice") + markPrice := posFloat64(pos, "markPrice") + quantity := posFloat64(pos, "positionAmt") if symbol == "" || side == "" || entryPrice == 0 { continue // skip malformed position data } @@ -55,7 +55,7 @@ func (at *AutoTrader) checkPositionDrawdown() { // Calculate current P&L percentage leverage := 10 // Default value - if lev, ok := pos["leverage"].(float64); ok { + if lev := posFloat64(pos, "leverage"); lev > 0 { leverage = int(lev) } diff --git a/trader/helpers.go b/trader/helpers.go index 6819ae32..7a204055 100644 --- a/trader/helpers.go +++ b/trader/helpers.go @@ -18,6 +18,30 @@ func posString(pos map[string]interface{}, key string) string { return v } +// posInt64 extracts an int64 from a position map, returning 0 on failure. +func posInt64(pos map[string]interface{}, key string) int64 { + value, ok := pos[key] + if !ok { + return 0 + } + switch v := value.(type) { + case int64: + return v + case int: + return int64(v) + case float64: + return int64(v) + case string: + parsed, err := strconv.ParseInt(v, 10, 64) + if err != nil { + return 0 + } + return parsed + default: + return 0 + } +} + // SafeFloat64 Safely extract float64 value from map func SafeFloat64(data map[string]interface{}, key string) (float64, error) { value, ok := data[key]