From 9884605c752e72c41695ea73d9bdbcaa5fe92040 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=20=E5=95=8F=E9=A1=8C?= =?UTF-8?q?=EF=BC=9A=20-=20=E8=AA=BF=E6=95=B4=E6=AD=A2=E6=90=8D/=E6=AD=A2?= =?UTF-8?q?=E7=9B=88=E6=99=82=EF=BC=8C=E7=9B=B4=E6=8E=A5=E8=AA=BF=E7=94=A8?= =?UTF-8?q?=20SetStopLoss/SetTakeProfit=20=E6=9C=83=E5=89=B5=E5=BB=BA?= =?UTF-8?q?=E6=96=B0=E8=A8=82=E5=96=AE=20-=20=E4=BD=86=E8=88=8A=E7=9A=84?= =?UTF-8?q?=E6=AD=A2=E6=90=8D/=E6=AD=A2=E7=9B=88=E5=96=AE=E4=BB=8D?= =?UTF-8?q?=E7=84=B6=E5=AD=98=E5=9C=A8=EF=BC=8C=E5=B0=8E=E8=87=B4=E5=A4=9A?= =?UTF-8?q?=E5=80=8B=E8=A8=82=E5=96=AE=E5=85=B1=E5=AD=98=20-=20=E5=8F=AF?= =?UTF-8?q?=E8=83=BD=E9=80=A0=E6=88=90=E6=84=8F=E5=A4=96=E8=A7=B8=E7=99=BC?= =?UTF-8?q?=E6=88=96=E8=A8=82=E5=96=AE=E8=A1=9D=E7=AA=81=20=E8=A7=A3?= =?UTF-8?q?=E6=B1=BA=E6=96=B9=E6=A1=88=EF=BC=88=E5=8F=83=E8=80=83=20PR=20#?= =?UTF-8?q?197=EF=BC=89=EF=BC=9A=201.=20=E5=9C=A8=20Trader=20=E6=8E=A5?= =?UTF-8?q?=E5=8F=A3=E6=B7=BB=E5=8A=A0=20CancelStopOrders=20=E6=96=B9?= =?UTF-8?q?=E6=B3=95=202.=20=E7=82=BA=E4=B8=89=E5=80=8B=E4=BA=A4=E6=98=93?= =?UTF-8?q?=E6=89=80=E5=AF=A6=E7=8F=BE=EF=BC=9A=20=20=20=20-=20binance=5Ff?= =?UTF-8?q?utures.go:=20=E9=81=8E=E6=BF=BE=20STOP=5FMARKET/TAKE=5FPROFIT?= =?UTF-8?q?=5FMARKET=20=E9=A1=9E=E5=9E=8B=20=20=20=20-=20aster=5Ftrader.go?= =?UTF-8?q?:=20=E5=90=8C=E6=A8=A3=E9=82=8F=E8=BC=AF=20=20=20=20-=20hyperli?= =?UTF-8?q?quid=5Ftrader.go:=20=E9=81=8E=E6=BF=BE=20trigger=20=E8=A8=82?= =?UTF-8?q?=E5=96=AE=EF=BC=88=E6=9C=89=20triggerPx=EF=BC=89=203.=20?= =?UTF-8?q?=E5=9C=A8=20executeUpdateStopLossWithRecord=20=E5=92=8C=20execu?= =?UTF-8?q?teUpdateTakeProfitWithRecord=20=E4=B8=AD=EF=BC=9A=20=20=20=20-?= =?UTF-8?q?=20=E5=85=88=E8=AA=BF=E7=94=A8=20CancelStopOrders=20=E5=8F=96?= =?UTF-8?q?=E6=B6=88=E8=88=8A=E5=96=AE=20=20=20=20-=20=E7=84=B6=E5=BE=8C?= =?UTF-8?q?=E8=A8=AD=E7=BD=AE=E6=96=B0=E6=AD=A2=E6=90=8D/=E6=AD=A2?= =?UTF-8?q?=E7=9B=88=20=20=20=20-=20=E5=8F=96=E6=B6=88=E5=A4=B1=E6=95=97?= =?UTF-8?q?=E4=B8=8D=E4=B8=AD=E6=96=B7=E5=9F=B7=E8=A1=8C=EF=BC=88=E8=A8=98?= =?UTF-8?q?=E9=8C=84=E8=AD=A6=E5=91=8A=EF=BC=89=20=E5=84=AA=E5=8B=A2?= =?UTF-8?q?=EF=BC=9A=20-=20=E2=9C=85=20=E9=81=BF=E5=85=8D=E5=A4=9A?= =?UTF-8?q?=E5=80=8B=E6=AD=A2=E6=90=8D=E5=96=AE=E5=90=8C=E6=99=82=E5=AD=98?= =?UTF-8?q?=E5=9C=A8=20-=20=E2=9C=85=20=E4=BF=9D=E7=95=99=E6=88=91?= =?UTF-8?q?=E5=80=91=E7=9A=84=E5=83=B9=E6=A0=BC=E9=A9=97=E8=AD=89=E9=82=8F?= =?UTF-8?q?=E8=BC=AF=20-=20=E2=9C=85=20=E4=BF=9D=E7=95=99=E5=9F=B7?= =?UTF-8?q?=E8=A1=8C=E5=83=B9=E6=A0=BC=E8=A8=98=E9=8C=84=20-=20=E2=9C=85?= =?UTF-8?q?=20=E8=A9=B3=E7=B4=B0=E9=8C=AF=E8=AA=A4=E4=BF=A1=E6=81=AF=20-?= =?UTF-8?q?=20=E2=9C=85=20=E5=8F=96=E6=B6=88=E5=A4=B1=E6=95=97=E6=99=82?= =?UTF-8?q?=E7=B9=BC=E7=BA=8C=E5=9F=B7=E8=A1=8C=EF=BC=88=E6=9B=B4=E5=81=A5?= =?UTF-8?q?=E5=A3=AF=EF=BC=89=20=E6=B8=AC=E8=A9=A6=E5=BB=BA=E8=AD=B0?= =?UTF-8?q?=EF=BC=9A=20-=20=E9=96=8B=E5=80=89=E5=BE=8C=E8=AA=BF=E6=95=B4?= =?UTF-8?q?=E6=AD=A2=E6=90=8D=EF=BC=8C=E6=AA=A2=E6=9F=A5=E8=88=8A=E6=AD=A2?= =?UTF-8?q?=E6=90=8D=E5=96=AE=E6=98=AF=E5=90=A6=E8=A2=AB=E5=8F=96=E6=B6=88?= =?UTF-8?q?=20-=20=E9=80=A3=E7=BA=8C=E8=AA=BF=E6=95=B4=E5=85=A9=E6=AC=A1?= =?UTF-8?q?=EF=BC=8C=E7=A2=BA=E8=AA=8D=E5=8F=AA=E6=9C=89=E6=9C=80=E6=96=B0?= =?UTF-8?q?=E6=AD=A2=E6=90=8D=E5=96=AE=E5=AD=98=E5=9C=A8=20=E8=87=B4?= =?UTF-8?q?=E8=AC=9D=EF=BC=9A=E5=8F=83=E8=80=83=20PR=20#197=20=E7=9A=84?= =?UTF-8?q?=E5=AF=A6=E7=8F=BE=E6=80=9D=E8=B7=AF?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- 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 d9ba82a6..e492942a 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 0226d87f..e402114a 100644 --- a/trader/auto_trader.go +++ b/trader/auto_trader.go @@ -823,6 +823,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) @@ -879,6 +885,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 354415a0..abaf5c9a 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 c189dbdc..4311734d 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) }