From 30436bbe7500eab9355d8e481c41b4e5f22ffabc Mon Sep 17 00:00:00 2001 From: jackyu66git Date: Mon, 4 May 2026 15:28:08 +0800 Subject: [PATCH] fix: BG paper trading API - tradeSide param, correct host, marginMode --- exchange/bitget_trade.go | 99 +++++++++++++++++++++++++++++++++++++--- trader.go | 6 +-- 2 files changed, 95 insertions(+), 10 deletions(-) diff --git a/exchange/bitget_trade.go b/exchange/bitget_trade.go index e1a9efe..260e7f9 100644 --- a/exchange/bitget_trade.go +++ b/exchange/bitget_trade.go @@ -9,49 +9,71 @@ import ( "io" "math" "net/http" + "strconv" "strings" "time" ) -// BitgetTrade handles order placement on Bitget. +// BitgetTrade handles order placement on Bitget (live or paper). type BitgetTrade struct { APIKey string APISecret string Passphrase string client *http.Client + paperMode bool } +// NewBitgetTrade creates a BitgetTrade. Paper mode is auto-detected +// from the API key prefix: "bg_" → Bitget paper trading. func NewBitgetTrade(apiKey, apiSecret, passphrase string) *BitgetTrade { return &BitgetTrade{ APIKey: apiKey, APISecret: apiSecret, Passphrase: passphrase, client: &http.Client{Timeout: 10 * time.Second}, + paperMode: strings.HasPrefix(apiKey, "bg_"), } } // PlaceMarketOrder places a market order on Bitget. // side: "buy" or "sell" -// symbol: "BTCUSDT" (we use UMCBL perpetual) +// symbol: "BTCUSDT" (always without _UMCBL suffix — appended internally for live) // size: contract size in coin units (e.g. 0.001 for BTC) -func (b *BitgetTrade) PlaceMarketOrder(side, symbol, size string) (string, error) { +// tradeSide: "open" or "close" — only used in paper mode +func (b *BitgetTrade) PlaceMarketOrder(side, symbol, size, tradeSide string) (string, error) { ts := fmt.Sprintf("%d", time.Now().UnixMilli()) method := "POST" - requestPath := "/api/v2/mix/order/place" + + var requestPath string + var host string + if b.paperMode { + requestPath = "/api/v2/mix/order/place-order" + host = "https://api.bitget.com" + } else { + requestPath = "/api/v2/mix/order/place" + host = "https://api.bitget.com" + } body := map[string]interface{}{ - "symbol": symbol + "_UMCBL", "marginCoin": "USDT", "side": side, "orderType": "market", - "size": size, "timeInForce": "IOC", // immediate-or-cancel for market orders } + + if b.paperMode { + body["symbol"] = symbol + body["marginMode"] = "crossed" + body["tradeSide"] = tradeSide + } else { + body["symbol"] = symbol + "_UMCBL" + } + bodyJSON, _ := json.Marshal(body) sign := b.sign(method, requestPath, ts, string(bodyJSON)) - url := "https://api.bitget.com" + requestPath + url := host + requestPath req, err := http.NewRequest(method, url, strings.NewReader(string(bodyJSON))) if err != nil { return "", fmt.Errorf("create request: %w", err) @@ -61,6 +83,9 @@ func (b *BitgetTrade) PlaceMarketOrder(side, symbol, size string) (string, error req.Header.Set("ACCESS-SIGN", sign) req.Header.Set("ACCESS-TIMESTAMP", ts) req.Header.Set("ACCESS-PASSPHRASE", b.Passphrase) + if b.paperMode { + req.Header.Set("paptrading", "1") + } resp, err := b.client.Do(req) if err != nil { @@ -93,6 +118,66 @@ func (b *BitgetTrade) sign(method, requestPath, timestamp, body string) string { return base64.StdEncoding.EncodeToString(mac.Sum(nil)) } +// GetBalance queries available balance. +func (b *BitgetTrade) GetBalance() (float64, error) { + ts := fmt.Sprintf("%d", time.Now().UnixMilli()) + method := "GET" + + var host, requestPath string + if b.paperMode { + host = "https://api.bitget.com" + requestPath = "/api/v2/mix/account/accounts?productType=USDT-FUTURES" + } else { + host = "https://api.bitget.com" + requestPath = "/api/v2/mix/account/accounts?productType=UMCBL" + } + + sign := b.sign(method, requestPath, ts, "") + + url := host + requestPath + req, err := http.NewRequest(method, url, nil) + if err != nil { + return 0, fmt.Errorf("create request: %w", err) + } + req.Header.Set("ACCESS-KEY", b.APIKey) + req.Header.Set("ACCESS-SIGN", sign) + req.Header.Set("ACCESS-TIMESTAMP", ts) + req.Header.Set("ACCESS-PASSPHRASE", b.Passphrase) + if b.paperMode { + req.Header.Set("paptrading", "1") + } + + resp, err := b.client.Do(req) + if err != nil { + return 0, fmt.Errorf("http request: %w", err) + } + defer resp.Body.Close() + + respBody, _ := io.ReadAll(resp.Body) + + var result struct { + Code string `json:"code"` + Msg string `json:"msg"` + Data []struct { + MarginCoin string `json:"marginCoin"` + Available string `json:"available"` + } `json:"data"` + } + if err := json.Unmarshal(respBody, &result); err != nil { + return 0, fmt.Errorf("parse: %s", string(respBody)) + } + if result.Code != "00000" { + return 0, fmt.Errorf("bitget error: %s - %s", result.Code, result.Msg) + } + for _, acct := range result.Data { + if acct.MarginCoin == "USDT" { + bal, _ := strconv.ParseFloat(acct.Available, 64) + return bal, nil + } + } + return 0, fmt.Errorf("no USDT margin account found") +} + // GetBitgetSize calculates the contract size for a given USD amount. // Returns size as a decimal string complying with Bitget's USDT-FUTURES precision. // Enforces the exchange's minimum: minTradeNum contracts AND $5 min notional. diff --git a/trader.go b/trader.go index ed3f56a..c3168c8 100644 --- a/trader.go +++ b/trader.go @@ -1013,7 +1013,7 @@ func (t *Trader) placeOrder(leg *PositionLeg, side string, store *PriceStore) st } if leg.Exchange == ExBitget { size := exchange.GetBitgetSize(leg.Coin+"USDT", t.cfg.TradeAmountUSD, leg.EntryPrice) - oid, err := t.bitget.PlaceMarketOrder(side, leg.Coin+"USDT", size) + oid, err := t.bitget.PlaceMarketOrder(side, leg.Coin+"USDT", size, "open") if err != nil { return fmt.Sprintf("BG %s error: %v", side, err) } @@ -1064,7 +1064,7 @@ func (t *Trader) closeLeg(leg *PositionLeg) string { } if leg.Exchange == ExBitget { - resp, err := t.bitget.PlaceMarketOrder(side, leg.Coin+"USDT", leg.Size) + resp, err := t.bitget.PlaceMarketOrder(side, leg.Coin+"USDT", leg.Size, "close") if err != nil { return fmt.Sprintf("%v", err) } @@ -1234,7 +1234,7 @@ func (t *Trader) placeOrderAt(leg *PositionLeg, side string, store *PriceStore, } if leg.Exchange == ExBitget { size := exchange.GetBitgetSize(leg.Coin+"USDT", t.cfg.TradeAmountUSD, price) - _, err := t.bitget.PlaceMarketOrder(side, leg.Coin+"USDT", size) + _, err := t.bitget.PlaceMarketOrder(side, leg.Coin+"USDT", size, "open") if err != nil { return fmt.Sprintf("BG %s error: %v", side, err) }