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) }