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,9 @@
package huobi
import "github.com/thrasher-corp/gocryptotrader/types"
import (
"github.com/thrasher-corp/gocryptotrader/currency"
"github.com/thrasher-corp/gocryptotrader/types"
)
// WsSwapReqKline stores req kline data for swap websocket
type WsSwapReqKline struct {
@@ -631,20 +634,20 @@ type BasisData struct {
// SwapAccountInformation stores swap account information
type SwapAccountInformation struct {
Data []struct {
Symbol string `json:"symbol"`
ContractCode string `json:"contract_code"`
MarginBalance float64 `json:"margin_balance"`
MarginPosition float64 `json:"margin_position"`
MarginFrozen float64 `json:"margin_frozen"`
MarginAvailable float64 `json:"margin_available"`
ProfitReal float64 `json:"profit_real"`
ProfitUnreal float64 `json:"profit_unreal"`
WithdrawAvailable float64 `json:"withdraw_available"`
RiskRate float64 `json:"risk_rate"`
LiquidationPrice float64 `json:"liquidation_price"`
AdjustFactor float64 `json:"adjust_factor"`
LeverageRate float64 `json:"lever_rate"`
MarginStatic float64 `json:"margin_static"`
Symbol currency.Code `json:"symbol"`
ContractCode string `json:"contract_code"`
MarginBalance float64 `json:"margin_balance"`
MarginPosition float64 `json:"margin_position"`
MarginFrozen float64 `json:"margin_frozen"`
MarginAvailable float64 `json:"margin_available"`
ProfitReal float64 `json:"profit_real"`
ProfitUnreal float64 `json:"profit_unreal"`
WithdrawAvailable float64 `json:"withdraw_available"`
RiskRate float64 `json:"risk_rate"`
LiquidationPrice float64 `json:"liquidation_price"`
AdjustFactor float64 `json:"adjust_factor"`
LeverageRate float64 `json:"lever_rate"`
MarginStatic float64 `json:"margin_static"`
} `json:"data"`
}
@@ -724,20 +727,20 @@ type SubAccountsAssetData struct {
type SingleSubAccountAssetsInfo struct {
Timestamp types.Time `json:"ts"`
Data []struct {
Symbol string `json:"symbol"`
ContractCode string `json:"contract_code"`
MarginBalance float64 `json:"margin_balance"`
MarginPosition float64 `json:"margin_position"`
MarginFrozen float64 `json:"margin_frozen"`
MarginAvailable float64 `json:"margin_available"`
ProfitReal float64 `json:"profit_real"`
ProfitUnreal float64 `json:"profit_unreal"`
WithdrawAvailable float64 `json:"withdraw_available"`
RiskRate float64 `json:"risk_rate"`
LiquidationPrice float64 `json:"liquidation_price"`
AdjustFactor float64 `json:"adjust_factor"`
LeverageRate float64 `json:"lever_rate"`
MarginStatic float64 `json:"margin_static"`
Symbol currency.Code `json:"symbol"`
ContractCode string `json:"contract_code"`
MarginBalance float64 `json:"margin_balance"`
MarginPosition float64 `json:"margin_position"`
MarginFrozen float64 `json:"margin_frozen"`
MarginAvailable float64 `json:"margin_available"`
ProfitReal float64 `json:"profit_real"`
ProfitUnreal float64 `json:"profit_unreal"`
WithdrawAvailable float64 `json:"withdraw_available"`
RiskRate float64 `json:"risk_rate"`
LiquidationPrice float64 `json:"liquidation_price"`
AdjustFactor float64 `json:"adjust_factor"`
LeverageRate float64 `json:"lever_rate"`
MarginStatic float64 `json:"margin_static"`
} `json:"data"`
}

View File

@@ -1,6 +1,9 @@
package huobi
import "github.com/thrasher-corp/gocryptotrader/types"
import (
"github.com/thrasher-corp/gocryptotrader/currency"
"github.com/thrasher-corp/gocryptotrader/types"
)
// FContractInfoData gets contract info data for futures
type FContractInfoData struct {
@@ -311,19 +314,19 @@ type FBasisData struct {
// FUserAccountData stores user account data info for futures
type FUserAccountData struct {
AccData []struct {
Symbol string `json:"symbol"`
MarginBalance float64 `json:"margin_balance"`
MarginPosition float64 `json:"margin_position"`
MarginFrozen float64 `json:"margin_frozen"`
MarginAvailable float64 `json:"margin_available"`
ProfitReal float64 `json:"profit_real"`
ProfitUnreal float64 `json:"profit_unreal"`
RiskRate float64 `json:"risk_rate"`
LiquidationPrice float64 `json:"liquidation_price"`
WithdrawAvailable float64 `json:"withdraw_available"`
LeverageRate float64 `json:"lever_rate"`
AdjustFactor float64 `json:"adjust_factor"`
MarginStatic float64 `json:"margin_static"`
Symbol currency.Code `json:"symbol"`
MarginBalance float64 `json:"margin_balance"`
MarginPosition float64 `json:"margin_position"`
MarginFrozen float64 `json:"margin_frozen"`
MarginAvailable float64 `json:"margin_available"`
ProfitReal float64 `json:"profit_real"`
ProfitUnreal float64 `json:"profit_unreal"`
RiskRate float64 `json:"risk_rate"`
LiquidationPrice float64 `json:"liquidation_price"`
WithdrawAvailable float64 `json:"withdraw_available"`
LeverageRate float64 `json:"lever_rate"`
AdjustFactor float64 `json:"adjust_factor"`
MarginStatic float64 `json:"margin_static"`
} `json:"data"`
Timestamp types.Time `json:"ts"`
}
@@ -367,19 +370,19 @@ type FSubAccountAssetsInfo struct {
// FSingleSubAccountAssetsInfo stores futures assets info for a single subaccount
type FSingleSubAccountAssetsInfo struct {
AssetsData []struct {
Symbol string `json:"symbol"`
MarginBalance float64 `json:"margin_balance"`
MarginPosition float64 `json:"margin_position"`
MarginFrozen float64 `json:"margin_frozen"`
MarginAvailable float64 `json:"margin_available"`
ProfitReal float64 `json:"profit_real"`
ProfitUnreal float64 `json:"profit_unreal"`
WithdrawAvailable float64 `json:"withdraw_available"`
RiskRate float64 `json:"risk_rate"`
LiquidationPrice float64 `json:"liquidation_price"`
AdjustFactor float64 `json:"adjust_factor"`
LeverageRate float64 `json:"lever_rate"`
MarginStatic float64 `json:"margin_static"`
Symbol currency.Code `json:"symbol"`
MarginBalance float64 `json:"margin_balance"`
MarginPosition float64 `json:"margin_position"`
MarginFrozen float64 `json:"margin_frozen"`
MarginAvailable float64 `json:"margin_available"`
ProfitReal float64 `json:"profit_real"`
ProfitUnreal float64 `json:"profit_unreal"`
WithdrawAvailable float64 `json:"withdraw_available"`
RiskRate float64 `json:"risk_rate"`
LiquidationPrice float64 `json:"liquidation_price"`
AdjustFactor float64 `json:"adjust_factor"`
LeverageRate float64 `json:"lever_rate"`
MarginStatic float64 `json:"margin_static"`
} `json:"data"`
Timestamp types.Time `json:"ts"`
}

View File

@@ -1211,12 +1211,12 @@ func TestCancelAllExchangeOrders(t *testing.T) {
require.NoError(t, err)
}
func TestUpdateAccountInfo(t *testing.T) {
func TestUpdateAccountBalances(t *testing.T) {
t.Parallel()
sharedtestvalues.SkipTestIfCredentialsUnset(t, e, canManipulateRealOrders)
for _, a := range []asset.Item{asset.Spot, asset.CoinMarginedFutures, asset.Futures} {
_, err := e.UpdateAccountInfo(t.Context(), a)
assert.NoErrorf(t, err, "UpdateAccountInfo should not error for asset %s", a)
_, err := e.UpdateAccountBalances(t.Context(), a)
assert.NoErrorf(t, err, "UpdateAccountBalances should not error for asset %s", a)
}
}

View File

@@ -655,9 +655,9 @@ type AccountBalance struct {
// AccountBalanceDetail stores the user account balance
type AccountBalanceDetail struct {
Currency string `json:"currency"`
Type string `json:"type"`
Balance float64 `json:"balance,string"`
Currency currency.Code `json:"currency"`
Type string `json:"type"`
Balance float64 `json:"balance,string"`
}
// AggregatedBalance stores balances of all the sub-account
@@ -743,8 +743,7 @@ type MarginAccountBalance struct {
List []AccountBalance `json:"list"`
}
// SpotNewOrderRequestParams holds the params required to place
// an order
// SpotNewOrderRequestParams holds the params required to place an order
type SpotNewOrderRequestParams struct {
AccountID int `json:"account-id,string"` // Account ID, obtained using the accounts method. Currency trades use the accountid of the spot account; for loan asset transactions, please use the accountid of the margin account.
Amount float64 `json:"amount"` // The limit price indicates the quantity of the order, the market price indicates how much to buy when the order is paid, and the market price indicates how much the coin is sold when the order is sold.

View File

@@ -18,8 +18,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"
@@ -555,7 +555,7 @@ func (e *Exchange) manageSubs(ctx context.Context, op string, subs subscription.
return err
}
func (e *Exchange) wsGenerateSignature(creds *account.Credentials, timestamp string) ([]byte, error) {
func (e *Exchange) wsGenerateSignature(creds *accounts.Credentials, timestamp string) ([]byte, error) {
values := url.Values{}
values.Set("accessKey", creds.Key)
values.Set("signatureMethod", signatureMethod)

View File

@@ -15,9 +15,9 @@ 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/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/deposit"
"github.com/thrasher-corp/gocryptotrader/exchanges/fundingrate"
@@ -653,161 +653,102 @@ func (e *Exchange) GetAccountID(ctx context.Context) ([]Account, error) {
return acc, nil
}
// UpdateAccountInfo retrieves balances for all enabled currencies for the
// HUOBI exchange - to-do
func (e *Exchange) UpdateAccountInfo(ctx context.Context, assetType asset.Item) (account.Holdings, error) {
var info account.Holdings
var acc account.SubAccount
info.Exchange = e.Name
// UpdateAccountBalances retrieves currency balances
func (e *Exchange) UpdateAccountBalances(ctx context.Context, assetType asset.Item) (subAccts accounts.SubAccounts, err error) {
switch assetType {
case asset.Spot:
accounts, err := e.GetAccountID(ctx)
resp, err := e.GetAccountID(ctx)
if err != nil {
return info, err
return nil, err
}
for i := range accounts {
if accounts[i].Type != "spot" {
subAccts = make(accounts.SubAccounts, 0, len(resp))
for i := range resp {
if resp[i].Type != "spot" {
continue
}
acc.ID = strconv.FormatInt(accounts[i].ID, 10)
balances, err := e.GetAccountBalance(ctx, acc.ID)
a := accounts.NewSubAccount(assetType, strconv.FormatInt(resp[i].ID, 10))
balances, err := e.GetAccountBalance(ctx, a.ID)
if err != nil {
return info, err
return nil, err
}
var currencyDetails []account.Balance
balance:
for j := range balances {
frozen := balances[j].Type == "frozen"
for i := range currencyDetails {
if currencyDetails[i].Currency.String() == balances[j].Currency {
if frozen {
currencyDetails[i].Hold = balances[j].Balance
} else {
currencyDetails[i].Total = balances[j].Balance
}
continue balance
}
}
if frozen {
currencyDetails = append(currencyDetails,
account.Balance{
Currency: currency.NewCode(balances[j].Currency),
Hold: balances[j].Balance,
})
if balances[j].Type == "frozen" {
err = a.Balances.Add(balances[j].Currency, accounts.Balance{Hold: balances[j].Balance})
} else {
currencyDetails = append(currencyDetails,
account.Balance{
Currency: currency.NewCode(balances[j].Currency),
Total: balances[j].Balance,
})
err = a.Balances.Add(balances[j].Currency, accounts.Balance{Total: balances[j].Balance})
}
if err != nil {
return nil, err
}
}
acc.Currencies = currencyDetails
subAccts = subAccts.Merge(a)
}
case asset.CoinMarginedFutures:
// fetch swap account info
acctInfo, err := e.GetSwapAccountInfo(ctx, currency.EMPTYPAIR)
mainResp, err := e.GetSwapAccountInfo(ctx, currency.EMPTYPAIR)
if err != nil {
return info, err
return nil, err
}
var mainAcctBalances []account.Balance
for x := range acctInfo.Data {
mainAcctBalances = append(mainAcctBalances, account.Balance{
Currency: currency.NewCode(acctInfo.Data[x].Symbol),
Total: acctInfo.Data[x].MarginBalance,
Hold: acctInfo.Data[x].MarginFrozen,
Free: acctInfo.Data[x].MarginAvailable,
subAccts = accounts.SubAccounts{accounts.NewSubAccount(assetType, "")}
for i := range mainResp.Data {
subAccts[0].Balances.Set(mainResp.Data[i].Symbol, accounts.Balance{
Total: mainResp.Data[i].MarginBalance,
Hold: mainResp.Data[i].MarginFrozen,
Free: mainResp.Data[i].MarginAvailable,
})
}
info.Accounts = append(info.Accounts, account.SubAccount{
Currencies: mainAcctBalances,
AssetType: assetType,
})
// fetch subaccounts data
subAccsData, err := e.GetSwapAllSubAccAssets(ctx, currency.EMPTYPAIR)
subResp, err := e.GetSwapAllSubAccAssets(ctx, currency.EMPTYPAIR)
if err != nil {
return info, err
return nil, err
}
var currencyDetails []account.Balance
for x := range subAccsData.Data {
a, err := e.SwapSingleSubAccAssets(ctx,
currency.EMPTYPAIR,
subAccsData.Data[x].SubUID)
for i := range subResp.Data {
resp, err := e.SwapSingleSubAccAssets(ctx, currency.EMPTYPAIR, subResp.Data[i].SubUID)
if err != nil {
return info, err
return nil, err
}
for y := range a.Data {
currencyDetails = append(currencyDetails, account.Balance{
Currency: currency.NewCode(a.Data[y].Symbol),
Total: a.Data[y].MarginBalance,
Hold: a.Data[y].MarginFrozen,
Free: a.Data[y].MarginAvailable,
a := accounts.NewSubAccount(assetType, strconv.FormatInt(subResp.Data[i].SubUID, 10))
for j := range resp.Data {
a.Balances.Set(resp.Data[j].Symbol, accounts.Balance{
Total: resp.Data[j].MarginBalance,
Hold: resp.Data[j].MarginFrozen,
Free: resp.Data[j].MarginAvailable,
})
}
subAccts = subAccts.Merge(a)
}
acc.Currencies = currencyDetails
case asset.Futures:
// fetch main account data
mainAcctData, err := e.FGetAccountInfo(ctx, currency.EMPTYCODE)
mainResp, err := e.FGetAccountInfo(ctx, currency.EMPTYCODE)
if err != nil {
return info, err
return nil, err
}
var mainAcctBalances []account.Balance
for x := range mainAcctData.AccData {
mainAcctBalances = append(mainAcctBalances, account.Balance{
Currency: currency.NewCode(mainAcctData.AccData[x].Symbol),
Total: mainAcctData.AccData[x].MarginBalance,
Hold: mainAcctData.AccData[x].MarginFrozen,
Free: mainAcctData.AccData[x].MarginAvailable,
subAccts = accounts.SubAccounts{accounts.NewSubAccount(assetType, "")}
for i := range mainResp.AccData {
subAccts[0].Balances.Set(mainResp.AccData[i].Symbol, accounts.Balance{
Total: mainResp.AccData[i].MarginBalance,
Hold: mainResp.AccData[i].MarginFrozen,
Free: mainResp.AccData[i].MarginAvailable,
})
}
info.Accounts = append(info.Accounts, account.SubAccount{
Currencies: mainAcctBalances,
AssetType: assetType,
})
// fetch subaccounts data
subAccsData, err := e.FGetAllSubAccountAssets(ctx, currency.EMPTYCODE)
subResp, err := e.FGetAllSubAccountAssets(ctx, currency.EMPTYCODE)
if err != nil {
return info, err
return nil, err
}
var currencyDetails []account.Balance
for x := range subAccsData.Data {
a, err := e.FGetSingleSubAccountInfo(ctx,
"",
strconv.FormatInt(subAccsData.Data[x].SubUID, 10))
for i := range subResp.Data {
a := accounts.NewSubAccount(assetType, strconv.FormatInt(subResp.Data[i].SubUID, 10))
resp, err := e.FGetSingleSubAccountInfo(ctx, "", a.ID)
if err != nil {
return info, err
return nil, err
}
for y := range a.AssetsData {
currencyDetails = append(currencyDetails, account.Balance{
Currency: currency.NewCode(a.AssetsData[y].Symbol),
Total: a.AssetsData[y].MarginBalance,
Hold: a.AssetsData[y].MarginFrozen,
Free: a.AssetsData[y].MarginAvailable,
for j := range resp.AssetsData {
a.Balances.Set(resp.AssetsData[j].Symbol, accounts.Balance{
Total: resp.AssetsData[j].MarginBalance,
Hold: resp.AssetsData[j].MarginFrozen,
Free: resp.AssetsData[j].MarginAvailable,
})
}
subAccts = subAccts.Merge(a)
}
acc.Currencies = currencyDetails
}
acc.AssetType = assetType
info.Accounts = append(info.Accounts, acc)
creds, err := e.GetCredentials(ctx)
if err != nil {
return account.Holdings{}, err
}
if err := account.Process(&info, creds); err != nil {
return info, err
}
return info, nil
return subAccts, e.Accounts.Save(ctx, subAccts, true)
}
// GetAccountFundingHistory returns funding history, deposits and
@@ -1793,10 +1734,9 @@ func (e *Exchange) AuthenticateWebsocket(ctx context.Context) error {
return e.wsLogin(ctx)
}
// 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)
}