package exchange import ( "crypto/hmac" "crypto/sha256" "encoding/base64" "encoding/json" "fmt" "io" "math" "net/http" "strings" "time" ) // BitgetTrade handles order placement on Bitget. type BitgetTrade struct { APIKey string APISecret string Passphrase string client *http.Client } func NewBitgetTrade(apiKey, apiSecret, passphrase string) *BitgetTrade { return &BitgetTrade{ APIKey: apiKey, APISecret: apiSecret, Passphrase: passphrase, client: &http.Client{Timeout: 10 * time.Second}, } } // PlaceMarketOrder places a market order on Bitget. // side: "buy" or "sell" // symbol: "BTCUSDT" (we use UMCBL perpetual) // size: contract size in coin units (e.g. 0.001 for BTC) func (b *BitgetTrade) PlaceMarketOrder(side, symbol, size string) (string, error) { ts := fmt.Sprintf("%d", time.Now().UnixMilli()) method := "POST" requestPath := "/api/v2/mix/order/place" body := map[string]interface{}{ "symbol": symbol + "_UMCBL", "marginCoin": "USDT", "side": side, "orderType": "market", "size": size, "timeInForce": "IOC", // immediate-or-cancel for market orders } bodyJSON, _ := json.Marshal(body) sign := b.sign(method, requestPath, ts, string(bodyJSON)) url := "https://api.bitget.com" + requestPath req, err := http.NewRequest(method, url, strings.NewReader(string(bodyJSON))) if err != nil { return "", fmt.Errorf("create request: %w", err) } req.Header.Set("Content-Type", "application/json") 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) resp, err := b.client.Do(req) if err != nil { return "", 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 { OrderID string `json:"orderId"` } `json:"data"` } if err := json.Unmarshal(respBody, &result); err != nil { return "", fmt.Errorf("parse response: %s", string(respBody)) } if result.Code != "00000" { return "", fmt.Errorf("bitget error: %s - %s", result.Code, result.Msg) } return result.Data.OrderID, nil } func (b *BitgetTrade) sign(method, requestPath, timestamp, body string) string { raw := timestamp + method + requestPath + body mac := hmac.New(sha256.New, []byte(b.APISecret)) mac.Write([]byte(raw)) return base64.StdEncoding.EncodeToString(mac.Sum(nil)) } // 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. // Uses math.Floor to round DOWN to the nearest valid step (B#5: prevent rounding up). func GetBitgetSize(symbol string, amountUSD, price float64) string { if amountUSD < 5 { amountUSD = 5 // Bitget minimum notional } sz := amountUSD / price // raw coin count switch symbol { case "DOGEUSDT": if sz < 1 { sz = 1 } sz = math.Floor(sz) // step=1 return fmt.Sprintf("%.0f", sz) // minTradeNum=1, sizeMultiplier=1 case "LINKUSDT": if sz < 1 { sz = 1 } sz = math.Floor(sz) // step=1 return fmt.Sprintf("%.0f", sz) // minTradeNum=1, sizeMultiplier=1 case "ONDOUSDT": sz = math.Floor(sz*10) / 10 // step=0.1 if sz < 0.1 { sz = 0.1 } return fmt.Sprintf("%.1f", sz) // minTradeNum=0.1, sizeMultiplier=0.1 case "OPUSDT": sz = math.Floor(sz*10) / 10 // step=0.1 if sz < 0.1 { sz = 0.1 } return fmt.Sprintf("%.1f", sz) // minTradeNum=0.1, sizeMultiplier=0.1 case "WIFUSDT": sz = math.Floor(sz*10) / 10 // step=0.1 if sz < 0.1 { sz = 0.1 } return fmt.Sprintf("%.1f", sz) // minTradeNum=0.1, sizeMultiplier=0.1 case "ARBUSDT": sz = math.Floor(sz*100) / 100 // step=0.01 if sz < 0.01 { sz = 0.01 } return fmt.Sprintf("%.2f", sz) // minTradeNum=0.01, sizeMultiplier=0.01 default: return fmt.Sprintf("%.4f", sz) } }