mirror of
https://github.com/NoFxAiOS/nofx.git
synced 2026-07-03 02:50:59 +08:00
- 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
170 lines
4.4 KiB
Go
170 lines
4.4 KiB
Go
package gate
|
|
|
|
import (
|
|
"fmt"
|
|
"nofx/trader/types"
|
|
"strconv"
|
|
"time"
|
|
|
|
"github.com/antihax/optional"
|
|
"github.com/gateio/gateapi-go/v6"
|
|
)
|
|
|
|
// GetBalance retrieves account balance
|
|
func (t *GateTrader) GetBalance() (map[string]interface{}, error) {
|
|
// Check cache
|
|
t.balanceCacheMutex.RLock()
|
|
if t.cachedBalance != nil && time.Since(t.balanceCacheTime) < t.cacheDuration {
|
|
cached := t.cachedBalance
|
|
t.balanceCacheMutex.RUnlock()
|
|
return cached, nil
|
|
}
|
|
t.balanceCacheMutex.RUnlock()
|
|
|
|
// Fetch from API
|
|
accounts, _, err := t.client.FuturesApi.ListFuturesAccounts(t.ctx, "usdt")
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to get balance: %w", err)
|
|
}
|
|
|
|
total, err := types.ParseFloatField("total", accounts.Total)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
available, err := types.ParseFloatField("available", accounts.Available)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
unrealizedPnl, err := types.ParseFloatField("unrealisedPnl", accounts.UnrealisedPnl)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
result := map[string]interface{}{
|
|
"totalWalletBalance": total,
|
|
"availableBalance": available,
|
|
"totalUnrealizedProfit": unrealizedPnl,
|
|
}
|
|
|
|
// Update cache
|
|
t.balanceCacheMutex.Lock()
|
|
t.cachedBalance = result
|
|
t.balanceCacheTime = time.Now()
|
|
t.balanceCacheMutex.Unlock()
|
|
|
|
return result, nil
|
|
}
|
|
|
|
// GetPositions retrieves all open positions
|
|
func (t *GateTrader) GetPositions() ([]map[string]interface{}, error) {
|
|
// Check cache
|
|
t.positionsCacheMutex.RLock()
|
|
if t.cachedPositions != nil && time.Since(t.positionsCacheTime) < t.cacheDuration {
|
|
cached := t.cachedPositions
|
|
t.positionsCacheMutex.RUnlock()
|
|
return cached, nil
|
|
}
|
|
t.positionsCacheMutex.RUnlock()
|
|
|
|
// Fetch from API
|
|
positions, _, err := t.client.FuturesApi.ListPositions(t.ctx, "usdt", nil)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to get positions: %w", err)
|
|
}
|
|
|
|
var result []map[string]interface{}
|
|
for _, pos := range positions {
|
|
if pos.Size == 0 {
|
|
continue // Skip empty positions
|
|
}
|
|
|
|
entryPrice, _ := strconv.ParseFloat(pos.EntryPrice, 64)
|
|
markPrice, _ := strconv.ParseFloat(pos.MarkPrice, 64)
|
|
liqPrice, _ := strconv.ParseFloat(pos.LiqPrice, 64)
|
|
unrealizedPnl, _ := strconv.ParseFloat(pos.UnrealisedPnl, 64)
|
|
leverage, _ := strconv.ParseFloat(pos.Leverage, 64)
|
|
|
|
// Gate returns position size in contracts, need to convert to base currency
|
|
// Each contract = quanto_multiplier base currency
|
|
contractSize := float64(pos.Size)
|
|
if pos.Size < 0 {
|
|
contractSize = float64(-pos.Size)
|
|
}
|
|
|
|
// Get quanto_multiplier from contract info to convert contracts to actual quantity
|
|
quantoMultiplier := 1.0
|
|
contract, err := t.getContract(pos.Contract)
|
|
if err == nil && contract != nil {
|
|
qm, _ := strconv.ParseFloat(contract.QuantoMultiplier, 64)
|
|
if qm > 0 {
|
|
quantoMultiplier = qm
|
|
}
|
|
}
|
|
|
|
// Convert contract count to actual token quantity
|
|
positionAmt := contractSize * quantoMultiplier
|
|
|
|
// Determine side based on position size
|
|
side := "long"
|
|
if pos.Size < 0 {
|
|
side = "short"
|
|
}
|
|
|
|
result = append(result, map[string]interface{}{
|
|
"symbol": pos.Contract,
|
|
"positionAmt": positionAmt,
|
|
"entryPrice": entryPrice,
|
|
"markPrice": markPrice,
|
|
"unRealizedProfit": unrealizedPnl,
|
|
"leverage": int(leverage),
|
|
"liquidationPrice": liqPrice,
|
|
"side": side,
|
|
})
|
|
}
|
|
|
|
// Update cache
|
|
t.positionsCacheMutex.Lock()
|
|
t.cachedPositions = result
|
|
t.positionsCacheTime = time.Now()
|
|
t.positionsCacheMutex.Unlock()
|
|
|
|
return result, nil
|
|
}
|
|
|
|
// GetClosedPnL retrieves closed position PnL records
|
|
func (t *GateTrader) GetClosedPnL(startTime time.Time, limit int) ([]types.ClosedPnLRecord, error) {
|
|
if limit <= 0 {
|
|
limit = 100
|
|
}
|
|
if limit > 100 {
|
|
limit = 100
|
|
}
|
|
|
|
opts := &gateapi.ListPositionCloseOpts{
|
|
Limit: optional.NewInt32(int32(limit)),
|
|
From: optional.NewInt64(startTime.Unix()),
|
|
}
|
|
|
|
closedPositions, _, err := t.client.FuturesApi.ListPositionClose(t.ctx, "usdt", opts)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to get closed positions: %w", err)
|
|
}
|
|
|
|
records := make([]types.ClosedPnLRecord, 0, len(closedPositions))
|
|
for _, pos := range closedPositions {
|
|
pnl, _ := strconv.ParseFloat(pos.Pnl, 64)
|
|
|
|
record := types.ClosedPnLRecord{
|
|
Symbol: t.revertSymbol(pos.Contract),
|
|
Side: pos.Side,
|
|
RealizedPnL: pnl,
|
|
ExitTime: time.Unix(int64(pos.Time), 0).UTC(),
|
|
CloseType: "unknown",
|
|
}
|
|
|
|
records = append(records, record)
|
|
}
|
|
|
|
return records, nil
|
|
}
|