From ed34201c2dde50d942dfd2c3d55523a8cab6c4ee Mon Sep 17 00:00:00 2001 From: ZhouYongyou <128128010+zhouyongyou@users.noreply.github.com> Date: Sun, 2 Nov 2025 06:23:02 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BF=AE=E5=BE=A9=E9=97=9C=E9=8D=B5=E7=BC=BA?= =?UTF-8?q?=E9=99=B7=EF=BC=9A=E6=B7=BB=E5=8A=A0=20CancelStopOrders=20?= =?UTF-8?q?=E6=96=B9=E6=B3=95=E9=81=BF=E5=85=8D=E5=A4=9A=E5=80=8B=E6=AD=A2?= =?UTF-8?q?=E6=90=8D=E5=96=AE=E5=85=B1=E5=AD=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 問題: - 調整止損/止盈時,直接調用 SetStopLoss/SetTakeProfit 會創建新訂單 - 但舊的止損/止盈單仍然存在,導致多個訂單共存 - 可能造成意外觸發或訂單衝突 解決方案(參考 PR #197): 1. 在 Trader 接口添加 CancelStopOrders 方法 2. 為三個交易所實現: - binance_futures.go: 過濾 STOP_MARKET/TAKE_PROFIT_MARKET 類型 - aster_trader.go: 同樣邏輯 - hyperliquid_trader.go: 過濾 trigger 訂單(有 triggerPx) 3. 在 executeUpdateStopLossWithRecord 和 executeUpdateTakeProfitWithRecord 中: - 先調用 CancelStopOrders 取消舊單 - 然後設置新止損/止盈 - 取消失敗不中斷執行(記錄警告) 優勢: - ✅ 避免多個止損單同時存在 - ✅ 保留我們的價格驗證邏輯 - ✅ 保留執行價格記錄 - ✅ 詳細錯誤信息 - ✅ 取消失敗時繼續執行(更健壯) 測試建議: - 開倉後調整止損,檢查舊止損單是否被取消 - 連續調整兩次,確認只有最新止損單存在 致謝:參考 PR #197 的實現思路 --- trader/aster_trader.go | 55 ++++++++++++++++++++++++++++++++++++ trader/auto_trader.go | 12 ++++++++ trader/binance_futures.go | 47 ++++++++++++++++++++++++++++++ trader/hyperliquid_trader.go | 41 +++++++++++++++++++++++++++ trader/interface.go | 3 ++ 5 files changed, 158 insertions(+) diff --git a/trader/aster_trader.go b/trader/aster_trader.go index e4d7f12d..5427e4da 100644 --- a/trader/aster_trader.go +++ b/trader/aster_trader.go @@ -981,6 +981,61 @@ func (t *AsterTrader) CancelAllOrders(symbol string) error { 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 +} + // FormatQuantity 格式化数量(实现Trader接口) func (t *AsterTrader) FormatQuantity(symbol string, quantity float64) (string, error) { formatted, err := t.formatQuantity(symbol, quantity) diff --git a/trader/auto_trader.go b/trader/auto_trader.go index f3e6cbc4..a40434b8 100644 --- a/trader/auto_trader.go +++ b/trader/auto_trader.go @@ -837,6 +837,12 @@ func (at *AutoTrader) executeUpdateStopLossWithRecord(decision *decision.Decisio return fmt.Errorf("空单止损必须高于当前价格 (当前: %.2f, 新止损: %.2f)", marketData.CurrentPrice, decision.NewStopLoss) } + // 取消旧的止损单(避免多个止损单共存) + if err := at.trader.CancelStopOrders(decision.Symbol); err != nil { + log.Printf(" ⚠ 取消旧止损单失败: %v", err) + // 不中断执行,继续设置新止损 + } + // 调用交易所 API 修改止损 quantity := math.Abs(positionAmt) err = at.trader.SetStopLoss(decision.Symbol, positionSide, quantity, decision.NewStopLoss) @@ -893,6 +899,12 @@ func (at *AutoTrader) executeUpdateTakeProfitWithRecord(decision *decision.Decis return fmt.Errorf("空单止盈必须低于当前价格 (当前: %.2f, 新止盈: %.2f)", marketData.CurrentPrice, decision.NewTakeProfit) } + // 取消旧的止盈单(避免多个止盈单共存) + if err := at.trader.CancelStopOrders(decision.Symbol); err != nil { + log.Printf(" ⚠ 取消旧止盈单失败: %v", err) + // 不中断执行,继续设置新止盈 + } + // 调用交易所 API 修改止盈 quantity := math.Abs(positionAmt) err = at.trader.SetTakeProfit(decision.Symbol, positionSide, quantity, decision.NewTakeProfit) diff --git a/trader/binance_futures.go b/trader/binance_futures.go index c10fadeb..d04eeaae 100644 --- a/trader/binance_futures.go +++ b/trader/binance_futures.go @@ -425,6 +425,53 @@ func (t *FuturesTrader) CancelAllOrders(symbol string) error { return 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 +} + // GetMarketPrice 获取市场价格 func (t *FuturesTrader) GetMarketPrice(symbol string) (float64, error) { prices, err := t.client.NewListPricesService().Symbol(symbol).Do(context.Background()) diff --git a/trader/hyperliquid_trader.go b/trader/hyperliquid_trader.go index 3073b342..c89f6b14 100644 --- a/trader/hyperliquid_trader.go +++ b/trader/hyperliquid_trader.go @@ -501,6 +501,47 @@ func (t *HyperliquidTrader) CancelAllOrders(symbol string) error { return 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) + } + + // 过滤出止盈止损单并取消 + canceledCount := 0 + for _, order := range openOrders { + if order.Coin == coin { + // Hyperliquid 的止损止盈订单通常是 trigger 订单 + // 检查是否有 triggerPx 字段(表示触发价格) + isTriggerOrder := order.TriggerPx != "" && order.TriggerPx != "0" + + if isTriggerOrder { + _, err := t.exchange.Cancel(t.ctx, coin, order.Oid) + if err != nil { + log.Printf(" ⚠ 取消止盈/止损单失败 (oid=%d): %v", order.Oid, err) + continue + } + + canceledCount++ + log.Printf(" ✓ 已取消 %s 的止盈/止损单 (订单ID: %d, 触发价: %s)", + symbol, order.Oid, order.TriggerPx) + } + } + } + + if canceledCount == 0 { + log.Printf(" ℹ %s 没有止盈/止损单需要取消", symbol) + } else { + log.Printf(" ✓ 已取消 %s 的 %d 个止盈/止损单", symbol, canceledCount) + } + + return nil +} + // GetMarketPrice 获取市场价格 func (t *HyperliquidTrader) GetMarketPrice(symbol string) (float64, error) { coin := convertSymbolToHyperliquid(symbol) diff --git a/trader/interface.go b/trader/interface.go index 18d75ee7..edf70d32 100644 --- a/trader/interface.go +++ b/trader/interface.go @@ -39,6 +39,9 @@ type Trader interface { // CancelAllOrders 取消该币种的所有挂单 CancelAllOrders(symbol string) error + // CancelStopOrders 取消该币种的止盈/止损单(用于调整止盈止损位置) + CancelStopOrders(symbol string) error + // FormatQuantity 格式化数量到正确的精度 FormatQuantity(symbol string, quantity float64) (string, error) }