diff --git a/trader/auto_trader.go b/trader/auto_trader.go index 929e19497..cfd8bcef2 100644 --- a/trader/auto_trader.go +++ b/trader/auto_trader.go @@ -1136,10 +1136,35 @@ func (at *AutoTrader) executeOpenLongWithRecord(decision *kernel.Decision, actio // Continue execution, doesn't affect trading } - // Open position - order, err := at.trader.OpenLong(decision.Symbol, quantity, decision.Leverage) - if err != nil { - return err + // Open position - try to use OpenLongWithTPSL if available (single API call with TP/SL) + var order map[string]interface{} + if trader, ok := at.trader.(interface { + OpenLongWithTPSL(symbol string, quantity float64, leverage int, stopLoss, takeProfit float64) (map[string]interface{}, error) + }); ok && (decision.StopLoss > 0 || decision.TakeProfit > 0) { + // Use optimized method that includes TP/SL in single API call (e.g., Bitget) + order, err = trader.OpenLongWithTPSL(decision.Symbol, quantity, decision.Leverage, decision.StopLoss, decision.TakeProfit) + if err != nil { + return err + } + logger.Infof(" ✓ Position opened with preset TP/SL (single API call)") + } else { + // Fallback to legacy method (separate API calls) + order, err = at.trader.OpenLong(decision.Symbol, quantity, decision.Leverage) + if err != nil { + return err + } + + // Set stop loss and take profit separately + 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 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) + } + } } // Record order ID @@ -1156,14 +1181,6 @@ 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) - } - if err := at.trader.SetTakeProfit(decision.Symbol, "LONG", quantity, decision.TakeProfit); err != nil { - logger.Infof(" ⚠ Failed to set take profit: %v", err) - } - return nil } @@ -1253,10 +1270,35 @@ func (at *AutoTrader) executeOpenShortWithRecord(decision *kernel.Decision, acti // Continue execution, doesn't affect trading } - // Open position - order, err := at.trader.OpenShort(decision.Symbol, quantity, decision.Leverage) - if err != nil { - return err + // Open position - try to use OpenShortWithTPSL if available (single API call with TP/SL) + var order map[string]interface{} + if trader, ok := at.trader.(interface { + OpenShortWithTPSL(symbol string, quantity float64, leverage int, stopLoss, takeProfit float64) (map[string]interface{}, error) + }); ok && (decision.StopLoss > 0 || decision.TakeProfit > 0) { + // Use optimized method that includes TP/SL in single API call (e.g., Bitget) + order, err = trader.OpenShortWithTPSL(decision.Symbol, quantity, decision.Leverage, decision.StopLoss, decision.TakeProfit) + if err != nil { + return err + } + logger.Infof(" ✓ Position opened with preset TP/SL (single API call)") + } else { + // Fallback to legacy method (separate API calls) + order, err = at.trader.OpenShort(decision.Symbol, quantity, decision.Leverage) + if err != nil { + return err + } + + // Set stop loss and take profit separately + 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 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) + } + } } // Record order ID @@ -1273,14 +1315,6 @@ func (at *AutoTrader) executeOpenShortWithRecord(decision *kernel.Decision, acti posKey := decision.Symbol + "_short" 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 err := at.trader.SetTakeProfit(decision.Symbol, "SHORT", quantity, decision.TakeProfit); err != nil { - logger.Infof(" ⚠ Failed to set take profit: %v", err) - } - return nil } diff --git a/trader/bitget/trader.go b/trader/bitget/trader.go index d44f7e015..290348434 100644 --- a/trader/bitget/trader.go +++ b/trader/bitget/trader.go @@ -488,6 +488,17 @@ func (t *BitgetTrader) SetLeverage(symbol string, leverage int) error { // OpenLong opens long position func (t *BitgetTrader) OpenLong(symbol string, quantity float64, leverage int) (map[string]interface{}, error) { + return t.openLongInternal(symbol, quantity, leverage, 0, 0) +} + +// OpenLongWithTPSL opens long position with stop-loss and take-profit preset in the same API call +// This is more reliable than setting TP/SL separately after opening the position +func (t *BitgetTrader) OpenLongWithTPSL(symbol string, quantity float64, leverage int, stopLoss, takeProfit float64) (map[string]interface{}, error) { + return t.openLongInternal(symbol, quantity, leverage, stopLoss, takeProfit) +} + +// openLongInternal is the internal implementation for opening long positions +func (t *BitgetTrader) openLongInternal(symbol string, quantity float64, leverage int, stopLoss, takeProfit float64) (map[string]interface{}, error) { symbol = t.convertSymbol(symbol) // Cancel old orders first @@ -512,7 +523,18 @@ func (t *BitgetTrader) OpenLong(symbol string, quantity float64, leverage int) ( "clientOid": genBitgetClientOid(), } - logger.Infof(" 📊 Bitget OpenLong: symbol=%s, qty=%s, leverage=%d", symbol, qtyStr, leverage) + // Add preset stop-loss if provided (formatted to correct precision) + if stopLoss > 0 { + body["presetStopLossPrice"] = t.FormatPrice(symbol, stopLoss) + } + + // Add preset take-profit if provided (formatted to correct precision) + if takeProfit > 0 { + body["presetStopSurplusPrice"] = t.FormatPrice(symbol, takeProfit) + } + + logger.Infof(" 📊 Bitget OpenLong: symbol=%s, qty=%s, leverage=%d, SL=%s, TP=%s", + symbol, qtyStr, leverage, t.FormatPrice(symbol, stopLoss), t.FormatPrice(symbol, takeProfit)) data, err := t.doRequest("POST", bitgetOrderPath, body) if err != nil { @@ -542,6 +564,16 @@ func (t *BitgetTrader) OpenLong(symbol string, quantity float64, leverage int) ( // OpenShort opens short position func (t *BitgetTrader) OpenShort(symbol string, quantity float64, leverage int) (map[string]interface{}, error) { + return t.openShortInternal(symbol, quantity, leverage, 0, 0) +} + +// OpenShortWithTPSL opens short position with stop-loss and take-profit preset in the same API call +func (t *BitgetTrader) OpenShortWithTPSL(symbol string, quantity float64, leverage int, stopLoss, takeProfit float64) (map[string]interface{}, error) { + return t.openShortInternal(symbol, quantity, leverage, stopLoss, takeProfit) +} + +// openShortInternal is the internal implementation for opening short positions +func (t *BitgetTrader) openShortInternal(symbol string, quantity float64, leverage int, stopLoss, takeProfit float64) (map[string]interface{}, error) { symbol = t.convertSymbol(symbol) // Cancel old orders first @@ -566,7 +598,18 @@ func (t *BitgetTrader) OpenShort(symbol string, quantity float64, leverage int) "clientOid": genBitgetClientOid(), } - logger.Infof(" 📊 Bitget OpenShort: symbol=%s, qty=%s, leverage=%d", symbol, qtyStr, leverage) + // Add preset stop-loss if provided (formatted to correct precision) + if stopLoss > 0 { + body["presetStopLossPrice"] = t.FormatPrice(symbol, stopLoss) + } + + // Add preset take-profit if provided (formatted to correct precision) + if takeProfit > 0 { + body["presetStopSurplusPrice"] = t.FormatPrice(symbol, takeProfit) + } + + logger.Infof(" 📊 Bitget OpenShort: symbol=%s, qty=%s, leverage=%d, SL=%s, TP=%s", + symbol, qtyStr, leverage, t.FormatPrice(symbol, stopLoss), t.FormatPrice(symbol, takeProfit)) data, err := t.doRequest("POST", bitgetOrderPath, body) if err != nil { @@ -949,6 +992,19 @@ func (t *BitgetTrader) FormatQuantity(symbol string, quantity float64) (string, return fmt.Sprintf(format, quantity), nil } +// FormatPrice formats price according to contract precision +func (t *BitgetTrader) FormatPrice(symbol string, price float64) string { + contract, err := t.getContract(symbol) + if err != nil { + // Default: 1 decimal place (0.1 precision) for BTC-like contracts + return fmt.Sprintf("%.1f", price) + } + + // Format according to price precision + format := fmt.Sprintf("%%.%df", contract.PricePlace) + return fmt.Sprintf(format, price) +} + // GetOrderStatus gets order status func (t *BitgetTrader) GetOrderStatus(symbol string, orderID string) (map[string]interface{}, error) { symbol = t.convertSymbol(symbol) diff --git a/trader/bitget/trader_test.go b/trader/bitget/trader_test.go new file mode 100644 index 000000000..184e83b7b --- /dev/null +++ b/trader/bitget/trader_test.go @@ -0,0 +1,119 @@ +package bitget + +import ( + "fmt" + "os" + "testing" +) + +// 测试 Bitget 下单(带止盈止损) +// 运行: go test -v -run TestOpenLongWithTPSL ./trader/bitget/ +func TestOpenLongWithTPSL(t *testing.T) { + apiKey := os.Getenv("BITGET_API_KEY") + secretKey := os.Getenv("BITGET_SECRET_KEY") + passphrase := os.Getenv("BITGET_PASSPHRASE") + + if apiKey == "" || secretKey == "" || passphrase == "" { + t.Skip("跳过测试: 需要设置环境变量 BITGET_API_KEY, BITGET_SECRET_KEY, BITGET_PASSPHRASE") + } + + trader := NewBitgetTrader(apiKey, secretKey, passphrase) + + // 测试参数 - 使用最小仓位 + symbol := "BTCUSDT" + quantity := 0.001 // 最小数量 + leverage := 5 + stopLoss := 70000.0 // 止损价格(根据当前价格调整) + takeProfit := 110000.0 // 止盈价格 + + fmt.Printf("📊 测试 Bitget OpenLongWithTPSL\n") + fmt.Printf(" Symbol: %s\n", symbol) + fmt.Printf(" Quantity: %.4f\n", quantity) + fmt.Printf(" Leverage: %dx\n", leverage) + fmt.Printf(" StopLoss: %.2f\n", stopLoss) + fmt.Printf(" TakeProfit: %.2f\n", takeProfit) + + // 先获取当前价格 + price, err := trader.GetMarketPrice(symbol) + if err != nil { + t.Fatalf("获取价格失败: %v", err) + } + fmt.Printf(" 当前价格: %.2f\n", price) + + // 调整止盈止损 + stopLoss = price * 0.95 // 5% 止损 + takeProfit = price * 1.05 // 5% 止盈 + fmt.Printf(" 调整后 StopLoss: %.2f (%.1f%%)\n", stopLoss, (1-stopLoss/price)*100) + fmt.Printf(" 调整后 TakeProfit: %.2f (+%.1f%%)\n", takeProfit, (takeProfit/price-1)*100) + + // 执行下单 + fmt.Println("\n🚀 开始下单...") + result, err := trader.OpenLongWithTPSL(symbol, quantity, leverage, stopLoss, takeProfit) + if err != nil { + t.Fatalf("下单失败: %v", err) + } + + fmt.Printf("✅ 下单成功!\n") + fmt.Printf(" OrderId: %v\n", result["orderId"]) + fmt.Printf(" Symbol: %v\n", result["symbol"]) + fmt.Printf(" Status: %v\n", result["status"]) + + // 查看持仓确认 + fmt.Println("\n📋 查看持仓...") + positions, err := trader.GetPositions() + if err != nil { + t.Logf("⚠️ 获取持仓失败: %v", err) + } else { + for _, pos := range positions { + if pos["symbol"] == symbol || pos["symbol"] == "BTCUSDT" { + fmt.Printf(" 持仓: %v\n", pos) + } + } + } +} + +// 测试获取账户余额 +func TestGetBalance(t *testing.T) { + apiKey := os.Getenv("BITGET_API_KEY") + secretKey := os.Getenv("BITGET_SECRET_KEY") + passphrase := os.Getenv("BITGET_PASSPHRASE") + + if apiKey == "" || secretKey == "" || passphrase == "" { + t.Skip("跳过测试: 需要设置环境变量") + } + + trader := NewBitgetTrader(apiKey, secretKey, passphrase) + + balance, err := trader.GetBalance() + if err != nil { + t.Fatalf("获取余额失败: %v", err) + } + + fmt.Printf("💰 账户余额:\n") + for k, v := range balance { + fmt.Printf(" %s: %v\n", k, v) + } +} + +// 测试获取持仓 +func TestGetPositions(t *testing.T) { + apiKey := os.Getenv("BITGET_API_KEY") + secretKey := os.Getenv("BITGET_SECRET_KEY") + passphrase := os.Getenv("BITGET_PASSPHRASE") + + if apiKey == "" || secretKey == "" || passphrase == "" { + t.Skip("跳过测试: 需要设置环境变量") + } + + trader := NewBitgetTrader(apiKey, secretKey, passphrase) + + positions, err := trader.GetPositions() + if err != nil { + t.Fatalf("获取持仓失败: %v", err) + } + + fmt.Printf("📋 当前持仓 (%d):\n", len(positions)) + for i, pos := range positions { + fmt.Printf(" [%d] %v\n", i+1, pos) + } +}