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

@@ -3374,10 +3374,10 @@ func TestUpdateOrderbook(t *testing.T) {
}
}
func TestUpdateAccountInfo(t *testing.T) {
func TestUpdateAccountBalances(t *testing.T) {
t.Parallel()
sharedtestvalues.SkipTestIfCredentialsUnset(t, e)
result, err := e.UpdateAccountInfo(contextGenerate(), asset.Spot)
result, err := e.UpdateAccountBalances(contextGenerate(), asset.Spot)
require.NoError(t, err)
assert.NotNil(t, result)
}
@@ -4009,24 +4009,30 @@ var pushDataMap = map[string]string{
"Liquidation Orders": `{"arg": {"channel": "liquidation-orders", "instType": "SWAP" }, "data": [ { "details": [ { "bkLoss": "0", "bkPx": "0.007831", "ccy": "", "posSide": "short", "side": "buy", "sz": "13", "ts": "1692266434010" } ], "instFamily": "IOST-USDT", "instId": "IOST-USDT-SWAP", "instType": "SWAP", "uly": "IOST-USDT"}]}`,
"Economic Calendar": `{"arg": {"channel": "economic-calendar" }, "data": [ { "calendarId": "319275", "date": "1597026383085", "region": "United States", "category": "Manufacturing PMI", "event": "S&P Global Manufacturing PMI Final", "refDate": "1597026383085", "actual": "49.2", "previous": "47.3", "forecast": "49.3", "importance": "2", "prevInitial": "", "ccy": "", "unit": "", "ts": "1698648096590" } ] }`,
"Failure": `{ "event": "error", "code": "60012", "msg": "Invalid request: {\"op\": \"subscribe\", \"args\":[{ \"channel\" : \"block-tickers\", \"instId\" : \"LTC-USD-200327\"}]}", "connId": "a4d3ae55" }`,
"Balance Save Error": `{"arg": {"channel": "balance_and_position","uid": "77982378738415880"},"data": [{"pTime": "1597026383085","eventType": "snapshot","balData": [{"ccy": "BTC","cashBal": "1","uTime": "1597026383085"}],"posData": [{"posId": "1111111111","tradeId": "2","instId": "BTC-USD-191018","instType": "FUTURES","mgnMode": "cross","posSide": "long","pos": "10","ccy": "BTC","posCcy": "","avgPx": "3320","uTIme": "1597026383085"}]}]}`,
}
func TestPushData(t *testing.T) {
func TestWsHandleData(t *testing.T) {
t.Parallel()
e := new(Exchange) //nolint:govet // Intentional shadow
require.NoError(t, testexch.Setup(e), "Setup must not error")
for x := range pushDataMap {
if x == "Balance And Position" {
for name, msg := range pushDataMap {
switch name {
case "Balance And Position":
e.API.AuthenticatedSupport = true
e.API.AuthenticatedWebsocketSupport = true
e.SetCredentials("test", "test", "test", "", "", "")
} else {
default:
e.API.AuthenticatedSupport = false
e.API.AuthenticatedWebsocketSupport = false
}
err := e.WsHandleData(t.Context(), []byte(pushDataMap[x]))
require.NoErrorf(t, err, "Okx %s error %s", x, err)
err := e.WsHandleData(t.Context(), []byte(msg))
if name == "Balance Save Error" {
assert.ErrorIs(t, err, exchange.ErrAuthenticationSupportNotEnabled, "wsProcessBalanceAndPosition Accounts.Save should error without credentials")
} else {
require.NoErrorf(t, err, "%s must not error", name)
}
}
}

View File

@@ -3288,9 +3288,9 @@ type PositionDataDetail struct {
// BalanceData represents currency and it's Cash balance with the update time
type BalanceData struct {
Currency string `json:"ccy"`
CashBalance types.Number `json:"cashBal"`
UpdateTime types.Time `json:"uTime"`
Currency currency.Code `json:"ccy"`
CashBalance types.Number `json:"cashBal"`
UpdateTime types.Time `json:"uTime"`
}
// BalanceAndPositionData represents balance and position data with the push time

View File

@@ -20,8 +20,8 @@ import (
"github.com/thrasher-corp/gocryptotrader/common/crypto"
"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"
"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"
@@ -1459,28 +1459,22 @@ func (e *Exchange) wsProcessBalanceAndPosition(ctx context.Context, data []byte)
if err := json.Unmarshal(data, &resp); err != nil {
return err
}
creds, err := e.GetCredentials(ctx)
if err != nil {
return err
}
var changes []account.Change
subAccts := accounts.SubAccounts{accounts.NewSubAccount(asset.Spot, resp.Argument.UID)}
for i := range resp.Data {
for j := range resp.Data[i].BalanceData {
changes = append(changes, account.Change{
AssetType: asset.Spot,
Account: resp.Argument.UID,
Balance: &account.Balance{
Currency: currency.NewCode(resp.Data[i].BalanceData[j].Currency),
Total: resp.Data[i].BalanceData[j].CashBalance.Float64(),
Free: resp.Data[i].BalanceData[j].CashBalance.Float64(),
UpdatedAt: resp.Data[i].BalanceData[j].UpdateTime.Time(),
},
subAccts[0].Balances.Set(resp.Data[i].BalanceData[j].Currency, accounts.Balance{
Total: resp.Data[i].BalanceData[j].CashBalance.Float64(),
Free: resp.Data[i].BalanceData[j].CashBalance.Float64(),
UpdatedAt: resp.Data[i].BalanceData[j].UpdateTime.Time(),
})
}
// TODO: Handle position data
}
e.Websocket.DataHandler <- changes
return account.ProcessChange(e.Name, changes, creds)
if err := e.Accounts.Save(ctx, subAccts, false); err != nil {
return err
}
e.Websocket.DataHandler <- subAccts
return nil
}
// wsProcessPushData processes push data coming through the websocket channel

View File

@@ -16,10 +16,10 @@ 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"
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"
@@ -614,44 +614,26 @@ func (e *Exchange) UpdateOrderbook(ctx context.Context, pair currency.Pair, asse
return orderbook.Get(e.Name, pair, assetType)
}
// UpdateAccountInfo retrieves balances for all enabled currencies.
func (e *Exchange) UpdateAccountInfo(ctx context.Context, assetType asset.Item) (account.Holdings, error) {
// 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 account.Holdings{}, err
return nil, err
}
var info account.Holdings
var acc account.SubAccount
info.Exchange = e.Name
if !e.SupportsAsset(assetType) {
return info, fmt.Errorf("%w: %v", asset.ErrNotSupported, assetType)
}
accountBalances, err := e.AccountBalance(ctx, currency.EMPTYCODE)
resp, err := e.AccountBalance(ctx, currency.EMPTYCODE)
if err != nil {
return info, err
return nil, err
}
currencyBalances := []account.Balance{}
for i := range accountBalances {
for j := range accountBalances[i].Details {
currencyBalances = append(currencyBalances, account.Balance{
Currency: accountBalances[i].Details[j].Currency,
Total: accountBalances[i].Details[j].EquityOfCurrency.Float64(),
Hold: accountBalances[i].Details[j].FrozenBalance.Float64(),
Free: accountBalances[i].Details[j].AvailableBalance.Float64(),
subAccts := accounts.SubAccounts{accounts.NewSubAccount(assetType, "")}
for i := range resp {
for j := range resp[i].Details {
subAccts[0].Balances.Set(resp[i].Details[j].Currency, accounts.Balance{
Total: resp[i].Details[j].EquityOfCurrency.Float64(),
Hold: resp[i].Details[j].FrozenBalance.Float64(),
Free: resp[i].Details[j].AvailableBalance.Float64(),
})
}
}
acc.Currencies = currencyBalances
acc.AssetType = assetType
info.Accounts = append(info.Accounts, acc)
creds, err := e.GetCredentials(ctx)
if err != nil {
return info, err
}
if err := account.Process(&info, creds); err != nil {
return account.Holdings{}, err
}
return info, nil
return subAccts, e.Accounts.Save(ctx, subAccts, true)
}
// GetAccountFundingHistory returns funding history, deposits and withdrawals
@@ -1957,7 +1939,7 @@ func (e *Exchange) GetFeeByType(ctx context.Context, feeBuilder *exchange.FeeBui
// ValidateAPICredentials validates current credentials used for wrapper
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)
}