Files
gocryptotrader/exchanges/gateio/gateio_websocket_test.go
Ryan O'Hara-Reid cf54764cb7 GateIO: Update websocket orderbook manager (#1989)
* gateio: websocket ob manager fix (cherry-pick me)

* fix(gateio): update websocket orderbook manager to support delay and deadline parameters
    feat(subscriptions): mv ChannelKey type for subscription management from gateio to subscriptions
    test(gateio): enhance tests for orderbook update manager and subscription keys

* ai: nits

* linter: fix

* Fix asset typo and add in case test

* cranktakular: nits

* cranktakular: nits after merge master

* bump time delay for cache

* fix bug where on error it never initiates another orderbook fetch

* lint: fix

* Update exchanges/gateio/ws_ob_update_manager.go

Co-authored-by: Scott <gloriousCode@users.noreply.github.com>

* glorious: nits

* linter: fix

* Update exchanges/gateio/gateio_wrapper_test.go

Co-authored-by: Gareth Kirwan <gbjkirwan@gmail.com>

* Update exchanges/gateio/gateio_wrapper.go

Co-authored-by: Gareth Kirwan <gbjkirwan@gmail.com>

* Update exchanges/gateio/gateio_wrapper_test.go

Co-authored-by: Gareth Kirwan <gbjkirwan@gmail.com>

* Update exchanges/gateio/ws_ob_update_manager.go

Co-authored-by: Gareth Kirwan <gbjkirwan@gmail.com>

* Update exchanges/gateio/ws_ob_update_manager.go

Co-authored-by: Gareth Kirwan <gbjkirwan@gmail.com>

* Update exchanges/gateio/ws_ob_update_manager.go

Co-authored-by: Gareth Kirwan <gbjkirwan@gmail.com>

* Update exchanges/gateio/ws_ob_update_manager_test.go

Co-authored-by: Gareth Kirwan <gbjkirwan@gmail.com>

* gk: nits

* Update exchanges/gateio/gateio_wrapper.go

Co-authored-by: Gareth Kirwan <gbjkirwan@gmail.com>

* Update exchanges/gateio/ws_ob_update_manager.go

Co-authored-by: Gareth Kirwan <gbjkirwan@gmail.com>

* gk: nits

* bossking: nits

* Update exchanges/gateio/ws_ob_update_manager_test.go

Co-authored-by: Gareth Kirwan <gbjkirwan@gmail.com>

* gk: nits

* apply patch

* rm error state as this was on the same thread as cacheStateQueuing and had the potential to drop messages, add tests.

* linter: fix

* mock live request

* misc: fix

* Update exchanges/gateio/ws_ob_update_manager.go

Co-authored-by: Gareth Kirwan <gbjkirwan@gmail.com>

* Update exchanges/gateio/ws_ob_update_manager.go

Co-authored-by: Gareth Kirwan <gbjkirwan@gmail.com>

* Update exchanges/gateio/ws_ob_update_manager.go

Co-authored-by: Gareth Kirwan <gbjkirwan@gmail.com>

* Update exchanges/gateio/ws_ob_update_manager.go

Co-authored-by: Gareth Kirwan <gbjkirwan@gmail.com>

* Update exchanges/gateio/ws_ob_update_manager.go

Co-authored-by: Gareth Kirwan <gbjkirwan@gmail.com>

* gk: nits

* field name from mtx -> m

* lint: fix

* race: check fix

* thrasher-: patch adams

---------

Co-authored-by: Ryan O'Hara-Reid <ryan.oharareid@thrasher.io>
Co-authored-by: Scott <gloriousCode@users.noreply.github.com>
Co-authored-by: Gareth Kirwan <gbjkirwan@gmail.com>
2025-11-27 12:21:17 +11:00

254 lines
8.9 KiB
Go

