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
This commit is contained in:
shinchan-zhai
2026-03-23 19:51:03 +08:00
parent 812972c5e8
commit f94c9bc3c8
4 changed files with 43 additions and 12 deletions

View File

@@ -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")

View File

@@ -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 {

View File

@@ -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

View File

@@ -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)