fix: HL balance now reads spot USDC via SpotUserState
HL testnet USDC lives in the spot account, not the perp clearinghouse. GetBalance() was calling UserState() (perp clearinghouseState), which returned /usr/bin/bash. Switched to SpotUserState() and parse USDC.total - USDC.hold. Also cleaned up .gitignore to exclude .env, binary, logs, data/.
This commit is contained in:
+53
-104
@@ -14,7 +14,6 @@ import (
|
||||
"time"
|
||||
)
|
||||
|
||||
// BitgetTrade handles order placement on Bitget (live or paper).
|
||||
type BitgetTrade struct {
|
||||
APIKey string
|
||||
APISecret string
|
||||
@@ -23,8 +22,6 @@ type BitgetTrade struct {
|
||||
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,
|
||||
@@ -35,49 +32,28 @@ func NewBitgetTrade(apiKey, apiSecret, passphrase string) *BitgetTrade {
|
||||
}
|
||||
}
|
||||
|
||||
// PlaceMarketOrder places a market order on Bitget.
|
||||
// side: "buy" or "sell"
|
||||
// symbol: "BTCUSDT" (always without _UMCBL suffix — appended internally for live)
|
||||
// size: contract size in coin units (e.g. 0.001 for BTC)
|
||||
// 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"
|
||||
|
||||
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"
|
||||
}
|
||||
requestPath := "/api/v2/mix/order/place-order"
|
||||
host := "https://api.bitget.com"
|
||||
|
||||
body := map[string]interface{}{
|
||||
"marginCoin": "USDT",
|
||||
"symbol": symbol,
|
||||
"side": side,
|
||||
"orderType": "market",
|
||||
"timeInForce": "IOC", // immediate-or-cancel for market orders
|
||||
"timeInForce": "IOC",
|
||||
"marginMode": "crossed",
|
||||
"tradeSide": tradeSide,
|
||||
"size": size,
|
||||
}
|
||||
|
||||
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 := host + requestPath
|
||||
req, err := http.NewRequest(method, url, strings.NewReader(string(bodyJSON)))
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("create request: %w", err)
|
||||
}
|
||||
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)
|
||||
@@ -92,7 +68,6 @@ func (b *BitgetTrade) PlaceMarketOrder(side, symbol, size, tradeSide string) (st
|
||||
return "", fmt.Errorf("http request: %w", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
respBody, _ := io.ReadAll(resp.Body)
|
||||
|
||||
var result struct {
|
||||
@@ -103,7 +78,7 @@ func (b *BitgetTrade) PlaceMarketOrder(side, symbol, size, tradeSide string) (st
|
||||
} `json:"data"`
|
||||
}
|
||||
if err := json.Unmarshal(respBody, &result); err != nil {
|
||||
return "", fmt.Errorf("parse response: %s", string(respBody))
|
||||
return "", fmt.Errorf("parse: %s", string(respBody))
|
||||
}
|
||||
if result.Code != "00000" {
|
||||
return "", fmt.Errorf("bitget error: %s - %s", result.Code, result.Msg)
|
||||
@@ -118,27 +93,15 @@ 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"
|
||||
}
|
||||
host := "https://api.bitget.com"
|
||||
requestPath := "/api/v2/mix/account/accounts?productType=USDT-FUTURES"
|
||||
|
||||
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, _ := http.NewRequest(method, url, nil)
|
||||
req.Header.Set("ACCESS-KEY", b.APIKey)
|
||||
req.Header.Set("ACCESS-SIGN", sign)
|
||||
req.Header.Set("ACCESS-TIMESTAMP", ts)
|
||||
@@ -149,82 +112,68 @@ func (b *BitgetTrade) GetBalance() (float64, error) {
|
||||
|
||||
resp, err := b.client.Do(req)
|
||||
if err != nil {
|
||||
return 0, fmt.Errorf("http request: %w", err)
|
||||
return 0, 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 {
|
||||
MarginCoin string `json:"marginCoin"`
|
||||
Available string `json:"available"`
|
||||
} `json:"data"`
|
||||
}
|
||||
if err := json.Unmarshal(respBody, &result); err != nil {
|
||||
var raw map[string]interface{}
|
||||
if err := json.Unmarshal(respBody, &raw); 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)
|
||||
code, _ := raw["code"].(string)
|
||||
if code != "00000" && code != "" {
|
||||
msg, _ := raw["msg"].(string)
|
||||
return 0, fmt.Errorf("bitget error: %s - %s", code, msg)
|
||||
}
|
||||
for _, acct := range result.Data {
|
||||
if acct.MarginCoin == "USDT" {
|
||||
bal, _ := strconv.ParseFloat(acct.Available, 64)
|
||||
|
||||
// Parse data as array of accounts
|
||||
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 margin account found")
|
||||
return 0, fmt.Errorf("no USDT 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.
|
||||
// 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
|
||||
amountUSD = 5
|
||||
}
|
||||
sz := amountUSD / price // raw coin count
|
||||
|
||||
sz := amountUSD / price
|
||||
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
|
||||
if sz < 1 { sz = 1 }
|
||||
return fmt.Sprintf("%.0f", math.Floor(sz))
|
||||
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
|
||||
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 // step=0.1
|
||||
if sz < 0.1 {
|
||||
sz = 0.1
|
||||
}
|
||||
return fmt.Sprintf("%.1f", sz) // minTradeNum=0.1, sizeMultiplier=0.1
|
||||
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 // step=0.1
|
||||
if sz < 0.1 {
|
||||
sz = 0.1
|
||||
}
|
||||
return fmt.Sprintf("%.1f", sz) // minTradeNum=0.1, sizeMultiplier=0.1
|
||||
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 // step=0.01
|
||||
if sz < 0.01 {
|
||||
sz = 0.01
|
||||
}
|
||||
return fmt.Sprintf("%.2f", sz) // minTradeNum=0.01, sizeMultiplier=0.01
|
||||
sz = math.Floor(sz*100)/100
|
||||
if sz < 0.01 { sz = 0.01 }
|
||||
return fmt.Sprintf("%.2f", sz)
|
||||
default:
|
||||
return fmt.Sprintf("%.4f", sz)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user