diff --git a/logger/decision_logger.go b/logger/decision_logger.go index e12fd1dc..c9630508 100644 --- a/logger/decision_logger.go +++ b/logger/decision_logger.go @@ -243,8 +243,10 @@ func (l *DecisionLogger) GetStatistics() (*Statistics, error) { switch action.Action { case "open_long", "open_short": stats.TotalOpenPositions++ - case "close_long", "close_short", "partial_close", "auto_close_long", "auto_close_short": + case "close_long", "close_short", "auto_close_long", "auto_close_short": stats.TotalClosePositions++ + // 🔧 BUG FIX:partial_close 不計入 TotalClosePositions,避免重複計數 + // case "partial_close": // 不計數,因為只有完全平倉才算一次 // update_stop_loss 和 update_take_profit 不計入統計 } } @@ -418,11 +420,15 @@ func (l *DecisionLogger) AnalyzePerformance(lookbackCycles int) (*PerformanceAna case "open_long", "open_short": // 更新开仓记录(可能已经在预填充时记录过了) openPositions[posKey] = map[string]interface{}{ - "side": side, - "openPrice": action.Price, - "openTime": action.Timestamp, - "quantity": action.Quantity, - "leverage": action.Leverage, + "side": side, + "openPrice": action.Price, + "openTime": action.Timestamp, + "quantity": action.Quantity, + "leverage": action.Leverage, + "remainingQuantity": action.Quantity, // 🔧 BUG FIX:追蹤剩餘數量 + "accumulatedPnL": 0.0, // 🔧 BUG FIX:累積部分平倉盈虧 + "partialCloseCount": 0, // 🔧 BUG FIX:部分平倉次數 + "partialCloseVolume": 0.0, // 🔧 BUG FIX:部分平倉總量 } case "close_long", "close_short", "partial_close", "auto_close_long", "auto_close_short": @@ -434,15 +440,22 @@ func (l *DecisionLogger) AnalyzePerformance(lookbackCycles int) (*PerformanceAna quantity := openPos["quantity"].(float64) leverage := openPos["leverage"].(int) - // 对于 partial_close,使用实际平仓数量;否则使用完整仓位数量 - actualQuantity := quantity + // 🔧 BUG FIX:取得追蹤字段(若不存在則初始化) + remainingQty, _ := openPos["remainingQuantity"].(float64) + if remainingQty == 0 { + remainingQty = quantity // 兼容舊數據(沒有 remainingQuantity 字段) + } + accumulatedPnL, _ := openPos["accumulatedPnL"].(float64) + partialCloseCount, _ := openPos["partialCloseCount"].(int) + partialCloseVolume, _ := openPos["partialCloseVolume"].(float64) + + // 对于 partial_close,使用实际平仓数量;否则使用剩余仓位数量 + actualQuantity := remainingQty if action.Action == "partial_close" { actualQuantity = action.Quantity } - // 计算实际盈亏(USDT) - // 合约交易 PnL 计算:actualQuantity × 价格差 - // 注意:杠杆不影响绝对盈亏,只影响保证金需求 + // 计算本次平仓的盈亏(USDT) var pnl float64 if side == "long" { pnl = actualQuantity * (action.Price - openPrice) @@ -450,61 +463,134 @@ func (l *DecisionLogger) AnalyzePerformance(lookbackCycles int) (*PerformanceAna pnl = actualQuantity * (openPrice - action.Price) } - // 计算盈亏百分比(相对保证金) - positionValue := actualQuantity * openPrice - marginUsed := positionValue / float64(leverage) - pnlPct := 0.0 - if marginUsed > 0 { - pnlPct = (pnl / marginUsed) * 100 - } + // 🔧 BUG FIX:處理 partial_close 聚合邏輯 + if action.Action == "partial_close" { + // 累積盈虧和數量 + accumulatedPnL += pnl + remainingQty -= actualQuantity + partialCloseCount++ + partialCloseVolume += actualQuantity - // 记录交易结果 - outcome := TradeOutcome{ - Symbol: symbol, - Side: side, - Quantity: actualQuantity, - Leverage: leverage, - OpenPrice: openPrice, - ClosePrice: action.Price, - PositionValue: positionValue, - MarginUsed: marginUsed, - PnL: pnl, - PnLPct: pnlPct, - Duration: action.Timestamp.Sub(openTime).String(), - OpenTime: openTime, - CloseTime: action.Timestamp, - } + // 更新 openPositions(保留持倉記錄,但更新追蹤數據) + openPos["remainingQuantity"] = remainingQty + openPos["accumulatedPnL"] = accumulatedPnL + openPos["partialCloseCount"] = partialCloseCount + openPos["partialCloseVolume"] = partialCloseVolume - analysis.RecentTrades = append(analysis.RecentTrades, outcome) - analysis.TotalTrades++ + // 判斷是否已完全平倉 + if remainingQty <= 0.0001 { // 使用小閾值避免浮點誤差 + // ✅ 完全平倉:記錄為一筆完整交易 + positionValue := quantity * openPrice + marginUsed := positionValue / float64(leverage) + pnlPct := 0.0 + if marginUsed > 0 { + pnlPct = (accumulatedPnL / marginUsed) * 100 + } - // 分类交易:盈利、亏损、持平(避免将pnl=0算入亏损) - if pnl > 0 { - analysis.WinningTrades++ - analysis.AvgWin += pnl - } else if pnl < 0 { - analysis.LosingTrades++ - analysis.AvgLoss += pnl - } - // pnl == 0 的交易不计入盈利也不计入亏损,但计入总交易数 + outcome := TradeOutcome{ + Symbol: symbol, + Side: side, + Quantity: quantity, // 使用原始總量 + Leverage: leverage, + OpenPrice: openPrice, + ClosePrice: action.Price, // 最後一次平倉價格 + PositionValue: positionValue, + MarginUsed: marginUsed, + PnL: accumulatedPnL, // 🔧 使用累積盈虧 + PnLPct: pnlPct, + Duration: action.Timestamp.Sub(openTime).String(), + OpenTime: openTime, + CloseTime: action.Timestamp, + } - // 更新币种统计 - if _, exists := analysis.SymbolStats[symbol]; !exists { - analysis.SymbolStats[symbol] = &SymbolPerformance{ - Symbol: symbol, + analysis.RecentTrades = append(analysis.RecentTrades, outcome) + analysis.TotalTrades++ // 🔧 只在完全平倉時計數 + + // 分类交易 + if accumulatedPnL > 0 { + analysis.WinningTrades++ + analysis.AvgWin += accumulatedPnL + } else if accumulatedPnL < 0 { + analysis.LosingTrades++ + analysis.AvgLoss += accumulatedPnL + } + + // 更新币种统计 + if _, exists := analysis.SymbolStats[symbol]; !exists { + analysis.SymbolStats[symbol] = &SymbolPerformance{ + Symbol: symbol, + } + } + stats := analysis.SymbolStats[symbol] + stats.TotalTrades++ + stats.TotalPnL += accumulatedPnL + if accumulatedPnL > 0 { + stats.WinningTrades++ + } else if accumulatedPnL < 0 { + stats.LosingTrades++ + } + + // 刪除持倉記錄 + delete(openPositions, posKey) } - } - stats := analysis.SymbolStats[symbol] - stats.TotalTrades++ - stats.TotalPnL += pnl - if pnl > 0 { - stats.WinningTrades++ - } else if pnl < 0 { - stats.LosingTrades++ - } + // ⚠️ 否則不做任何操作(等待後續 partial_close 或 full close) - // 移除已平仓记录(partial_close 不刪除,因為還有剩餘倉位) - if action.Action != "partial_close" { + } else { + // 🔧 完全平倉(close_long/close_short/auto_close) + // 如果之前有部分平倉,需要加上累積的 PnL + totalPnL := accumulatedPnL + pnl + + positionValue := quantity * openPrice + marginUsed := positionValue / float64(leverage) + pnlPct := 0.0 + if marginUsed > 0 { + pnlPct = (totalPnL / marginUsed) * 100 + } + + outcome := TradeOutcome{ + Symbol: symbol, + Side: side, + Quantity: quantity, // 使用原始總量 + Leverage: leverage, + OpenPrice: openPrice, + ClosePrice: action.Price, + PositionValue: positionValue, + MarginUsed: marginUsed, + PnL: totalPnL, // 🔧 包含之前部分平倉的 PnL + PnLPct: pnlPct, + Duration: action.Timestamp.Sub(openTime).String(), + OpenTime: openTime, + CloseTime: action.Timestamp, + } + + analysis.RecentTrades = append(analysis.RecentTrades, outcome) + analysis.TotalTrades++ + + // 分类交易 + if totalPnL > 0 { + analysis.WinningTrades++ + analysis.AvgWin += totalPnL + } else if totalPnL < 0 { + analysis.LosingTrades++ + analysis.AvgLoss += totalPnL + } + + // 更新币种统计 + if _, exists := analysis.SymbolStats[symbol]; !exists { + analysis.SymbolStats[symbol] = &SymbolPerformance{ + Symbol: symbol, + } + } + stats := analysis.SymbolStats[symbol] + stats.TotalTrades++ + stats.TotalPnL += totalPnL + if totalPnL > 0 { + stats.WinningTrades++ + } else if totalPnL < 0 { + stats.LosingTrades++ + } + + // 刪除持倉記錄 delete(openPositions, posKey) } } diff --git a/prompts/adaptive.txt b/prompts/adaptive.txt index 390917d0..290ec642 100644 --- a/prompts/adaptive.txt +++ b/prompts/adaptive.txt @@ -71,8 +71,18 @@ ## 仓位计算公式 -**Position Size (USD) = Available Cash × Leverage × Allocation %** -**Position Size (Coins) = Position Size (USD) / Current Price** +**重要**:position_size_usd 是**名义价值**(包含杠杆),非保证金需求。 + +**计算步骤**: +1. **可用保证金** = Available Cash × 0.95 × Allocation %(预留5%给手续费) +2. **名义价值** = 可用保证金 × Leverage +3. **position_size_usd** = 名义价值(这是 JSON 中应填写的值) +4. **Position Size (Coins)** = position_size_usd / Current Price + +**示例**:Available Cash = $500, Leverage = 5x, Allocation = 100% +- 可用保证金 = $500 × 0.95 × 100% = $475 +- position_size_usd = $475 × 5 = **$2,375** ← JSON 中填写此值 +- 实际占用保证金 = $475,剩余 $25 用于手续费 ## 杠杆选择指引 diff --git a/prompts/default.txt b/prompts/default.txt index 0d66eb69..a85cf870 100644 --- a/prompts/default.txt +++ b/prompts/default.txt @@ -142,6 +142,20 @@ confidence=0-100 - **部分平仓** (`partial_close`):需填写 `close_percentage`(1-100),说明目的(如锁定利润)。 - **观望或持有** (`wait/hold`):`reasoning` 必须说明观望或继续持有的原因(例如信号不足、冷却中、趋势未变)。 +### 仓位大小计算 +**重要**:`position_size_usd` 是**名义价值**(包含杠杆),非保证金需求。 + +**计算步骤**: +1. **可用保证金** = Available Cash × 0.95 × 配置比例(预留5%手续费) +2. **名义价值** = 可用保证金 × Leverage +3. **position_size_usd** = 名义价值(JSON中填写此值) +4. **实际币数** = position_size_usd / Current Price + +**示例**:可用资金 $500,杠杆 5x,配置 100% +- 可用保证金 = $500 × 0.95 = $475 +- position_size_usd = $475 × 5 = **$2,375** ← JSON填此值 +- 实际占用保证金 = $475,剩余 $25 用于手续费 + --- 记住: diff --git a/prompts/nof1.txt b/prompts/nof1.txt index 921aa080..062bcc40 100644 --- a/prompts/nof1.txt +++ b/prompts/nof1.txt @@ -114,10 +114,19 @@ stop tightened to $2,950 (break-even). Exit fully if 4h MACD crosses down." # POSITION SIZING FRAMEWORK -Calculate position size using this formula: +**IMPORTANT**: `position_size_usd` is the **notional value** (includes leverage), NOT margin requirement. -Position Size (USD) = Available Cash × Leverage × Allocation % -Position Size (Coins) = Position Size (USD) / Current Price +## Calculation Steps: + +1. **Available Margin** = Available Cash × 0.95 × Allocation % (reserve 5% for fees) +2. **Notional Value** = Available Margin × Leverage +3. **position_size_usd** = Notional Value (this is the value for JSON) +4. **Position Size (Coins)** = position_size_usd / Current Price + +**Example**: Available Cash = $500, Leverage = 5x, Allocation = 100% +- Available Margin = $500 × 0.95 × 100% = $475 +- position_size_usd = $475 × 5 = **$2,375** ← Fill this value in JSON +- Actual margin used = $475, remaining $25 for fees ## Sizing Considerations diff --git a/trader/aster_trader.go b/trader/aster_trader.go index e492942a..fb5273bc 100644 --- a/trader/aster_trader.go +++ b/trader/aster_trader.go @@ -1036,6 +1036,106 @@ func (t *AsterTrader) CancelStopOrders(symbol string) error { 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 +} + // 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 28df5979..17dae19a 100644 --- a/trader/auto_trader.go +++ b/trader/auto_trader.go @@ -758,6 +758,32 @@ func (at *AutoTrader) executeOpenLongWithRecord(decision *decision.Decision, act return err } + // ⚠️ 保证金验证:防止保证金不足错误(code=-2019) + // position_size_usd 是名义价值(包含杠杆),实际需要的保证金 = position_size_usd / leverage + requiredMargin := decision.PositionSizeUSD / float64(decision.Leverage) + + // 获取当前可用余额 + balance, err := at.trader.GetBalance() + if err != nil { + return fmt.Errorf("获取账户余额失败: %w", err) + } + availableBalance := 0.0 + if avail, ok := balance["availableBalance"].(float64); ok { + availableBalance = avail + } + + // 手续费估算(Taker费率 0.04%) + estimatedFee := decision.PositionSizeUSD * 0.0004 + totalRequired := requiredMargin + estimatedFee + + // 验证保证金充足(需要保证金 + 手续费 <= 可用余额) + if totalRequired > availableBalance { + return fmt.Errorf("❌ 保证金不足: 需要 %.2f USDT(保证金 %.2f + 手续费 %.2f),可用 %.2f USDT。建议降低仓位或杠杆", + totalRequired, requiredMargin, estimatedFee, availableBalance) + } + + log.Printf(" ✓ 保证金检查通过: 需要 %.2f USDT,可用 %.2f USDT", totalRequired, availableBalance) + // 计算数量 quantity := decision.PositionSizeUSD / marketData.CurrentPrice actionRecord.Quantity = quantity @@ -817,6 +843,32 @@ func (at *AutoTrader) executeOpenShortWithRecord(decision *decision.Decision, ac return err } + // ⚠️ 保证金验证:防止保证金不足错误(code=-2019) + // position_size_usd 是名义价值(包含杠杆),实际需要的保证金 = position_size_usd / leverage + requiredMargin := decision.PositionSizeUSD / float64(decision.Leverage) + + // 获取当前可用余额 + balance, err := at.trader.GetBalance() + if err != nil { + return fmt.Errorf("获取账户余额失败: %w", err) + } + availableBalance := 0.0 + if avail, ok := balance["availableBalance"].(float64); ok { + availableBalance = avail + } + + // 手续费估算(Taker费率 0.04%) + estimatedFee := decision.PositionSizeUSD * 0.0004 + totalRequired := requiredMargin + estimatedFee + + // 验证保证金充足(需要保证金 + 手续费 <= 可用余额) + if totalRequired > availableBalance { + return fmt.Errorf("❌ 保证金不足: 需要 %.2f USDT(保证金 %.2f + 手续费 %.2f),可用 %.2f USDT。建议降低仓位或杠杆", + totalRequired, requiredMargin, estimatedFee, availableBalance) + } + + log.Printf(" ✓ 保证金检查通过: 需要 %.2f USDT,可用 %.2f USDT", totalRequired, availableBalance) + // 计算数量 quantity := decision.PositionSizeUSD / marketData.CurrentPrice actionRecord.Quantity = quantity @@ -953,8 +1005,8 @@ 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 { + // 取消旧的止损单(只删除止损单,不影响止盈单) + if err := at.trader.CancelStopLossOrders(decision.Symbol); err != nil { log.Printf(" ⚠ 取消旧止损单失败: %v", err) // 不中断执行,继续设置新止损 } @@ -1015,8 +1067,8 @@ 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 { + // 取消旧的止盈单(只删除止盈单,不影响止损单) + if err := at.trader.CancelTakeProfitOrders(decision.Symbol); err != nil { log.Printf(" ⚠ 取消旧止盈单失败: %v", err) // 不中断执行,继续设置新止盈 } diff --git a/trader/binance_futures.go b/trader/binance_futures.go index f5c9c9b7..a4b68250 100644 --- a/trader/binance_futures.go +++ b/trader/binance_futures.go @@ -49,6 +49,12 @@ func NewFuturesTrader(apiKey, secretKey string) *FuturesTrader { log.Printf("⚠️ 初始化同步币安服务器时间失败: %v", err) } + // 设置双向持仓模式(Hedge Mode) + // 这是必需的,因为代码中使用了 PositionSide (LONG/SHORT) + if err := trader.setDualSidePosition(); err != nil { + log.Printf("⚠️ 设置双向持仓模式失败: %v (如果已是双向模式则忽略此警告)", err) + } + return trader } @@ -246,6 +252,30 @@ func (t *FuturesTrader) SetMarginMode(symbol string, isCrossMargin bool) error { return nil } +// setDualSidePosition 设置双向持仓模式(初始化时调用) +func (t *FuturesTrader) setDualSidePosition() error { + // 尝试设置双向持仓模式 + err := t.callWithTimeSync("设置双向持仓模式", func() error { + return t.client.NewChangePositionModeService(). + DualSide(true). // true = 双向持仓(Hedge Mode) + Do(context.Background()) + }) + + if err != nil { + // 如果错误信息包含"No need to change",说明已经是双向持仓模式 + if contains(err.Error(), "No need to change position side") { + log.Printf(" ✓ 账户已是双向持仓模式(Hedge Mode)") + return nil + } + // 其他错误则返回(但在调用方不会中断初始化) + return err + } + + log.Printf(" ✓ 账户已切换为双向持仓模式(Hedge Mode)") + log.Printf(" ℹ️ 双向持仓模式允许同时持有多单和空单") + return nil +} + // SetLeverage 设置杠杆(智能判断+冷却期) func (t *FuturesTrader) SetLeverage(symbol string, leverage int) error { // 先尝试获取当前杠杆(从持仓信息) @@ -572,6 +602,90 @@ func (t *FuturesTrader) CancelStopOrders(symbol string) error { 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 +} + // 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 d59a419e..d580ded5 100644 --- a/trader/hyperliquid_trader.go +++ b/trader/hyperliquid_trader.go @@ -535,6 +535,22 @@ func (t *HyperliquidTrader) CancelStopOrders(symbol string) error { 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) +} + // GetMarketPrice 获取市场价格 func (t *HyperliquidTrader) GetMarketPrice(symbol string) (float64, error) { coin := convertSymbolToHyperliquid(symbol) diff --git a/trader/interface.go b/trader/interface.go index edf70d32..07d522fe 100644 --- a/trader/interface.go +++ b/trader/interface.go @@ -39,9 +39,16 @@ type Trader interface { // CancelAllOrders 取消该币种的所有挂单 CancelAllOrders(symbol string) error - // CancelStopOrders 取消该币种的止盈/止损单(用于调整止盈止损位置) + // CancelStopOrders 取消该币种的止盈/止损单(已废弃:会同时删除止损和止盈) + // 请使用 CancelStopLossOrders 或 CancelTakeProfitOrders CancelStopOrders(symbol string) error + // CancelStopLossOrders 仅取消止损单(修复 BUG:调整止损时不删除止盈) + CancelStopLossOrders(symbol string) error + + // CancelTakeProfitOrders 仅取消止盈单(修复 BUG:调整止盈时不删除止损) + CancelTakeProfitOrders(symbol string) error + // FormatQuantity 格式化数量到正确的精度 FormatQuantity(symbol string, quantity float64) (string, error) } diff --git a/web/src/components/TraderConfigModal.tsx b/web/src/components/TraderConfigModal.tsx index e6a0bd71..b0e03200 100644 --- a/web/src/components/TraderConfigModal.tsx +++ b/web/src/components/TraderConfigModal.tsx @@ -97,7 +97,8 @@ export function TraderConfigModal({ }); } // 确保旧数据也有默认的 system_prompt_template - if (traderData && !traderData.system_prompt_template) { + // 修复BUG:只处理 undefined,不覆盖已有值(包括空字符串) + if (traderData && traderData.system_prompt_template === undefined) { setFormData(prev => ({ ...prev, system_prompt_template: 'default'