250 lines
6.9 KiB
Go
250 lines
6.9 KiB
Go
package main
|
|
|
|
import (
|
|
"crypto/hmac"
|
|
"crypto/sha256"
|
|
"encoding/base64"
|
|
"encoding/json"
|
|
"fmt"
|
|
"io"
|
|
"log"
|
|
"net/http"
|
|
"os"
|
|
"strconv"
|
|
"strings"
|
|
"time"
|
|
)
|
|
|
|
func main() {
|
|
log.SetFlags(log.Ltime | log.Lmicroseconds)
|
|
log.Println("=== Bitget 开仓/平仓测试 v2 ===")
|
|
|
|
apiKey := os.Getenv("BITGET_API_KEY")
|
|
apiSecret := os.Getenv("BITGET_API_SECRET")
|
|
passphrase := os.Getenv("BITGET_PASSPHRASE")
|
|
if apiKey == "" || apiSecret == "" || passphrase == "" {
|
|
log.Fatal("环境变量: BITGET_API_KEY, BITGET_API_SECRET, BITGET_PASSPHRASE")
|
|
}
|
|
|
|
client := &http.Client{Timeout: 10 * time.Second}
|
|
headers := func(method, path, body string) map[string]string {
|
|
ts := fmt.Sprintf("%d", time.Now().UnixMilli())
|
|
raw := ts + method + path + body
|
|
mac := hmac.New(sha256.New, []byte(apiSecret))
|
|
mac.Write([]byte(raw))
|
|
sign := base64.StdEncoding.EncodeToString(mac.Sum(nil))
|
|
return map[string]string{
|
|
"ACCESS-KEY": apiKey,
|
|
"ACCESS-SIGN": sign,
|
|
"ACCESS-TIMESTAMP": ts,
|
|
"ACCESS-PASSPHRASE": passphrase,
|
|
}
|
|
}
|
|
|
|
symbol := "MEMEUSDT"
|
|
mode := "open"
|
|
if len(os.Args) > 1 {
|
|
mode = os.Args[1]
|
|
}
|
|
|
|
if mode == "close" {
|
|
// === 只平仓 (手动) ===
|
|
log.Println("--- 只用 holdSide 测试平仓 ---")
|
|
closeWithHoldSide(client, headers, symbol, "17280.1105", "long")
|
|
pos := checkPos(client, headers, symbol)
|
|
if pos != "" {
|
|
log.Printf("⚠️ 仍有持仓: %s", pos)
|
|
} else {
|
|
log.Println("✅ 已清仓")
|
|
}
|
|
return
|
|
}
|
|
|
|
// === 开仓 + 平仓 (自动) ===
|
|
price := getPrice(client)
|
|
log.Printf("当前价格: $%.6f", price)
|
|
|
|
sz := flooredSize(price, 10, 4)
|
|
log.Printf("目标开仓数量: %s (for $10)", sz)
|
|
|
|
// 开仓
|
|
log.Println("\n--- 开仓: BUY (Long, open) ---")
|
|
openOID := doPlace(client, headers, map[string]interface{}{
|
|
"marginCoin": "USDT", "symbol": symbol,
|
|
"productType": "USDT-FUTURES", "side": "buy",
|
|
"orderType": "market", "timeInForce": "IOC",
|
|
"marginMode": "crossed", "tradeSide": "open",
|
|
"size": sz,
|
|
})
|
|
log.Printf("开仓 orderID=%s", openOID)
|
|
|
|
// 等 + 查持仓
|
|
time.Sleep(3 * time.Second)
|
|
pos := checkPos(client, headers, symbol)
|
|
log.Printf("开仓后持仓: %s", pos)
|
|
|
|
if pos != "" {
|
|
// 有仓 → 尝试平仓 (holdSide)
|
|
log.Println("\n--- 平仓: SELL close holdSide=long ---")
|
|
closeWithHoldSide(client, headers, symbol, sz, "long")
|
|
time.Sleep(1 * time.Second)
|
|
pos2 := checkPos(client, headers, symbol)
|
|
if pos2 != "" {
|
|
// 试另一种方式: 不带 holdSide
|
|
log.Println("\n--- 再试: SELL close 不带holdSide ---")
|
|
_, err := doPlaceRaw(client, headers, map[string]interface{}{
|
|
"marginCoin": "USDT", "symbol": symbol,
|
|
"productType": "USDT-FUTURES", "side": "sell",
|
|
"orderType": "market", "timeInForce": "IOC",
|
|
"marginMode": "crossed", "tradeSide": "close",
|
|
"size": sz,
|
|
})
|
|
if err != nil {
|
|
log.Printf("❌ 无 holdSide 也失败: %v", err)
|
|
} else {
|
|
log.Println("✅ 无holdSide平仓成功!")
|
|
}
|
|
}
|
|
time.Sleep(1 * time.Second)
|
|
log.Printf("最终持仓: %s", checkPos(client, headers, symbol))
|
|
} else {
|
|
log.Println("ℹ️ 无持仓,可能开仓未成交")
|
|
}
|
|
|
|
log.Println("\n=== 测试完成 ===")
|
|
}
|
|
|
|
func closeWithHoldSide(client *http.Client, hdr func(m, p, b string) map[string]string, symbol, size, holdSide string) {
|
|
oid, err := doPlaceRaw(client, hdr, map[string]interface{}{
|
|
"marginCoin": "USDT", "symbol": symbol,
|
|
"productType": "USDT-FUTURES", "side": "sell",
|
|
"orderType": "market", "timeInForce": "IOC",
|
|
"marginMode": "crossed", "tradeSide": "close",
|
|
"holdSide": holdSide, "size": size,
|
|
})
|
|
if err != nil {
|
|
if strings.Contains(err.Error(), "22002") {
|
|
log.Printf("❌ holdSide=%s 返回 22002(无仓位可平)", holdSide)
|
|
} else {
|
|
log.Printf("❌ holdSide=%s 失败: %v", holdSide, err)
|
|
}
|
|
} else {
|
|
log.Printf("✅ holdSide=%s 平仓成功 orderID=%s", holdSide, oid)
|
|
}
|
|
}
|
|
|
|
// --- helpers 跟之前一样 ---
|
|
|
|
func doPlace(client *http.Client, hdr func(m, p, b string) map[string]string, body map[string]interface{}) string {
|
|
oid, err := doPlaceRaw(client, hdr, body)
|
|
if err != nil {
|
|
log.Fatalf("下单失败: %v", err)
|
|
}
|
|
return oid
|
|
}
|
|
|
|
func doPlaceRaw(client *http.Client, hdr func(m, p, b string) map[string]string, body map[string]interface{}) (string, error) {
|
|
method := "POST"
|
|
path := "/api/v2/mix/order/place-order"
|
|
bodyJSON, _ := json.Marshal(body)
|
|
h := hdr(method, path, string(bodyJSON))
|
|
|
|
req, _ := http.NewRequest(method, "https://api.bitget.com"+path, strings.NewReader(string(bodyJSON)))
|
|
req.Header.Set("Content-Type", "application/json")
|
|
for k, v := range h {
|
|
req.Header.Set(k, v)
|
|
}
|
|
resp, err := client.Do(req)
|
|
if err != nil {
|
|
return "", fmt.Errorf("http: %w", err)
|
|
}
|
|
defer resp.Body.Close()
|
|
respBody, _ := io.ReadAll(resp.Body)
|
|
|
|
var result struct {
|
|
Code string `json:"code"`
|
|
Msg string `json:"msg"`
|
|
Data struct {
|
|
OrderID string `json:"orderId"`
|
|
} `json:"data"`
|
|
}
|
|
json.Unmarshal(respBody, &result)
|
|
log.Printf(" → 返回: code=%s msg=%s orderID=%s", result.Code, result.Msg, result.Data.OrderID)
|
|
if result.Code != "00000" {
|
|
return "", fmt.Errorf("%s - %s", result.Code, result.Msg)
|
|
}
|
|
return result.Data.OrderID, nil
|
|
}
|
|
|
|
func checkPos(client *http.Client, hdr func(m, p, b string) map[string]string, symbol string) string {
|
|
method := "GET"
|
|
path := "/api/v2/mix/position/single-position?symbol=" + symbol + "&productType=USDT-FUTURES&marginCoin=USDT"
|
|
h := hdr(method, path, "")
|
|
req, _ := http.NewRequest(method, "https://api.bitget.com"+path, nil)
|
|
for k, v := range h {
|
|
req.Header.Set(k, v)
|
|
}
|
|
resp, err := client.Do(req)
|
|
if err != nil {
|
|
return ""
|
|
}
|
|
defer resp.Body.Close()
|
|
respBody, _ := io.ReadAll(resp.Body)
|
|
log.Printf(" → 持仓API: %s", string(respBody))
|
|
|
|
var raw struct {
|
|
Code string `json:"code"`
|
|
Msg string `json:"msg"`
|
|
Data []struct {
|
|
Symbol string `json:"symbol"`
|
|
HoldSide string `json:"holdSide"`
|
|
Total string `json:"total"`
|
|
Available string `json:"available"`
|
|
} `json:"data"`
|
|
}
|
|
json.Unmarshal(respBody, &raw)
|
|
if raw.Code != "00000" {
|
|
return ""
|
|
}
|
|
if len(raw.Data) > 0 {
|
|
d := raw.Data[0]
|
|
return fmt.Sprintf("%s %s total=%s avai=%s", d.Symbol, d.HoldSide, d.Total, d.Available)
|
|
}
|
|
return ""
|
|
}
|
|
|
|
func getPrice(client *http.Client) float64 {
|
|
resp, err := client.Get("https://api.bitget.com/api/v2/mix/market/tickers?productType=USDT-FUTURES")
|
|
if err != nil {
|
|
return 0
|
|
}
|
|
defer resp.Body.Close()
|
|
body, _ := io.ReadAll(resp.Body)
|
|
var raw struct {
|
|
Code string `json:"code"`
|
|
Data []struct {
|
|
Symbol string `json:"symbol"`
|
|
Last string `json:"lastPr"`
|
|
} `json:"data"`
|
|
}
|
|
json.Unmarshal(body, &raw)
|
|
for _, d := range raw.Data {
|
|
if d.Symbol == "MEMEUSDT" {
|
|
p, _ := strconv.ParseFloat(d.Last, 64)
|
|
return p
|
|
}
|
|
}
|
|
return 0
|
|
}
|
|
|
|
func flooredSize(price float64, usd, decimals int) string {
|
|
sz := float64(usd) / price
|
|
div := 1
|
|
for i := 0; i < decimals; i++ {
|
|
div *= 10
|
|
}
|
|
f := float64(div)
|
|
floored := float64(int64(sz*f)) / f
|
|
return fmt.Sprintf("%."+fmt.Sprintf("%d", decimals)+"f", floored)
|
|
}
|