Files
nofx/trader/gate/trader.go
tinkle-community 9ea9bd705f fix(trader): harden API calls with timeouts, strict balance parsing, error context
- binance/bybit/gate: SDK default http.DefaultClient has no timeout; use a
  dedicated 30s-timeout client so a hung connection cannot stall the loop
- bybit: stop mutating http.DefaultClient.Transport, which leaked the
  referer header into every other HTTP request in the process
- add types.ParseFloatField: empty exchange fields stay zero, but malformed
  numeric values now surface as errors instead of silently becoming zero
  balances (applied to GetBalance across 8 exchanges)
- wrap order/market-data errors in auto_trader_orders and okx cancel paths
  with symbol context; log per-order cancel failures in okx CancelAllOrders
2026-06-11 00:30:34 +08:00

119 lines
3.0 KiB
Go

package gate
import (
"context"
"fmt"
"net/http"
"nofx/trader/types"
"strings"
"sync"
"time"
"github.com/gateio/gateapi-go/v6"
)
// GateTrader implements types.Trader interface for Gate.io Futures
type GateTrader struct {
apiKey string
secretKey string
client *gateapi.APIClient
ctx context.Context
// Cache fields
cachedBalance map[string]interface{}
balanceCacheTime time.Time
balanceCacheMutex sync.RWMutex
cachedPositions []map[string]interface{}
positionsCacheTime time.Time
positionsCacheMutex sync.RWMutex
contractsCache map[string]*gateapi.Contract
contractsCacheMutex sync.RWMutex
cacheDuration time.Duration
}
// NewGateTrader creates a new Gate trader instance
func NewGateTrader(apiKey, secretKey string) *GateTrader {
config := gateapi.NewConfiguration()
config.AddDefaultHeader("X-Gate-Channel-Id", "nofx")
// The SDK default HTTP client has no timeout — a hung connection would
// stall the trading loop indefinitely.
config.HTTPClient = &http.Client{Timeout: 30 * time.Second}
client := gateapi.NewAPIClient(config)
ctx := context.WithValue(context.Background(),
gateapi.ContextGateAPIV4,
gateapi.GateAPIV4{
Key: apiKey,
Secret: secretKey,
},
)
return &GateTrader{
apiKey: apiKey,
secretKey: secretKey,
client: client,
ctx: ctx,
contractsCache: make(map[string]*gateapi.Contract),
cacheDuration: 15 * time.Second,
}
}
// convertSymbol converts symbol format (e.g., BTCUSDT -> BTC_USDT)
func (t *GateTrader) convertSymbol(symbol string) string {
// If already in correct format
if strings.Contains(symbol, "_") {
return symbol
}
// Convert BTCUSDT to BTC_USDT
if strings.HasSuffix(symbol, "USDT") {
base := strings.TrimSuffix(symbol, "USDT")
return base + "_USDT"
}
return symbol
}
// revertSymbol converts symbol back to standard format (e.g., BTC_USDT -> BTCUSDT)
func (t *GateTrader) revertSymbol(symbol string) string {
return strings.ReplaceAll(symbol, "_", "")
}
// getContract fetches contract info with caching
func (t *GateTrader) getContract(symbol string) (*gateapi.Contract, error) {
symbol = t.convertSymbol(symbol)
// Check cache
t.contractsCacheMutex.RLock()
if contract, ok := t.contractsCache[symbol]; ok {
t.contractsCacheMutex.RUnlock()
return contract, nil
}
t.contractsCacheMutex.RUnlock()
// Fetch from API
contract, _, err := t.client.FuturesApi.GetFuturesContract(t.ctx, "usdt", symbol)
if err != nil {
return nil, fmt.Errorf("failed to get contract info: %w", err)
}
// Update cache
t.contractsCacheMutex.Lock()
t.contractsCache[symbol] = &contract
t.contractsCacheMutex.Unlock()
return &contract, nil
}
// clearCache clears all caches
func (t *GateTrader) clearCache() {
t.balanceCacheMutex.Lock()
t.cachedBalance = nil
t.balanceCacheMutex.Unlock()
t.positionsCacheMutex.Lock()
t.cachedPositions = nil
t.positionsCacheMutex.Unlock()
}
// Ensure GateTrader implements Trader interface
var _ types.Trader = (*GateTrader)(nil)