From c9d5aed1b6a45b681a9faa1faaae96070a1e4b95 Mon Sep 17 00:00:00 2001 From: ZhouYongyou <128128010+zhouyongyou@users.noreply.github.com> Date: Tue, 4 Nov 2025 19:05:54 +0800 Subject: [PATCH] =?UTF-8?q?fix(trader):=20separate=20stop-loss=20and=20tak?= =?UTF-8?q?e-profit=20order=20cancellation=20to=20prevent=20accidental=20d?= =?UTF-8?q?eletions=20##=20Problem=20When=20adjusting=20stop-loss=20or=20t?= =?UTF-8?q?ake-profit=20levels,=20`CancelStopOrders()`=20deleted=20BOTH=20?= =?UTF-8?q?stop-loss=20AND=20take-profit=20orders=20simultaneously,=20caus?= =?UTF-8?q?ing:=20-=20**Adjusting=20stop-loss**=20=E2=86=92=20Take-profit?= =?UTF-8?q?=20order=20deleted=20=E2=86=92=20Position=20has=20no=20exit=20p?= =?UTF-8?q?lan=20=E2=9D=8C=20-=20**Adjusting=20take-profit**=20=E2=86=92?= =?UTF-8?q?=20Stop-loss=20order=20deleted=20=E2=86=92=20Position=20unprote?= =?UTF-8?q?cted=20=E2=9D=8C=20**Root=20cause:**=20```go=20CancelStopOrders?= =?UTF-8?q?(symbol)=20{=20=20=20//=20Cancelled=20ALL=20orders=20with=20typ?= =?UTF-8?q?e=20STOP=5FMARKET=20or=20TAKE=5FPROFIT=5FMARKET=20=20=20//=20No?= =?UTF-8?q?=20distinction=20between=20stop-loss=20and=20take-profit=20}=20?= =?UTF-8?q?```=20##=20Solution=20###=201.=20Added=20new=20interface=20meth?= =?UTF-8?q?ods=20(trader/interface.go)=20```go=20CancelStopLossOrders(symb?= =?UTF-8?q?ol=20string)=20error=20=20=20=20=20=20//=20Only=20cancel=20stop?= =?UTF-8?q?-loss=20orders=20CancelTakeProfitOrders(symbol=20string)=20erro?= =?UTF-8?q?r=20=20=20=20//=20Only=20cancel=20take-profit=20orders=20Cancel?= =?UTF-8?q?StopOrders(symbol=20string)=20error=20=20=20=20=20=20=20=20=20?= =?UTF-8?q?=20//=20Deprecated=20(cancels=20both)=20```=20###=202.=20Implem?= =?UTF-8?q?ented=20for=20all=203=20exchanges=20**Binance=20(trader/binance?= =?UTF-8?q?=5Ffutures.go)**:=20-=20`CancelStopLossOrders`:=20Filters=20`Or?= =?UTF-8?q?derTypeStopMarket=20|=20OrderTypeStop`=20-=20`CancelTakeProfitO?= =?UTF-8?q?rders`:=20Filters=20`OrderTypeTakeProfitMarket=20|=20OrderTypeT?= =?UTF-8?q?akeProfit`=20-=20Full=20order=20type=20differentiation=20?= =?UTF-8?q?=E2=9C=85=20**Hyperliquid=20(trader/hyperliquid=5Ftrader.go)**:?= =?UTF-8?q?=20-=20=E2=9A=A0=EF=B8=8F=20Limitation:=20SDK's=20OpenOrder=20s?= =?UTF-8?q?truct=20doesn't=20expose=20trigger=20field=20-=20Both=20methods?= =?UTF-8?q?=20call=20`CancelStopOrders`=20(cancels=20all=20pending=20order?= =?UTF-8?q?s)=20-=20Trade-off:=20Safe=20but=20less=20precise=20**Aster=20(?= =?UTF-8?q?trader/aster=5Ftrader.go)**:=20-=20`CancelStopLossOrders`:=20Fi?= =?UTF-8?q?lters=20`STOP=5FMARKET=20|=20STOP`=20-=20`CancelTakeProfitOrder?= =?UTF-8?q?s`:=20Filters=20`TAKE=5FPROFIT=5FMARKET=20|=20TAKE=5FPROFIT`=20?= =?UTF-8?q?-=20Full=20order=20type=20differentiation=20=E2=9C=85=20###=203?= =?UTF-8?q?.=20Usage=20in=20auto=5Ftrader.go=20When=20`update=5Fstop=5Flos?= =?UTF-8?q?s`=20or=20`update=5Ftake=5Fprofit`=20actions=20are=20implemente?= =?UTF-8?q?d,=20they=20will=20use:=20```go=20//=20update=5Fstop=5Floss:=20?= =?UTF-8?q?at.trader.CancelStopLossOrders(symbol)=20=20//=20Only=20cancel?= =?UTF-8?q?=20SL,=20keep=20TP=20at.trader.SetStopLoss(...)=20//=20update?= =?UTF-8?q?=5Ftake=5Fprofit:=20at.trader.CancelTakeProfitOrders(symbol)=20?= =?UTF-8?q?=20//=20Only=20cancel=20TP,=20keep=20SL=20at.trader.SetTakeProf?= =?UTF-8?q?it(...)=20```=20##=20Impact=20-=20=E2=9C=85=20Adjusting=20stop-?= =?UTF-8?q?loss=20no=20longer=20deletes=20take-profit=20-=20=E2=9C=85=20Ad?= =?UTF-8?q?justing=20take-profit=20no=20longer=20deletes=20stop-loss=20-?= =?UTF-8?q?=20=E2=9C=85=20Backward=20compatible:=20`CancelStopOrders`=20st?= =?UTF-8?q?ill=20exists=20(deprecated)=20-=20=E2=9A=A0=EF=B8=8F=20Hyperliq?= =?UTF-8?q?uid=20limitation:=20still=20cancels=20all=20orders=20(SDK=20con?= =?UTF-8?q?straint)=20##=20Testing=20-=20=E2=9C=85=20Compiles=20successful?= =?UTF-8?q?ly=20across=20all=203=20exchanges=20-=20=E2=9A=A0=EF=B8=8F=20Re?= =?UTF-8?q?quires=20live=20testing:=20=20=20-=20[=20]=20Binance:=20Adjust?= =?UTF-8?q?=20SL=20=E2=86=92=20verify=20TP=20remains=20=20=20-=20[=20]=20B?= =?UTF-8?q?inance:=20Adjust=20TP=20=E2=86=92=20verify=20SL=20remains=20=20?= =?UTF-8?q?=20-=20[=20]=20Hyperliquid:=20Verify=20behavior=20with=20limita?= =?UTF-8?q?tion=20=20=20-=20[=20]=20Aster:=20Verify=20order=20filtering=20?= =?UTF-8?q?works=20correctly=20##=20Code=20Changes=20```=20trader/interfac?= =?UTF-8?q?e.go:=20+9=20lines=20(new=20interface=20methods)=20trader/binan?= =?UTF-8?q?ce=5Ffutures.go:=20+133=20lines=20(3=20new=20functions)=20trade?= =?UTF-8?q?r/hyperliquid=5Ftrader.go:=20+56=20lines=20(3=20new=20functions?= =?UTF-8?q?)=20trader/aster=5Ftrader.go:=20+157=20lines=20(3=20new=20funct?= =?UTF-8?q?ions)=20Total:=20+355=20lines=20```?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- trader/aster_trader.go | 155 +++++++++++++++++++++++++++++++++++ trader/binance_futures.go | 131 +++++++++++++++++++++++++++++ trader/hyperliquid_trader.go | 50 +++++++++++ trader/interface.go | 10 +++ 4 files changed, 346 insertions(+) diff --git a/trader/aster_trader.go b/trader/aster_trader.go index d9ba82a6..f0cd9f9a 100644 --- a/trader/aster_trader.go +++ b/trader/aster_trader.go @@ -971,6 +971,161 @@ func (t *AsterTrader) SetTakeProfit(symbol string, positionSide string, quantity return err } +// CancelStopOrders 取消该币种的止盈/止损单(用于调整止盈止损位置) +func (t *AsterTrader) CancelStopOrders(symbol string) error { + // 获取该币种的所有未完成订单 + params := map[string]interface{}{ + "symbol": symbol, + } + + body, err := t.request("GET", "/fapi/v3/openOrders", params) + if err != nil { + return fmt.Errorf("获取未完成订单失败: %w", err) + } + + var orders []map[string]interface{} + if err := json.Unmarshal(body, &orders); err != nil { + return fmt.Errorf("解析订单数据失败: %w", err) + } + + // 过滤出止盈止损单并取消 + canceledCount := 0 + for _, order := range orders { + orderType, _ := order["type"].(string) + + // 只取消止损和止盈订单 + if orderType == "STOP_MARKET" || + orderType == "TAKE_PROFIT_MARKET" || + orderType == "STOP" || + orderType == "TAKE_PROFIT" { + + orderID, _ := order["orderId"].(float64) + cancelParams := map[string]interface{}{ + "symbol": symbol, + "orderId": int64(orderID), + } + + _, err := t.request("DELETE", "/fapi/v3/order", cancelParams) + if err != nil { + log.Printf(" ⚠ 取消订单 %d 失败: %v", int64(orderID), err) + continue + } + + canceledCount++ + log.Printf(" ✓ 已取消 %s 的止盈/止损单 (订单ID: %d, 类型: %s)", + symbol, int64(orderID), orderType) + } + } + + if canceledCount == 0 { + log.Printf(" ℹ %s 没有止盈/止损单需要取消", symbol) + } else { + log.Printf(" ✓ 已取消 %s 的 %d 个止盈/止损单", symbol, canceledCount) + } + + return nil +} + +// CancelStopLossOrders 仅取消止损单(不影响止盈单) +func (t *AsterTrader) CancelStopLossOrders(symbol string) error { + // 获取该币种的所有未完成订单 + params := map[string]interface{}{ + "symbol": symbol, + } + + body, err := t.request("GET", "/fapi/v3/openOrders", params) + if err != nil { + return fmt.Errorf("获取未完成订单失败: %w", err) + } + + var orders []map[string]interface{} + if err := json.Unmarshal(body, &orders); err != nil { + return fmt.Errorf("解析订单数据失败: %w", err) + } + + // 过滤出止损单并取消 + canceledCount := 0 + for _, order := range orders { + orderType, _ := order["type"].(string) + + // 只取消止损订单(不取消止盈订单) + if orderType == "STOP_MARKET" || orderType == "STOP" { + orderID, _ := order["orderId"].(float64) + cancelParams := map[string]interface{}{ + "symbol": symbol, + "orderId": int64(orderID), + } + + _, err := t.request("DELETE", "/fapi/v1/order", cancelParams) + if err != nil { + log.Printf(" ⚠ 取消止损单 %d 失败: %v", int64(orderID), err) + continue + } + + canceledCount++ + log.Printf(" ✓ 已取消止损单 (订单ID: %d, 类型: %s)", int64(orderID), orderType) + } + } + + if canceledCount == 0 { + log.Printf(" ℹ %s 没有止损单需要取消", symbol) + } else { + log.Printf(" ✓ 已取消 %s 的 %d 个止损单", symbol, canceledCount) + } + + return nil +} + +// CancelTakeProfitOrders 仅取消止盈单(不影响止损单) +func (t *AsterTrader) CancelTakeProfitOrders(symbol string) error { + // 获取该币种的所有未完成订单 + params := map[string]interface{}{ + "symbol": symbol, + } + + body, err := t.request("GET", "/fapi/v3/openOrders", params) + if err != nil { + return fmt.Errorf("获取未完成订单失败: %w", err) + } + + var orders []map[string]interface{} + if err := json.Unmarshal(body, &orders); err != nil { + return fmt.Errorf("解析订单数据失败: %w", err) + } + + // 过滤出止盈单并取消 + canceledCount := 0 + for _, order := range orders { + orderType, _ := order["type"].(string) + + // 只取消止盈订单(不取消止损订单) + if orderType == "TAKE_PROFIT_MARKET" || orderType == "TAKE_PROFIT" { + orderID, _ := order["orderId"].(float64) + cancelParams := map[string]interface{}{ + "symbol": symbol, + "orderId": int64(orderID), + } + + _, err := t.request("DELETE", "/fapi/v1/order", cancelParams) + if err != nil { + log.Printf(" ⚠ 取消止盈单 %d 失败: %v", int64(orderID), err) + continue + } + + canceledCount++ + log.Printf(" ✓ 已取消止盈单 (订单ID: %d, 类型: %s)", int64(orderID), orderType) + } + } + + if canceledCount == 0 { + log.Printf(" ℹ %s 没有止盈单需要取消", symbol) + } else { + log.Printf(" ✓ 已取消 %s 的 %d 个止盈单", symbol, canceledCount) + } + + return nil +} + // CancelAllOrders 取消所有订单 func (t *AsterTrader) CancelAllOrders(symbol string) error { params := map[string]interface{}{ diff --git a/trader/binance_futures.go b/trader/binance_futures.go index 354415a0..e5aea02a 100644 --- a/trader/binance_futures.go +++ b/trader/binance_futures.go @@ -411,6 +411,137 @@ func (t *FuturesTrader) CloseShort(symbol string, quantity float64) (map[string] return result, nil } +// CancelStopOrders 取消该币种的止盈/止损单(已废弃:会同时删除止损和止盈) +func (t *FuturesTrader) CancelStopOrders(symbol string) error { + // 获取该币种的所有未完成订单 + orders, err := t.client.NewListOpenOrdersService(). + Symbol(symbol). + Do(context.Background()) + + if err != nil { + return fmt.Errorf("获取未完成订单失败: %w", err) + } + + // 过滤出止盈止损单并取消 + canceledCount := 0 + for _, order := range orders { + orderType := order.Type + + // 只取消止损和止盈订单 + if orderType == futures.OrderTypeStopMarket || + orderType == futures.OrderTypeTakeProfitMarket || + orderType == futures.OrderTypeStop || + orderType == futures.OrderTypeTakeProfit { + + _, err := t.client.NewCancelOrderService(). + Symbol(symbol). + OrderID(order.OrderID). + Do(context.Background()) + + if err != nil { + log.Printf(" ⚠ 取消订单 %d 失败: %v", order.OrderID, err) + continue + } + + canceledCount++ + log.Printf(" ✓ 已取消 %s 的止盈/止损单 (订单ID: %d, 类型: %s)", + symbol, order.OrderID, orderType) + } + } + + if canceledCount == 0 { + log.Printf(" ℹ %s 没有止盈/止损单需要取消", symbol) + } else { + log.Printf(" ✓ 已取消 %s 的 %d 个止盈/止损单", symbol, canceledCount) + } + + return nil +} + +// CancelStopLossOrders 仅取消止损单(不影响止盈单) +func (t *FuturesTrader) CancelStopLossOrders(symbol string) error { + // 获取该币种的所有未完成订单 + orders, err := t.client.NewListOpenOrdersService(). + Symbol(symbol). + Do(context.Background()) + + if err != nil { + return fmt.Errorf("获取未完成订单失败: %w", err) + } + + // 过滤出止损单并取消 + canceledCount := 0 + for _, order := range orders { + orderType := order.Type + + // 只取消止损订单(不取消止盈订单) + if orderType == futures.OrderTypeStopMarket || orderType == futures.OrderTypeStop { + _, err := t.client.NewCancelOrderService(). + Symbol(symbol). + OrderID(order.OrderID). + Do(context.Background()) + + if err != nil { + log.Printf(" ⚠ 取消止损单 %d 失败: %v", order.OrderID, err) + continue + } + + canceledCount++ + log.Printf(" ✓ 已取消止损单 (订单ID: %d, 类型: %s)", order.OrderID, orderType) + } + } + + if canceledCount == 0 { + log.Printf(" ℹ %s 没有止损单需要取消", symbol) + } else { + log.Printf(" ✓ 已取消 %s 的 %d 个止损单", symbol, canceledCount) + } + + return nil +} + +// CancelTakeProfitOrders 仅取消止盈单(不影响止损单) +func (t *FuturesTrader) CancelTakeProfitOrders(symbol string) error { + // 获取该币种的所有未完成订单 + orders, err := t.client.NewListOpenOrdersService(). + Symbol(symbol). + Do(context.Background()) + + if err != nil { + return fmt.Errorf("获取未完成订单失败: %w", err) + } + + // 过滤出止盈单并取消 + canceledCount := 0 + for _, order := range orders { + orderType := order.Type + + // 只取消止盈订单(不取消止损订单) + if orderType == futures.OrderTypeTakeProfitMarket || orderType == futures.OrderTypeTakeProfit { + _, err := t.client.NewCancelOrderService(). + Symbol(symbol). + OrderID(order.OrderID). + Do(context.Background()) + + if err != nil { + log.Printf(" ⚠ 取消止盈单 %d 失败: %v", order.OrderID, err) + continue + } + + canceledCount++ + log.Printf(" ✓ 已取消止盈单 (订单ID: %d, 类型: %s)", order.OrderID, orderType) + } + } + + if canceledCount == 0 { + log.Printf(" ℹ %s 没有止盈单需要取消", symbol) + } else { + log.Printf(" ✓ 已取消 %s 的 %d 个止盈单", symbol, canceledCount) + } + + return nil +} + // CancelAllOrders 取消该币种的所有挂单 func (t *FuturesTrader) CancelAllOrders(symbol string) error { err := t.client.NewCancelAllOpenOrdersService(). diff --git a/trader/hyperliquid_trader.go b/trader/hyperliquid_trader.go index c189dbdc..d7884259 100644 --- a/trader/hyperliquid_trader.go +++ b/trader/hyperliquid_trader.go @@ -477,6 +477,56 @@ func (t *HyperliquidTrader) CloseShort(symbol string, quantity float64) (map[str return result, nil } +// CancelStopOrders 取消该币种的止盈/止损单 +func (t *HyperliquidTrader) CancelStopOrders(symbol string) error { + coin := convertSymbolToHyperliquid(symbol) + + // 获取所有挂单 + openOrders, err := t.exchange.Info().OpenOrders(t.ctx, t.walletAddr) + if err != nil { + return fmt.Errorf("获取挂单失败: %w", err) + } + + // 注意:Hyperliquid SDK 的 OpenOrder 结构不暴露 trigger 字段 + // 因此暂时取消该币种的所有挂单(包括止盈止损单) + // 这是安全的,因为在设置新的止盈止损之前,应该清理所有旧订单 + canceledCount := 0 + for _, order := range openOrders { + if order.Coin == coin { + _, err := t.exchange.Cancel(t.ctx, coin, order.Oid) + if err != nil { + log.Printf(" ⚠ 取消订单失败 (oid=%d): %v", order.Oid, err) + continue + } + canceledCount++ + } + } + + if canceledCount == 0 { + log.Printf(" ℹ %s 没有挂单需要取消", symbol) + } else { + log.Printf(" ✓ 已取消 %s 的 %d 个挂单(包括止盈/止损单)", symbol, canceledCount) + } + + return nil +} + +// CancelStopLossOrders 仅取消止损单(Hyperliquid 暂无法区分止损和止盈,取消所有) +func (t *HyperliquidTrader) CancelStopLossOrders(symbol string) error { + // Hyperliquid SDK 的 OpenOrder 结构不暴露 trigger 字段 + // 无法区分止损和止盈单,因此取消该币种的所有挂单 + log.Printf(" ⚠️ Hyperliquid 无法区分止损/止盈单,将取消所有挂单") + return t.CancelStopOrders(symbol) +} + +// CancelTakeProfitOrders 仅取消止盈单(Hyperliquid 暂无法区分止损和止盈,取消所有) +func (t *HyperliquidTrader) CancelTakeProfitOrders(symbol string) error { + // Hyperliquid SDK 的 OpenOrder 结构不暴露 trigger 字段 + // 无法区分止损和止盈单,因此取消该币种的所有挂单 + log.Printf(" ⚠️ Hyperliquid 无法区分止损/止盈单,将取消所有挂单") + return t.CancelStopOrders(symbol) +} + // CancelAllOrders 取消该币种的所有挂单 func (t *HyperliquidTrader) CancelAllOrders(symbol string) error { coin := convertSymbolToHyperliquid(symbol) diff --git a/trader/interface.go b/trader/interface.go index 18d75ee7..660b09b9 100644 --- a/trader/interface.go +++ b/trader/interface.go @@ -36,6 +36,16 @@ type Trader interface { // SetTakeProfit 设置止盈单 SetTakeProfit(symbol string, positionSide string, quantity, takeProfitPrice float64) error + // CancelStopOrders 取消该币种的止盈/止损单(已废弃:会同时删除止损和止盈) + // 请使用 CancelStopLossOrders 或 CancelTakeProfitOrders + CancelStopOrders(symbol string) error + + // CancelStopLossOrders 仅取消止损单(修复 BUG:调整止损时不删除止盈) + CancelStopLossOrders(symbol string) error + + // CancelTakeProfitOrders 仅取消止盈单(修复 BUG:调整止盈时不删除止损) + CancelTakeProfitOrders(symbol string) error + // CancelAllOrders 取消该币种的所有挂单 CancelAllOrders(symbol string) error