package exchange import ( "crypto/hmac" "crypto/sha256" "encoding/base64" "encoding/json" "fmt" "io" "math" "net/http" "strconv" "strings" "time" ) type BitgetTrade struct { APIKey string APISecret string Passphrase string client *http.Client paperMode bool } 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_"), } } 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-order" host := "https://api.bitget.com" body := map[string]interface{}{ "marginCoin": "USDT", "symbol": symbol, "productType": "USDT-FUTURES", "side": side, "orderType": "market", "timeInForce": "IOC", "marginMode": "crossed", "tradeSide": tradeSide, "size": size, } bodyJSON, _ := json.Marshal(body) sign := b.sign(method, requestPath, ts, string(bodyJSON)) url := host + requestPath req, _ := http.NewRequest(method, url, strings.NewReader(string(bodyJSON))) 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) if b.paperMode { req.Header.Set("paptrading", "1") } 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: %s", string(respBody)) } if result.Code != "00000" { return "", fmt.Errorf("bitget error: %s - %s", result.Code, result.Msg) } return result.Data.OrderID, nil } // GetTradeFee queries the fills endpoint for actual fee charged. func (b *BitgetTrade) GetTradeFee(symbol, orderID string) (feeUSD float64, err error) { ts := fmt.Sprintf("%d", time.Now().UnixMilli()) method := "GET" requestPath := "/api/v2/mix/order/fills?symbol=" + symbol + "&orderId=" + orderID host := "https://api.bitget.com" sign := b.sign(method, requestPath, ts, "") url := host + requestPath req, _ := http.NewRequest(method, url, nil) 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: %w", err) } defer resp.Body.Close() respBody, _ := io.ReadAll(resp.Body) var raw struct { Code string `json:"code"` Msg string `json:"msg"` Data []json.RawMessage `json:"data"` } if err := json.Unmarshal(respBody, &raw); err != nil { return 0, fmt.Errorf("parse: %s", string(respBody)) } if raw.Code != "00000" { return 0, fmt.Errorf("bitget error: %s - %s", raw.Code, raw.Msg) } var totalFee float64 for _, item := range raw.Data { var fill struct { FillFee string `json:"fillFee"` } if err := json.Unmarshal(item, &fill); err != nil { continue } f, _ := strconv.ParseFloat(fill.FillFee, 64) totalFee += math.Abs(f) } return totalFee, 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)) } func (b *BitgetTrade) GetBalance() (float64, error) { ts := fmt.Sprintf("%d", time.Now().UnixMilli()) method := "GET" host := "https://api.bitget.com" requestPath := "/api/v2/mix/account/accounts?productType=USDT-FUTURES" sign := b.sign(method, requestPath, ts, "") url := host + requestPath req, _ := http.NewRequest(method, url, nil) 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: %w", err) } defer resp.Body.Close() respBody, _ := io.ReadAll(resp.Body) var raw map[string]interface{} if err := json.Unmarshal(respBody, &raw); err != nil { return 0, fmt.Errorf("parse: %s", string(respBody)) } code, _ := raw["code"].(string) if code != "00000" && code != "" { msg, _ := raw["msg"].(string) return 0, fmt.Errorf("bitget error: %s - %s", code, msg) } dataRaw, ok := raw["data"] if !ok || dataRaw == nil { return 0, fmt.Errorf("no data in response") } dataArr, ok := dataRaw.([]interface{}) if !ok { return 0, fmt.Errorf("unexpected data format") } for _, item := range dataArr { acct, ok := item.(map[string]interface{}) if !ok { continue } if acct["marginCoin"] == "USDT" { bal, _ := strconv.ParseFloat(fmt.Sprint(acct["available"]), 64) return bal, nil } } return 0, fmt.Errorf("no USDT account found") } func GetBitgetSize(symbol string, amountUSD, price float64) string { if amountUSD < 5 { amountUSD = 5 } sz := amountUSD / price switch symbol { case "DOGEUSDT": if sz < 1 { sz = 1 } return fmt.Sprintf("%.0f", math.Floor(sz)) case "ONDOUSDT": sz = math.Floor(sz*10)/10 if sz < 0.1 { sz = 0.1 } return fmt.Sprintf("%.1f", sz) case "OPUSDT": sz = math.Floor(sz*10)/10 if sz < 0.1 { sz = 0.1 } return fmt.Sprintf("%.1f", sz) case "WIFUSDT": sz = math.Floor(sz*10)/10 if sz < 0.1 { sz = 0.1 } return fmt.Sprintf("%.1f", sz) case "ARBUSDT": sz = math.Floor(sz*100)/100 if sz < 0.01 { sz = 0.01 } return fmt.Sprintf("%.2f", sz) default: return fmt.Sprintf("%.4f", sz) } }