mirror of
https://github.com/d0zingcat/gocryptotrader.git
synced 2026-05-13 15:09:42 +00:00
GateIO: Fix account sequence issue, expand structs, add tests (#2011)
* gateio: fix sequence issue, expand structs, add tests (cherry-pick my nose) * Update exchanges/gateio/gateio_websocket_futures.go Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * Update exchanges/gateio/gateio_types.go Co-authored-by: Gareth Kirwan <gbjkirwan@gmail.com> * Update exchanges/gateio/gateio_websocket_test.go Co-authored-by: Gareth Kirwan <gbjkirwan@gmail.com> * gk: nits * GateIO: Avoid nolint on containedctx * Update exchanges/gateio/gateio_websocket_test.go Co-authored-by: Gareth Kirwan <gbjkirwan@gmail.com> * fixup * Update exchanges/gateio/gateio_websocket_test.go Co-authored-by: Scott <gloriousCode@users.noreply.github.com> --------- Co-authored-by: Ryan O'Hara-Reid <ryan.oharareid@thrasher.io> Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> Co-authored-by: Gareth Kirwan <gbjkirwan@gmail.com> Co-authored-by: Scott <gloriousCode@users.noreply.github.com>
This commit is contained in:
@@ -396,7 +396,7 @@ func (b *ProtectedBalance) load(change *Balance) error {
|
||||
}
|
||||
b.m.Lock()
|
||||
defer b.m.Unlock()
|
||||
if !b.updatedAt.IsZero() && !b.updatedAt.Before(change.UpdatedAt) {
|
||||
if !b.updatedAt.IsZero() && b.updatedAt.After(change.UpdatedAt) {
|
||||
return errOutOfSequence
|
||||
}
|
||||
if b.total == change.Total &&
|
||||
|
||||
@@ -290,8 +290,11 @@ func TestBalanceInternalLoad(t *testing.T) {
|
||||
|
||||
assert.Equal(t, 3.0, bi.GetFree())
|
||||
|
||||
err = bi.load(&Balance{UpdatedAt: now.Add(-time.Second), Total: 2, Hold: 3, Free: 4, AvailableWithoutBorrow: 5, Borrowed: 6})
|
||||
assert.ErrorIs(t, err, errOutOfSequence, "should error correctly with old update trying to store")
|
||||
|
||||
err = bi.load(&Balance{UpdatedAt: now, Total: 2, Hold: 3, Free: 4, AvailableWithoutBorrow: 5, Borrowed: 6})
|
||||
assert.ErrorIs(t, err, errOutOfSequence, "should error correctly with same UpdatedAt")
|
||||
assert.NoError(t, err, "should not error when timestamps are the same")
|
||||
|
||||
err = bi.load(&Balance{UpdatedAt: now.Add(time.Second), Total: 2, Hold: 3, Free: 4, AvailableWithoutBorrow: 5, Borrowed: 6})
|
||||
assert.NoError(t, err)
|
||||
|
||||
@@ -2119,12 +2119,15 @@ type WsUserPersonalTrade struct {
|
||||
|
||||
// WsSpotBalance represents a spot balance.
|
||||
type WsSpotBalance struct {
|
||||
Timestamp types.Time `json:"timestamp_ms"`
|
||||
User string `json:"user"`
|
||||
Currency string `json:"currency"`
|
||||
Change types.Number `json:"change"`
|
||||
Total types.Number `json:"total"`
|
||||
Available types.Number `json:"available"`
|
||||
Timestamp types.Time `json:"timestamp_ms"`
|
||||
User string `json:"user"`
|
||||
Currency currency.Code `json:"currency"`
|
||||
Change types.Number `json:"change"`
|
||||
Total types.Number `json:"total"`
|
||||
Available types.Number `json:"available"`
|
||||
Freeze types.Number `json:"freeze"`
|
||||
FreezeChange types.Number `json:"freeze_change"`
|
||||
ChangeType string `json:"change_type"` // e.g. "order-create", "order-match"
|
||||
}
|
||||
|
||||
// WsMarginBalance represents margin account balance push data
|
||||
@@ -2329,12 +2332,13 @@ type WsPositionClose struct {
|
||||
|
||||
// WsBalance represents a options and futures balance push data
|
||||
type WsBalance struct {
|
||||
Balance float64 `json:"balance"`
|
||||
Change float64 `json:"change"`
|
||||
Text string `json:"text"`
|
||||
Time types.Time `json:"time_ms"`
|
||||
Type string `json:"type"`
|
||||
User string `json:"user"`
|
||||
Balance float64 `json:"balance"`
|
||||
Change float64 `json:"change"`
|
||||
Currency currency.Code `json:"currency"`
|
||||
Text string `json:"text"`
|
||||
Time types.Time `json:"time_ms"`
|
||||
Type string `json:"type"`
|
||||
User string `json:"user"`
|
||||
}
|
||||
|
||||
// WsFuturesReduceRiskLimitNotification represents a futures reduced risk limit push data
|
||||
|
||||
@@ -196,7 +196,7 @@ func (e *Exchange) WsHandleSpotData(ctx context.Context, conn websocket.Connecti
|
||||
case spotUserTradesChannel:
|
||||
return e.processUserPersonalTrades(respRaw)
|
||||
case spotBalancesChannel:
|
||||
return e.processSpotBalances(ctx, respRaw)
|
||||
return e.processSpotBalances(ctx, push.Result)
|
||||
case marginBalancesChannel:
|
||||
return e.processMarginBalances(ctx, respRaw)
|
||||
case spotFundingBalanceChannel:
|
||||
@@ -510,31 +510,28 @@ func (e *Exchange) processUserPersonalTrades(data []byte) error {
|
||||
}
|
||||
|
||||
func (e *Exchange) processSpotBalances(ctx context.Context, data []byte) error {
|
||||
resp := struct {
|
||||
Time types.Time `json:"time"`
|
||||
Channel string `json:"channel"`
|
||||
Event string `json:"event"`
|
||||
Result []WsSpotBalance `json:"result"`
|
||||
}{}
|
||||
err := json.Unmarshal(data, &resp)
|
||||
if err != nil {
|
||||
var resp []WsSpotBalance
|
||||
if err := json.Unmarshal(data, &resp); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
creds, err := e.GetCredentials(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
changes := make([]account.Change, len(resp.Result))
|
||||
for i := range resp.Result {
|
||||
|
||||
changes := make([]account.Change, len(resp))
|
||||
for i := range resp {
|
||||
changes[i] = account.Change{
|
||||
Account: resp.Result[i].User,
|
||||
Account: resp[i].User,
|
||||
AssetType: asset.Spot,
|
||||
Balance: &account.Balance{
|
||||
Currency: currency.NewCode(resp.Result[i].Currency),
|
||||
Total: resp.Result[i].Total.Float64(),
|
||||
Free: resp.Result[i].Available.Float64(),
|
||||
Hold: resp.Result[i].Total.Float64() - resp.Result[i].Available.Float64(),
|
||||
UpdatedAt: resp.Result[i].Timestamp.Time(),
|
||||
Currency: resp[i].Currency,
|
||||
Total: resp[i].Total.Float64(),
|
||||
Free: resp[i].Available.Float64(),
|
||||
Hold: resp[i].Freeze.Float64(),
|
||||
AvailableWithoutBorrow: resp[i].Available.Float64(),
|
||||
UpdatedAt: resp[i].Timestamp.Time(),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
@@ -177,7 +177,7 @@ func (e *Exchange) WsHandleFuturesData(ctx context.Context, conn websocket.Conne
|
||||
case futuresAutoPositionCloseChannel:
|
||||
return e.processPositionCloseData(respRaw)
|
||||
case futuresBalancesChannel:
|
||||
return e.processBalancePushData(ctx, respRaw, a)
|
||||
return e.processBalancePushData(ctx, push.Result, a)
|
||||
case futuresReduceRiskLimitsChannel:
|
||||
return e.processFuturesReduceRiskLimitNotification(respRaw)
|
||||
case futuresPositionsChannel:
|
||||
@@ -632,34 +632,31 @@ func (e *Exchange) processPositionCloseData(data []byte) error {
|
||||
}
|
||||
|
||||
func (e *Exchange) processBalancePushData(ctx context.Context, data []byte, assetType asset.Item) error {
|
||||
resp := struct {
|
||||
Time types.Time `json:"time"`
|
||||
Channel string `json:"channel"`
|
||||
Event string `json:"event"`
|
||||
Result []WsBalance `json:"result"`
|
||||
}{}
|
||||
err := json.Unmarshal(data, &resp)
|
||||
if err != nil {
|
||||
var resp []WsBalance
|
||||
if err := json.Unmarshal(data, &resp); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
creds, err := e.GetCredentials(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
changes := make([]account.Change, len(resp.Result))
|
||||
for x, bal := range resp.Result {
|
||||
info := strings.Split(bal.Text, currency.UnderscoreDelimiter)
|
||||
if len(info) != 2 {
|
||||
return errors.New("malformed text")
|
||||
|
||||
changes := make([]account.Change, len(resp))
|
||||
for x, bal := range resp {
|
||||
c := bal.Currency
|
||||
if assetType == asset.Options && c.IsEmpty() {
|
||||
c = currency.USDT // Settlement currency is USDT
|
||||
}
|
||||
changes[x] = account.Change{
|
||||
AssetType: assetType,
|
||||
Account: bal.User,
|
||||
Balance: &account.Balance{
|
||||
Currency: currency.NewCode(info[0]),
|
||||
Total: bal.Balance,
|
||||
Free: bal.Balance,
|
||||
UpdatedAt: bal.Time.Time(),
|
||||
Currency: c,
|
||||
Total: bal.Balance,
|
||||
Free: bal.Balance,
|
||||
AvailableWithoutBorrow: bal.Balance,
|
||||
UpdatedAt: bal.Time.Time(),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
@@ -330,7 +330,7 @@ func (e *Exchange) WsHandleOptionsData(ctx context.Context, conn websocket.Conne
|
||||
case optionsPositionCloseChannel:
|
||||
return e.processPositionCloseData(respRaw)
|
||||
case optionsBalancesChannel:
|
||||
return e.processBalancePushData(ctx, respRaw, asset.Options)
|
||||
return e.processBalancePushData(ctx, push.Result, asset.Options)
|
||||
case optionsPositionsChannel:
|
||||
return e.processOptionsPositionPushData(respRaw)
|
||||
case "options.pong":
|
||||
|
||||
@@ -1,11 +1,17 @@
|
||||
package gateio
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
gws "github.com/gorilla/websocket"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
"github.com/thrasher-corp/gocryptotrader/currency"
|
||||
exchange "github.com/thrasher-corp/gocryptotrader/exchanges"
|
||||
"github.com/thrasher-corp/gocryptotrader/exchanges/account"
|
||||
"github.com/thrasher-corp/gocryptotrader/exchanges/asset"
|
||||
)
|
||||
|
||||
func TestGetWSPingHandler(t *testing.T) {
|
||||
@@ -31,3 +37,163 @@ func TestGetWSPingHandler(t *testing.T) {
|
||||
require.Contains(t, string(got.Message), tc.channel)
|
||||
}
|
||||
}
|
||||
|
||||
type websocketBalancesTest struct {
|
||||
input []byte
|
||||
err error
|
||||
deployCreds bool
|
||||
expected []account.Change
|
||||
}
|
||||
|
||||
func TestProcessSpotBalances(t *testing.T) {
|
||||
t.Parallel()
|
||||
e := new(Exchange) //nolint:govet // Intentional shadow
|
||||
e.SetDefaults()
|
||||
e.Name = "ProcessSpotBalancesTest"
|
||||
|
||||
// Sequential tests, do not use t.Run(); Some timestamps are deliberately identical from trading activity
|
||||
for _, tc := range []websocketBalancesTest{
|
||||
{
|
||||
input: []byte(`[{"timestamp":"1755718222"}]`),
|
||||
err: exchange.ErrCredentialsAreEmpty,
|
||||
},
|
||||
{
|
||||
deployCreds: true,
|
||||
input: []byte(`[{"timestamp":"1755718222","timestamp_ms":"1755718222394","user":"12870774","currency":"USDT","change":"0","total":"3087.01142272991036062136","available":"3081.68642272991036062136","freeze":"5.325","freeze_change":"5.32500000000000000000","change_type":"order-create"}]`),
|
||||
expected: []account.Change{
|
||||
{
|
||||
Account: "12870774",
|
||||
AssetType: asset.Spot,
|
||||
Balance: &account.Balance{
|
||||
Currency: currency.USDT,
|
||||
Total: 3087.01142272991036062136,
|
||||
Free: 3081.68642272991036062136,
|
||||
Hold: 5.325,
|
||||
AvailableWithoutBorrow: 3081.68642272991036062136,
|
||||
UpdatedAt: time.UnixMilli(1755718222394),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
deployCreds: true,
|
||||
input: []byte(`[{"timestamp":"1755718222","timestamp_ms":"1755718222394","user":"12870774","currency":"USDT","change":"-3.99375000000000000000","total":"3083.01767272991036062136","available":"3081.68642272991036062136","freeze":"1.33125","freeze_change":"-3.99375000000000000000","change_type":"order-match"}]`),
|
||||
expected: []account.Change{
|
||||
{
|
||||
Account: "12870774",
|
||||
AssetType: asset.Spot,
|
||||
Balance: &account.Balance{
|
||||
Currency: currency.USDT,
|
||||
Total: 3083.01767272991036062136,
|
||||
Free: 3081.68642272991036062136,
|
||||
Hold: 1.33125,
|
||||
AvailableWithoutBorrow: 3081.68642272991036062136,
|
||||
UpdatedAt: time.UnixMilli(1755718222394),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
} {
|
||||
ctx := t.Context()
|
||||
if tc.deployCreds {
|
||||
ctx = account.DeployCredentialsToContext(ctx, &account.Credentials{Key: "test", Secret: "test"})
|
||||
}
|
||||
err := e.processSpotBalances(ctx, tc.input)
|
||||
if tc.err != nil {
|
||||
require.ErrorIs(t, err, tc.err)
|
||||
continue
|
||||
}
|
||||
require.NoError(t, err, "processSpotBalances must not error")
|
||||
checkAccountChange(ctx, t, e, &tc)
|
||||
}
|
||||
}
|
||||
|
||||
func TestProcessBalancePushData(t *testing.T) {
|
||||
t.Parallel()
|
||||
e := new(Exchange) //nolint:govet // Intentional shadow
|
||||
e.SetDefaults()
|
||||
e.Name = "ProcessFuturesBalancesTest"
|
||||
|
||||
// Sequential tests, do not use t.Run(); Some timestamps are deliberately identical from trading activity
|
||||
for _, tc := range []websocketBalancesTest{
|
||||
{
|
||||
input: []byte(`[{"timestamp":"1755718222"}]`),
|
||||
err: exchange.ErrCredentialsAreEmpty,
|
||||
},
|
||||
{
|
||||
deployCreds: true,
|
||||
input: []byte(`[{"balance":2214.191673190433,"change":-0.0025776,"currency":"usdt","text":"TCOM_USDT:263179103241933596","time":1755738515,"time_ms":1755738515671,"type":"fee","user":"12870774"}]`),
|
||||
expected: []account.Change{
|
||||
{
|
||||
Account: "12870774",
|
||||
AssetType: asset.USDTMarginedFutures,
|
||||
Balance: &account.Balance{
|
||||
Currency: currency.USDT,
|
||||
Total: 2214.191673190433,
|
||||
Free: 2214.191673190433,
|
||||
AvailableWithoutBorrow: 2214.191673190433,
|
||||
UpdatedAt: time.UnixMilli(1755738515671),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
deployCreds: true,
|
||||
input: []byte(`[{"balance":2214.189114310433,"change":-0.00255888,"currency":"usdt","text":"TCOM_USDT:263179103241933644","time":1755738516,"time_ms":1755738516430,"type":"fee","user":"12870774"}]`),
|
||||
expected: []account.Change{
|
||||
{
|
||||
Account: "12870774",
|
||||
AssetType: asset.USDTMarginedFutures,
|
||||
Balance: &account.Balance{
|
||||
Currency: currency.USDT,
|
||||
Total: 2214.189114310433,
|
||||
Free: 2214.189114310433,
|
||||
AvailableWithoutBorrow: 2214.189114310433,
|
||||
UpdatedAt: time.UnixMilli(1755738516430),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
} {
|
||||
ctx := t.Context()
|
||||
if tc.deployCreds {
|
||||
ctx = account.DeployCredentialsToContext(ctx, &account.Credentials{Key: "test", Secret: "test"})
|
||||
}
|
||||
err := e.processBalancePushData(ctx, tc.input, asset.USDTMarginedFutures)
|
||||
if tc.err != nil {
|
||||
require.ErrorIs(t, err, tc.err)
|
||||
continue
|
||||
}
|
||||
require.NoError(t, err, "processBalancePushData must not error")
|
||||
require.Len(t, e.Websocket.DataHandler, 1)
|
||||
checkAccountChange(ctx, t, e, &tc)
|
||||
}
|
||||
}
|
||||
|
||||
func checkAccountChange(ctx context.Context, t *testing.T, exch *Exchange, tc *websocketBalancesTest) {
|
||||
t.Helper()
|
||||
|
||||
require.Len(t, exch.Websocket.DataHandler, 1)
|
||||
payload := <-exch.Websocket.DataHandler
|
||||
received, ok := payload.([]account.Change)
|
||||
require.Truef(t, ok, "Expected account changes, got %T", payload)
|
||||
|
||||
require.Lenf(t, received, len(tc.expected), "Expected %d changes, got %d", len(tc.expected), len(received))
|
||||
for i, change := range received {
|
||||
assert.Equal(t, tc.expected[i].Account, change.Account, "account should equal")
|
||||
assert.Equal(t, tc.expected[i].AssetType, change.AssetType, "asset type should equal")
|
||||
assert.True(t, tc.expected[i].Balance.Currency.Equal(change.Balance.Currency), "currency should equal")
|
||||
assert.Equal(t, tc.expected[i].Balance.Total, change.Balance.Total, "total should equal")
|
||||
assert.Equal(t, tc.expected[i].Balance.Hold, change.Balance.Hold, "hold should equal")
|
||||
assert.Equal(t, tc.expected[i].Balance.Free, change.Balance.Free, "free should equal")
|
||||
assert.Equal(t, tc.expected[i].Balance.AvailableWithoutBorrow, change.Balance.AvailableWithoutBorrow, "available without borrow should equal")
|
||||
assert.Equal(t, tc.expected[i].Balance.Borrowed, change.Balance.Borrowed, "borrowed should equal")
|
||||
assert.Equal(t, tc.expected[i].Balance.UpdatedAt, change.Balance.UpdatedAt, "updated at should equal")
|
||||
|
||||
creds, err := exch.GetCredentials(ctx)
|
||||
require.NoError(t, err, "GetCredentials must not error")
|
||||
stored, err := account.GetBalance(exch.Name, tc.expected[i].Account, creds, tc.expected[i].AssetType, tc.expected[i].Balance.Currency)
|
||||
require.NoError(t, err, "GetBalance must not error")
|
||||
assert.Equal(t, tc.expected[i].Balance.Free, stored.GetFree(), "free balance should equal with accounts stored value")
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user