From f94c9bc3c85ce1e95b3f816a3cdd9ce9f06c0f78 Mon Sep 17 00:00:00 2001 From: shinchan-zhai Date: Mon, 23 Mar 2026 19:51:03 +0800 Subject: [PATCH] fix: cancel orphaned stop/TP orders on position close + guard zero SL/TP prices Critical bugs fixed: - executeCloseLong/CloseShort: cancel stop-loss and take-profit orders BEFORE closing position to prevent orphaned orders from triggering and creating unintended positions - emergencyClosePosition: same fix for drawdown monitor's emergency close path - Skip SetStopLoss/SetTakeProfit when AI returns price=0 (prevents exchange API errors) Also includes: - Add agent.Stop() to graceful shutdown sequence in main.go - Remove unused variable suppression in Hyperliquid trader --- main.go | 4 +++ trader/auto_trader_orders.go | 42 ++++++++++++++++++++++------ trader/auto_trader_risk.go | 6 ++++ trader/hyperliquid/trader_account.go | 3 -- 4 files changed, 43 insertions(+), 12 deletions(-) diff --git a/main.go b/main.go index 610e99ed..e94ecfaf 100644 --- a/main.go +++ b/main.go @@ -192,6 +192,10 @@ func main() { } logger.Info("✅ HTTP server stopped") + // Stop NOFXi agent (sentinel, brain, scheduler, cleanup goroutines) + nofxiAgent.Stop() + logger.Info("✅ NOFXi agent stopped") + // Stop all traders traderManager.StopAll() logger.Info("✅ System shut down safely") diff --git a/trader/auto_trader_orders.go b/trader/auto_trader_orders.go index de1e6ae7..4328d8d4 100644 --- a/trader/auto_trader_orders.go +++ b/trader/auto_trader_orders.go @@ -134,12 +134,16 @@ func (at *AutoTrader) executeOpenLongWithRecord(decision *kernel.Decision, actio posKey := decision.Symbol + "_long" at.positionFirstSeenTime[posKey] = time.Now().UnixMilli() - // Set stop loss and take profit - if err := at.trader.SetStopLoss(decision.Symbol, "LONG", quantity, decision.StopLoss); err != nil { - logger.Infof(" ⚠ Failed to set stop loss: %v", err) + // Set stop loss and take profit (skip if AI didn't specify a price) + if decision.StopLoss > 0 { + if err := at.trader.SetStopLoss(decision.Symbol, "LONG", quantity, decision.StopLoss); err != nil { + logger.Infof(" ⚠ Failed to set stop loss: %v", err) + } } - if err := at.trader.SetTakeProfit(decision.Symbol, "LONG", quantity, decision.TakeProfit); err != nil { - logger.Infof(" ⚠ Failed to set take profit: %v", err) + if decision.TakeProfit > 0 { + if err := at.trader.SetTakeProfit(decision.Symbol, "LONG", quantity, decision.TakeProfit); err != nil { + logger.Infof(" ⚠ Failed to set take profit: %v", err) + } } return nil @@ -252,11 +256,15 @@ func (at *AutoTrader) executeOpenShortWithRecord(decision *kernel.Decision, acti at.positionFirstSeenTime[posKey] = time.Now().UnixMilli() // Set stop loss and take profit - if err := at.trader.SetStopLoss(decision.Symbol, "SHORT", quantity, decision.StopLoss); err != nil { - logger.Infof(" ⚠ Failed to set stop loss: %v", err) + if decision.StopLoss > 0 { + if err := at.trader.SetStopLoss(decision.Symbol, "SHORT", quantity, decision.StopLoss); err != nil { + logger.Infof(" ⚠ Failed to set stop loss: %v", err) + } } - if err := at.trader.SetTakeProfit(decision.Symbol, "SHORT", quantity, decision.TakeProfit); err != nil { - logger.Infof(" ⚠ Failed to set take profit: %v", err) + if decision.TakeProfit > 0 { + if err := at.trader.SetTakeProfit(decision.Symbol, "SHORT", quantity, decision.TakeProfit); err != nil { + logger.Infof(" ⚠ Failed to set take profit: %v", err) + } } return nil @@ -306,6 +314,14 @@ func (at *AutoTrader) executeCloseLongWithRecord(decision *kernel.Decision, acti logger.Infof(" 📊 Using exchange position data: qty=%.8f, entry=%.2f", quantity, entryPrice) } + // Cancel existing stop-loss and take-profit orders BEFORE closing position + // Critical: orphaned SL/TP orders could trigger after position is closed and create unintended positions + if err := at.trader.CancelStopOrders(decision.Symbol); err != nil { + logger.Warnf(" ⚠️ Failed to cancel stop orders for %s: %v (proceeding with close)", decision.Symbol, err) + } else { + logger.Infof(" 🗑️ Cancelled stop/TP orders for %s", decision.Symbol) + } + // Close position order, err := at.trader.CloseLong(decision.Symbol, 0) // 0 = close all if err != nil { @@ -368,6 +384,14 @@ func (at *AutoTrader) executeCloseShortWithRecord(decision *kernel.Decision, act logger.Infof(" 📊 Using exchange position data: qty=%.8f, entry=%.2f", quantity, entryPrice) } + // Cancel existing stop-loss and take-profit orders BEFORE closing position + // Critical: orphaned SL/TP orders could trigger after position is closed and create unintended positions + if err := at.trader.CancelStopOrders(decision.Symbol); err != nil { + logger.Warnf(" ⚠️ Failed to cancel stop orders for %s: %v (proceeding with close)", decision.Symbol, err) + } else { + logger.Infof(" 🗑️ Cancelled stop/TP orders for %s", decision.Symbol) + } + // Close position order, err := at.trader.CloseShort(decision.Symbol, 0) // 0 = close all if err != nil { diff --git a/trader/auto_trader_risk.go b/trader/auto_trader_risk.go index 890efebb..8ec65e6c 100644 --- a/trader/auto_trader_risk.go +++ b/trader/auto_trader_risk.go @@ -112,6 +112,12 @@ func (at *AutoTrader) checkPositionDrawdown() { // emergencyClosePosition emergency close position function func (at *AutoTrader) emergencyClosePosition(symbol, side string) error { + // Cancel existing stop-loss and take-profit orders BEFORE closing position + // Critical: orphaned SL/TP orders could trigger after position is closed and create unintended positions + if err := at.trader.CancelStopOrders(symbol); err != nil { + logger.Warnf(" ⚠️ Emergency close: failed to cancel stop orders for %s: %v", symbol, err) + } + switch side { case "long": order, err := at.trader.CloseLong(symbol, 0) // 0 = close all diff --git a/trader/hyperliquid/trader_account.go b/trader/hyperliquid/trader_account.go index 8b63b60a..7dee04d6 100644 --- a/trader/hyperliquid/trader_account.go +++ b/trader/hyperliquid/trader_account.go @@ -132,9 +132,6 @@ func (t *HyperliquidTrader) GetBalance() (map[string]interface{}, error) { spotUSDCBalance, availableBalance) } - // Suppress unused variable warning - _ = totalUnrealizedPnlAll - result["totalWalletBalance"] = totalWalletBalance // Total assets (Perp + Spot + xyz) - unrealized result["totalEquity"] = totalEquityCalculated // Total equity = Perp AV + Spot + xyz AV result["availableBalance"] = availableBalance // Available balance (Perp + Spot if unified)