Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
82 changes: 58 additions & 24 deletions trader/auto_trader.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
}

Expand Down Expand Up @@ -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
Expand All @@ -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
}

Expand Down
60 changes: 58 additions & 2 deletions trader/bitget/trader.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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 {
Expand Down Expand Up @@ -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
Expand All @@ -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 {
Expand Down Expand Up @@ -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)
Expand Down
119 changes: 119 additions & 0 deletions trader/bitget/trader_test.go
Original file line number Diff line number Diff line change
@@ -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)
}
}