package gateio
import (
"context"
"maps"
"slices"
"strconv"
"testing"
"time"
gws "github.com/gorilla/websocket"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/thrasher-corp/gocryptotrader/currency"
"github.com/thrasher-corp/gocryptotrader/exchange/accounts"
exchange "github.com/thrasher-corp/gocryptotrader/exchanges"
"github.com/thrasher-corp/gocryptotrader/exchanges/asset"
"github.com/thrasher-corp/gocryptotrader/exchanges/kline"
"github.com/thrasher-corp/gocryptotrader/exchanges/subscription"
testexch "github.com/thrasher-corp/gocryptotrader/internal/testing/exchange"
)
func TestGetWSPingHandler(t *testing.T) {
t.Parallel()
for _, tc := range []struct {
channel string
err error
}{
{optionsPingChannel, nil},
{futuresPingChannel, nil},
{spotPingChannel, nil},
{"dong", errInvalidPingChannel},
} {
got, err := getWSPingHandler(tc.channel)
if tc.err != nil {
require.ErrorIs(t, err, tc.err)
continue
}
require.NoError(t, err)
require.Equal(t, time.Second*10, got.Delay)
require.Equal(t, gws.TextMessage, got.MessageType)
require.Contains(t, string(got.Message), tc.channel)
}
}
type websocketBalancesTest struct {
input []byte
err error
deployCreds bool
expected accounts.SubAccounts
}
func TestProcessSpotBalances(t *testing.T) { //nolint:tparallel // Sequential tests, do not use t.Parallel(); Some timestamps are deliberately identical from trading activity
t.Parallel()
e := new(Exchange)
e.SetDefaults()
e.Name = "ProcessSpotBalancesTest"
e.Accounts = accounts.MustNewAccounts(e)
for i, 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: accounts.SubAccounts{
{
ID: "12870774",
AssetType: asset.Spot,
Balances: accounts.CurrencyBalances{
currency.USDT: accounts.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: accounts.SubAccounts{
{
ID: "12870774",
AssetType: asset.Spot,
Balances: accounts.CurrencyBalances{
currency.USDT: accounts.Balance{
Currency: currency.USDT,
Total: 3083.01767272991036062136,
Free: 3081.68642272991036062136,
Hold: 1.33125,
AvailableWithoutBorrow: 3081.68642272991036062136,
UpdatedAt: time.UnixMilli(1755718222394),
},
},
},
},
},
} {
t.Run(strconv.Itoa(i), func(t *testing.T) {
// Sequential tests, do not use t.Parallel(); Some timestamps are deliberately identical from trading activity
ctx := t.Context()
if tc.deployCreds {
ctx = accounts.DeployCredentialsToContext(ctx, &accounts.Credentials{Key: "test", Secret: "test"})
}
err := e.processSpotBalances(ctx, tc.input)
if tc.err != nil {
require.ErrorIs(t, err, tc.err)
} else {
require.NoError(t, err, "processSpotBalances must not error")
checkAccountChange(ctx, t, e, &tc)
}
})
}
}
func TestProcessBalancePushData(t *testing.T) { //nolint:tparallel // Sequential tests, do not use t.Parallel(); Some timestamps are deliberately identical from trading activity
t.Parallel()
e := new(Exchange)
e.SetDefaults()
e.Name = "ProcessFuturesBalancesTest"
e.Accounts = accounts.MustNewAccounts(e)
usdtLower := currency.USDT.Lower()
for i, 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: accounts.SubAccounts{
{
ID: "12870774",
AssetType: asset.USDTMarginedFutures,
Balances: accounts.CurrencyBalances{
usdtLower: accounts.Balance{
Currency: usdtLower,
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: accounts.SubAccounts{
{
ID: "12870774",
AssetType: asset.USDTMarginedFutures,
Balances: accounts.CurrencyBalances{
usdtLower: accounts.Balance{
Currency: usdtLower,
Total: 2214.189114310433,
Free: 2214.189114310433,
AvailableWithoutBorrow: 2214.189114310433,
UpdatedAt: time.UnixMilli(1755738516430),
},
},
},
},
},
} {
t.Run(strconv.Itoa(i), func(t *testing.T) {
// Sequential tests, do not use t.Parallel(); Some timestamps are deliberately identical from trading activity
ctx := t.Context()
if tc.deployCreds {
ctx = accounts.DeployCredentialsToContext(ctx, &accounts.Credentials{Key: "test", Secret: "test"})
}
err := e.processBalancePushData(ctx, tc.input, asset.USDTMarginedFutures)
if tc.err != nil {
require.ErrorIs(t, err, tc.err)
} else {
require.NoError(t, err, "processBalancePushData must not error")
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.(accounts.SubAccounts)
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))
require.Equal(t, tc.expected, received)
creds, err := exch.GetCredentials(ctx)
require.NoError(t, err, "GetCredentials must not error")
for _, change := range received {
bal := slices.Collect(maps.Values(change.Balances))[0]
stored, err := exch.Accounts.GetBalance(change.ID, creds, change.AssetType, bal.Currency)
require.NoError(t, err, "GetBalance must not error")
assert.Equal(t, bal.Free, stored.Free, "free balance should equal with accounts stored value")
}
}
func TestExtractOrderbookLimit(t *testing.T) {
t.Parallel()
e := new(Exchange)
require.NoError(t, testexch.Setup(e), "Setup must not error")
_, err := e.extractOrderbookLimit(1337)
require.ErrorIs(t, err, asset.ErrNotSupported)
_, err = e.extractOrderbookLimit(asset.Spot)
require.ErrorIs(t, err, subscription.ErrNotFound)
err = e.Websocket.AddSubscriptions(nil, &subscription.Subscription{Channel: subscription.OrderbookChannel, Interval: kline.Interval(time.Millisecond * 420)})
require.NoError(t, err)
_, err = e.extractOrderbookLimit(asset.Spot)
require.ErrorIs(t, err, errInvalidOrderbookUpdateInterval)
err = e.Websocket.RemoveSubscriptions(nil, &subscription.Subscription{Channel: subscription.OrderbookChannel, Interval: kline.Interval(time.Millisecond * 420)})
require.NoError(t, err)
// Add dummy subscription so that it can be matched and a limit/level can be extracted for initial orderbook sync spot.
err = e.Websocket.AddSubscriptions(nil, &subscription.Subscription{Channel: subscription.OrderbookChannel, Interval: kline.HundredMilliseconds})
require.NoError(t, err)
for _, tc := range []struct {
asset asset.Item
exp uint64
}{
{asset: asset.Spot, exp: 100},
{asset: asset.USDTMarginedFutures, exp: futuresOrderbookUpdateLimit},
{asset: asset.CoinMarginedFutures, exp: futuresOrderbookUpdateLimit},
{asset: asset.DeliveryFutures, exp: deliveryFuturesUpdateLimit},
{asset: asset.Options, exp: optionOrderbookUpdateLimit},
} {
limit, err := e.extractOrderbookLimit(tc.asset)
require.NoError(t, err)
require.Equal(t, tc.exp, limit)
}
}