accounts: Move to instance methods, fix races and isolate tests (#1923)

* Bybit: Fix race in TestUpdateAccountInfo and  TestWSHandleData

* DriveBy rename TestWSHandleData
* This doesn't address running with -race=2+ due to the singleton

* Accounts: Add account.GetService()

* exchange: Assertify TestSetupDefaults

* Exchanges: Add account.Service override for testing

* Exchanges: Remove duplicate IsWebsocketEnabled test from TestSetupDefaults

* Dispatch: Replace nil checks with NilGuard

* Engine: Remove deprecated printAccountHoldingsChangeSummary

* Dispatcher: Add EnsureRunning method

* Accounts: Move singleton accounts service to exchange Accounts

* Move singleton accounts service to exchange Accounts

This maintains the concept of a global store, whilst allowing exchanges
to override it when needed, particularly for testing.

APIServer:

* Remove getAllActiveAccounts from apiserver

Deprecated apiserver only thing using this, so remove it instead of
updating it

* Update comment for UpdateAccountBalances everywhere

* Docs: Add punctuation to function comments

* Bybit: Coverage for wsProcessWalletPushData Save
This commit is contained in:
Gareth Kirwan
2025-10-28 09:52:45 +07:00
committed by GitHub
parent bda9bbec66
commit 73e200e4e7
140 changed files with 3515 additions and 4025 deletions

View File

@@ -1,6 +1,7 @@
package kucoin
import (
"github.com/thrasher-corp/gocryptotrader/currency"
"github.com/thrasher-corp/gocryptotrader/encoding/json"
"github.com/thrasher-corp/gocryptotrader/exchanges/order"
"github.com/thrasher-corp/gocryptotrader/exchanges/orderbook"
@@ -330,14 +331,14 @@ type FuturesFundingHistory struct {
// FuturesAccount holds futures account detail information
type FuturesAccount struct {
AccountEquity float64 `json:"accountEquity"` // marginBalance + Unrealised PNL
UnrealisedPNL float64 `json:"unrealisedPNL"` // unrealised profit and loss
MarginBalance float64 `json:"marginBalance"` // positionMargin + orderMargin + frozenFunds + availableBalance - unrealisedPNL
PositionMargin float64 `json:"positionMargin"`
OrderMargin float64 `json:"orderMargin"`
FrozenFunds float64 `json:"frozenFunds"` // frozen funds for withdrawal and out-transfer
AvailableBalance float64 `json:"availableBalance"`
Currency string `json:"currency"`
AccountEquity float64 `json:"accountEquity"` // marginBalance + Unrealised PNL
UnrealisedPNL float64 `json:"unrealisedPNL"` // unrealised profit and loss
MarginBalance float64 `json:"marginBalance"` // positionMargin + orderMargin + frozenFunds + availableBalance - unrealisedPNL
PositionMargin float64 `json:"positionMargin"`
OrderMargin float64 `json:"orderMargin"`
FrozenFunds float64 `json:"frozenFunds"` // frozen funds for withdrawal and out-transfer
AvailableBalance float64 `json:"availableBalance"`
Currency currency.Code `json:"currency"`
}
// FuturesTransactionHistory represents a transaction history

View File

@@ -1,6 +1,7 @@
package kucoin
import (
"bytes"
"context"
"errors"
"fmt"
@@ -2338,11 +2339,24 @@ func TestGetAuthenticatedServersInstances(t *testing.T) {
func TestPushData(t *testing.T) {
t.Parallel()
ku := testInstance(t)
ku.SetCredentials("mock", "test", "test", "", "", "")
ku.API.AuthenticatedSupport = true
ku.API.AuthenticatedWebsocketSupport = true
testexch.FixtureToDataHandler(t, "testdata/wsHandleData.json", ku.wsHandleData)
e := testInstance(t) //nolint:govet // Intentional shadow
e.SetCredentials("mock", "test", "test", "", "", "")
e.API.AuthenticatedSupport = true
e.API.AuthenticatedWebsocketSupport = true
fErrs := testexch.FixtureToDataHandlerWithErrors(t, "testdata/wsHandleData.json", func(ctx context.Context, r []byte) error {
if bytes.Contains(r, []byte("FANGLE-ACCOUNTS")) {
hold := e.Accounts
e.Accounts = nil
defer func() { e.Accounts = hold }()
}
return e.wsHandleData(ctx, r)
})
close(e.Websocket.DataHandler)
assert.Len(t, e.Websocket.DataHandler, 29, "Should see correct number of messages")
require.Len(t, fErrs, 1, "Must get exactly one error message")
assert.ErrorContains(t, fErrs[0].Err, "cannot save holdings: nil pointer: *accounts.Accounts")
}
func TestGenerateSubscriptions(t *testing.T) {
@@ -2954,12 +2968,12 @@ func getFirstTradablePairOfAssets(ctx context.Context) {
futuresTradablePair.Delimiter = ""
}
func TestUpdateAccountInfo(t *testing.T) {
func TestUpdateAccountBalances(t *testing.T) {
t.Parallel()
sharedtestvalues.SkipTestIfCredentialsUnset(t, e)
assetTypes := e.GetAssetTypes(true)
for _, assetType := range assetTypes {
result, err := e.UpdateAccountInfo(t.Context(), assetType)
result, err := e.UpdateAccountBalances(t.Context(), assetType)
assert.NoError(t, err)
assert.NotNil(t, result)
}

View File

@@ -763,12 +763,12 @@ type StopOrder struct {
// AccountInfo represents account information
type AccountInfo struct {
ID string `json:"id"`
Currency string `json:"currency"`
AccountType string `json:"type"` // Account type:maintradetrade_hfmargin
Balance types.Number `json:"balance"`
Available types.Number `json:"available"`
Holds types.Number `json:"holds"`
ID string `json:"id"`
Currency currency.Code `json:"currency"`
AccountType string `json:"type"` // Account type: main, trade, trade_hf, margin
Balance types.Number `json:"balance"`
Available types.Number `json:"available"`
Holds types.Number `json:"holds"`
}
// CrossMarginAccountDetail represents a cross-margin account details
@@ -1410,14 +1410,14 @@ type WsTradeOrder struct {
// WsAccountBalance represents a Account Balance push data
type WsAccountBalance struct {
Total float64 `json:"total,string"`
Available float64 `json:"available,string"`
AvailableChange float64 `json:"availableChange,string"`
Currency string `json:"currency"`
Hold float64 `json:"hold,string"`
HoldChange float64 `json:"holdChange,string"`
RelationEvent string `json:"relationEvent"`
RelationEventID string `json:"relationEventId"`
Total float64 `json:"total,string"`
Available float64 `json:"available,string"`
AvailableChange float64 `json:"availableChange,string"`
Currency currency.Code `json:"currency"`
Hold float64 `json:"hold,string"`
HoldChange float64 `json:"holdChange,string"`
RelationEvent string `json:"relationEvent"`
RelationEventID string `json:"relationEventId"`
RelationContext struct {
Symbol string `json:"symbol"`
TradeID string `json:"tradeId"`
@@ -1630,10 +1630,10 @@ type WsFuturesOrderMarginEvent struct {
// WsFuturesAvailableBalance represents an available balance push data for futures account
type WsFuturesAvailableBalance struct {
AvailableBalance float64 `json:"availableBalance"`
HoldBalance float64 `json:"holdBalance"`
Currency string `json:"currency"`
Timestamp types.Time `json:"timestamp"`
AvailableBalance float64 `json:"availableBalance"`
HoldBalance float64 `json:"holdBalance"`
Currency currency.Code `json:"currency"`
Timestamp types.Time `json:"timestamp"`
}
// WsFuturesWithdrawalAmountAndTransferOutAmountEvent represents Withdrawal Amount & Transfer-Out Amount Event push data

View File

@@ -16,9 +16,9 @@ import (
"github.com/thrasher-corp/gocryptotrader/common"
"github.com/thrasher-corp/gocryptotrader/currency"
"github.com/thrasher-corp/gocryptotrader/encoding/json"
"github.com/thrasher-corp/gocryptotrader/exchange/accounts"
"github.com/thrasher-corp/gocryptotrader/exchange/websocket"
exchange "github.com/thrasher-corp/gocryptotrader/exchanges"
"github.com/thrasher-corp/gocryptotrader/exchanges/account"
"github.com/thrasher-corp/gocryptotrader/exchanges/asset"
"github.com/thrasher-corp/gocryptotrader/exchanges/kline"
"github.com/thrasher-corp/gocryptotrader/exchanges/order"
@@ -359,24 +359,18 @@ func (e *Exchange) processFuturesAccountBalanceEvent(ctx context.Context, respDa
if err := json.Unmarshal(respData, &resp); err != nil {
return err
}
creds, err := e.GetCredentials(ctx)
if err != nil {
subAccts := accounts.SubAccounts{accounts.NewSubAccount(asset.Futures, "")}
subAccts[0].Balances.Set(resp.Currency, accounts.Balance{
Total: resp.AvailableBalance + resp.HoldBalance,
Hold: resp.HoldBalance,
Free: resp.AvailableBalance,
UpdatedAt: resp.Timestamp.Time(),
})
if err := e.Accounts.Save(ctx, subAccts, false); err != nil {
return err
}
changes := []account.Change{
{
AssetType: asset.Futures,
Balance: &account.Balance{
Currency: currency.NewCode(resp.Currency),
Total: resp.AvailableBalance + resp.HoldBalance,
Hold: resp.HoldBalance,
Free: resp.AvailableBalance,
UpdatedAt: resp.Timestamp.Time(),
},
},
}
e.Websocket.DataHandler <- changes
return account.ProcessChange(e.Name, changes, creds)
e.Websocket.DataHandler <- subAccts
return nil
}
// processFuturesStopOrderLifecycleEvent processes futures stop orders lifecycle events.
@@ -684,29 +678,22 @@ func (e *Exchange) processMarginLendingTradeOrderEvent(respData []byte) error {
// processAccountBalanceChange processes an account balance change
func (e *Exchange) processAccountBalanceChange(ctx context.Context, respData []byte) error {
response := WsAccountBalance{}
err := json.Unmarshal(respData, &response)
if err != nil {
resp := WsAccountBalance{}
if err := json.Unmarshal(respData, &resp); err != nil {
return err
}
creds, err := e.GetCredentials(ctx)
if err != nil {
subAccts := accounts.SubAccounts{accounts.NewSubAccount(asset.Futures, "")}
subAccts[0].Balances.Set(resp.Currency, accounts.Balance{
Total: resp.Total,
Hold: resp.Hold,
Free: resp.Available,
UpdatedAt: resp.Time.Time(),
})
if err := e.Accounts.Save(ctx, subAccts, false); err != nil {
return err
}
changes := []account.Change{
{
AssetType: asset.Futures,
Balance: &account.Balance{
Currency: currency.NewCode(response.Currency),
Total: response.Total,
Hold: response.Hold,
Free: response.Available,
UpdatedAt: response.Time.Time(),
},
},
}
e.Websocket.DataHandler <- changes
return account.ProcessChange(e.Name, changes, creds)
e.Websocket.DataHandler <- subAccts
return nil
}
// processOrderChangeEvent processes order update events.

View File

@@ -13,11 +13,11 @@ import (
"github.com/thrasher-corp/gocryptotrader/common/key"
"github.com/thrasher-corp/gocryptotrader/config"
"github.com/thrasher-corp/gocryptotrader/currency"
"github.com/thrasher-corp/gocryptotrader/exchange/accounts"
"github.com/thrasher-corp/gocryptotrader/exchange/order/limits"
"github.com/thrasher-corp/gocryptotrader/exchange/websocket"
"github.com/thrasher-corp/gocryptotrader/exchange/websocket/buffer"
exchange "github.com/thrasher-corp/gocryptotrader/exchanges"
"github.com/thrasher-corp/gocryptotrader/exchanges/account"
"github.com/thrasher-corp/gocryptotrader/exchanges/asset"
"github.com/thrasher-corp/gocryptotrader/exchanges/collateral"
"github.com/thrasher-corp/gocryptotrader/exchanges/deposit"
@@ -406,60 +406,46 @@ func (e *Exchange) UpdateOrderbook(ctx context.Context, p currency.Pair, a asset
return orderbook.Get(e.Name, p, a)
}
// UpdateAccountInfo retrieves balances for all enabled currencies
func (e *Exchange) UpdateAccountInfo(ctx context.Context, assetType asset.Item) (account.Holdings, error) {
holding := account.Holdings{Exchange: e.Name}
err := e.CurrencyPairs.IsAssetEnabled(assetType)
if err != nil {
return holding, fmt.Errorf("%w %v", asset.ErrNotSupported, assetType)
// UpdateAccountBalances retrieves currency balances
func (e *Exchange) UpdateAccountBalances(ctx context.Context, assetType asset.Item) (accounts.SubAccounts, error) {
if err := e.CurrencyPairs.IsAssetEnabled(assetType); err != nil {
return nil, fmt.Errorf("%w: %q", asset.ErrNotSupported, assetType)
}
subAccts := accounts.SubAccounts{accounts.NewSubAccount(assetType, "")}
switch assetType {
case asset.Futures:
balances := make([]account.Balance, 2)
for i, settlement := range []string{"XBT", "USDT"} {
accountH, err := e.GetFuturesAccountOverview(ctx, settlement)
for _, settlement := range []string{"XBT", "USDT"} {
resp, err := e.GetFuturesAccountOverview(ctx, settlement)
if err != nil {
return account.Holdings{}, err
}
balances[i] = account.Balance{
Currency: currency.NewCode(accountH.Currency),
Total: accountH.AvailableBalance + accountH.FrozenFunds,
Hold: accountH.FrozenFunds,
Free: accountH.AvailableBalance,
return nil, err
}
subAccts[0].Balances.Set(resp.Currency, accounts.Balance{
Total: resp.AvailableBalance + resp.FrozenFunds,
Hold: resp.FrozenFunds,
Free: resp.AvailableBalance,
})
}
holding.Accounts = append(holding.Accounts, account.SubAccount{
AssetType: assetType,
Currencies: balances,
})
case asset.Spot, asset.Margin:
accountH, err := e.GetAllAccounts(ctx, currency.EMPTYCODE, "")
resp, err := e.GetAllAccounts(ctx, currency.EMPTYCODE, "")
if err != nil {
return account.Holdings{}, err
return nil, err
}
for x := range accountH {
if accountH[x].AccountType == "margin" && assetType == asset.Spot {
for i := range resp {
if resp[i].AccountType == "margin" && assetType == asset.Spot {
continue
} else if accountH[x].AccountType == "trade" && assetType == asset.Margin {
} else if resp[i].AccountType == "trade" && assetType == asset.Margin {
continue
}
holding.Accounts = append(holding.Accounts, account.SubAccount{
AssetType: assetType,
Currencies: []account.Balance{
{
Currency: currency.NewCode(accountH[x].Currency),
Total: accountH[x].Balance.Float64(),
Hold: accountH[x].Holds.Float64(),
Free: accountH[x].Available.Float64(),
},
},
subAccts[0].Balances.Set(resp[i].Currency, accounts.Balance{
Total: resp[i].Balance.Float64(),
Hold: resp[i].Holds.Float64(),
Free: resp[i].Available.Float64(),
})
}
default:
return holding, fmt.Errorf("%w %v", asset.ErrNotSupported, assetType)
return nil, fmt.Errorf("%w %v", asset.ErrNotSupported, assetType)
}
return holding, nil
return subAccts, e.Accounts.Save(ctx, subAccts, true)
}
// GetAccountFundingHistory returns funding history, deposits and
@@ -1717,7 +1703,7 @@ func (e *Exchange) ValidateCredentials(ctx context.Context, assetType asset.Item
if err != nil {
return err
}
_, err = e.UpdateAccountInfo(ctx, assetType)
_, err = e.UpdateAccountBalances(ctx, assetType)
return e.CheckTransientError(err)
}
@@ -1860,10 +1846,9 @@ func (e *Exchange) GetAvailableTransferChains(ctx context.Context, cryptocurrenc
return chains, nil
}
// ValidateAPICredentials validates current credentials used for wrapper
// functionality
// ValidateAPICredentials validates current credentials used for wrapper functionality
func (e *Exchange) ValidateAPICredentials(ctx context.Context, assetType asset.Item) error {
_, err := e.UpdateAccountInfo(ctx, assetType)
_, err := e.UpdateAccountBalances(ctx, assetType)
return e.CheckTransientError(err)
}

View File

@@ -37,3 +37,4 @@
{"topic":"/contract/instrument:ETHUSDCM","subject":"funding.rate","data":{"granularity":60000,"fundingRate":-0.002966,"timestamp":1551770400000}}
{"topic":"/contract/instrument:ETHUSDCM","subject":"mark.index.price","data":{"granularity":1000,"indexPrice":4000.23,"markPrice":4010.52,"timestamp":1551770400000}}
{"type":"message","topic":"/market/level2:BTC-USDT","subject":"trade.l2update","data":{"changes":{"asks":[["18906","0.00331","14103845"],["18907.3","0.58751503","14103844"]],"bids":[["18891.9","0.15688","14103847"]]},"sequenceEnd":14103847,"sequenceStart":14103844,"symbol":"BTC-USDT","time":1663747970273}}
{"userId":"xbc453tg732eba53a88ggyt8c","topic":"/contractAccount/wallet","subject":"availableBalance.change","data":{"availableBalance":5923,"holdBalance":2312,"currency":"FANGLE-ACCOUNTS","timestamp":1553842862614}}