diff --git a/backtester/config/strategyconfig_test.go b/backtester/config/strategyconfig_test.go index eb384427..e72ff43b 100644 --- a/backtester/config/strategyconfig_test.go +++ b/backtester/config/strategyconfig_test.go @@ -17,7 +17,7 @@ import ( "github.com/thrasher-corp/gocryptotrader/database" "github.com/thrasher-corp/gocryptotrader/database/drivers" "github.com/thrasher-corp/gocryptotrader/encoding/json" - "github.com/thrasher-corp/gocryptotrader/exchanges/account" + "github.com/thrasher-corp/gocryptotrader/exchange/accounts" "github.com/thrasher-corp/gocryptotrader/exchanges/asset" "github.com/thrasher-corp/gocryptotrader/exchanges/kline" ) @@ -855,7 +855,7 @@ func TestGenerateConfigForDCALiveCandles(t *testing.T) { ExchangeCredentials: []Credentials{ { Exchange: mainExchange, - Keys: account.Credentials{ + Keys: accounts.Credentials{ Key: "", Secret: "", }, @@ -1388,7 +1388,7 @@ func TestGenerateConfigForLiveCashAndCarry(t *testing.T) { ExchangeCredentials: []Credentials{ { Exchange: mainExchange, - Keys: account.Credentials{ + Keys: accounts.Credentials{ Key: "", Secret: "", SubAccount: "", diff --git a/backtester/config/strategyconfig_types.go b/backtester/config/strategyconfig_types.go index 56ea8455..3cfc1058 100644 --- a/backtester/config/strategyconfig_types.go +++ b/backtester/config/strategyconfig_types.go @@ -7,7 +7,7 @@ import ( "github.com/shopspring/decimal" "github.com/thrasher-corp/gocryptotrader/currency" "github.com/thrasher-corp/gocryptotrader/database" - "github.com/thrasher-corp/gocryptotrader/exchanges/account" + "github.com/thrasher-corp/gocryptotrader/exchange/accounts" "github.com/thrasher-corp/gocryptotrader/exchanges/asset" "github.com/thrasher-corp/gocryptotrader/exchanges/kline" ) @@ -201,6 +201,6 @@ type LiveData struct { // Credentials holds each exchanges credentials type Credentials struct { - Exchange string `json:"exchange"` - Keys account.Credentials `json:"credentials"` + Exchange string `json:"exchange"` + Keys accounts.Credentials `json:"credentials"` } diff --git a/backtester/engine/backtest_test.go b/backtester/engine/backtest_test.go index ecf89c74..a3873c25 100644 --- a/backtester/engine/backtest_test.go +++ b/backtester/engine/backtest_test.go @@ -39,8 +39,8 @@ import ( "github.com/thrasher-corp/gocryptotrader/database" "github.com/thrasher-corp/gocryptotrader/database/drivers" "github.com/thrasher-corp/gocryptotrader/engine" + "github.com/thrasher-corp/gocryptotrader/exchange/accounts" gctexchange "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/binance" "github.com/thrasher-corp/gocryptotrader/exchanges/binanceus" @@ -337,7 +337,7 @@ func TestLoadDataLive(t *testing.T) { ExchangeCredentials: []config.Credentials{ { Exchange: testExchange, - Keys: account.Credentials{ + Keys: accounts.Credentials{ Key: "test", Secret: "test", ClientID: "test", @@ -1392,7 +1392,7 @@ func TestSetExchangeCredentials(t *testing.T) { // enter them here cfg.DataSettings.LiveData.ExchangeCredentials = []config.Credentials{{ Exchange: testExchange, - Keys: account.Credentials{ + Keys: accounts.Credentials{ Key: "test", Secret: "test", }, diff --git a/backtester/engine/fakeinterfaces_test.go b/backtester/engine/fakeinterfaces_test.go index 5f689c10..55567441 100644 --- a/backtester/engine/fakeinterfaces_test.go +++ b/backtester/engine/fakeinterfaces_test.go @@ -18,7 +18,7 @@ import ( "github.com/thrasher-corp/gocryptotrader/backtester/eventtypes/signal" "github.com/thrasher-corp/gocryptotrader/backtester/funding" "github.com/thrasher-corp/gocryptotrader/currency" - "github.com/thrasher-corp/gocryptotrader/exchanges/account" + "github.com/thrasher-corp/gocryptotrader/exchange/accounts" "github.com/thrasher-corp/gocryptotrader/exchanges/asset" "github.com/thrasher-corp/gocryptotrader/exchanges/futures" gctkline "github.com/thrasher-corp/gocryptotrader/exchanges/kline" @@ -219,7 +219,7 @@ func (f fakeFunding) UpdateFundingFromLiveData(bool) error { return nil } -func (f fakeFunding) SetFunding(string, asset.Item, *account.Balance, bool) error { +func (f fakeFunding) SetFunding(string, asset.Item, *accounts.Balance, bool) error { return nil } diff --git a/backtester/engine/grpcserver.go b/backtester/engine/grpcserver.go index 2d3fb36e..9422abe4 100644 --- a/backtester/engine/grpcserver.go +++ b/backtester/engine/grpcserver.go @@ -23,7 +23,7 @@ import ( "github.com/thrasher-corp/gocryptotrader/database" "github.com/thrasher-corp/gocryptotrader/database/drivers" gctengine "github.com/thrasher-corp/gocryptotrader/engine" - "github.com/thrasher-corp/gocryptotrader/exchanges/account" + "github.com/thrasher-corp/gocryptotrader/exchange/accounts" "github.com/thrasher-corp/gocryptotrader/exchanges/asset" gctkline "github.com/thrasher-corp/gocryptotrader/exchanges/kline" "github.com/thrasher-corp/gocryptotrader/gctrpc/auth" @@ -547,7 +547,7 @@ func (s *GRPCServer) ExecuteStrategyFromConfig(_ context.Context, request *btrpc for i := range request.Config.DataSettings.LiveData.Credentials { creds[i] = config.Credentials{ Exchange: request.Config.DataSettings.LiveData.Credentials[i].Exchange, - Keys: account.Credentials{ + Keys: accounts.Credentials{ Key: request.Config.DataSettings.LiveData.Credentials[i].Keys.Key, Secret: request.Config.DataSettings.LiveData.Credentials[i].Keys.Secret, ClientID: request.Config.DataSettings.LiveData.Credentials[i].Keys.ClientId, diff --git a/backtester/funding/funding.go b/backtester/funding/funding.go index 12de275b..10e36124 100644 --- a/backtester/funding/funding.go +++ b/backtester/funding/funding.go @@ -15,8 +15,8 @@ import ( gctcommon "github.com/thrasher-corp/gocryptotrader/common" "github.com/thrasher-corp/gocryptotrader/currency" "github.com/thrasher-corp/gocryptotrader/engine" + "github.com/thrasher-corp/gocryptotrader/exchange/accounts" 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/futures" gctkline "github.com/thrasher-corp/gocryptotrader/exchanges/kline" @@ -564,31 +564,21 @@ func (f *FundManager) UpdateFundingFromLiveData(initialFundsSet bool) error { if err != nil { return err } - for x := range exchanges { - var creds *account.Credentials - creds, err = exchanges[x].GetCredentials(context.TODO()) - if err != nil { - return err - } - assets := exchanges[x].GetAssetTypes(false) - for y := range assets { - if assets[y].IsFutures() { + for _, e := range exchanges { + eName := e.GetName() + for _, a := range e.GetAssetTypes(false) { + if a.IsFutures() { // we set all holdings as spot // futures currency holdings are collateral in the collateral currency continue } - var acc account.Holdings - acc, err = exchanges[x].UpdateAccountInfo(context.TODO(), assets[y]) + subAccts, err := e.UpdateAccountBalances(context.TODO(), a) if err != nil { return err } - for z := range acc.Accounts { - if !acc.Accounts[z].Credentials.Equal(creds) { - continue - } - for i := range acc.Accounts[z].Currencies { - err = f.SetFunding(exchanges[x].GetName(), assets[y], &acc.Accounts[z].Currencies[i], initialFundsSet) - if err != nil { + for _, subAcct := range subAccts { + for _, bal := range subAcct.Balances { + if err := f.SetFunding(eName, a, &bal, initialFundsSet); err != nil { return err } } @@ -799,7 +789,7 @@ func (f *FundManager) HasExchangeBeenLiquidated(ev common.Event) bool { // As external sources may have additional currencies and balances // versus the strategy currencies, they must be appended to // help calculate collateral -func (f *FundManager) SetFunding(exchName string, item asset.Item, balance *account.Balance, initialFundsSet bool) error { +func (f *FundManager) SetFunding(exchName string, item asset.Item, balance *accounts.Balance, initialFundsSet bool) error { if exchName == "" { return gctcommon.ErrExchangeNameNotSet } diff --git a/backtester/funding/funding_test.go b/backtester/funding/funding_test.go index e5d7e7d4..a103ec5d 100644 --- a/backtester/funding/funding_test.go +++ b/backtester/funding/funding_test.go @@ -15,8 +15,8 @@ import ( gctcommon "github.com/thrasher-corp/gocryptotrader/common" "github.com/thrasher-corp/gocryptotrader/currency" "github.com/thrasher-corp/gocryptotrader/engine" + "github.com/thrasher-corp/gocryptotrader/exchange/accounts" 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/binance" gctkline "github.com/thrasher-corp/gocryptotrader/exchanges/kline" @@ -717,7 +717,7 @@ func TestSetFunding(t *testing.T) { err = f.SetFunding(exchName, asset.Spot, nil, false) assert.ErrorIs(t, err, gctcommon.ErrNilPointer) - bal := &account.Balance{} + bal := &accounts.Balance{} err = f.SetFunding(exchName, asset.Spot, bal, false) assert.ErrorIs(t, err, currency.ErrCurrencyCodeEmpty) diff --git a/backtester/funding/funding_types.go b/backtester/funding/funding_types.go index d36f5ed5..bde252f2 100644 --- a/backtester/funding/funding_types.go +++ b/backtester/funding/funding_types.go @@ -9,7 +9,7 @@ import ( "github.com/thrasher-corp/gocryptotrader/backtester/data/kline" "github.com/thrasher-corp/gocryptotrader/currency" "github.com/thrasher-corp/gocryptotrader/engine" - "github.com/thrasher-corp/gocryptotrader/exchanges/account" + "github.com/thrasher-corp/gocryptotrader/exchange/accounts" "github.com/thrasher-corp/gocryptotrader/exchanges/asset" "github.com/thrasher-corp/gocryptotrader/exchanges/order" ) @@ -51,7 +51,7 @@ type IFundingManager interface { HasFutures() bool HasExchangeBeenLiquidated(handler common.Event) bool RealisePNL(receivingExchange string, receivingAsset asset.Item, receivingCurrency currency.Code, realisedPNL decimal.Decimal) error - SetFunding(string, asset.Item, *account.Balance, bool) error + SetFunding(string, asset.Item, *accounts.Balance, bool) error } // IFundingTransferer allows for funding amounts to be transferred diff --git a/cmd/exchange_template/wrapper.tmpl b/cmd/exchange_template/wrapper.tmpl index 9e865eea..46fee71c 100644 --- a/cmd/exchange_template/wrapper.tmpl +++ b/cmd/exchange_template/wrapper.tmpl @@ -9,9 +9,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" @@ -314,11 +314,11 @@ 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 fetching requires more than one asset type please set // HasAssetTypeAccountSegregation to true in RESTCapabilities above. - return account.Holdings{}, common.ErrNotYetImplemented + return accounts.SubAccounts{}, common.ErrNotYetImplemented } // GetAccountFundingHistory returns funding history, deposits and withdrawals @@ -464,7 +464,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) } @@ -525,4 +525,4 @@ func (e *Exchange) SetLeverage(_ context.Context, _ asset.Item, _ currency.Pair, return common.ErrNotYetImplemented } -{{end}} \ No newline at end of file +{{end}} diff --git a/cmd/exchange_wrapper_issues/main.go b/cmd/exchange_wrapper_issues/main.go index bb4cb5b4..7b7f6a90 100644 --- a/cmd/exchange_wrapper_issues/main.go +++ b/cmd/exchange_wrapper_issues/main.go @@ -20,8 +20,8 @@ import ( "github.com/thrasher-corp/gocryptotrader/currency" "github.com/thrasher-corp/gocryptotrader/encoding/json" "github.com/thrasher-corp/gocryptotrader/engine" + "github.com/thrasher-corp/gocryptotrader/exchange/accounts" 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" @@ -546,17 +546,17 @@ func testWrappers(e exchange.IBotExchange, base *exchange.Base, cfg *Config) []E }) } - var GetCachedAccountInfoResponse account.Holdings - GetCachedAccountInfoResponse, err = e.GetCachedAccountInfo(context.TODO(), assetTypes[i]) + var GetCachedSubAccountsResponse accounts.SubAccounts + GetCachedSubAccountsResponse, err = e.GetCachedSubAccounts(context.TODO(), assetTypes[i]) msg = "" if err != nil { msg = err.Error() responseContainer.ErrorCount++ } responseContainer.EndpointResponses = append(responseContainer.EndpointResponses, EndpointResponse{ - Function: "GetCachedAccountInfo", + Function: "GetCachedSubAccounts", Error: msg, - Response: jsonifyInterface([]any{GetCachedAccountInfoResponse}), + Response: jsonifyInterface([]any{GetCachedSubAccountsResponse}), }) var getFundingHistoryResponse []exchange.FundingHistory diff --git a/cmd/exchange_wrapper_standards/exchange_wrapper_standards_test.go b/cmd/exchange_wrapper_standards/exchange_wrapper_standards_test.go index ce5ae01e..84194da4 100644 --- a/cmd/exchange_wrapper_standards/exchange_wrapper_standards_test.go +++ b/cmd/exchange_wrapper_standards/exchange_wrapper_standards_test.go @@ -15,11 +15,12 @@ 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/dispatch" "github.com/thrasher-corp/gocryptotrader/engine" + "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" @@ -50,9 +51,9 @@ func TestAllExchangeWrappers(t *testing.T) { t.Parallel() cfg := config.GetConfig() err := cfg.LoadConfig("../../testdata/configtest.json", true) - if err != nil { - t.Fatal("load config error", err) - } + require.NoError(t, err, "LoadConfig must not error") + err = dispatch.EnsureRunning(dispatch.DefaultMaxWorkers, dispatch.DefaultJobsLimit) + require.NoError(t, err, "dispatch.EnsureRunning must not error") for i := range cfg.Exchanges { name := strings.ToLower(cfg.Exchanges[i].Name) t.Run(name+" wrapper tests", func(t *testing.T) { @@ -144,6 +145,7 @@ assets: }) } assetPairs = append(assetPairs, assetPair{}) + return exch, assetPairs } @@ -284,7 +286,7 @@ var ( withdrawRequestParam = reflect.TypeOf((**withdraw.Request)(nil)).Elem() stringParam = reflect.TypeOf((*string)(nil)).Elem() feeBuilderParam = reflect.TypeOf((**exchange.FeeBuilder)(nil)).Elem() - credentialsParam = reflect.TypeOf((**account.Credentials)(nil)).Elem() + credentialsParam = reflect.TypeOf((**accounts.Credentials)(nil)).Elem() orderSideParam = reflect.TypeOf((*order.Side)(nil)).Elem() collateralModeParam = reflect.TypeOf((*collateral.Mode)(nil)).Elem() marginTypeParam = reflect.TypeOf((*margin.Type)(nil)).Elem() @@ -334,7 +336,7 @@ func generateMethodArg(ctx context.Context, t *testing.T, argGenerator *MethodAr Asset: argGenerator.AssetParams.Asset, }) case argGenerator.MethodInputType.AssignableTo(credentialsParam): - input = reflect.ValueOf(&account.Credentials{ + input = reflect.ValueOf(&accounts.Credentials{ Key: "test", Secret: "test", ClientID: "test", @@ -643,7 +645,8 @@ var acceptableErrors = []error{ limits.ErrExchangeLimitNotLoaded, // Is thrown when the limits aren't loaded for a particular exchange, asset, pair limits.ErrOrderLimitNotFound, // Is thrown when the order limit isn't found for a particular exchange, asset, pair limits.ErrEmptyLevels, // Is thrown if limits are not provided for the asset - account.ErrExchangeHoldingsNotFound, + accounts.ErrNoBalances, + accounts.ErrNoSubAccounts, ticker.ErrTickerNotFound, orderbook.ErrOrderbookNotFound, websocket.ErrNotConnected, diff --git a/cmd/gctcli/commands.go b/cmd/gctcli/commands.go index f6b073df..3219b189 100644 --- a/cmd/gctcli/commands.go +++ b/cmd/gctcli/commands.go @@ -575,24 +575,26 @@ func getTickers(c *cli.Context) error { return nil } -var getAccountInfoCommand = &cli.Command{ - Name: "getaccountinfo", - Usage: "gets the exchange account balance info", +var getAccountBalancesCommand = &cli.Command{ + Name: "getaccountbalances", + Usage: "gets the exchange account balances", ArgsUsage: " ", - Action: getAccountInfo, + Action: getAccountBalances, Flags: []cli.Flag{ &cli.StringFlag{ - Name: "exchange", - Usage: "the exchange to get the account info for", + Name: "exchange", + Usage: "the exchange to get the account balances for", + Required: true, }, &cli.StringFlag{ - Name: "asset", - Usage: "the asset type to get the account info for", + Name: "asset", + Usage: "the asset type to get the account balances for", + Required: true, }, }, } -func getAccountInfo(c *cli.Context) error { +func getAccountBalances(c *cli.Context) error { if c.NArg() == 0 && c.NumFlags() == 0 { return cli.ShowSubcommandHelp(c) } @@ -621,8 +623,8 @@ func getAccountInfo(c *cli.Context) error { defer closeConn(conn, cancel) client := gctrpc.NewGoCryptoTraderServiceClient(conn) - result, err := client.GetAccountInfo(c.Context, - &gctrpc.GetAccountInfoRequest{ + result, err := client.GetAccountBalances(c.Context, + &gctrpc.GetAccountBalancesRequest{ Exchange: exchange, AssetType: assetType, }, @@ -635,24 +637,24 @@ func getAccountInfo(c *cli.Context) error { return nil } -var getAccountInfoStreamCommand = &cli.Command{ - Name: "getaccountinfostream", - Usage: "gets the account info stream for a specific exchange", +var getAccountBalancesStreamCommand = &cli.Command{ + Name: "getaccountbalancesstream", + Usage: "gets the account balances stream for a specific exchange", ArgsUsage: " ", - Action: getAccountInfoStream, + Action: getAccountBalancesStream, Flags: []cli.Flag{ &cli.StringFlag{ Name: "exchange", - Usage: "the exchange to get the account info stream from", + Usage: "the exchange to get the account balances stream from", }, &cli.StringFlag{ Name: "asset", - Usage: "the asset type to get the account info stream for", + Usage: "the asset type to get the account balances stream for", }, }, } -func getAccountInfoStream(c *cli.Context) error { +func getAccountBalancesStream(c *cli.Context) error { if c.NArg() == 0 && c.NumFlags() == 0 { return cli.ShowSubcommandHelp(c) } @@ -683,8 +685,8 @@ func getAccountInfoStream(c *cli.Context) error { defer closeConn(conn, cancel) client := gctrpc.NewGoCryptoTraderServiceClient(conn) - result, err := client.GetAccountInfoStream(c.Context, - &gctrpc.GetAccountInfoRequest{Exchange: exchangeName, AssetType: assetType}) + result, err := client.GetAccountBalancesStream(c.Context, + &gctrpc.GetAccountBalancesRequest{Exchange: exchangeName, AssetType: assetType}) if err != nil { return err } @@ -706,24 +708,24 @@ func getAccountInfoStream(c *cli.Context) error { } } -var updateAccountInfoCommand = &cli.Command{ - Name: "updateaccountinfo", - Usage: "updates the exchange account balance info", +var updateAccountBalancesCommand = &cli.Command{ + Name: "updateaccountbalances", + Usage: "updates the exchange account balances", ArgsUsage: " ", - Action: updateAccountInfo, + Action: updateAccountBalances, Flags: []cli.Flag{ &cli.StringFlag{ Name: "exchange", - Usage: "the exchange to get the account info for", + Usage: "the exchange to get the account balances for", }, &cli.StringFlag{ Name: "asset", - Usage: "the asset type to get the account info for", + Usage: "the asset type to get the account balances for", }, }, } -func updateAccountInfo(c *cli.Context) error { +func updateAccountBalances(c *cli.Context) error { if c.NArg() == 0 && c.NumFlags() == 0 { return cli.ShowSubcommandHelp(c) } @@ -753,8 +755,8 @@ func updateAccountInfo(c *cli.Context) error { defer closeConn(conn, cancel) client := gctrpc.NewGoCryptoTraderServiceClient(conn) - result, err := client.UpdateAccountInfo(c.Context, - &gctrpc.GetAccountInfoRequest{ + result, err := client.UpdateAccountBalances(c.Context, + &gctrpc.GetAccountBalancesRequest{ Exchange: exchange, AssetType: assetType, }, diff --git a/cmd/gctcli/main.go b/cmd/gctcli/main.go index 59249ff9..ab5cca14 100644 --- a/cmd/gctcli/main.go +++ b/cmd/gctcli/main.go @@ -12,7 +12,7 @@ import ( "github.com/thrasher-corp/gocryptotrader/common" "github.com/thrasher-corp/gocryptotrader/core" "github.com/thrasher-corp/gocryptotrader/encoding/json" - "github.com/thrasher-corp/gocryptotrader/exchanges/account" + "github.com/thrasher-corp/gocryptotrader/exchange/accounts" "github.com/thrasher-corp/gocryptotrader/gctrpc/auth" "github.com/thrasher-corp/gocryptotrader/signaler" "github.com/urfave/cli/v2" @@ -28,7 +28,7 @@ var ( pairDelimiter string certPath string timeout time.Duration - exchangeCreds account.Credentials + exchangeCreds accounts.Credentials verbose bool ignoreTimeout bool ) @@ -172,9 +172,9 @@ func main() { getExchangeInfoCommand, getTickerCommand, getTickersCommand, - getAccountInfoCommand, - getAccountInfoStreamCommand, - updateAccountInfoCommand, + getAccountBalancesCommand, + getAccountBalancesStreamCommand, + updateAccountBalancesCommand, getConfigCommand, getPortfolioCommand, getPortfolioSummaryCommand, diff --git a/currency/forexprovider/currencylayer/currencylayer_types.go b/currency/forexprovider/currencylayer/currencylayer_types.go index a6d5b439..285c0eb2 100644 --- a/currency/forexprovider/currencylayer/currencylayer_types.go +++ b/currency/forexprovider/currencylayer/currencylayer_types.go @@ -22,9 +22,8 @@ const ( APIEndpointChange = "change" ) -// CurrencyLayer is a foreign exchange rate provider at -// https://currencylayer.com NOTE default base currency is USD when using a free -// account. Has automatic upgrade to a SSL connection. +// CurrencyLayer is a foreign exchange rate provider at https://currencylayer.com +// NOTE default base currency is USD when using a free account type CurrencyLayer struct { base.Base Requester *request.Requester diff --git a/dispatch/dispatch.go b/dispatch/dispatch.go index 2f460060..ee8ebce5 100644 --- a/dispatch/dispatch.go +++ b/dispatch/dispatch.go @@ -8,15 +8,17 @@ import ( "time" "github.com/gofrs/uuid" + "github.com/thrasher-corp/gocryptotrader/common" "github.com/thrasher-corp/gocryptotrader/log" ) +// Public errors. var ( - // ErrNotRunning defines an error when the dispatcher is not running - ErrNotRunning = errors.New("dispatcher not running") + ErrNotRunning = errors.New("dispatcher not running") + ErrDispatcherAlreadyRunning = errors.New("dispatcher already running") +) - errDispatcherNotInitialized = errors.New("dispatcher not initialised") - errDispatcherAlreadyRunning = errors.New("dispatcher already running") +var ( errDispatchShutdown = errors.New("dispatcher did not shutdown properly, routines failed to close") errDispatcherUUIDNotFoundInRouteList = errors.New("dispatcher uuid not found in route list") errTypeAssertionFailure = errors.New("type assertion failure") @@ -29,7 +31,7 @@ var ( limitMessage = "%w [%d] current worker count [%d]. Spawn more workers via --dispatchworkers=x, or increase the jobs limit via --dispatchjobslimit=x" ) -// Name is an exported subsystem name +// Name is an exported subsystem name. const Name = "dispatch" func init() { @@ -41,59 +43,58 @@ func NewDispatcher() *Dispatcher { return &Dispatcher{ routes: make(map[uuid.UUID][]chan any), outbound: sync.Pool{ - New: getChan, + New: func() any { return make(chan any) }, }, } } -func getChan() any { - // Create unbuffered channel for data pass - return make(chan any) -} - -// Start starts the dispatch system by spawning workers and allocating memory +// Start starts the dispatch system and spawns workers. func Start(workers, jobsLimit int) error { + dispatcher.m.Lock() + defer dispatcher.m.Unlock() return dispatcher.start(workers, jobsLimit) } -// Stop attempts to stop the dispatch service, this will close all pipe channels -// flush job list and drop all workers +// EnsureRunning starts the global dispatcher if it's not already running. +func EnsureRunning(workers, jobsLimit int) error { + dispatcher.m.Lock() + defer dispatcher.m.Unlock() + if dispatcher.running { + return nil + } + return dispatcher.start(workers, jobsLimit) +} + +// Stop will halt the dispatch service. func Stop() error { log.Debugln(log.DispatchMgr, "Dispatch manager shutting down...") return dispatcher.stop() } -// IsRunning checks to see if the dispatch service is running +// IsRunning checks to see if the dispatch service is running. func IsRunning() bool { return dispatcher.isRunning() } -// start compares atomic running value, sets defaults, overrides with -// configuration, then spawns workers +// start sets defaults and config and spawns workers. +// Does not provide locking protection. func (d *Dispatcher) start(workers, channelCapacity int) error { - if d == nil { - return errDispatcherNotInitialized + if err := common.NilGuard(d); err != nil { + return err } - d.m.Lock() - defer d.m.Unlock() - if d.running { - return errDispatcherAlreadyRunning + return ErrDispatcherAlreadyRunning } d.running = true if workers < 1 { - log.Warnf(log.DispatchMgr, - "workers cannot be zero, using default value %d\n", - DefaultMaxWorkers) + log.Warnf(log.DispatchMgr, "Dispatcher workers cannot be zero, using default value %d\n", DefaultMaxWorkers) workers = DefaultMaxWorkers } if channelCapacity < 1 { - log.Warnf(log.DispatchMgr, - "jobs limit cannot be zero, using default values %d\n", - DefaultJobsLimit) + log.Warnf(log.DispatchMgr, "Dispatcher jobs limit cannot be zero, using default values %d\n", DefaultJobsLimit) channelCapacity = DefaultJobsLimit } d.jobs = make(chan job, channelCapacity) @@ -107,10 +108,10 @@ func (d *Dispatcher) start(workers, channelCapacity int) error { return nil } -// stop stops the service and shuts down all worker routines +// stop stops the service and shuts down all worker routines. func (d *Dispatcher) stop() error { - if d == nil { - return errDispatcherNotInitialized + if err := common.NilGuard(d); err != nil { + return err } d.m.Lock() @@ -155,7 +156,7 @@ func (d *Dispatcher) stop() error { return nil } -// isRunning returns if the dispatch system is running +// isRunning returns if the dispatch system is running. func (d *Dispatcher) isRunning() bool { if d == nil { return false @@ -166,7 +167,7 @@ func (d *Dispatcher) isRunning() bool { return d.running } -// relayer routine relays communications across the defined routes +// relayer routine relays communications across the defined routes. func (d *Dispatcher) relayer() { for { select { @@ -201,20 +202,16 @@ func (d *Dispatcher) relayer() { } } -// publish relays data to the subscribed subsystems +// publish relays data to the subscribed subsystems. func (d *Dispatcher) publish(id uuid.UUID, data any) error { - if d == nil { - return errDispatcherNotInitialized + if err := common.NilGuard(d, data); err != nil { + return err } if id.IsNil() { return errIDNotSet } - if data == nil { - return errNoData - } - d.m.RLock() defer d.m.RUnlock() @@ -226,18 +223,14 @@ func (d *Dispatcher) publish(id uuid.UUID, data any) error { case d.jobs <- job{data, id}: // Push job into job channel. return nil default: - return fmt.Errorf(limitMessage, - errDispatcherJobsAtLimit, - len(d.jobs), - d.maxWorkers) + return fmt.Errorf(limitMessage, errDispatcherJobsAtLimit, len(d.jobs), d.maxWorkers) } } -// Subscribe subscribes a system and returns a communication chan, this does not -// ensure initial push. +// Subscribe subscribes a system and returns a communication chan, this does not ensure initial push. func (d *Dispatcher) subscribe(id uuid.UUID) (chan any, error) { - if d == nil { - return nil, errDispatcherNotInitialized + if err := common.NilGuard(d); err != nil { + return nil, err } if id.IsNil() { @@ -268,10 +261,10 @@ func (d *Dispatcher) subscribe(id uuid.UUID) (chan any, error) { return ch, nil } -// Unsubscribe unsubs a routine from the dispatcher +// Unsubscribe unsubs a routine from the dispatcher. func (d *Dispatcher) unsubscribe(id uuid.UUID, usedChan chan any) error { - if d == nil { - return errDispatcherNotInitialized + if err := common.NilGuard(d); err != nil { + return err } if id.IsNil() { @@ -321,10 +314,10 @@ func (d *Dispatcher) unsubscribe(id uuid.UUID, usedChan chan any) error { return errChannelNotFoundInUUIDRef } -// GetNewID returns a new ID +// GetNewID returns a new ID. func (d *Dispatcher) getNewID(genFn func() (uuid.UUID, error)) (uuid.UUID, error) { - if d == nil { - return uuid.Nil, errDispatcherNotInitialized + if err := common.NilGuard(d); err != nil { + return uuid.Nil, err } if genFn == nil { diff --git a/dispatch/dispatch_test.go b/dispatch/dispatch_test.go index 1c6111a6..a5688030 100644 --- a/dispatch/dispatch_test.go +++ b/dispatch/dispatch_test.go @@ -9,6 +9,7 @@ import ( "github.com/gofrs/uuid" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" + "github.com/thrasher-corp/gocryptotrader/common" ) var ( @@ -22,8 +23,17 @@ func TestGlobalDispatcher(t *testing.T) { assert.True(t, IsRunning(), "IsRunning should return true") err = Stop() - assert.NoError(t, err, "Stop should not error") + require.NoError(t, err, "Stop must not error") assert.False(t, IsRunning(), "IsRunning should return false") + + err = EnsureRunning(0, 0) + require.NoError(t, err, "EnsureRunning must not error when starting") + assert.True(t, IsRunning(), "IsRunning should return true after EnsureRunning") + + err = EnsureRunning(0, 0) + require.NoError(t, err, "EnsureRunning must not error when called twice") + + assert.NoError(t, Stop(), "Stop should not error") } func TestStartStop(t *testing.T) { @@ -33,10 +43,10 @@ func TestStartStop(t *testing.T) { assert.False(t, d.isRunning(), "IsRunning should return false") err := d.stop() - assert.ErrorIs(t, err, errDispatcherNotInitialized, "stop should error correctly") + assert.ErrorIs(t, err, common.ErrNilPointer, "stop should error correctly") err = d.start(10, 0) - assert.ErrorIs(t, err, errDispatcherNotInitialized, "start should error correctly") + assert.ErrorIs(t, err, common.ErrNilPointer, "start should error correctly") d = NewDispatcher() @@ -49,7 +59,7 @@ func TestStartStop(t *testing.T) { assert.True(t, d.isRunning(), "IsRunning should return true") err = d.start(0, 0) - assert.ErrorIs(t, err, errDispatcherAlreadyRunning, "start should error correctly") + assert.ErrorIs(t, err, ErrDispatcherAlreadyRunning, "start should error correctly") // Add route option id, err := d.getNewID(uuid.NewV4) @@ -74,7 +84,7 @@ func TestSubscribe(t *testing.T) { t.Parallel() var d *Dispatcher _, err := d.subscribe(uuid.Nil) - assert.ErrorIs(t, err, errDispatcherNotInitialized, "subscribe should error correctly") + assert.ErrorIs(t, err, common.ErrNilPointer, "subscribe should error correctly") d = NewDispatcher() @@ -97,7 +107,7 @@ func TestSubscribe(t *testing.T) { _, err = d.subscribe(id) assert.ErrorIs(t, err, errTypeAssertionFailure, "subscribe should error correctly") - d.outbound.New = getChan + d.outbound.New = func() any { return make(chan any) } ch, err := d.subscribe(id) assert.NoError(t, err, "subscribe should not error") assert.NotNil(t, ch, "Channel should not be nil") @@ -108,7 +118,7 @@ func TestUnsubscribe(t *testing.T) { var d *Dispatcher err := d.unsubscribe(uuid.Nil, nil) - assert.ErrorIs(t, err, errDispatcherNotInitialized, "unsubscribe should error correctly") + assert.ErrorIs(t, err, common.ErrNilPointer, "unsubscribe should error correctly") d = NewDispatcher() @@ -152,7 +162,7 @@ func TestPublish(t *testing.T) { var d *Dispatcher err := d.publish(uuid.Nil, nil) - assert.ErrorIs(t, err, errDispatcherNotInitialized, "publish should error correctly") + assert.ErrorIs(t, err, common.ErrNilPointer, "publish should error correctly") d = NewDispatcher() @@ -162,11 +172,11 @@ func TestPublish(t *testing.T) { err = d.start(2, 10) require.NoError(t, err, "start must not error") - err = d.publish(uuid.Nil, nil) + err = d.publish(uuid.Nil, "test") assert.ErrorIs(t, err, errIDNotSet, "publish should error correctly") err = d.publish(nonEmptyUUID, nil) - assert.ErrorIs(t, err, errNoData, "publish should error correctly") + assert.ErrorIs(t, err, common.ErrNilPointer, "publish should error correctly") // demonstrate job limit error d.routes[nonEmptyUUID] = []chan any{ @@ -209,7 +219,7 @@ func TestGetNewID(t *testing.T) { var d *Dispatcher _, err := d.getNewID(uuid.NewV4) - assert.ErrorIs(t, err, errDispatcherNotInitialized, "getNewID should error correctly") + assert.ErrorIs(t, err, common.ErrNilPointer, "getNewID should error correctly") d = NewDispatcher() @@ -233,16 +243,16 @@ func TestMux(t *testing.T) { t.Parallel() var mux *Mux _, err := mux.Subscribe(uuid.Nil) - assert.ErrorIs(t, err, errMuxIsNil, "Subscribe should error correctly") + assert.ErrorIs(t, err, common.ErrNilPointer, "Subscribe should error correctly") err = mux.Unsubscribe(uuid.Nil, nil) - assert.ErrorIs(t, err, errMuxIsNil, "Unsubscribe should error correctly") + assert.ErrorIs(t, err, common.ErrNilPointer, "Unsubscribe should error correctly") err = mux.Publish(nil) - assert.ErrorIs(t, err, errMuxIsNil, "Publish should error correctly") + assert.ErrorIs(t, err, common.ErrNilPointer, "Publish should error correctly") _, err = mux.GetID() - assert.ErrorIs(t, err, errMuxIsNil, "GetID should error correctly") + assert.ErrorIs(t, err, common.ErrNilPointer, "GetID should error correctly") d := NewDispatcher() err = d.start(0, 0) @@ -251,7 +261,7 @@ func TestMux(t *testing.T) { mux = GetNewMux(d) err = mux.Publish(nil) - assert.ErrorIs(t, err, errNoData, "Publish should error correctly") + assert.ErrorIs(t, err, common.ErrNilPointer, "Publish should error correctly") err = mux.Publish("lol") assert.ErrorIs(t, err, errNoIDs, "Publish should error correctly") diff --git a/dispatch/mux.go b/dispatch/mux.go index 8ac7bc95..9becf087 100644 --- a/dispatch/mux.go +++ b/dispatch/mux.go @@ -5,12 +5,11 @@ import ( "sync/atomic" "github.com/gofrs/uuid" + "github.com/thrasher-corp/gocryptotrader/common" ) var ( - errMuxIsNil = errors.New("mux is nil") errIDNotSet = errors.New("id not set") - errNoData = errors.New("data payload is nil") errNoIDs = errors.New("no IDs to publish data to") ) @@ -26,8 +25,8 @@ func GetNewMux(d *Dispatcher) *Mux { // Subscribe takes in a package defined signature element pointing to an ID set // and returns the associated pipe func (m *Mux) Subscribe(id uuid.UUID) (Pipe, error) { - if m == nil { - return Pipe{}, errMuxIsNil + if err := common.NilGuard(m); err != nil { + return Pipe{}, err } if id.IsNil() { @@ -44,8 +43,8 @@ func (m *Mux) Subscribe(id uuid.UUID) (Pipe, error) { // Unsubscribe returns channel to the pool for the full signature set func (m *Mux) Unsubscribe(id uuid.UUID, ch chan any) error { - if m == nil { - return errMuxIsNil + if err := common.NilGuard(m); err != nil { + return err } return m.d.unsubscribe(id, ch) } @@ -53,12 +52,8 @@ func (m *Mux) Unsubscribe(id uuid.UUID, ch chan any) error { // Publish takes in a persistent memory address and dispatches changes to // required pipes. func (m *Mux) Publish(data any, ids ...uuid.UUID) error { - if m == nil { - return errMuxIsNil - } - - if data == nil { - return errNoData + if err := common.NilGuard(m, data); err != nil { + return err } if len(ids) == 0 { @@ -69,8 +64,7 @@ func (m *Mux) Publish(data any, ids ...uuid.UUID) error { } for i := range ids { - err := m.d.publish(ids[i], data) - if err != nil { + if err := m.d.publish(ids[i], data); err != nil { return err } } @@ -79,8 +73,8 @@ func (m *Mux) Publish(data any, ids ...uuid.UUID) error { // GetID a new unique ID to track routing information in the dispatch system func (m *Mux) GetID() (uuid.UUID, error) { - if m == nil { - return uuid.UUID{}, errMuxIsNil + if err := common.NilGuard(m); err != nil { + return uuid.UUID{}, err } return m.d.getNewID(uuid.NewV4) } diff --git a/engine/helpers.go b/engine/helpers.go index 319a617e..50be29ca 100644 --- a/engine/helpers.go +++ b/engine/helpers.go @@ -28,7 +28,6 @@ import ( "github.com/thrasher-corp/gocryptotrader/currency" "github.com/thrasher-corp/gocryptotrader/dispatch" 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/binance" "github.com/thrasher-corp/gocryptotrader/exchanges/binanceus" @@ -539,45 +538,6 @@ func GetRelatableCurrencies(p currency.Pair, incOrig, incUSDT bool) currency.Pai return pairs } -// GetCollatedExchangeAccountInfoByCoin collates individual exchange account -// information and turns it into a map string of exchange.AccountCurrencyInfo -func GetCollatedExchangeAccountInfoByCoin(accounts []account.Holdings) map[currency.Code]account.Balance { - result := make(map[currency.Code]account.Balance) - for x := range accounts { - for y := range accounts[x].Accounts { - for z := range accounts[x].Accounts[y].Currencies { - currencyName := accounts[x].Accounts[y].Currencies[z].Currency - total := accounts[x].Accounts[y].Currencies[z].Total - onHold := accounts[x].Accounts[y].Currencies[z].Hold - avail := accounts[x].Accounts[y].Currencies[z].AvailableWithoutBorrow - free := accounts[x].Accounts[y].Currencies[z].Free - borrowed := accounts[x].Accounts[y].Currencies[z].Borrowed - - info, ok := result[currencyName] - if !ok { - accountInfo := account.Balance{ - Currency: currencyName, - Total: total, - Hold: onHold, - Free: free, - AvailableWithoutBorrow: avail, - Borrowed: borrowed, - } - result[currencyName] = accountInfo - } else { - info.Hold += onHold - info.Total += total - info.Free += free - info.AvailableWithoutBorrow += avail - info.Borrowed += borrowed - result[currencyName] = info - } - } - } - } - return result -} - // GetExchangeHighestPriceByCurrencyPair returns the exchange with the highest // price for a given currency pair and asset type func GetExchangeHighestPriceByCurrencyPair(p currency.Pair, a asset.Item) (string, error) { diff --git a/engine/helpers_test.go b/engine/helpers_test.go index c6ac82e1..3426e1f5 100644 --- a/engine/helpers_test.go +++ b/engine/helpers_test.go @@ -29,7 +29,6 @@ import ( "github.com/thrasher-corp/gocryptotrader/database" "github.com/thrasher-corp/gocryptotrader/dispatch" 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/protocol" @@ -707,67 +706,6 @@ func TestGetExchangeNamesByCurrency(t *testing.T) { } } -func TestGetCollatedExchangeAccountInfoByCoin(t *testing.T) { - t.Parallel() - CreateTestBot(t) - - var exchangeInfo []account.Holdings - - var bitfinexHoldings account.Holdings - bitfinexHoldings.Exchange = "Bitfinex" - bitfinexHoldings.Accounts = append(bitfinexHoldings.Accounts, - account.SubAccount{ - Currencies: []account.Balance{ - { - Currency: currency.BTC, - Total: 100, - Hold: 0, - }, - }, - }) - - exchangeInfo = append(exchangeInfo, bitfinexHoldings) - - var bitstampHoldings account.Holdings - bitstampHoldings.Exchange = testExchange - bitstampHoldings.Accounts = append(bitstampHoldings.Accounts, - account.SubAccount{ - Currencies: []account.Balance{ - { - Currency: currency.LTC, - Total: 100, - Hold: 0, - }, - { - Currency: currency.BTC, - Total: 100, - Hold: 0, - }, - }, - }) - - exchangeInfo = append(exchangeInfo, bitstampHoldings) - - result := GetCollatedExchangeAccountInfoByCoin(exchangeInfo) - if len(result) == 0 { - t.Fatal("Unexpected result") - } - - amount, ok := result[currency.BTC] - if !ok { - t.Fatal("Expected currency was not found in result map") - } - - if amount.Total != 200 { - t.Fatal("Unexpected result") - } - - _, ok = result[currency.ETH] - if ok { - t.Fatal("Unexpected result") - } -} - func TestGetExchangeHighestPriceByCurrencyPair(t *testing.T) { t.Parallel() CreateTestBot(t) diff --git a/engine/portfolio_manager.go b/engine/portfolio_manager.go index b19244c9..69f0c747 100644 --- a/engine/portfolio_manager.go +++ b/engine/portfolio_manager.go @@ -7,9 +7,9 @@ import ( "sync/atomic" "time" + "github.com/thrasher-corp/gocryptotrader/common" "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" "github.com/thrasher-corp/gocryptotrader/log" "github.com/thrasher-corp/gocryptotrader/portfolio" @@ -124,152 +124,97 @@ func (m *portfolioManager) processPortfolio() { } m.m.Lock() defer m.m.Unlock() - exchanges, err := m.exchangeManager.GetExchanges() - if err != nil { - log.Errorf(log.PortfolioMgr, "Portfolio manager cannot get exchanges: %v", err) + if err := m.updateExchangeBalances(); err != nil { + log.Errorf(log.PortfolioMgr, "Portfolio updateExchangeBalances error: %v", err) } - allExchangesHoldings := m.getExchangeAccountInfo(exchanges) - m.seedExchangeAccountInfo(allExchangesHoldings) data := m.base.GetPortfolioAddressesGroupedByCoin() for key, value := range data { if err := m.base.UpdatePortfolio(context.TODO(), value, key); err != nil { - log.Errorf(log.PortfolioMgr, "Portfolio manager: UpdatePortfolio error: %s for currency %s\n", err, key) + log.Errorf(log.PortfolioMgr, "Portfolio manager: UpdatePortfolio error: %s for currency %s", err, key) continue } - log.Debugf(log.PortfolioMgr, "Portfolio manager: Successfully updated address balance for %s address(es) %s\n", key, value) + log.Debugf(log.PortfolioMgr, "Portfolio manager: Successfully updated address balance for %s address(es) %s", key, value) } atomic.CompareAndSwapInt32(&m.processing, 1, 0) } -// seedExchangeAccountInfo seeds account info -func (m *portfolioManager) seedExchangeAccountInfo(accounts []account.Holdings) { - if len(accounts) == 0 { - return +// updateExchangeBalances calls UpdateAccountBalance on each exchange, and transfers the account balances into portfolio +func (m *portfolioManager) updateExchangeBalances() error { + if err := common.NilGuard(m); err != nil { + return err } - for x := range accounts { - var currencies []account.Balance - for y := range accounts[x].Accounts { - next: - for z := range accounts[x].Accounts[y].Currencies { - for i := range currencies { - if !accounts[x].Accounts[y].Currencies[z].Currency.Equal(currencies[i].Currency) { - continue - } - currencies[i].Hold += accounts[x].Accounts[y].Currencies[z].Hold - currencies[i].Total += accounts[x].Accounts[y].Currencies[z].Total - currencies[i].AvailableWithoutBorrow += accounts[x].Accounts[y].Currencies[z].AvailableWithoutBorrow - currencies[i].Free += accounts[x].Accounts[y].Currencies[z].Free - currencies[i].Borrowed += accounts[x].Accounts[y].Currencies[z].Borrowed - continue next - } - currencies = append(currencies, account.Balance{ - Currency: accounts[x].Accounts[y].Currencies[z].Currency, - Total: accounts[x].Accounts[y].Currencies[z].Total, - Hold: accounts[x].Accounts[y].Currencies[z].Hold, - Free: accounts[x].Accounts[y].Currencies[z].Free, - AvailableWithoutBorrow: accounts[x].Accounts[y].Currencies[z].AvailableWithoutBorrow, - Borrowed: accounts[x].Accounts[y].Currencies[z].Borrowed, - }) + exchanges, errs := m.exchangeManager.GetExchanges() + if errs != nil { + return fmt.Errorf("portfolio manager cannot get exchanges: %w", errs) + } + for _, e := range exchanges { + if !e.IsEnabled() { + continue + } + if !e.IsRESTAuthenticationSupported() { + if m.base.Verbose { + log.Debugf(log.PortfolioMgr, "Portfolio skipping %s due to disabled authenticated API support", e.GetName()) } + continue + } + assetTypes := asset.Items{asset.Spot} + if e.HasAssetTypeAccountSegregation() { + assetTypes = e.GetAssetTypes(true) } - for j := range currencies { - if !m.base.ExchangeAddressCoinExists(accounts[x].Exchange, currencies[j].Currency) { - if currencies[j].Total <= 0 { - continue - } - - log.Debugf(log.PortfolioMgr, "Portfolio: Adding new exchange address: %s, %s, %f, %s\n", - accounts[x].Exchange, - currencies[j].Currency, - currencies[j].Total, - portfolio.ExchangeAddress) - - m.base.Addresses = append(m.base.Addresses, portfolio.Address{ - Address: accounts[x].Exchange, - CoinType: currencies[j].Currency, - Balance: currencies[j].Total, - Description: portfolio.ExchangeAddress, - }) - continue - } - - if currencies[j].Total <= 0 { - log.Debugf(log.PortfolioMgr, "Portfolio: Removing %s %s entry.\n", - accounts[x].Exchange, - currencies[j].Currency) - m.base.RemoveExchangeAddress(accounts[x].Exchange, currencies[j].Currency) - continue - } - - balance, ok := m.base.GetAddressBalance(accounts[x].Exchange, - portfolio.ExchangeAddress, - currencies[j].Currency) - if !ok { - continue - } - - if balance != currencies[j].Total { - log.Debugf(log.PortfolioMgr, "Portfolio: Updating %s %s entry with balance %f.\n", - accounts[x].Exchange, - currencies[j].Currency, - currencies[j].Total) - m.base.UpdateExchangeAddressBalance(accounts[x].Exchange, - currencies[j].Currency, - currencies[j].Total) + for _, a := range assetTypes { + if _, err := e.UpdateAccountBalances(context.TODO(), a); err != nil { + errs = common.AppendError(errs, fmt.Errorf("error updating %s %s account balances: %w", e.GetName(), a, err)) } } + if err := m.updateExchangeAddressBalances(e); err != nil { + errs = common.AppendError(errs, fmt.Errorf("error updating %s account balances: %w", e.GetName(), err)) + } } + return errs } -// getExchangeAccountInfo returns all the current enabled exchanges -func (m *portfolioManager) getExchangeAccountInfo(exchanges []exchange.IBotExchange) []account.Holdings { - response := make([]account.Holdings, 0, len(exchanges)) - for x := range exchanges { - if !exchanges[x].IsEnabled() { - continue - } - if !exchanges[x].IsRESTAuthenticationSupported() { - if m.base.Verbose { - log.Debugf(log.PortfolioMgr, - "skipping %s due to disabled authenticated API support.\n", - exchanges[x].GetName()) - } - continue - } - - assetTypes := asset.Items{asset.Spot} - if exchanges[x].HasAssetTypeAccountSegregation() { - // Get enabled exchange asset types to sync account information. - // TODO: Update with further api key asset segration e.g. Kraken has - // individual keys associated with different asset types. - assetTypes = exchanges[x].GetAssetTypes(true) - } - - exchangeHoldings := account.Holdings{ - Exchange: exchanges[x].GetName(), - Accounts: make([]account.SubAccount, 0, len(assetTypes)), - } - for y := range assetTypes { - // Update account info to process account updates in memory on - // every fetch. - accountHoldings, err := exchanges[x].UpdateAccountInfo(context.TODO(), assetTypes[y]) - if err != nil { - log.Errorf(log.PortfolioMgr, - "Error encountered retrieving exchange account info for %s. Error %s\n", - exchanges[x].GetName(), - err) +// updateExchangeAddressBalances fetches and collates all account balances with their deposit addresses +func (m *portfolioManager) updateExchangeAddressBalances(e exchange.IBotExchange) error { + if err := common.NilGuard(m, e); err != nil { + return err + } + currs, err := e.GetBase().Accounts.CurrencyBalances(nil, asset.All) + if err != nil { + return err + } + eName := e.GetName() + for c, b := range currs { + if !m.base.ExchangeAddressCoinExists(e.GetName(), c) { + if b.Total <= 0 { continue } - exchangeHoldings.Accounts = append(exchangeHoldings.Accounts, accountHoldings.Accounts...) + + log.Debugf(log.PortfolioMgr, "Portfolio: Adding new exchange address: %s, %s, %f, %s", eName, c, b.Total, portfolio.ExchangeAddress) + + m.base.Addresses = append(m.base.Addresses, portfolio.Address{ + Address: eName, + CoinType: c, + Balance: b.Total, + Description: portfolio.ExchangeAddress, + }) + continue } - if len(exchangeHoldings.Accounts) > 0 { - response = append(response, exchangeHoldings) + + if b.Total <= 0 { + log.Debugf(log.PortfolioMgr, "Portfolio: Removing %s %s entry", eName, c) + m.base.RemoveExchangeAddress(eName, c) + continue + } + + if balance, ok := m.base.GetAddressBalance(eName, portfolio.ExchangeAddress, c); ok && balance != b.Total { + log.Debugf(log.PortfolioMgr, "Portfolio: Updating %s %s entry with balance %f", eName, c, b.Total) + m.base.UpdateExchangeAddressBalance(eName, c, b.Total) } } - return response + return nil } // AddAddress adds a new portfolio address for the portfolio manager to track diff --git a/engine/portfolio_manager_test.go b/engine/portfolio_manager_test.go index 70da56a8..8807bb25 100644 --- a/engine/portfolio_manager_test.go +++ b/engine/portfolio_manager_test.go @@ -1,11 +1,18 @@ package engine import ( + "context" + "errors" "sync" "testing" "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/portfolio" ) func TestSetupPortfolioManager(t *testing.T) { @@ -94,3 +101,98 @@ func TestProcessPortfolio(t *testing.T) { m.processPortfolio() } + +func TestUpdateExchangeBalances(t *testing.T) { + t.Parallel() + + assert.ErrorContains(t, (*portfolioManager)(nil).updateExchangeBalances(), "nil pointer: *engine.portfolioManager") + assert.ErrorIs(t, new(portfolioManager).updateExchangeBalances(), ErrNilSubsystem) + + m, err := setupPortfolioManager(NewExchangeManager(), 0, &portfolio.Base{Verbose: true}) + require.NoError(t, err, "setupPortfolioManager must not error") + assert.NoError(t, m.updateExchangeBalances(), "updateExchangeBalances should not error with an empty exchange list") + + e := &mockExchange{err: errors.New("Mock UpdateBalanceError")} + m.exchangeManager.exchanges = map[string]exchange.IBotExchange{"mock": e} + assert.NoError(t, m.updateExchangeBalances(), "updateExchangeBalances should not error on disabled exchanges") + + e.enabled = true + assert.NoError(t, m.updateExchangeBalances(), "updateExchangeBalances should skip exchange without auth support") + + e.authSupported = true + assert.ErrorIs(t, m.updateExchangeBalances(), e.err, "error should contain the UpdateAccountBalances error message") +} + +func TestUpdateExchangeAddressBalances(t *testing.T) { + t.Parallel() + + assert.ErrorContains(t, (*portfolioManager)(nil).updateExchangeAddressBalances(nil), "nil pointer: *engine.portfolioManager") + assert.ErrorContains(t, new(portfolioManager).updateExchangeAddressBalances(nil), "nil pointer: ") + + e := &mockExchange{enabled: false, err: errors.New("Mock UpdateBalanceError")} + m, err := setupPortfolioManager(NewExchangeManager(), 0, nil) + require.NoError(t, err, "setupPortfolioManager must not error") + assert.ErrorContains(t, m.updateExchangeAddressBalances(e), "nil pointer: *accounts.Accounts", "updateExchangeAddressBalances should propagate CurrencyBalances errors") + + a := accounts.MustNewAccounts(e) + e.accounts = a + subAcct := accounts.NewSubAccount(asset.Spot, "") + subAcct.Balances.Set(currency.BTC, accounts.Balance{Total: 1.5}) + subAcct.Balances.Set(currency.ETH, accounts.Balance{Total: 0}) + require.NoError(t, a.Save(t.Context(), accounts.SubAccounts{subAcct}, false), "accounts.Save must not error") + require.NoError(t, m.updateExchangeAddressBalances(e)) + require.Len(t, m.base.Addresses, 1, "must have one address for the positive balance") + assert.Equal(t, 1.5, m.base.Addresses[0].Balance, "balance should match on a new address") + + subAcct.Balances.Set(currency.BTC, accounts.Balance{Total: 2}) + require.NoError(t, a.Save(t.Context(), accounts.SubAccounts{subAcct}, true), "accounts.Save must not error") + require.NoError(t, m.updateExchangeAddressBalances(e)) + require.Len(t, m.base.Addresses, 1, "must have one address for the positive balance") + assert.Equal(t, 2.0, m.base.Addresses[0].Balance, "balance should match after update existing address") + + subAcct.Balances.Set(currency.BTC, accounts.Balance{Total: 0}) + require.NoError(t, a.Save(t.Context(), accounts.SubAccounts{subAcct}, true), "accounts.Save must not error") + require.NoError(t, m.updateExchangeAddressBalances(e)) + assert.Empty(t, m.base.Addresses, "should have removed address with no balance") +} + +// mockExchange is a minimal mock for testing +type mockExchange struct { + exchange.IBotExchange + enabled bool + authSupported bool + err error + accounts *accounts.Accounts +} + +func (m *mockExchange) GetName() string { + return "mocky" +} + +func (m *mockExchange) IsEnabled() bool { + return m.enabled +} + +func (m *mockExchange) IsRESTAuthenticationSupported() bool { + return m.authSupported +} + +func (m *mockExchange) HasAssetTypeAccountSegregation() bool { + return true +} + +func (m *mockExchange) GetAssetTypes(bool) asset.Items { + return asset.Items{asset.Spot, asset.Futures} +} + +func (m *mockExchange) UpdateAccountBalances(context.Context, asset.Item) (accounts.SubAccounts, error) { + return nil, m.err +} + +func (m *mockExchange) GetBase() *exchange.Base { + return &exchange.Base{Name: "mocky", Accounts: m.accounts} +} + +func (m *mockExchange) GetCredentials(context.Context) (*accounts.Credentials, error) { + return &accounts.Credentials{Key: m.GetName()}, nil +} diff --git a/engine/rpcserver.go b/engine/rpcserver.go index 40e23bf7..24cd3d48 100644 --- a/engine/rpcserver.go +++ b/engine/rpcserver.go @@ -32,8 +32,8 @@ import ( "github.com/thrasher-corp/gocryptotrader/database/repository/audit" exchangeDB "github.com/thrasher-corp/gocryptotrader/database/repository/exchange" "github.com/thrasher-corp/gocryptotrader/encoding/json" + "github.com/thrasher-corp/gocryptotrader/exchange/accounts" 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/fundingrate" @@ -73,7 +73,6 @@ var ( errCurrencyPairInvalid = errors.New("currency provided is not found in the available pairs list") errNoTrades = errors.New("no trades returned from supplied params") errNilRequestData = errors.New("nil request data received, cannot continue") - errNoAccountInformation = errors.New("account information does not exist") errShutdownNotAllowed = errors.New("shutting down this bot instance is not allowed via gRPC, please enable by command line flag --grpcshutdown or config.json field grpcAllowBotShutdown") errGRPCShutdownSignalIsNil = errors.New("cannot shutdown, gRPC shutdown channel is nil") errInvalidStrategy = errors.New("invalid strategy") @@ -114,7 +113,7 @@ func (s *RPCServer) authenticateClient(ctx context.Context) (context.Context, er password != s.Config.RemoteControl.Password { return ctx, errors.New("username/password mismatch") } - ctx, err = account.ParseCredentialsMetadata(ctx, md) + ctx, err = accounts.ParseCredentialsMetadata(ctx, md) if err != nil { return ctx, err } @@ -556,87 +555,80 @@ func (s *RPCServer) GetOrderbooks(_ context.Context, _ *gctrpc.GetOrderbooksRequ return &gctrpc.GetOrderbooksResponse{Orderbooks: obResponse}, nil } -// GetAccountInfo returns an account balance for a specific exchange -func (s *RPCServer) GetAccountInfo(ctx context.Context, r *gctrpc.GetAccountInfoRequest) (*gctrpc.GetAccountInfoResponse, error) { +// GetAccountBalances returns an account balance for a specific exchange. +func (s *RPCServer) GetAccountBalances(ctx context.Context, r *gctrpc.GetAccountBalancesRequest) (*gctrpc.GetAccountBalancesResponse, error) { assetType, err := asset.New(r.AssetType) if err != nil { return nil, err } - exch, err := s.GetExchangeByName(r.Exchange) + e, err := s.GetExchangeByName(r.Exchange) if err != nil { return nil, err } - err = checkParams(r.Exchange, exch, assetType, currency.EMPTYPAIR) + if err := checkParams(r.Exchange, e, assetType, currency.EMPTYPAIR); err != nil { + return nil, err + } + + resp, err := e.GetCachedSubAccounts(ctx, assetType) if err != nil { return nil, err } - resp, err := exch.GetCachedAccountInfo(ctx, assetType) - if err != nil { - return nil, err - } - - return createAccountInfoRequest(resp) + return accountBalanceResp(r.Exchange, resp), nil } -// UpdateAccountInfo forces an update of the account info -func (s *RPCServer) UpdateAccountInfo(ctx context.Context, r *gctrpc.GetAccountInfoRequest) (*gctrpc.GetAccountInfoResponse, error) { +// UpdateAccountBalances forces an update of the account balances. +func (s *RPCServer) UpdateAccountBalances(ctx context.Context, r *gctrpc.GetAccountBalancesRequest) (*gctrpc.GetAccountBalancesResponse, error) { assetType, err := asset.New(r.AssetType) if err != nil { return nil, err } - exch, err := s.GetExchangeByName(r.Exchange) + e, err := s.GetExchangeByName(r.Exchange) if err != nil { return nil, err } - err = checkParams(r.Exchange, exch, assetType, currency.EMPTYPAIR) + if err := checkParams(r.Exchange, e, assetType, currency.EMPTYPAIR); err != nil { + return nil, err + } + + resp, err := e.UpdateAccountBalances(ctx, assetType) if err != nil { return nil, err } - resp, err := exch.UpdateAccountInfo(ctx, assetType) - if err != nil { - return nil, err - } - - return createAccountInfoRequest(resp) + return accountBalanceResp(r.Exchange, resp), nil } -func createAccountInfoRequest(h account.Holdings) (*gctrpc.GetAccountInfoResponse, error) { - accounts := make([]*gctrpc.Account, len(h.Accounts)) - for x := range h.Accounts { - var a gctrpc.Account - a.Id = h.Accounts[x].Credentials.String() - for _, y := range h.Accounts[x].Currencies { - if y.Total == 0 && - y.Hold == 0 && - y.Free == 0 && - y.AvailableWithoutBorrow == 0 && - y.Borrowed == 0 { - continue - } - a.Currencies = append(a.Currencies, &gctrpc.AccountCurrencyInfo{ - Currency: y.Currency.String(), - TotalValue: y.Total, - Hold: y.Hold, - Free: y.Free, - FreeWithoutBorrow: y.AvailableWithoutBorrow, - Borrowed: y.Borrowed, - UpdatedAt: timestamppb.New(y.UpdatedAt), +func accountBalanceResp(eName string, s accounts.SubAccounts) *gctrpc.GetAccountBalancesResponse { + subAccts := make([]*gctrpc.Account, len(s)) + for i, sa := range s { + subAccts[i] = &gctrpc.Account{ + Id: sa.ID, + } + for curr, bal := range sa.Balances { + subAccts[i].Currencies = append(subAccts[i].Currencies, &gctrpc.AccountCurrencyInfo{ + Currency: curr.String(), + TotalValue: bal.Total, + Hold: bal.Hold, + Free: bal.Free, + FreeWithoutBorrow: bal.AvailableWithoutBorrow, + Borrowed: bal.Borrowed, + UpdatedAt: timestamppb.New(bal.UpdatedAt), }) } - accounts[x] = &a } - - return &gctrpc.GetAccountInfoResponse{Exchange: h.Exchange, Accounts: accounts}, nil + return &gctrpc.GetAccountBalancesResponse{ + Exchange: eName, + Accounts: subAccts, + } } -// GetAccountInfoStream streams an account balance for a specific exchange -func (s *RPCServer) GetAccountInfoStream(r *gctrpc.GetAccountInfoRequest, stream gctrpc.GoCryptoTraderService_GetAccountInfoStreamServer) error { +// GetAccountBalancesStream streams an account balance for a specific exchange +func (s *RPCServer) GetAccountBalancesStream(r *gctrpc.GetAccountBalancesRequest, stream gctrpc.GoCryptoTraderService_GetAccountBalancesStreamServer) error { assetType, err := asset.New(r.AssetType) if err != nil { return err @@ -652,7 +644,7 @@ func (s *RPCServer) GetAccountInfoStream(r *gctrpc.GetAccountInfoRequest, stream return err } - pipe, err := account.SubscribeToExchangeAccount(r.Exchange) + pipe, err := exch.SubscribeAccountBalances() if err != nil { return err } @@ -677,32 +669,12 @@ func (s *RPCServer) GetAccountInfoStream(r *gctrpc.GetAccountInfoRequest, stream case <-init: } - holdings, err := exch.GetCachedAccountInfo(stream.Context(), assetType) + subAccts, err := exch.GetCachedSubAccounts(stream.Context(), assetType) if err != nil { return err } - accounts := make([]*gctrpc.Account, len(holdings.Accounts)) - for x := range holdings.Accounts { - subAccounts := make([]*gctrpc.AccountCurrencyInfo, len(holdings.Accounts[x].Currencies)) - for y := range holdings.Accounts[x].Currencies { - subAccounts[y] = &gctrpc.AccountCurrencyInfo{ - Currency: holdings.Accounts[x].Currencies[y].Currency.String(), - TotalValue: holdings.Accounts[x].Currencies[y].Total, - Hold: holdings.Accounts[x].Currencies[y].Hold, - UpdatedAt: timestamppb.New(holdings.Accounts[x].Currencies[y].UpdatedAt), - } - } - accounts[x] = &gctrpc.Account{ - Id: holdings.Accounts[x].ID, - Currencies: subAccounts, - } - } - - if err := stream.Send(&gctrpc.GetAccountInfoResponse{ - Exchange: holdings.Exchange, - Accounts: accounts, - }); err != nil { + if err := stream.Send(accountBalanceResp(r.Exchange, subAccts)); err != nil { return err } } @@ -4756,8 +4728,7 @@ func (s *RPCServer) GetCollateral(ctx context.Context, r *gctrpc.GetCollateralRe if err != nil { return nil, err } - feat := exch.GetSupportedFeatures() - if !feat.FuturesCapabilities.Collateral { + if f := exch.GetSupportedFeatures(); !f.FuturesCapabilities.Collateral { return nil, fmt.Errorf("%w Get Collateral for exchange %v", common.ErrFunctionNotSupported, exch.GetName()) } @@ -4766,42 +4737,16 @@ func (s *RPCServer) GetCollateral(ctx context.Context, r *gctrpc.GetCollateralRe return nil, err } - err = checkParams(r.Exchange, exch, a, currency.EMPTYPAIR) - if err != nil { + if err := checkParams(r.Exchange, exch, a, currency.EMPTYPAIR); err != nil { return nil, err } if !a.IsFutures() { return nil, fmt.Errorf("%s %w", a, futures.ErrNotFuturesAsset) } - ai, err := exch.GetCachedAccountInfo(ctx, a) + currBalances, err := exch.GetCachedCurrencyBalances(ctx, a) if err != nil { return nil, err } - creds, err := exch.GetCredentials(ctx) - if err != nil { - return nil, err - } - - subAccounts := make([]string, len(ai.Accounts)) - var acc *account.SubAccount - for i := range ai.Accounts { - subAccounts[i] = ai.Accounts[i].ID - if ai.Accounts[i].ID == "main" && creds.SubAccount == "" { - acc = &ai.Accounts[i] - break - } - if strings.EqualFold(creds.SubAccount, ai.Accounts[i].ID) { - acc = &ai.Accounts[i] - break - } - } - if acc == nil { - return nil, fmt.Errorf("%w for %s %s and stored credentials - available subaccounts: %s", - errNoAccountInformation, - exch.GetName(), - creds.SubAccount, - strings.Join(subAccounts, ",")) - } var spotPairs currency.Pairs if r.CalculateOffline { spotPairs, err = exch.GetAvailablePairs(asset.Spot) @@ -4810,24 +4755,22 @@ func (s *RPCServer) GetCollateral(ctx context.Context, r *gctrpc.GetCollateralRe } } - calculators := make([]futures.CollateralCalculator, 0, len(acc.Currencies)) - for i := range acc.Currencies { - total := decimal.NewFromFloat(acc.Currencies[i].Total) - free := decimal.NewFromFloat(acc.Currencies[i].AvailableWithoutBorrow) + calculators := make([]futures.CollateralCalculator, 0, len(currBalances)) + for curr, balance := range currBalances { + total := decimal.NewFromFloat(balance.Total) + free := decimal.NewFromFloat(balance.AvailableWithoutBorrow) cal := futures.CollateralCalculator{ CalculateOffline: r.CalculateOffline, - CollateralCurrency: acc.Currencies[i].Currency, + CollateralCurrency: curr, Asset: a, FreeCollateral: free, LockedCollateral: total.Sub(free), } - if r.CalculateOffline && - !acc.Currencies[i].Currency.Equal(currency.USD) { + if r.CalculateOffline && !curr.Equal(currency.USD) { var tick *ticker.Price - tickerCurr := currency.NewPair(acc.Currencies[i].Currency, currency.USD) + tickerCurr := currency.NewPair(curr, currency.USD) if !spotPairs.Contains(tickerCurr, true) { - // cannot price currency to calculate collateral - continue + continue // cannot price currency to calculate collateral } tick, err = exch.GetCachedTicker(tickerCurr, asset.Spot) if err != nil { @@ -4855,7 +4798,6 @@ func (s *RPCServer) GetCollateral(ctx context.Context, r *gctrpc.GetCollateralRe collateralDisplayCurrency := " " + c.CollateralCurrency.String() result := &gctrpc.GetCollateralResponse{ - SubAccount: creds.SubAccount, CollateralCurrency: c.CollateralCurrency.String(), AvailableCollateral: c.AvailableCollateral.String() + collateralDisplayCurrency, UsedCollateral: c.UsedCollateral.String() + collateralDisplayCurrency, diff --git a/engine/rpcserver_test.go b/engine/rpcserver_test.go index 22bce155..001d5f73 100644 --- a/engine/rpcserver_test.go +++ b/engine/rpcserver_test.go @@ -34,8 +34,8 @@ import ( dbexchange "github.com/thrasher-corp/gocryptotrader/database/repository/exchange" sqltrade "github.com/thrasher-corp/gocryptotrader/database/repository/trade" "github.com/thrasher-corp/gocryptotrader/encoding/json" + "github.com/thrasher-corp/gocryptotrader/exchange/accounts" 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/binance" "github.com/thrasher-corp/gocryptotrader/exchanges/collateral" @@ -316,28 +316,33 @@ func (f fExchange) GetCachedTicker(p currency.Pair, a asset.Item) (*ticker.Price }, nil } -// GetCachedAccountInfo overrides testExchange's fetch account info function -// to do the bare minimum required with no API calls or credentials required -func (f fExchange) GetCachedAccountInfo(_ context.Context, a asset.Item) (account.Holdings, error) { - return account.Holdings{ - Exchange: f.GetName(), - Accounts: []account.SubAccount{ - { - ID: "1337", - AssetType: a, - Currencies: []account.Balance{ - { - Currency: currency.USD, - Total: 1337, - }, - { - Currency: currency.BTC, - Total: 13337, - }, - }, - }, +// GetCachedSubAccounts overrides testExchange's fetch account info function to do the bare minimum required with no API calls or credentials required +// Only returns balances for creds with a SubAccount populated +func (f fExchange) GetCachedSubAccounts(ctx context.Context, a asset.Item) (accounts.SubAccounts, error) { + creds, err := f.GetCredentials(ctx) + if err != nil { + return nil, err + } + if creds.SubAccount == "" { + return nil, fmt.Errorf("%w for %s credentials %s asset %s", accounts.ErrNoBalances, f.GetName(), creds, a) + } + return accounts.SubAccounts{{ + ID: creds.SubAccount, + Balances: accounts.CurrencyBalances{ + currency.USD: {Currency: currency.USD, Total: 1337}, + currency.BTC: {Currency: currency.BTC, Total: 13337}, }, - }, nil + }}, nil +} + +// GetCachedCurrencyBalances overrides testExchange's fetch account info function to do the bare minimum required with no API calls or credentials required +// Only returns balances for creds with a SubAccount populated +func (f fExchange) GetCachedCurrencyBalances(ctx context.Context, a asset.Item) (accounts.CurrencyBalances, error) { + subAccts, err := f.GetCachedSubAccounts(ctx, a) + if err != nil { + return nil, err + } + return subAccts[0].Balances, nil } // CalculateTotalCollateral overrides testExchange's CalculateTotalCollateral function @@ -386,22 +391,13 @@ func (f fExchange) CalculateTotalCollateral(context.Context, *futures.TotalColla }, nil } -// UpdateAccountInfo overrides testExchange's update account info function +// UpdateAccountBalances overrides testExchange's update account info function // to do the bare minimum required with no API calls or credentials required -func (f fExchange) UpdateAccountInfo(_ context.Context, a asset.Item) (account.Holdings, error) { +func (f fExchange) UpdateAccountBalances(_ context.Context, a asset.Item) (accounts.SubAccounts, error) { if a == asset.Futures { - return account.Holdings{}, asset.ErrNotSupported + return accounts.SubAccounts{}, asset.ErrNotSupported } - return account.Holdings{ - Exchange: f.GetName(), - Accounts: []account.SubAccount{ - { - ID: "1337", - AssetType: a, - Currencies: nil, - }, - }, - }, nil + return accounts.SubAccounts{accounts.NewSubAccount(a, "1337")}, nil } // GetCurrencyStateSnapshot overrides interface function @@ -1216,63 +1212,48 @@ func TestGetHistoricTrades(t *testing.T) { } } -func TestGetAccountInfo(t *testing.T) { +func TestGetAccountBalances(t *testing.T) { t.Parallel() em := NewExchangeManager() exch, err := em.NewExchangeByName(testExchange) - if err != nil { - t.Fatal(err) - } + require.NoError(t, err) b := exch.GetBase() b.Name = fakeExchangeName b.Enabled = true b.CurrencyPairs.Pairs = make(map[asset.Item]*currency.PairStore) - b.CurrencyPairs.Pairs[asset.Spot] = ¤cy.PairStore{ - AssetEnabled: true, - } - fakeExchange := fExchange{ - IBotExchange: exch, - } + b.CurrencyPairs.Pairs[asset.Spot] = ¤cy.PairStore{AssetEnabled: true} + fakeExchange := fExchange{IBotExchange: exch} err = em.Add(fakeExchange) require.NoError(t, err) - + ctx := accounts.DeployCredentialsToContext(t.Context(), &accounts.Credentials{Key: "fakerino", Secret: "supafake", SubAccount: "42"}) s := RPCServer{Engine: &Engine{ExchangeManager: em}} - _, err = s.GetAccountInfo(t.Context(), &gctrpc.GetAccountInfoRequest{Exchange: fakeExchangeName, AssetType: asset.Spot.String()}) + _, err = s.GetAccountBalances(ctx, &gctrpc.GetAccountBalancesRequest{Exchange: fakeExchangeName, AssetType: asset.Spot.String()}) assert.NoError(t, err) } -func TestUpdateAccountInfo(t *testing.T) { +func TestUpdateAccountBalances(t *testing.T) { t.Parallel() em := NewExchangeManager() exch, err := em.NewExchangeByName(testExchange) - if err != nil { - t.Fatal(err) - } + require.NoError(t, err) b := exch.GetBase() b.Name = fakeExchangeName b.Enabled = true b.CurrencyPairs.Pairs = make(map[asset.Item]*currency.PairStore) - b.CurrencyPairs.Pairs[asset.Spot] = ¤cy.PairStore{ - AssetEnabled: true, - } - fakeExchange := fExchange{ - IBotExchange: exch, - } + b.CurrencyPairs.Pairs[asset.Spot] = ¤cy.PairStore{AssetEnabled: true} + fakeExchange := fExchange{IBotExchange: exch} err = em.Add(fakeExchange) require.NoError(t, err) s := RPCServer{Engine: &Engine{ExchangeManager: em}} - - _, err = s.GetAccountInfo(t.Context(), &gctrpc.GetAccountInfoRequest{Exchange: fakeExchangeName, AssetType: asset.Spot.String()}) + ctx := accounts.DeployCredentialsToContext(t.Context(), &accounts.Credentials{Key: "fakerino", Secret: "supafake", SubAccount: "42"}) + _, err = s.GetAccountBalances(ctx, &gctrpc.GetAccountBalancesRequest{Exchange: fakeExchangeName, AssetType: asset.Spot.String()}) assert.NoError(t, err) - _, err = s.UpdateAccountInfo(t.Context(), &gctrpc.GetAccountInfoRequest{Exchange: fakeExchangeName, AssetType: asset.Futures.String()}) + _, err = s.UpdateAccountBalances(ctx, &gctrpc.GetAccountBalancesRequest{Exchange: fakeExchangeName, AssetType: asset.Futures.String()}) assert.ErrorIs(t, err, currency.ErrAssetNotFound) - _, err = s.UpdateAccountInfo(t.Context(), &gctrpc.GetAccountInfoRequest{ - Exchange: fakeExchangeName, - AssetType: asset.Spot.String(), - }) + _, err = s.UpdateAccountBalances(ctx, &gctrpc.GetAccountBalancesRequest{Exchange: fakeExchangeName, AssetType: asset.Spot.String()}) assert.NoError(t, err) } @@ -2196,6 +2177,8 @@ func TestGetCollateral(t *testing.T) { b := exch.GetBase() b.Name = fakeExchangeName b.Enabled = true + b.Accounts, err = accounts.GetStore().GetExchangeAccounts(b) + require.NoError(t, err, "GetExchangeAccounts must not error") cp, err := currency.NewPairFromString("btc-usd") require.NoError(t, err) @@ -2235,17 +2218,15 @@ func TestGetCollateral(t *testing.T) { }) require.ErrorIs(t, err, exchange.ErrCredentialsAreEmpty) - ctx := account.DeployCredentialsToContext(t.Context(), - &account.Credentials{Key: "fakerino", Secret: "supafake"}) + ctx := accounts.DeployCredentialsToContext(t.Context(), &accounts.Credentials{Key: "fakerino", Secret: "supafake"}) _, err = s.GetCollateral(ctx, &gctrpc.GetCollateralRequest{ Exchange: fakeExchangeName, Asset: asset.Futures.String(), }) - require.ErrorIs(t, err, errNoAccountInformation) + require.ErrorIs(t, err, accounts.ErrNoBalances) - ctx = account.DeployCredentialsToContext(t.Context(), - &account.Credentials{Key: "fakerino", Secret: "supafake", SubAccount: "1337"}) + ctx = accounts.DeployCredentialsToContext(t.Context(), &accounts.Credentials{Key: "fakerino", Secret: "supafake", SubAccount: "1337"}) r, err := s.GetCollateral(ctx, &gctrpc.GetCollateralRequest{ Exchange: fakeExchangeName, diff --git a/engine/websocketroutine_manager.go b/engine/websocketroutine_manager.go index b2ee0ced..0c02af3c 100644 --- a/engine/websocketroutine_manager.go +++ b/engine/websocketroutine_manager.go @@ -7,8 +7,8 @@ import ( "github.com/thrasher-corp/gocryptotrader/common" "github.com/thrasher-corp/gocryptotrader/currency" + "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/fill" "github.com/thrasher-corp/gocryptotrader/exchanges/order" "github.com/thrasher-corp/gocryptotrader/exchanges/orderbook" @@ -329,15 +329,9 @@ func (m *WebsocketRoutineManager) websocketDataHandler(exchName string, data any return fmt.Errorf("%w %s", d.Err, d.Error()) case websocket.UnhandledMessageWarning: log.Warnf(log.WebsocketMgr, "%s unhandled message - %s", exchName, d.Message) - case account.Change: + case []accounts.Change, accounts.Change: if m.verbose { - m.printAccountHoldingsChangeSummary(exchName, d) - } - case []account.Change: - if m.verbose { - for x := range d { - m.printAccountHoldingsChangeSummary(exchName, d[x]) - } + log.Debugf(log.WebsocketMgr, "%s %+v", exchName, d) } case []trade.Data, trade.Data: if m.verbose { @@ -349,10 +343,7 @@ func (m *WebsocketRoutineManager) websocketDataHandler(exchName string, data any } default: if m.verbose { - log.Warnf(log.WebsocketMgr, - "%s websocket Unknown type: %+v", - exchName, - d) + log.Warnf(log.WebsocketMgr, "%s websocket Unknown type: %+v", exchName, d) } } return nil @@ -396,21 +387,6 @@ func (m *WebsocketRoutineManager) printOrderSummary(o *order.Detail, isUpdate bo o.RemainingAmount) } -// printAccountHoldingsChangeSummary this function will be deprecated when a -// account holdings update is done. -func (m *WebsocketRoutineManager) printAccountHoldingsChangeSummary(exch string, o account.Change) { - if m == nil || atomic.LoadInt32(&m.state) == stoppedState || o.Balance == nil { - return - } - log.Debugf(log.WebsocketMgr, - "Account Holdings Balance Changed: %s %s %s has changed balance by %f for account: %s", - exch, - o.AssetType, - o.Balance.Currency, - o.Balance.Total, - o.Account) -} - // registerWebsocketDataHandler registers an externally (GCT Library) defined // dedicated filter specific data types for internal & external strategy use. // InterceptorOnly as true will purge all other registered handlers diff --git a/exchange/accounts/accounts.go b/exchange/accounts/accounts.go new file mode 100644 index 00000000..ef6982da --- /dev/null +++ b/exchange/accounts/accounts.go @@ -0,0 +1,312 @@ +package accounts + +import ( + "context" + "errors" + "fmt" + "maps" + "slices" + "sync" + "time" + + "github.com/gofrs/uuid" + "github.com/thrasher-corp/gocryptotrader/common" + "github.com/thrasher-corp/gocryptotrader/common/key" + "github.com/thrasher-corp/gocryptotrader/currency" + "github.com/thrasher-corp/gocryptotrader/dispatch" + "github.com/thrasher-corp/gocryptotrader/exchanges/asset" +) + +// Public errors. +var ( + ErrNoBalances = errors.New("no balances found") + ErrNoSubAccounts = errors.New("no subAccounts found") +) + +var ( + errCredentialsEmpty = errors.New("no credentials provided") + errUpdatingBalance = errors.New("error updating balance") + errPublish = errors.New("error publishing account changes") +) + +// Accounts holds a stream ID and a map to the exchange holdings. +type Accounts struct { + Exchange exchange + routingID uuid.UUID // GCT internal routing mux id + subAccounts credSubAccounts + mu sync.RWMutex + mux *dispatch.Mux +} + +type ( + credSubAccounts map[Credentials]subAccounts + subAccounts map[key.SubAccountAsset]currencyBalances +) + +// SubAccount contains an account for an asset type and its balances. +// The SubAccount may be the main account depending on exchange structure. +type SubAccount struct { + ID string + AssetType asset.Item + Balances CurrencyBalances +} + +// SubAccounts contains a list of public SubAccounts. +type SubAccounts []*SubAccount + +// MustNewAccounts returns an initialised Accounts store for use in isolation from a global exchange accounts store. +// mux is set to the global dispatch.Dispatcher. +// Any errors in mux ID generation will panic, so users should balance risk vs utility accordingly depending on use-case. +func MustNewAccounts(e exchange) *Accounts { + a, err := NewAccounts(e, dispatch.GetNewMux(nil)) + if err != nil { + panic(err) + } + return a +} + +// NewAccounts returns an initialised Accounts store for use in isolation from a global exchange accounts store. +func NewAccounts(e exchange, mux *dispatch.Mux) (*Accounts, error) { + if err := common.NilGuard(e); err != nil { + return nil, err + } + id, err := mux.GetID() + if err != nil { + return nil, err + } + return &Accounts{ + Exchange: e, + subAccounts: make(credSubAccounts), + routingID: id, + mux: mux, + }, nil +} + +// NewSubAccount returns a new SubAccount. +// id may be empty. +func NewSubAccount(a asset.Item, id string) *SubAccount { + return &SubAccount{ + AssetType: a, + ID: id, + Balances: CurrencyBalances{}, + } +} + +// Subscribe subscribes to your exchange accounts. +func (a *Accounts) Subscribe() (dispatch.Pipe, error) { + if err := common.NilGuard(a); err != nil { + return dispatch.Pipe{}, err + } + return a.mux.Subscribe(a.routingID) +} + +// CurrencyBalances returns the balances for the Accounts grouped by currency. +// If creds is nil, all credential SubAccounts will be collated. +// If assetType is asset.All, all assets will be collated. +func (a *Accounts) CurrencyBalances(creds *Credentials, assetType asset.Item) (CurrencyBalances, error) { + if err := common.NilGuard(a); err != nil { + return nil, err + } + if !assetType.IsValid() && assetType != asset.All { + return nil, fmt.Errorf("%s %s %w", a.Exchange.GetName(), assetType, asset.ErrNotSupported) + } + + currs := CurrencyBalances{} + + a.mu.RLock() + defer a.mu.RUnlock() + + for credsKey, subAccountsForCreds := range a.subAccounts { + if !creds.IsEmpty() && *creds != credsKey { + continue + } + for subAcctKey, balances := range subAccountsForCreds { + if assetType != asset.All && assetType != subAcctKey.Asset { + continue + } + for curr, bal := range balances { + if err := currs.Add(curr.Currency(), bal.Balance()); err != nil { + return nil, err // Should be impossible, so return immediately + } + } + } + } + if len(currs) == 0 { + return nil, fmt.Errorf("%w for %s credentials %s asset %s", ErrNoBalances, a.Exchange.GetName(), creds, assetType) + } + return currs, nil +} + +// SubAccounts returns the public SubAccounts and their balances. +// If creds is nil, all credential SubAccounts will be returned. +// If assetType is asset.All, all assets will be returned. +func (a *Accounts) SubAccounts(creds *Credentials, assetType asset.Item) (SubAccounts, error) { + if err := common.NilGuard(a); err != nil { + return nil, err + } + + if !assetType.IsValid() && assetType != asset.All { + return nil, fmt.Errorf("%s %s %w", a.Exchange.GetName(), assetType, asset.ErrNotSupported) + } + + var subAccts SubAccounts + + a.mu.RLock() + defer a.mu.RUnlock() + + for credsKey, subAccountsForCreds := range a.subAccounts { + if !creds.IsEmpty() && *creds != credsKey { + continue + } + for subAcctKey, balances := range subAccountsForCreds { + if assetType != asset.All && assetType != subAcctKey.Asset { + continue + } + subAccts = append(subAccts, &SubAccount{ + ID: subAcctKey.SubAccount, + AssetType: subAcctKey.Asset, + Balances: balances.Public(), + }) + } + } + + if len(subAccts) == 0 { + return nil, fmt.Errorf("%w for %s credentials %s asset %s", ErrNoSubAccounts, a.Exchange.GetName(), creds, assetType) + } + return subAccts, nil +} + +// GetBalance returns a copy of the balance for that asset item. +func (a *Accounts) GetBalance(subAccount string, creds *Credentials, aType asset.Item, c currency.Code) (Balance, error) { + if err := common.NilGuard(a); err != nil { + return Balance{}, err + } + if !aType.IsValid() { + return Balance{}, fmt.Errorf("cannot get balance: %w: %q", asset.ErrNotSupported, aType) + } + + if creds.IsEmpty() { + return Balance{}, fmt.Errorf("cannot get balance: %w", errCredentialsEmpty) + } + + if c.IsEmpty() { + return Balance{}, fmt.Errorf("cannot get balance: %w", currency.ErrCurrencyCodeEmpty) + } + + a.mu.RLock() + defer a.mu.RUnlock() + + subAccts, ok := a.subAccounts[*creds] + if !ok { + return Balance{}, fmt.Errorf("%w for %s", ErrNoBalances, creds) + } + + assets, ok := subAccts[key.SubAccountAsset{ + SubAccount: subAccount, + Asset: aType, + }] + if !ok { + return Balance{}, fmt.Errorf("%w for %s SubAccount %q %s", ErrNoBalances, a.Exchange.GetName(), subAccount, aType) + } + b, ok := assets[c.Item] + if !ok { + return Balance{}, fmt.Errorf("%w for %s SubAccount %q %s %s", ErrNoBalances, a.Exchange.GetName(), subAccount, aType, c) + } + return b.Balance(), nil +} + +// Save updates the account balances. +// If isSnapshot is true any missing currencies will be removed. +// Credentials will be retrieved from ctx, Use DeployCredentialsToContext. +// Changes to balances are published individually. +func (a *Accounts) Save(ctx context.Context, subAccts SubAccounts, isSnapshot bool) error { + if err := common.NilGuard(a); err != nil { + return fmt.Errorf("cannot save holdings: %w", err) + } + if err := common.NilGuard(a.subAccounts); err != nil { + return fmt.Errorf("cannot save holdings: %w", err) + } + + creds, err := a.Exchange.GetCredentials(ctx) + if err != nil { + return err + } + if creds.IsEmpty() { + return fmt.Errorf("%w: %w", errUpdatingBalance, errCredentialsEmpty) + } + + var errs error + + a.mu.Lock() + defer a.mu.Unlock() + + for _, s := range subAccts { + if !s.AssetType.IsValid() { + errs = common.AppendError(errs, fmt.Errorf("error loading %s[%s] SubAccount holdings: %w", s.ID, s.AssetType, asset.ErrNotSupported)) + continue + } + + accBalances := a.currencyBalances(creds, s.ID, s.AssetType) + + updated := false + missing := maps.Clone(accBalances) + for curr, newBal := range s.Balances { + delete(missing, curr.Item) + if newBal.UpdatedAt.IsZero() { + newBal.UpdatedAt = time.Now() + } + if newBal.Currency.IsEmpty() { + newBal.Currency = curr + } + s.Balances[curr] = newBal + if u, err := accBalances.balance(curr.Item).update(newBal); err != nil { + errs = common.AppendError(errs, fmt.Errorf("%w for account ID %q [%s %s]: %w", errUpdatingBalance, s.ID, s.AssetType, curr, err)) + } else if u { + updated = true + } + } + if isSnapshot { + for cur := range missing { + delete(accBalances, cur) + updated = true + } + } + if updated { + if err := a.mux.Publish(s, a.routingID); err != nil { + errs = common.AppendError(errs, fmt.Errorf("%w for %s %w", errPublish, a.Exchange, err)) + } + } + } + + return errs +} + +// Merge adds CurrencyBalances in s to the SubAccount in l with a matching AssetType and ID. +// If no SubAccount matches, s is appended. +// Duplicate Currency Balances are added together. +func (l SubAccounts) Merge(s *SubAccount) SubAccounts { + if err := common.NilGuard(s); err != nil { + return nil + } + i := slices.IndexFunc(l, func(b *SubAccount) bool { return s.AssetType == b.AssetType && s.ID == b.ID }) + if i == -1 { + return append(l, s) + } + for curr, newBal := range s.Balances { + l[i].Balances[curr] = newBal.Add(l[i].Balances[curr]) + } + return l +} + +// currencyBalances returns a currencyBalances entry for Credentials, SubAccount and asset. +// No nilguard protection provided, since this is a private function. +func (a *Accounts) currencyBalances(c *Credentials, subAcct string, aType asset.Item) currencyBalances { + k := key.SubAccountAsset{SubAccount: subAcct, Asset: aType} + if _, ok := a.subAccounts[*c]; !ok { + a.subAccounts[*c] = make(subAccounts) + } + if _, ok := a.subAccounts[*c][k]; !ok { + a.subAccounts[*c][k] = make(currencyBalances) + } + return a.subAccounts[*c][k] +} diff --git a/exchange/accounts/accounts_test.go b/exchange/accounts/accounts_test.go new file mode 100644 index 00000000..ed430fd4 --- /dev/null +++ b/exchange/accounts/accounts_test.go @@ -0,0 +1,533 @@ +package accounts + +import ( + "context" + "fmt" + "maps" + "reflect" + "runtime" + "slices" + "testing" + "time" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "github.com/thrasher-corp/gocryptotrader/common" + "github.com/thrasher-corp/gocryptotrader/common/key" + "github.com/thrasher-corp/gocryptotrader/currency" + "github.com/thrasher-corp/gocryptotrader/dispatch" + "github.com/thrasher-corp/gocryptotrader/exchanges/asset" +) + +var ( + creds1 = &Credentials{Key: "1"} + creds2 = &Credentials{Key: "2"} + creds3 = &Credentials{Key: "3"} +) + +func TestNewAccounts(t *testing.T) { + t.Parallel() + a, err := NewAccounts(&mockEx{"mocky"}, dispatch.GetNewMux(nil)) + require.NoError(t, err) + require.NotNil(t, a) + assert.Equal(t, "mocky", a.Exchange.GetName(), "Exchange name should set correctly") + assert.NotNil(t, a.subAccounts, "subAccounts should be initialised") + assert.NotEmpty(t, a.routingID, "routingID should not be empty") + assert.NotNil(t, a.mux, "mux should be set correctly") + _, err = NewAccounts(nil, dispatch.GetNewMux(nil)) + assert.ErrorIs(t, err, common.ErrNilPointer) + _, err = NewAccounts(&mockEx{"mocky"}, nil) + assert.ErrorContains(t, err, "nil pointer: *dispatch.Mux") +} + +func TestMustNewAccounts(t *testing.T) { + t.Parallel() + a := MustNewAccounts(&mockEx{"mocky"}) + require.NotNil(t, a) + require.Panics(t, func() { _ = MustNewAccounts(nil) }) +} + +func TestNewSubAccount(t *testing.T) { + t.Parallel() + a := NewSubAccount(asset.Spot, "") + require.NotNil(t, a, "must not return nil with no id") + assert.Equal(t, asset.Spot, a.AssetType, "AssetType should be correct") + assert.Empty(t, a.ID, "ID should not default to anything") + a = NewSubAccount(asset.Spot, "42") + assert.Equal(t, "42", a.ID, "ID should be correct") +} + +func TestSubscribe(t *testing.T) { + t.Parallel() + err := dispatch.EnsureRunning(dispatch.DefaultMaxWorkers, dispatch.DefaultJobsLimit) + require.NoError(t, err, "dispatch.EnsureRunning must not error") + p, err := MustNewAccounts(&mockEx{}).Subscribe() + require.NoError(t, err) + require.NotNil(t, p, "Subscribe must return a pipe") + require.Empty(t, p.Channel(), "Pipe must be empty before Saving anything") +} + +func TestAccountsCurrencyBalances(t *testing.T) { + t.Parallel() + + a := accountsFixture(t) + + _, err := (*Accounts)(nil).CurrencyBalances(nil, asset.Spot) + assert.ErrorIs(t, err, common.ErrNilPointer) + + _, err = a.CurrencyBalances(nil, asset.Empty) + assert.ErrorIs(t, err, asset.ErrNotSupported) + + _, err = a.CurrencyBalances(creds3, asset.All) + require.ErrorIs(t, err, ErrNoBalances) + + _, err = a.CurrencyBalances(creds3, asset.All) + assert.ErrorIs(t, err, ErrNoBalances) + assert.ErrorContains(t, err, "Key:[3") + + // Add a balance with inconsistent currencies to cover err from currs.Add + a.subAccounts[*creds3] = map[key.SubAccountAsset]currencyBalances{ + {Asset: asset.Futures}: {currency.DOGE.Item: &balance{internal: Balance{Currency: currency.ETH}}}, + } + + type cMap map[currency.Code]float64 + for _, tc := range []struct { + c *Credentials + aT asset.Item + exp cMap + err error + }{ + {nil, asset.Spot, cMap{currency.BTC: 6.0, currency.LTC: 10.0}, nil}, + {creds1, asset.All, cMap{currency.BTC: 3.0, currency.LTC: 30.0}, nil}, + {creds1, asset.Spot, cMap{currency.BTC: 3.0, currency.LTC: 10.0}, nil}, + {creds1, asset.Futures, cMap{currency.LTC: 20.0}, nil}, + {creds2, asset.Spot, cMap{currency.BTC: 3.0}, nil}, + {creds3, asset.Futures, cMap{currency.DOGE: 50.0}, errBalanceCurrencyMismatch}, + } { + t.Run(fmt.Sprintf("%s/%s", tc.c, tc.aT), func(t *testing.T) { + t.Parallel() + b, err := a.CurrencyBalances(tc.c, tc.aT) + if tc.err != nil { + require.ErrorIs(t, err, tc.err) + return + } + require.NoError(t, err) + require.Equal(t, len(tc.exp), len(b), "must get correct number of balances") + for c, expBal := range tc.exp { + assert.Contains(t, b, c) + assert.Equalf(t, expBal, b[c].Total, "should get correct total for %s", c) + } + }) + } +} + +func TestAccountsPrivateCurrencyBalances(t *testing.T) { + t.Parallel() + + a := accountsFixture(t) + b := a.currencyBalances(creds3, "", asset.Spot) + r1 := a.subAccounts[*creds3] + // Using reflect since assert.Same cannot be used on maps to ensure same underlying pointer + assert.Equal(t, + reflect.ValueOf(b).UnsafePointer(), + reflect.ValueOf(r1[key.SubAccountAsset{Asset: asset.Spot}]).UnsafePointer(), + "should make and return the same map") + assert.Equal(t, + reflect.ValueOf(b).UnsafePointer(), + reflect.ValueOf(a.currencyBalances(creds3, "", asset.Spot)).UnsafePointer(), + "should return the same map on subsequent calls") + b = a.currencyBalances(creds3, "", asset.Futures) + assert.Equal(t, + reflect.ValueOf(r1).UnsafePointer(), + reflect.ValueOf(a.subAccounts[*creds3]).UnsafePointer(), + "should not make a new cred key") + assert.Equal(t, + reflect.ValueOf(b).UnsafePointer(), + reflect.ValueOf(r1[key.SubAccountAsset{Asset: asset.Futures}]).UnsafePointer(), + "should make and return the same map") +} + +type tKey key.SubAccountAsset + +func TestAccountsSubAccounts(t *testing.T) { + t.Parallel() + + a := accountsFixture(t) + + _, err := (*Accounts)(nil).SubAccounts(nil, asset.Spot) + assert.ErrorIs(t, err, common.ErrNilPointer) + + _, err = a.SubAccounts(nil, asset.Empty) + assert.ErrorIs(t, err, asset.ErrNotSupported) + + _, err = a.SubAccounts(creds3, asset.All) + require.ErrorIs(t, err, ErrNoSubAccounts) + require.ErrorContains(t, err, "Key:[3") + + for _, tc := range []struct { + c *Credentials + aT asset.Item + exp []tKey + }{ + {nil, asset.All, []tKey{{"1a", asset.Spot}, {"1b", asset.Spot}, {"1b", asset.Futures}, {"2a", asset.Spot}}}, + {creds1, asset.All, []tKey{{"1a", asset.Spot}, {"1b", asset.Spot}, {"1b", asset.Futures}}}, + {creds1, asset.Spot, []tKey{{"1a", asset.Spot}, {"1b", asset.Spot}}}, + {creds1, asset.Futures, []tKey{{"1b", asset.Futures}}}, + {creds2, asset.Spot, []tKey{{"2a", asset.Spot}}}, + } { + t.Run(fmt.Sprintf("%v/%s", tc.c, tc.aT), func(t *testing.T) { + t.Parallel() + b, err := a.SubAccounts(tc.c, tc.aT) + require.NoError(t, err) + exp := subAccountsFixture(tc.exp) + require.Equal(t, len(exp), len(b), "must get correct number of subAccounts") + require.ElementsMatch(t, exp, b, "must get correct subAccounts") + }) + } +} + +func TestAccountsGetBalance(t *testing.T) { + t.Parallel() + + a := accountsFixture(t) + + _, err := (*Accounts)(nil).GetBalance("", nil, asset.Empty, currency.EMPTYCODE) + require.ErrorIs(t, err, common.ErrNilPointer) + + _, err = a.GetBalance("", nil, asset.Empty, currency.EMPTYCODE) + assert.ErrorIs(t, err, asset.ErrNotSupported) + + _, err = a.GetBalance("", nil, asset.Spot, currency.EMPTYCODE) + assert.ErrorIs(t, err, errCredentialsEmpty) + + _, err = a.GetBalance("", creds3, asset.Spot, currency.EMPTYCODE) + assert.ErrorIs(t, err, currency.ErrCurrencyCodeEmpty) + + _, err = a.GetBalance("", creds3, asset.Spot, currency.DOGE) + assert.ErrorIs(t, err, ErrNoBalances) + assert.ErrorContains(t, err, "for Key:[3") + + _, err = a.GetBalance("3a", creds1, asset.Spot, currency.DOGE) + assert.ErrorIs(t, err, ErrNoBalances) + assert.ErrorContains(t, err, `for mocky SubAccount "3a" spot`) + + _, err = a.GetBalance("1a", creds1, asset.Spot, currency.DOGE) + assert.ErrorIs(t, err, ErrNoBalances) + assert.ErrorContains(t, err, `for mocky SubAccount "1a" spot DOGE`) + + b, err := a.GetBalance("1b", creds1, asset.Spot, currency.BTC) + require.NoError(t, err) + assert.Equal(t, 2.0, b.Total, "Total should be correct") +} + +func TestAccountsSave(t *testing.T) { //nolint:tparallel // Save's internal tests are sequential + t.Parallel() + + a := accountsFixture(t) + relay := subscribeFixture(t, a) + beforeNow := time.Now() + + ctx := t.Context() + assert.ErrorContains(t, (*Accounts)(nil).Save(ctx, nil, false), "nil pointer: *accounts.Accounts") + assert.ErrorContains(t, new(Accounts).Save(ctx, nil, false), "nil pointer: accounts.credSubAccounts") + + for _, tc := range []struct { + name string + creds *Credentials + snapshot bool + accts SubAccounts + pre func(context.Context) context.Context + post func(t *testing.T) // Any additional assertions + err error + }{ + { + name: "NoCredentials", + accts: SubAccounts{}, + err: errCredentialsEmpty, + }, + { + name: "BadCredentials", + accts: SubAccounts{}, + err: common.ErrTypeAssertFailure, + pre: func(ctx context.Context) context.Context { return context.WithValue(ctx, ContextCredentialsFlag, 42) }, + }, + { + name: "BadAsset", + creds: creds1, + accts: SubAccounts{{AssetType: asset.All}}, + err: asset.ErrNotSupported, + }, + { + name: "CurrencyMismatch", + creds: creds1, + err: errBalanceCurrencyMismatch, + accts: SubAccounts{{ + AssetType: asset.Spot, + ID: "1a", + Balances: CurrencyBalances{currency.BTC: {Currency: currency.DOGE}}, + }}, + }, + { + name: "OutOfSequence", + creds: creds1, + err: errOutOfSequence, + accts: SubAccounts{{ + AssetType: asset.Spot, + ID: "1a", + Balances: CurrencyBalances{currency.BTC: {UpdatedAt: skynetDate.Add(-time.Hour)}}, + }}, + }, + { + name: "BasicSave", + creds: creds1, + accts: SubAccounts{ + { + AssetType: asset.Spot, + ID: "1a", + Balances: CurrencyBalances{currency.BTC: {Total: 4, UpdatedAt: skynetDate.Add(time.Minute)}}, + }, + { + AssetType: asset.Spot, + ID: "1c", + Balances: CurrencyBalances{currency.ETH: {Total: 6}}, + }, + }, + post: func(t *testing.T) { + t.Helper() + _, err := a.GetBalance("1a", creds1, asset.Spot, currency.LTC) + require.NoError(t, err, "Other balances must not be affected") + }, + }, + { + name: "NewCredsSaveAndPublish", + creds: creds3, + accts: SubAccounts{ + { + AssetType: asset.Futures, + ID: "3a", + Balances: CurrencyBalances{currency.DOGE: {Total: 6.2}}, + }, + }, + post: func(t *testing.T) { + t.Helper() + require.Eventually(t, func() bool { return len(relay) > 0 }, time.Second, time.Millisecond, "Publish must eventually send to Channel") + pub := <-relay + assert.Equal(t, "3a", pub.ID, "Publish should have correct ID") + assert.Contains(t, pub.Balances, currency.DOGE, "Should get DOGE Balance") + b := pub.Balances[currency.DOGE] + assert.Equal(t, currency.DOGE, b.Currency, "Currency should default to the Balances map key") + assert.WithinRange(t, b.UpdatedAt, beforeNow, time.Now(), "UpdatedAt should default to time.Now") + assert.Equal(t, 6.2, b.Total, "Total should be correct") + }, + }, + { + name: "SnapshotSave", + creds: creds1, + accts: SubAccounts{ + { + AssetType: asset.Spot, + ID: "1a", + Balances: CurrencyBalances{currency.LTC: {Total: 12}}, + }, + }, + snapshot: true, + post: func(t *testing.T) { + t.Helper() + _, err := a.GetBalance("1a", creds1, asset.Spot, currency.BTC) + require.ErrorIs(t, err, ErrNoBalances, "BTC balance must be removed") + }, + }, + { + name: "PublishError", + creds: creds1, + accts: SubAccounts{ + { + AssetType: asset.Spot, + ID: "1a", + Balances: CurrencyBalances{currency.DOGE: {Total: 7.2}}, + }, + }, + pre: func(ctx context.Context) context.Context { + a.mux = nil + return ctx + }, + err: errPublish, + }, + } { + t.Run(tc.name, func(t *testing.T) { + ctx := t.Context() + if tc.creds != nil { + ctx = DeployCredentialsToContext(ctx, tc.creds) + } + expAccts := tc.accts.clone() + if tc.pre != nil { + ctx = tc.pre(ctx) + } + err := a.Save(ctx, tc.accts, tc.snapshot) + if tc.err != nil { + require.ErrorIs(t, err, tc.err) + return + } + require.NoError(t, err) + for i, acct := range tc.accts { + for curr := range acct.Balances { + t.Run(fmt.Sprintf("%s/%s/%s", acct.AssetType, acct.ID, curr), func(t *testing.T) { + exp := expAccts[i].Balances[curr] + got, err := a.GetBalance(acct.ID, tc.creds, acct.AssetType, curr) + require.NoError(t, err, "GetBalance must not error") + if !exp.UpdatedAt.IsZero() { + assert.Equal(t, exp.UpdatedAt, got.UpdatedAt, "UpdatedAt should match balance") + } else { + assert.WithinRange(t, got.UpdatedAt, beforeNow, time.Now(), "UpdatedAt should default to time.Now") + } + assert.Equal(t, exp.Total, got.Total, "Total should be correct") + if tc.post != nil { + tc.post(t) + } + }) + } + } + }) + } +} + +var skynetDate = time.Unix(872896440, 0) + +// accountsFixture returns an Accounts store with SubAccount IDs per credentials, and a subscription channel for updates +func accountsFixture(t *testing.T) *Accounts { + t.Helper() + a := MustNewAccounts(&mockEx{}) + for _, f := range []struct { + c *Credentials + sA string + aT asset.Item + cC currency.Code + b float64 + }{ + {creds1, "1a", asset.Spot, currency.BTC, 1}, + {creds1, "1a", asset.Spot, currency.LTC, 10}, + {creds1, "1b", asset.Spot, currency.BTC, 2}, + {creds1, "1b", asset.Futures, currency.LTC, 20}, + {creds2, "2a", asset.Spot, currency.BTC, 3}, + } { + // Not using t.Run because this is a helper + u, err := a.currencyBalances(f.c, f.sA, f.aT).balance(f.cC.Item).update(Balance{Total: f.b, UpdatedAt: skynetDate}) + require.NoErrorf(t, err, "Deploy fixture balance must not error for %s/%s/%s/%s", f.c.Key, f.sA, f.aT, f.cC) + require.Truef(t, u, "Deploy fixture balance must apply an update for %s/%s/%s/%s", f.c.Key, f.sA, f.aT, f.cC) + } + return a +} + +var subAccts = SubAccounts{ + { + ID: "1a", + AssetType: asset.Spot, + Balances: CurrencyBalances{ + currency.LTC: Balance{Currency: currency.LTC, Total: 10, UpdatedAt: skynetDate}, + currency.BTC: Balance{Currency: currency.BTC, Total: 1, UpdatedAt: skynetDate}, + }, + }, + { + ID: "1b", + AssetType: asset.Spot, + Balances: CurrencyBalances{currency.BTC: Balance{Currency: currency.BTC, Total: 2.0, UpdatedAt: skynetDate}}, + }, + { + ID: "1b", + AssetType: asset.Futures, + Balances: CurrencyBalances{currency.LTC: Balance{Currency: currency.LTC, Total: 20.0, UpdatedAt: skynetDate}}, + }, + { + ID: "2a", + AssetType: asset.Spot, + Balances: CurrencyBalances{currency.BTC: Balance{Currency: currency.BTC, Total: 3.0, UpdatedAt: skynetDate}}, + }, +} + +func subAccountsFixture(keys []tKey) (a SubAccounts) { + if keys == nil { + return subAccts.clone() + } + for _, k := range keys { + i := slices.IndexFunc(subAccts, func(s *SubAccount) bool { + return k.SubAccount == s.ID && k.Asset == s.AssetType + }) + if i == -1 { + panic(fmt.Sprintf("subAccountsFixture called with unknown subAccount key: %v", k)) + } + a = append(a, subAccts[i]) + } + return a +} + +func subscribeFixture(t *testing.T, a *Accounts) chan *SubAccount { + t.Helper() + err := dispatch.EnsureRunning(dispatch.DefaultMaxWorkers, dispatch.DefaultJobsLimit) + require.NoError(t, err, "dispatch.EnsureRunning must not error") + p, err := a.Subscribe() + require.NoError(t, err, "Subscribe must not error") + require.NotNil(t, p, "Subscribe must return a pipe") + relay := make(chan *SubAccount, 64) + go func() { + for v := range p.Channel() { + if s, ok := v.(*SubAccount); ok && s.ID == "3a" { // Only interested in relaying events for a single test account + relay <- s + } + } + }() + runtime.Gosched() + return relay +} + +func TestMerge(t *testing.T) { + t.Parallel() + s := subAccountsFixture(nil) + assert.Nil(t, s.Merge(nil), "Should return nil for a merge of nil SubAccounts") + exp := len(s) + a := &SubAccount{ + ID: "1a", + AssetType: asset.Spot, + Balances: CurrencyBalances{currency.BTC: Balance{Total: 1}}, + } + s = s.Merge(a) + require.Equal(t, exp, len(s), "Must contain correct number of accounts after merging") + + for _, acct := range s { + if acct.ID == "1a" && acct.AssetType == asset.Spot { + assert.Equal(t, 2.0, acct.Balances[currency.BTC].Total) + } + } + + a = &SubAccount{ + ID: "new", + AssetType: asset.Spot, + Balances: CurrencyBalances{currency.BTC: Balance{Total: 1}}, + } + s = s.Merge(a) + assert.Contains(t, s, a, "Should contain the new subaccount") +} + +func TestSubAccountsClone(t *testing.T) { + t.Parallel() + s := SubAccounts{ + {ID: "1", AssetType: asset.Spot, Balances: CurrencyBalances{currency.BTC: {Total: 1}}}, + {ID: "2", AssetType: asset.Futures, Balances: CurrencyBalances{currency.LTC: {Total: 2}}}, + } + c := s.clone() + require.Equal(t, s, c, "Clone must match original") + c[0].ID = "3" + assert.NotEqual(t, s, c, "Should not be equal after modification") +} + +func (l SubAccounts) clone() (c SubAccounts) { + for _, s := range l { + bals := make(CurrencyBalances, len(s.Balances)) + maps.Copy(bals, s.Balances) + c = append(c, &SubAccount{ + ID: s.ID, + AssetType: s.AssetType, + Balances: bals, + }) + } + return c +} diff --git a/exchange/accounts/balance.go b/exchange/accounts/balance.go new file mode 100644 index 00000000..b165471d --- /dev/null +++ b/exchange/accounts/balance.go @@ -0,0 +1,148 @@ +package accounts + +import ( + "errors" + "fmt" + "sync" + "time" + + "github.com/thrasher-corp/gocryptotrader/common" + "github.com/thrasher-corp/gocryptotrader/currency" + "github.com/thrasher-corp/gocryptotrader/exchanges/asset" +) + +var ( + errBalanceCurrencyMismatch = errors.New("balance currency does not match update currency") + errOutOfSequence = errors.New("out of sequence") + errUpdatedAtIsZero = errors.New("updatedAt may not be zero") +) + +// Balance contains an exchange currency balance. +type Balance struct { + Currency currency.Code + Total float64 + Hold float64 + Free float64 + AvailableWithoutBorrow float64 + Borrowed float64 + UpdatedAt time.Time +} + +// Change defines incoming balance change on currency holdings. +type Change struct { + Account string + AssetType asset.Item + Balance Balance +} + +// balance contains a balance with live updates. +type balance struct { + internal Balance + m sync.RWMutex +} + +// CurrencyBalances provides a map of currencies to balances. +type CurrencyBalances map[currency.Code]Balance + +// currencyBalances provides a map of currencies to balances. +type currencyBalances map[*currency.Item]*balance + +// Set will set a currency balance, overwriting any previous Balance. +// +//nolint:gocritic // Ignoring hugeparam because we want the convenience of all callers passing by value +//nolint:gocritic // and we want to store a copy anyway so the hugeparam warning that this copies a value is not relevant +func (c *CurrencyBalances) Set(curr currency.Code, b Balance) { + b.Currency = curr + (*c)[curr] = b +} + +// Add will add to a currency balance. +func (c *CurrencyBalances) Add(curr currency.Code, b Balance) error { //nolint:gocritic // hugeparam not relevant; we want to store a value so we'd deref anyway + if curr == currency.EMPTYCODE { + return currency.ErrCurrencyCodeEmpty + } + if b.Currency != currency.EMPTYCODE && !b.Currency.Equal(curr) { + return fmt.Errorf("%w: %q != %q", errBalanceCurrencyMismatch, b.Currency, curr) + } + if e, ok := (*c)[curr]; !ok { + b.Currency = curr + (*c)[curr] = b + } else { + (*c)[curr] = e.Add(b) + } + return nil +} + +// Balance returns a snapshot copy of the Balance. +func (b *balance) Balance() Balance { + b.m.RLock() + defer b.m.RUnlock() + return b.internal +} + +// Add returns a new Balance adding together a and b. +// UpdatedAt is the later of the two Balances. +func (b *Balance) Add(a Balance) Balance { //nolint:gocritic // hugeparam not relevant; We'd need to copy it in map iterations anyway + var u time.Time + if a.UpdatedAt.After(b.UpdatedAt) { + u = a.UpdatedAt + } else { + u = b.UpdatedAt + } + return Balance{ + Total: b.Total + a.Total, + Hold: b.Hold + a.Hold, + Free: b.Free + a.Free, + AvailableWithoutBorrow: b.AvailableWithoutBorrow + a.AvailableWithoutBorrow, + Borrowed: b.Borrowed + a.Borrowed, + UpdatedAt: u, + } +} + +// Public returns a copy of the currencyBalances converted to CurrencyBalances for use outside this package. +func (c currencyBalances) Public() CurrencyBalances { + n := make(CurrencyBalances, len(c)) + for curr, bal := range c { + n[curr.Currency()] = bal.Balance() + } + return n +} + +// update checks that an incoming change has a valid change, and returns if the balances were changed. +// If change does not have a Currency set, the existing Currency is preserved. +func (b *balance) update(change Balance) (bool, error) { //nolint:gocritic // hugeparam not relevant; We'd need to copy it later anyway + if err := common.NilGuard(b); err != nil { + return false, err + } + if change.UpdatedAt.IsZero() { + return false, errUpdatedAtIsZero + } + b.m.Lock() + defer b.m.Unlock() + if b.internal.Currency != currency.EMPTYCODE { + if change.Currency == currency.EMPTYCODE { + change.Currency = b.internal.Currency + } else if !change.Currency.Equal(b.internal.Currency) { + return false, fmt.Errorf("%w %q != %q", errBalanceCurrencyMismatch, b.internal.Currency, change.Currency) + } + } + if b.internal.UpdatedAt.After(change.UpdatedAt) { + return false, errOutOfSequence + } + b.internal.UpdatedAt = change.UpdatedAt // Set just the time, and then can compare easily + if b.internal == change { + return false, nil + } + b.internal = change + return true, nil +} + +// balance returns a balance for a currency. +func (c currencyBalances) balance(curr *currency.Item) *balance { + b, ok := c[curr] + if !ok { + b = &balance{internal: Balance{Currency: curr.Currency()}} + c[curr] = b + } + return b +} diff --git a/exchange/accounts/balance_test.go b/exchange/accounts/balance_test.go new file mode 100644 index 00000000..a4617b9c --- /dev/null +++ b/exchange/accounts/balance_test.go @@ -0,0 +1,152 @@ +package accounts + +import ( + "testing" + "time" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "github.com/thrasher-corp/gocryptotrader/common" + "github.com/thrasher-corp/gocryptotrader/currency" +) + +// TestCurrencyBalancesSet exercises CurrencyBalances.Set +func TestCurrencyBalancesSet(t *testing.T) { + t.Parallel() + + c := CurrencyBalances{} + + c.Set(currency.BTC, Balance{Total: 4.2}) + require.Contains(t, c, currency.BTC, "must add an entry to an uninitialised CurrencyBalances") + assert.Equal(t, currency.BTC, c[currency.BTC].Currency, "should set the Currency") + + c.Set(currency.LTC, Balance{Currency: currency.ETH, Total: 52.4}) + require.Contains(t, c, currency.LTC, "must add an entry to an existing CurrencyBalances") + assert.Equal(t, currency.LTC, c[currency.LTC].Currency, "should overwrite Currency") +} + +// TestCurrencyBalancesAdd exercises CurrencyBalances.Add +func TestCurrencyBalancesAdd(t *testing.T) { + t.Parallel() + + c := CurrencyBalances{} + assert.ErrorIs(t, c.Add(currency.EMPTYCODE, Balance{}), currency.ErrCurrencyCodeEmpty) + + err := c.Add(currency.BTC, Balance{Total: 4.2}) + require.NoError(t, err) + + require.Contains(t, c, currency.BTC, "must add an entry to an uninitialised CurrencyBalances") + assert.Equal(t, currency.BTC, c[currency.BTC].Currency, "should set the Currency") + assert.Equal(t, 4.2, c[currency.BTC].Total, "should initialise the Total") + + err = c.Add(currency.BTC, Balance{Total: 1.3, Hold: 2.4}) + require.NoError(t, err) + assert.Equal(t, 5.5, c[currency.BTC].Total, "should add to existing Total") + assert.Equal(t, 2.4, c[currency.BTC].Hold, "should initialise Hold") + + err = c.Add(currency.LTC, Balance{Currency: currency.LTC, Total: 14.3}) + require.NoError(t, err) + require.Contains(t, c, currency.LTC, "must add an entry to an existing CurrencyBalances") + assert.Equal(t, 14.3, c[currency.LTC].Total, "should add when Balance.Currency is equal") + + err = c.Add(currency.ETH, Balance{Currency: currency.LTC, Total: 14.2}) + assert.ErrorIs(t, err, errBalanceCurrencyMismatch) +} + +// TestCurrencyBalancesPublic exercises currencyBalances.Public +func TestCurrencyBalancesPublic(t *testing.T) { + t.Parallel() + b := (¤cyBalances{ + currency.BTC.Item: &balance{internal: Balance{Total: 4.2}}, + currency.LTC.Item: &balance{internal: Balance{Total: 1.7}}, + }).Public() + require.Equal(t, 2, len(b), "Pulbic must return the correct number of Balances") + require.Contains(t, b, currency.BTC) + require.Contains(t, b, currency.LTC) + assert.Equal(t, 4.2, b[currency.BTC].Total) + assert.Equal(t, 1.7, b[currency.LTC].Total) +} + +// TestCurrencyBalancesBalance exercises currencyBalances.balance +func TestCurrencyBalancesBalance(t *testing.T) { + t.Parallel() + c := currencyBalances{} + b := c.balance(currency.BTC.Item) + require.NotNil(t, b) + assert.Same(t, c[currency.BTC.Item], b, "should make and return the same entry") + assert.Same(t, b, c.balance(currency.BTC.Item), "should make and return the same entry") +} + +// TestBalanceBalance exercises balance.Balance +func TestBalanceBalance(t *testing.T) { + t.Parallel() + b := &balance{internal: Balance{Currency: currency.BTC}} + i := b.Balance() + assert.Equal(t, b.internal, i) +} + +// TestBalanceAdd exercises Balance.Add +func TestBalanceAdd(t *testing.T) { + t.Parallel() + n1 := time.Now() + n2 := n1.Add(-2 * time.Minute) + b := new(Balance).Add(Balance{Total: 4.2, UpdatedAt: n2}) + assert.Equal(t, 4.2, b.Total, "should initialise Total") + assert.Equal(t, n2, b.UpdatedAt, "should set UpdatedAt") + b = b.Add(Balance{Total: 1.3, Hold: 3.0, UpdatedAt: n1}) + assert.Equal(t, 5.5, b.Total, "should add to Total") + assert.Equal(t, 3.0, b.Hold, "should initialise Hold") + assert.Equal(t, n1, b.UpdatedAt, "should set UpdatedAt") + b = b.Add(Balance{Total: 2.2, Hold: 4.0, UpdatedAt: n1.Add(-time.Minute)}) + assert.Equal(t, 7.7, b.Total, "should add to Total") + assert.Equal(t, 7.0, b.Hold, "should add to Hold") + assert.Equal(t, n1, b.UpdatedAt, "should keep newer UpdatedAt") +} + +// TestBalanceUpdate exercises balance.update +func TestBalanceUpdate(t *testing.T) { + t.Parallel() + + _, err := (*balance)(nil).update(Balance{}) + require.ErrorIs(t, err, common.ErrNilPointer) + + n := time.Now() + b := &balance{internal: Balance{ + Currency: currency.LTC, + Total: 4.2, + UpdatedAt: n, + }} + + _, err = b.update(Balance{}) + require.ErrorIs(t, err, errUpdatedAtIsZero) + + _, err = b.update(Balance{UpdatedAt: n, Currency: currency.ETH}) + assert.ErrorIs(t, err, errBalanceCurrencyMismatch) + + _, err = b.update(Balance{UpdatedAt: n.Add(-time.Millisecond)}) + assert.ErrorIs(t, err, errOutOfSequence, "should error when time out of sequence") + + u, err := b.update(Balance{UpdatedAt: n, Total: 5.1}) + require.NoError(t, err, "must not error when time is the same instant and currency is empty") + assert.Equal(t, 5.1, b.internal.Total, "Total should be correct") + assert.True(t, u, "should return updated") + + n = time.Now() + u, err = b.update(Balance{UpdatedAt: n, Total: 5.1}) + require.NoError(t, err) + assert.Equal(t, n, b.internal.UpdatedAt, "should update UpdatedAt") + assert.False(t, u, "should not return updated when nothing really changed") + + n = time.Now() + u, err = b.update(Balance{UpdatedAt: n, Currency: currency.LTC, Total: 5.1}) + require.NoError(t, err, "must not error when Currency matches") + assert.Equal(t, n, b.internal.UpdatedAt, "should update UpdatedAt") + assert.False(t, u, "should return not updated when only time changed") + + n = time.Now() + u, err = b.update(Balance{UpdatedAt: n, Currency: currency.LTC, Total: 4.4}) + require.NoError(t, err, "must not error when Currency matches") + assert.Equal(t, n, b.internal.UpdatedAt, "should update UpdatedAt") + assert.Equal(t, 4.4, b.internal.Total, "should update Total") + assert.True(t, u, "should return updated") +} diff --git a/exchanges/account/credentials.go b/exchange/accounts/credentials.go similarity index 95% rename from exchanges/account/credentials.go rename to exchange/accounts/credentials.go index 250de87a..28edfabf 100644 --- a/exchanges/account/credentials.go +++ b/exchange/accounts/credentials.go @@ -1,4 +1,4 @@ -package account +package accounts import ( "context" @@ -210,13 +210,3 @@ func DeployCredentialsToContext(ctx context.Context, creds *Credentials) context func DeploySubAccountOverrideToContext(ctx context.Context, subAccount string) context.Context { return context.WithValue(ctx, ContextSubAccountFlag, subAccount) } - -// String strings the credentials in a protected way. -func (p *Protected) String() string { - return p.creds.String() -} - -// Equal determines if the keys are the same -func (p *Protected) Equal(other *Credentials) bool { - return p.creds.Equal(other) -} diff --git a/exchanges/account/credentials_test.go b/exchange/accounts/credentials_test.go similarity index 78% rename from exchanges/account/credentials_test.go rename to exchange/accounts/credentials_test.go index 0d1cf0f0..f2f3f548 100644 --- a/exchanges/account/credentials_test.go +++ b/exchange/accounts/credentials_test.go @@ -1,4 +1,4 @@ -package account +package accounts import ( "testing" @@ -172,56 +172,3 @@ func TestCredentialsEqual(t *testing.T) { t.Fatal("unexpected value") } } - -func TestProtectedString(t *testing.T) { - t.Parallel() - p := Protected{} - if s := p.String(); s != "Key:[...] SubAccount:[] ClientID:[]" { - t.Fatal("unexpected value") - } - - p.creds.Key = "12345678910111234" - p.creds.SubAccount = "sub" - p.creds.ClientID = "client" - - if s := p.creds.String(); s != "Key:[1234567891011123...] SubAccount:[sub] ClientID:[client]" { - t.Fatal("unexpected value") - } -} - -func TestProtectedCredentialsEqual(t *testing.T) { - t.Parallel() - var this Protected - var that *Credentials - if this.Equal(that) { - t.Fatal("unexpected value") - } - this.creds = Credentials{} - if this.Equal(that) { - t.Fatal("unexpected value") - } - that = &Credentials{Key: "1337"} - if this.Equal(that) { - t.Fatal("unexpected value") - } - this.creds.Key = "1337" - if !this.Equal(that) { - t.Fatal("unexpected value") - } - this.creds.ClientID = "1337" - if this.Equal(that) { - t.Fatal("unexpected value") - } - that.ClientID = "1337" - if !this.Equal(that) { - t.Fatal("unexpected value") - } - this.creds.SubAccount = "someSub" - if this.Equal(that) { - t.Fatal("unexpected value") - } - that.SubAccount = "someSub" - if !this.Equal(that) { - t.Fatal("unexpected value") - } -} diff --git a/exchange/accounts/store.go b/exchange/accounts/store.go new file mode 100644 index 00000000..08d507db --- /dev/null +++ b/exchange/accounts/store.go @@ -0,0 +1,65 @@ +package accounts + +import ( + "context" + "sync" + "sync/atomic" + + "github.com/thrasher-corp/gocryptotrader/dispatch" +) + +// Store contains accounts for exchanges. +type Store struct { + exchangeAccounts exchangeMap + mu sync.Mutex + mux *dispatch.Mux +} + +type exchangeMap map[exchange]*Accounts + +type exchange interface { + GetName() string + GetCredentials(context.Context) (*Credentials, error) +} + +type exchangeWrapper interface { + GetBase() exchange +} + +var global atomic.Pointer[Store] + +// NewStore returns a new store with the default global dispatcher mux. +func NewStore() *Store { + return &Store{ + exchangeAccounts: make(exchangeMap), + mux: dispatch.GetNewMux(nil), + } +} + +// GetStore returns the singleton accounts store for global use; Initialising if necessary. +func GetStore() *Store { + if s := global.Load(); s != nil { + return s + } + _ = global.CompareAndSwap(nil, NewStore()) + return global.Load() +} + +// GetExchangeAccounts returns accounts for a specific exchange. +func (s *Store) GetExchangeAccounts(e exchange) (a *Accounts, err error) { + s.mu.Lock() + defer s.mu.Unlock() + if w, ok := e.(exchangeWrapper); ok { + // Because SetupDefaults is called on Base, it's easiest to just use the Base pointer as the key + e = w.GetBase() + } + a, ok := s.exchangeAccounts[e] + if !ok { + a, err = NewAccounts(e, s.mux) + if err != nil { + return nil, err + } + s.exchangeAccounts[e] = a + } + return a, nil +} diff --git a/exchange/accounts/store_test.go b/exchange/accounts/store_test.go new file mode 100644 index 00000000..efbbf3a1 --- /dev/null +++ b/exchange/accounts/store_test.go @@ -0,0 +1,86 @@ +package accounts + +import ( + "context" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "github.com/thrasher-corp/gocryptotrader/common" +) + +func TestNewStore(t *testing.T) { + t.Parallel() + s := NewStore() + require.NotNil(t, s, "NewStore must return a store") + require.NotNil(t, s.mux, "NewStore must set mux") + require.NotNil(t, s.exchangeAccounts, "NewStore must set exchangeAccounts") +} + +func TestGetStore(t *testing.T) { + t.Parallel() + // Initialise global in case of -count=N+; No other tests should be relying on it + global.Store(nil) + s := GetStore() + require.NotNil(t, s, "GetStore must return a Store") + require.Same(t, global.Load(), s, "GetStore must set the global store") + require.Same(t, s, GetStore(), "GetStore must return the global store on second call") +} + +func TestGetExchangeAccounts(t *testing.T) { + t.Parallel() + s := NewStore() + m := &mockEx{"mocky"} + a := &Accounts{} + s.exchangeAccounts[m] = a + got, err := s.GetExchangeAccounts(m) + require.NoError(t, err) + assert.Same(t, a, got, "Should retrieve same existing Accounts") + + m = &mockEx{"new"} + got, err = s.GetExchangeAccounts(m) + require.NoError(t, err) + assert.Same(t, s.exchangeAccounts[m], got, "Should retrieve the new exchange") + + w := &mockExBase{m} + got, err = s.GetExchangeAccounts(w) + require.NoError(t, err) + assert.NotNil(t, got) + + _, err = s.GetExchangeAccounts(nil) + assert.ErrorIs(t, err, common.ErrNilPointer, "Should error correctly on nil exchange") +} + +type mockEx struct { + name string +} + +func (m *mockEx) GetName() string { + return "mocky" +} + +func (m *mockEx) GetCredentials(ctx context.Context) (*Credentials, error) { + if value := ctx.Value(ContextCredentialsFlag); value != nil { + if s, ok := value.(*ContextCredentialsStore); ok { + return s.Get(), nil + } + return nil, common.GetTypeAssertError("*accounts.ContextCredentialsStore", value) + } + return nil, nil +} + +type mockExBase struct { + base exchange +} + +func (m *mockExBase) GetBase() exchange { + return m.base +} + +func (m *mockExBase) GetCredentials(ctx context.Context) (*Credentials, error) { + return m.base.GetCredentials(ctx) +} + +func (m *mockExBase) GetName() string { + return m.base.GetName() +} diff --git a/exchanges/account/account.go b/exchanges/account/account.go deleted file mode 100644 index 203f57b9..00000000 --- a/exchanges/account/account.go +++ /dev/null @@ -1,461 +0,0 @@ -package account - -import ( - "errors" - "fmt" - "strings" - "time" - - "github.com/thrasher-corp/gocryptotrader/common" - "github.com/thrasher-corp/gocryptotrader/common/key" - "github.com/thrasher-corp/gocryptotrader/currency" - "github.com/thrasher-corp/gocryptotrader/dispatch" - "github.com/thrasher-corp/gocryptotrader/exchanges/asset" -) - -func init() { - service.exchangeAccounts = make(map[string]*Accounts) - service.mux = dispatch.GetNewMux(nil) -} - -// Public errors -var ( - ErrExchangeHoldingsNotFound = errors.New("exchange holdings not found") -) - -var ( - errHoldingsIsNil = errors.New("holdings cannot be nil") - errNoExchangeSubAccountBalances = errors.New("no exchange sub account balances") - errBalanceIsNil = errors.New("balance is nil") - errNoCredentialBalances = errors.New("no balances associated with credentials") - errCredentialsAreNil = errors.New("credentials are nil") - errOutOfSequence = errors.New("out of sequence") - errUpdatedAtIsZero = errors.New("updatedAt may not be zero") - errLoadingBalance = errors.New("error loading balance") - errExchangeAlreadyExists = errors.New("exchange already exists") - errCannotUpdateBalance = errors.New("cannot update balance") -) - -// initAccounts adds a new empty shared account accounts entry for an exchange -// must be called with s.mu locked -func (s *Service) initAccounts(exch string) (*Accounts, error) { - id, err := s.mux.GetID() - if err != nil { - return nil, err - } - _, ok := s.exchangeAccounts[exch] - if ok { - return nil, errExchangeAlreadyExists - } - accounts := &Accounts{ - ID: id, - subAccounts: make(map[Credentials]map[key.SubAccountAsset]currencyBalances), - } - s.exchangeAccounts[exch] = accounts - return accounts, nil -} - -// CollectBalances converts a map of sub-account balances into a slice -func CollectBalances(accountBalances map[string][]Balance, assetType asset.Item) (accounts []SubAccount, err error) { - if accountBalances == nil { - return nil, errAccountBalancesIsNil - } - - if !assetType.IsValid() { - return nil, fmt.Errorf("%s, %w", assetType, asset.ErrNotSupported) - } - - accounts = make([]SubAccount, 0, len(accountBalances)) - for accountID, balances := range accountBalances { - accounts = append(accounts, SubAccount{ - ID: accountID, - AssetType: assetType, - Currencies: balances, - }) - } - return -} - -// SubscribeToExchangeAccount subscribes to your exchange account -func SubscribeToExchangeAccount(exchange string) (dispatch.Pipe, error) { - exchange = strings.ToLower(exchange) - service.mu.Lock() - defer service.mu.Unlock() - accounts, ok := service.exchangeAccounts[exchange] - if !ok { - var err error - if accounts, err = service.initAccounts(exchange); err != nil { - return dispatch.Pipe{}, fmt.Errorf("cannot subscribe to exchange account %w", err) - } - } - return service.mux.Subscribe(accounts.ID) -} - -// Process processes new account holdings updates -func Process(h *Holdings, c *Credentials) error { - return service.Save(h, c) -} - -// ProcessChange updates the changes to the exchange account -func ProcessChange(exch string, changes []Change, c *Credentials) error { - return service.Update(exch, changes, c) -} - -// GetHoldings returns full holdings for an exchange. -// NOTE: Due to credentials these amounts could be N*APIKEY actual holdings. -// TODO: Add jurisdiction and differentiation between APIKEY holdings. -func GetHoldings(exch string, creds *Credentials, assetType asset.Item) (Holdings, error) { - if exch == "" { - return Holdings{}, common.ErrExchangeNameNotSet - } - - if creds.IsEmpty() { - return Holdings{}, fmt.Errorf("%s %s %w", exch, assetType, errCredentialsAreNil) - } - - if !assetType.IsValid() { - return Holdings{}, fmt.Errorf("%s %s %w", exch, assetType, asset.ErrNotSupported) - } - - exch = strings.ToLower(exch) - - service.mu.Lock() - defer service.mu.Unlock() - accounts, ok := service.exchangeAccounts[exch] - if !ok { - return Holdings{}, fmt.Errorf("%s %w: %q", exch, ErrExchangeHoldingsNotFound, assetType) - } - - subAccountHoldings, ok := accounts.subAccounts[*creds] - if !ok { - return Holdings{}, fmt.Errorf("%s %s %s %w %w", exch, creds, assetType, errNoCredentialBalances, ErrExchangeHoldingsNotFound) - } - - currencyBalances := make([]Balance, 0, len(subAccountHoldings)) - cpy := *creds - for mapKey, assets := range subAccountHoldings { - if mapKey.Asset != assetType { - continue - } - for currItem, bal := range assets { - bal.m.Lock() - currencyBalances = append(currencyBalances, Balance{ - Currency: currItem.Currency().Upper(), - Total: bal.total, - Hold: bal.hold, - Free: bal.free, - AvailableWithoutBorrow: bal.availableWithoutBorrow, - Borrowed: bal.borrowed, - UpdatedAt: bal.updatedAt, - }) - bal.m.Unlock() - } - if cpy.SubAccount == "" && mapKey.SubAccount != "" { - // TODO: fix this backwards population - // the subAccount here may not be associated with the balance across all subAccountHoldings - cpy.SubAccount = mapKey.SubAccount - } - } - if len(currencyBalances) == 0 { - return Holdings{}, fmt.Errorf("%s %s %w", exch, assetType, ErrExchangeHoldingsNotFound) - } - return Holdings{Exchange: exch, Accounts: []SubAccount{{ - Credentials: Protected{creds: cpy}, - ID: cpy.SubAccount, - AssetType: assetType, - Currencies: currencyBalances, - }}}, nil -} - -// GetBalance returns the internal balance for that asset item. -func GetBalance(exch, subAccount string, creds *Credentials, a asset.Item, c currency.Code) (*ProtectedBalance, error) { - if exch == "" { - return nil, fmt.Errorf("cannot get balance: %w", common.ErrExchangeNameNotSet) - } - - if !a.IsValid() { - return nil, fmt.Errorf("cannot get balance: %s %w", a, asset.ErrNotSupported) - } - - if creds.IsEmpty() { - return nil, fmt.Errorf("cannot get balance: %w", errCredentialsAreNil) - } - - if c.IsEmpty() { - return nil, fmt.Errorf("cannot get balance: %w", currency.ErrCurrencyCodeEmpty) - } - - exch = strings.ToLower(exch) - service.mu.Lock() - defer service.mu.Unlock() - - accounts, ok := service.exchangeAccounts[exch] - if !ok { - return nil, fmt.Errorf("%w for %s", ErrExchangeHoldingsNotFound, exch) - } - - subAccounts, ok := accounts.subAccounts[*creds] - if !ok { - return nil, fmt.Errorf("%w for %s %s", errNoCredentialBalances, exch, creds) - } - - assets, ok := subAccounts[key.SubAccountAsset{ - SubAccount: subAccount, - Asset: a, - }] - if !ok { - return nil, fmt.Errorf("%w for %s SubAccount %q %s %s", errNoExchangeSubAccountBalances, exch, subAccount, a, c) - } - bal, ok := assets[c.Item] - if !ok { - return nil, fmt.Errorf("%w for %s SubAccount %q %s %s", errNoExchangeSubAccountBalances, exch, subAccount, a, c) - } - return bal, nil -} - -// Save saves the holdings with new account info -// incoming should be a full update, and any missing currencies will be zeroed -func (s *Service) Save(incoming *Holdings, creds *Credentials) error { - if incoming == nil { - return fmt.Errorf("cannot save holdings: %w", errHoldingsIsNil) - } - - if incoming.Exchange == "" { - return fmt.Errorf("cannot save holdings: %w", common.ErrExchangeNameNotSet) - } - - if creds.IsEmpty() { - return fmt.Errorf("cannot save holdings: %w", errCredentialsAreNil) - } - - exch := strings.ToLower(incoming.Exchange) - s.mu.Lock() - defer s.mu.Unlock() - - accounts, ok := s.exchangeAccounts[exch] - if !ok { - var err error - if accounts, err = s.initAccounts(exch); err != nil { - return fmt.Errorf("cannot save holdings for %s %w", exch, err) - } - } - - subAccounts, ok := accounts.subAccounts[*creds] - if !ok { - subAccounts = make(map[key.SubAccountAsset]currencyBalances) - accounts.subAccounts[*creds] = subAccounts - } - - var errs error - for x := range incoming.Accounts { - if !incoming.Accounts[x].AssetType.IsValid() { - errs = common.AppendError(errs, fmt.Errorf("cannot load sub account holdings for %s [%s] %w", - incoming.Accounts[x].ID, - incoming.Accounts[x].AssetType, - asset.ErrNotSupported)) - continue - } - - // This assignment outside of scope is designed to have minimal impact - // on the exchange implementation UpdateAccountInfo() and portfoio - // management. - // TODO: Update incoming Holdings type to already be populated. (Suggestion) - cpy := *creds - if cpy.SubAccount == "" { - cpy.SubAccount = incoming.Accounts[x].ID - } - incoming.Accounts[x].Credentials.creds = cpy - - accAsset := key.SubAccountAsset{ - SubAccount: incoming.Accounts[x].ID, - Asset: incoming.Accounts[x].AssetType, - } - assets, ok := subAccounts[accAsset] - if !ok { - assets = make(map[*currency.Item]*ProtectedBalance) - accounts.subAccounts[*creds][accAsset] = assets - } - - updated := make(map[*currency.Item]bool) - for y := range incoming.Accounts[x].Currencies { - accBal := &incoming.Accounts[x].Currencies[y] - if accBal.UpdatedAt.IsZero() { - accBal.UpdatedAt = time.Now() - } - bal, ok := assets[accBal.Currency.Item] - if !ok || bal == nil { - bal = &ProtectedBalance{} - } - if err := bal.load(accBal); err != nil { - errs = common.AppendError(errs, fmt.Errorf("%w for account ID %q [%s %s]: %w", - errLoadingBalance, - incoming.Accounts[x].ID, - incoming.Accounts[x].AssetType, - incoming.Accounts[x].Currencies[y].Currency, - err)) - continue - } - assets[accBal.Currency.Item] = bal - updated[accBal.Currency.Item] = true - } - for cur, bal := range assets { - if !updated[cur] { - bal.reset() - } - } - - if err := s.mux.Publish(incoming.Accounts[x], accounts.ID); err != nil { - errs = common.AppendError(errs, fmt.Errorf("cannot publish load for %s %w", exch, err)) - } - } - - return errs -} - -// Update updates the balance for a specific exchange and credentials -func (s *Service) Update(exch string, changes []Change, creds *Credentials) error { - if exch == "" { - return fmt.Errorf("%w: %w", errCannotUpdateBalance, common.ErrExchangeNameNotSet) - } - - if creds.IsEmpty() { - return fmt.Errorf("%w: %w", errCannotUpdateBalance, errCredentialsAreNil) - } - - exch = strings.ToLower(exch) - s.mu.Lock() - defer s.mu.Unlock() - - accounts, ok := s.exchangeAccounts[exch] - if !ok { - var err error - if accounts, err = s.initAccounts(exch); err != nil { - return fmt.Errorf("%w for %s %w", errCannotUpdateBalance, exch, err) - } - } - - subAccounts, ok := accounts.subAccounts[*creds] - if !ok { - subAccounts = make(map[key.SubAccountAsset]currencyBalances) - accounts.subAccounts[*creds] = subAccounts - } - - var errs error - for _, change := range changes { - if !change.AssetType.IsValid() { - errs = common.AppendError(errs, fmt.Errorf("%w for %s.%s %w", - errCannotUpdateBalance, change.Account, change.AssetType, asset.ErrNotSupported)) - continue - } - if change.Balance == nil { - errs = common.AppendError(errs, fmt.Errorf("%w for %s.%s %w", - errCannotUpdateBalance, change.Account, change.AssetType, errBalanceIsNil)) - continue - } - - accAsset := key.SubAccountAsset{ - SubAccount: change.Account, - Asset: change.AssetType, - } - assets, ok := subAccounts[accAsset] - if !ok { - assets = make(map[*currency.Item]*ProtectedBalance) - accounts.subAccounts[*creds][accAsset] = assets - } - bal, ok := assets[change.Balance.Currency.Item] - if !ok || bal == nil { - bal = &ProtectedBalance{} - assets[change.Balance.Currency.Item] = bal - } - - if err := bal.load(change.Balance); err != nil { - errs = common.AppendError(errs, fmt.Errorf("%w for %s.%s.%s %w", - errCannotUpdateBalance, - change.Account, - change.AssetType, - change.Balance.Currency, - err)) - continue - } - if err := s.mux.Publish(change, accounts.ID); err != nil { - errs = common.AppendError(errs, fmt.Errorf("cannot publish update balance for %s: %w", exch, err)) - } - } - return errs -} - -// load checks to see if there is a change from incoming balance, if there is a -// change it will change then alert external routines. -func (b *ProtectedBalance) load(change *Balance) error { - if change == nil { - return fmt.Errorf("%w for '%T'", common.ErrNilPointer, change) - } - if change.UpdatedAt.IsZero() { - return errUpdatedAtIsZero - } - b.m.Lock() - defer b.m.Unlock() - if !b.updatedAt.IsZero() && b.updatedAt.After(change.UpdatedAt) { - return errOutOfSequence - } - if b.total == change.Total && - b.hold == change.Hold && - b.free == change.Free && - b.availableWithoutBorrow == change.AvailableWithoutBorrow && - b.borrowed == change.Borrowed && - b.updatedAt.Equal(change.UpdatedAt) { - return nil - } - b.total = change.Total - b.hold = change.Hold - b.free = change.Free - b.availableWithoutBorrow = change.AvailableWithoutBorrow - b.borrowed = change.Borrowed - b.updatedAt = change.UpdatedAt - b.notice.Alert() - return nil -} - -// Wait waits for a change in amounts for an asset type. This will pause -// indefinitely if no change ever occurs. Max wait will return true if it failed -// to achieve a state change in the time specified. If Max wait is not specified -// it will default to a minute wait time. -func (b *ProtectedBalance) Wait(maxWait time.Duration) (wait <-chan bool, cancel chan<- struct{}, err error) { - if b == nil { - return nil, nil, errBalanceIsNil - } - - if maxWait <= 0 { - maxWait = time.Minute - } - ch := make(chan struct{}) - go func(ch chan<- struct{}, until time.Duration) { - time.Sleep(until) - close(ch) - }(ch, maxWait) - - return b.notice.Wait(ch), ch, nil -} - -// GetFree returns the current free balance for the exchange -func (b *ProtectedBalance) GetFree() float64 { - if b == nil { - return 0 - } - b.m.Lock() - defer b.m.Unlock() - return b.free -} - -func (b *ProtectedBalance) reset() { - b.m.Lock() - defer b.m.Unlock() - - b.total = 0 - b.hold = 0 - b.free = 0 - b.availableWithoutBorrow = 0 - b.borrowed = 0 - b.updatedAt = time.Now() - b.notice.Alert() -} diff --git a/exchanges/account/account_test.go b/exchanges/account/account_test.go deleted file mode 100644 index 9e2abe16..00000000 --- a/exchanges/account/account_test.go +++ /dev/null @@ -1,509 +0,0 @@ -package account - -import ( - "sync" - "testing" - "time" - - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" - "github.com/thrasher-corp/gocryptotrader/common" - "github.com/thrasher-corp/gocryptotrader/common/key" - "github.com/thrasher-corp/gocryptotrader/currency" - "github.com/thrasher-corp/gocryptotrader/dispatch" - "github.com/thrasher-corp/gocryptotrader/exchanges/asset" -) - -var happyCredentials = &Credentials{Key: "AAAAA"} - -func TestCollectBalances(t *testing.T) { - t.Parallel() - accounts, err := CollectBalances( - map[string][]Balance{ - "someAccountID": { - {Currency: currency.BTC, Total: 40000, Hold: 1}, - }, - }, - asset.Spot, - ) - subAccount := accounts[0] - balance := subAccount.Currencies[0] - if subAccount.ID != "someAccountID" { - t.Error("subAccount ID not set correctly") - } - if subAccount.AssetType != asset.Spot { - t.Error("subAccount AssetType not set correctly") - } - if balance.Currency != currency.BTC || balance.Total != 40000 || balance.Hold != 1 { - t.Error("subAccount currency balance not set correctly") - } - if err != nil { - t.Error("err is not expected") - } - - accounts, err = CollectBalances(map[string][]Balance{}, asset.Spot) - if len(accounts) != 0 { - t.Error("accounts should be empty") - } - if err != nil { - t.Error("err is not expected") - } - - accounts, err = CollectBalances(nil, asset.Spot) - if len(accounts) != 0 { - t.Error("accounts should be empty") - } - if err == nil { - t.Errorf("expecting err %s", errAccountBalancesIsNil.Error()) - } - - _, err = CollectBalances(map[string][]Balance{}, asset.Empty) - require.ErrorIs(t, err, asset.ErrNotSupported) -} - -func TestGetHoldings(t *testing.T) { - err := dispatch.Start(dispatch.DefaultMaxWorkers, dispatch.DefaultJobsLimit) - require.NoError(t, err) - err = Process(nil, nil) - assert.ErrorIs(t, err, errHoldingsIsNil) - - err = Process(&Holdings{}, nil) - assert.ErrorIs(t, err, common.ErrExchangeNameNotSet) - - holdings := Holdings{Exchange: "Test"} - - err = Process(&holdings, nil) - assert.ErrorIs(t, err, errCredentialsAreNil) - - err = Process(&holdings, happyCredentials) - require.NoError(t, err) - - err = Process(&Holdings{ - Exchange: "Test", - Accounts: []SubAccount{ - { - ID: "1337", - }, - }, - }, happyCredentials) - assert.ErrorIs(t, err, asset.ErrNotSupported) - - err = Process(&Holdings{ - Exchange: "Test", - Accounts: []SubAccount{ - { - AssetType: asset.UpsideProfitContract, - ID: "1337", - }, - { - AssetType: asset.Spot, - ID: "1337", - Currencies: []Balance{ - { - Currency: currency.BTC, - Total: 100, - Hold: 20, - }, - }, - }, - }, - }, happyCredentials) - assert.NoError(t, err) - - _, err = GetHoldings("", nil, asset.Spot) - assert.ErrorIs(t, err, common.ErrExchangeNameNotSet) - - _, err = GetHoldings("bla", nil, asset.Spot) - assert.ErrorIs(t, err, errCredentialsAreNil) - - _, err = GetHoldings("bla", happyCredentials, asset.Spot) - assert.ErrorIs(t, err, ErrExchangeHoldingsNotFound) - - _, err = GetHoldings("bla", happyCredentials, asset.Empty) - assert.ErrorIs(t, err, asset.ErrNotSupported) - - _, err = GetHoldings("Test", happyCredentials, asset.UpsideProfitContract) - assert.ErrorIs(t, err, ErrExchangeHoldingsNotFound) - - _, err = GetHoldings("Test", &Credentials{Key: "BBBBB"}, asset.Spot) - assert.ErrorIs(t, err, errNoCredentialBalances) - - u, err := GetHoldings("Test", happyCredentials, asset.Spot) - require.NoError(t, err) - - assert.Equal(t, "test", u.Exchange) - require.Len(t, u.Accounts, 1) - assert.Equal(t, "1337", u.Accounts[0].ID) - assert.Equal(t, asset.Spot, u.Accounts[0].AssetType) - require.Len(t, u.Accounts[0].Currencies, 1) - assert.Equal(t, currency.BTC, u.Accounts[0].Currencies[0].Currency) - assert.Equal(t, 100.0, u.Accounts[0].Currencies[0].Total) - assert.Equal(t, 20.0, u.Accounts[0].Currencies[0].Hold) - - _, err = SubscribeToExchangeAccount("nonsense") - require.NoError(t, err) - - p, err := SubscribeToExchangeAccount("Test") - require.NoError(t, err) - - var wg sync.WaitGroup - wg.Add(1) - go func(p dispatch.Pipe, wg *sync.WaitGroup) { - for range 2 { - c := time.NewTimer(time.Second) - select { - case <-p.Channel(): - case <-c.C: - } - } - wg.Done() - }(p, &wg) - - err = Process(&Holdings{ - Exchange: "Test", - Accounts: []SubAccount{{ - ID: "1337", - AssetType: asset.MarginFunding, - Currencies: []Balance{ - { - Currency: currency.BTC, - Total: 100000, - Hold: 20, - }, - }, - }}, - }, happyCredentials) - assert.NoError(t, err) - - wg.Wait() -} - -func TestGetBalance(t *testing.T) { - t.Parallel() - - _, err := GetBalance("", "", nil, asset.Empty, currency.Code{}) - assert.ErrorIs(t, err, common.ErrExchangeNameNotSet) - - _, err = GetBalance("bruh", "", nil, asset.Empty, currency.Code{}) - assert.ErrorIs(t, err, asset.ErrNotSupported) - - _, err = GetBalance("bruh", "", nil, asset.Spot, currency.Code{}) - assert.ErrorIs(t, err, errCredentialsAreNil) - - _, err = GetBalance("bruh", "", happyCredentials, asset.Spot, currency.Code{}) - assert.ErrorIs(t, err, currency.ErrCurrencyCodeEmpty) - - _, err = GetBalance("bruh", "", happyCredentials, asset.Spot, currency.BTC) - assert.ErrorIs(t, err, ErrExchangeHoldingsNotFound) - - err = Process(&Holdings{ - Exchange: "bruh", - Accounts: []SubAccount{ - { - AssetType: asset.Spot, - ID: "1337", - }, - }, - }, happyCredentials) - require.NoError(t, err, "process must not error") - - _, err = GetBalance("bruh", "1336", &Credentials{Key: "BBBBB"}, asset.Spot, currency.BTC) - assert.ErrorIs(t, err, errNoCredentialBalances) - - _, err = GetBalance("bruh", "1336", happyCredentials, asset.Spot, currency.BTC) - assert.ErrorIs(t, err, errNoExchangeSubAccountBalances) - - _, err = GetBalance("bruh", "1337", happyCredentials, asset.Futures, currency.BTC) - assert.ErrorIs(t, err, errNoExchangeSubAccountBalances) - - err = Process(&Holdings{ - Exchange: "bruh", - Accounts: []SubAccount{ - { - AssetType: asset.Spot, - ID: "1337", - Currencies: []Balance{ - { - Currency: currency.BTC, - Total: 2, - Hold: 1, - }, - }, - }, - }, - }, happyCredentials) - require.NoError(t, err, "process must not error") - - bal, err := GetBalance("bruh", "1337", happyCredentials, asset.Spot, currency.BTC) - require.NoError(t, err, "get balance must not error") - - bal.m.Lock() - assert.Equal(t, 2.0, bal.total) - assert.Equal(t, 1.0, bal.hold) - bal.m.Unlock() -} - -func TestBalanceInternalWait(t *testing.T) { - t.Parallel() - var bi *ProtectedBalance - _, _, err := bi.Wait(0) - require.ErrorIs(t, err, errBalanceIsNil) - - bi = &ProtectedBalance{} - waiter, _, err := bi.Wait(time.Nanosecond) - require.NoError(t, err) - - if !<-waiter { - t.Fatal("should been alerted by timeout") - } - - waiter, _, err = bi.Wait(0) - require.NoError(t, err) - - go bi.notice.Alert() - if <-waiter { - t.Fatal("should have been alerted by change notice") - } -} - -func TestBalanceInternalLoad(t *testing.T) { - t.Parallel() - bi := &ProtectedBalance{} - err := bi.load(nil) - assert.ErrorIs(t, err, common.ErrNilPointer, "should error nil pointer correctly") - - err = bi.load(&Balance{Total: 1, Hold: 2, Free: 3, AvailableWithoutBorrow: 4, Borrowed: 5}) - assert.ErrorIs(t, err, errUpdatedAtIsZero, "should error correctly when updatedAt is not set") - - now := time.Now() - err = bi.load(&Balance{UpdatedAt: now, Total: 1, Hold: 2, Free: 3, AvailableWithoutBorrow: 4, Borrowed: 5}) - require.NoError(t, err) - - bi.m.Lock() - assert.Equal(t, now, bi.updatedAt) - assert.Equal(t, 1.0, bi.total) - assert.Equal(t, 2.0, bi.hold) - assert.Equal(t, 3.0, bi.free) - assert.Equal(t, 4.0, bi.availableWithoutBorrow) - assert.Equal(t, 5.0, bi.borrowed) - bi.m.Unlock() - - 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.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) -} - -func TestGetFree(t *testing.T) { - t.Parallel() - var bi *ProtectedBalance - if bi.GetFree() != 0 { - t.Fatal("unexpected value") - } - bi = &ProtectedBalance{} - bi.free = 1 - if bi.GetFree() != 1 { - t.Fatal("unexpected value") - } -} - -func TestSave(t *testing.T) { - t.Parallel() - s := &Service{exchangeAccounts: make(map[string]*Accounts), mux: dispatch.GetNewMux(nil)} - err := s.Save(nil, nil) - assert.ErrorIs(t, err, errHoldingsIsNil) - - err = s.Save(&Holdings{}, nil) - assert.ErrorIs(t, err, common.ErrExchangeNameNotSet) - - err = s.Save(&Holdings{ - Exchange: "TeSt", - Accounts: []SubAccount{ - { - AssetType: 6969, - ID: "1337", - Currencies: []Balance{ - { - Currency: currency.BTC, - Total: 100, - Hold: 20, - }, - }, - }, - {AssetType: asset.UpsideProfitContract, ID: "1337"}, - }, - }, happyCredentials) - assert.ErrorIs(t, err, asset.ErrNotSupported) - - err = s.Save(&Holdings{ // No change - Exchange: "tEsT", - Accounts: []SubAccount{ - { - AssetType: asset.Spot, - ID: "1337", - Currencies: []Balance{ - { - Currency: currency.BTC, - Total: 100, - Hold: 20, - }, - }, - }, - }, - }, happyCredentials) - require.NoError(t, err) - - acc, ok := s.exchangeAccounts["test"] - require.True(t, ok) - - assets, ok := acc.subAccounts[*happyCredentials][key.SubAccountAsset{ - SubAccount: "1337", - Asset: asset.Spot, - }] - require.True(t, ok) - - b, ok := assets[currency.BTC.Item] - require.True(t, ok) - - assert.NotEmpty(t, b.updatedAt) - assert.Equal(t, 100.0, b.total) - assert.Equal(t, 20.0, b.hold) - - err = s.Save(&Holdings{ - Exchange: "tEsT", - Accounts: []SubAccount{ - { - AssetType: asset.Spot, - ID: "1337", - Currencies: []Balance{ - { - Currency: currency.ETH, - Total: 80, - Hold: 20, - }, - }, - }, - }, - }, happyCredentials) - require.NoError(t, err) - - b, ok = assets[currency.BTC.Item] - require.True(t, ok) - assert.NotEmpty(t, b.updatedAt) - assert.Zero(t, b.total) - assert.Zero(t, b.hold) - - e, ok := assets[currency.ETH.Item] - require.True(t, ok) - assert.NotEmpty(t, e.updatedAt) - assert.Equal(t, 80.0, e.total) - assert.Equal(t, 20.0, e.hold) -} - -func TestUpdate(t *testing.T) { - t.Parallel() - s := &Service{exchangeAccounts: make(map[string]*Accounts), mux: dispatch.GetNewMux(nil)} - err := s.Update("", nil, nil) - assert.ErrorIs(t, err, common.ErrExchangeNameNotSet) - - err = s.Update("test", nil, nil) - assert.ErrorIs(t, err, errCredentialsAreNil) - - err = s.Update("test", []Change{ - { - AssetType: 6969, - Balance: &Balance{ - Currency: currency.BTC, - Free: 100, - }, - }, - }, happyCredentials) - assert.ErrorIs(t, err, asset.ErrNotSupported) - - now := time.Now() - err = s.Update("test", []Change{ - { - AssetType: asset.Spot, - Account: "1337", - Balance: &Balance{ - Currency: currency.BTC, - Total: 100, - Free: 80, - UpdatedAt: now, - }, - }, - }, happyCredentials) - require.NoError(t, err) - - acc, ok := s.exchangeAccounts["test"] - require.True(t, ok, "Update must add the exchange") - - assets, ok := acc.subAccounts[*happyCredentials][key.SubAccountAsset{ - SubAccount: "1337", - Asset: asset.Spot, - }] - require.True(t, ok, "Update must add subAccount for the credentials") - - b, ok := assets[currency.BTC.Item] - require.True(t, ok, "Update must add currency to the subAccount") - - assert.Equal(t, 100.0, b.total, "Update should set total correctly") - assert.Equal(t, 80.0, b.free, "Update should set free correctly") - assert.Equal(t, now, b.updatedAt, "Update should set updatedAt correctly") - - err = s.Update("test", []Change{ - { - AssetType: asset.Spot, - Account: "1337", - Balance: &Balance{ - Currency: currency.BTC, - Total: 100, - Free: 100, - UpdatedAt: now.Add(-1 * time.Second), - }, - }, - }, happyCredentials) - assert.ErrorIs(t, err, errOutOfSequence) - - err = s.Update("test", []Change{ - { - AssetType: asset.Spot, - Account: "1337", - Balance: &Balance{ - Currency: currency.BTC, - Total: 100, - Free: 100, - UpdatedAt: now.Add(1 * time.Second), - }, - }, - }, happyCredentials) - require.NoError(t, err) - - assert.Equal(t, 100.0, b.total) - assert.Equal(t, 100.0, b.free) - assert.Equal(t, now.Add(1*time.Second), b.updatedAt) -} - -func TestTrackNewAccounts(t *testing.T) { - t.Parallel() - s := &Service{ - exchangeAccounts: make(map[string]*Accounts), - mux: dispatch.GetNewMux(nil), - } - - s.mu.Lock() - _, err := s.initAccounts("binance") - s.mu.Unlock() - require.NoError(t, err) - - s.mu.Lock() - _, err = s.initAccounts("binance") - s.mu.Unlock() - assert.ErrorIs(t, err, errExchangeAlreadyExists) -} diff --git a/exchanges/account/account_types.go b/exchanges/account/account_types.go deleted file mode 100644 index 421a5bf5..00000000 --- a/exchanges/account/account_types.go +++ /dev/null @@ -1,94 +0,0 @@ -package account - -import ( - "errors" - "sync" - "time" - - "github.com/gofrs/uuid" - "github.com/thrasher-corp/gocryptotrader/common/key" - "github.com/thrasher-corp/gocryptotrader/currency" - "github.com/thrasher-corp/gocryptotrader/dispatch" - "github.com/thrasher-corp/gocryptotrader/exchanges/alert" - "github.com/thrasher-corp/gocryptotrader/exchanges/asset" -) - -// Vars for the ticker package -var ( - service Service - errAccountBalancesIsNil = errors.New("account balances is nil") -) - -// Service holds ticker information for each individual exchange -type Service struct { - exchangeAccounts map[string]*Accounts - mux *dispatch.Mux - mu sync.Mutex -} - -// Accounts holds a stream ID and a map to the exchange holdings -type Accounts struct { - ID uuid.UUID - // NOTE: Credentials is a place holder for a future interface type, which - // will need - - // TODO: Credential tracker to match to keys that are managed and return - // pointer. - // TODO: Have different cred struct for centralized verse DEFI exchanges. - subAccounts map[Credentials]map[key.SubAccountAsset]currencyBalances -} - -type currencyBalances = map[*currency.Item]*ProtectedBalance - -// Holdings is a generic type to hold each exchange's holdings for all enabled -// currencies -type Holdings struct { - Exchange string - Accounts []SubAccount -} - -// SubAccount defines a singular account type with associated currency balances -type SubAccount struct { - Credentials Protected - ID string - AssetType asset.Item - Currencies []Balance -} - -// Balance is a sub-type to store currency name and individual totals -type Balance struct { - Currency currency.Code - Total float64 - Hold float64 - Free float64 - AvailableWithoutBorrow float64 - Borrowed float64 - UpdatedAt time.Time -} - -// Change defines incoming balance change on currency holdings -type Change struct { - Account string - AssetType asset.Item - Balance *Balance -} - -// ProtectedBalance stores the full balance information for that specific asset -type ProtectedBalance struct { - total float64 - hold float64 - free float64 - availableWithoutBorrow float64 - borrowed float64 - m sync.Mutex - updatedAt time.Time - - // notice alerts for when the balance changes for strategy inspection and - // usage. - notice alert.Notice -} - -// Protected limits the access to the underlying credentials outside of this -// package. -type Protected struct { - creds Credentials -} diff --git a/exchanges/binance/binance.go b/exchanges/binance/binance.go index 59a7354b..9c2d8fe8 100644 --- a/exchanges/binance/binance.go +++ b/exchanges/binance/binance.go @@ -820,17 +820,14 @@ func getOfflineTradeFee(price, amount float64) float64 { // getMultiplier retrieves account based taker/maker fees func (e *Exchange) getMultiplier(ctx context.Context, isMaker bool) (float64, error) { - var multiplier float64 - account, err := e.GetAccount(ctx) + a, err := e.GetAccount(ctx) if err != nil { return 0, err } if isMaker { - multiplier = float64(account.MakerCommission) - } else { - multiplier = float64(account.TakerCommission) + return float64(a.MakerCommission), nil } - return multiplier, nil + return float64(a.TakerCommission), nil } // calculateTradingFee returns the fee for trading any currency on Binance diff --git a/exchanges/binance/binance_test.go b/exchanges/binance/binance_test.go index ba76d96d..b00a0052 100644 --- a/exchanges/binance/binance_test.go +++ b/exchanges/binance/binance_test.go @@ -1707,9 +1707,11 @@ func TestCancelAllExchangeOrders(t *testing.T) { } } -func TestGetAccountInfo(t *testing.T) { +func TestUpdateAccountBalances(t *testing.T) { t.Parallel() sharedtestvalues.SkipTestIfCredentialsUnset(t, e) + e := new(Exchange) //nolint:govet // Intentional shadow to avoid future copy/paste mistakes + require.NoError(t, testexch.Setup(e), "Test instance Setup must not error") items := asset.Items{ asset.CoinMarginedFutures, asset.USDTMarginedFutures, @@ -1720,10 +1722,8 @@ func TestGetAccountInfo(t *testing.T) { assetType := items[i] t.Run(fmt.Sprintf("Update info of account [%s]", assetType.String()), func(t *testing.T) { t.Parallel() - _, err := e.UpdateAccountInfo(t.Context(), assetType) - if err != nil { - t.Error(err) - } + _, err := e.UpdateAccountBalances(t.Context(), assetType) + require.NoError(t, err) }) } } diff --git a/exchanges/binance/binance_types.go b/exchanges/binance/binance_types.go index c6a43e01..2780b9ce 100644 --- a/exchanges/binance/binance_types.go +++ b/exchanges/binance/binance_types.go @@ -444,7 +444,7 @@ type QueryOrderData struct { // Balance holds query order data type Balance struct { - Asset string `json:"asset"` + Asset currency.Code `json:"asset"` Free decimal.Decimal `json:"free"` Locked decimal.Decimal `json:"locked"` } @@ -476,12 +476,12 @@ type MarginAccount struct { // MarginAccountAsset holds each individual margin account asset type MarginAccountAsset struct { - Asset string `json:"asset"` - Borrowed float64 `json:"borrowed,string"` - Free float64 `json:"free,string"` - Interest float64 `json:"interest,string"` - Locked float64 `json:"locked,string"` - NetAsset float64 `json:"netAsset,string"` + Asset currency.Code `json:"asset"` + Borrowed float64 `json:"borrowed,string"` + Free float64 `json:"free,string"` + Interest float64 `json:"interest,string"` + Locked float64 `json:"locked,string"` + NetAsset float64 `json:"netAsset,string"` } // RequestParamsOrderType trade order type diff --git a/exchanges/binance/binance_wrapper.go b/exchanges/binance/binance_wrapper.go index 02c8936a..84e8f8f3 100644 --- a/exchanges/binance/binance_wrapper.go +++ b/exchanges/binance/binance_wrapper.go @@ -14,11 +14,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" @@ -552,113 +552,79 @@ 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 for the -// Binance exchange -func (e *Exchange) UpdateAccountInfo(ctx context.Context, assetType asset.Item) (account.Holdings, error) { - var info account.Holdings - var acc account.SubAccount - acc.AssetType = assetType - 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: creds, err := e.GetCredentials(ctx) if err != nil { - return info, err + return nil, err } if creds.SubAccount != "" { // TODO: implement sub-account endpoints - return info, common.ErrNotYetImplemented + return nil, common.ErrNotYetImplemented } - raw, err := e.GetAccount(ctx) + resp, err := e.GetAccount(ctx) if err != nil { - return info, err + return nil, err } - - var currencyBalance []account.Balance - for i := range raw.Balances { - free := raw.Balances[i].Free.InexactFloat64() - locked := raw.Balances[i].Locked.InexactFloat64() - - currencyBalance = append(currencyBalance, account.Balance{ - Currency: currency.NewCode(raw.Balances[i].Asset), - Total: free + locked, - Hold: locked, - Free: free, + subAccts = accounts.SubAccounts{accounts.NewSubAccount(assetType, "")} + for i := range resp.Balances { + free := resp.Balances[i].Free.InexactFloat64() + locked := resp.Balances[i].Locked.InexactFloat64() + subAccts[0].Balances.Set(resp.Balances[i].Asset, accounts.Balance{ + Total: free + locked, + Hold: locked, + Free: free, }) } - - acc.Currencies = currencyBalance - case asset.CoinMarginedFutures: - accData, err := e.GetFuturesAccountInfo(ctx) + resp, err := e.GetFuturesAccountInfo(ctx) if err != nil { - return info, err + return nil, err } - var currencyDetails []account.Balance - for i := range accData.Assets { - currencyDetails = append(currencyDetails, account.Balance{ - Currency: currency.NewCode(accData.Assets[i].Asset), - Total: accData.Assets[i].WalletBalance, - Hold: accData.Assets[i].WalletBalance - accData.Assets[i].AvailableBalance, - Free: accData.Assets[i].AvailableBalance, + subAccts = accounts.SubAccounts{accounts.NewSubAccount(assetType, "")} + for i := range resp.Assets { + subAccts[0].Balances.Set(resp.Assets[i].Asset, accounts.Balance{ + Total: resp.Assets[i].WalletBalance, + Hold: resp.Assets[i].WalletBalance - resp.Assets[i].AvailableBalance, + Free: resp.Assets[i].AvailableBalance, }) } - - acc.Currencies = currencyDetails - case asset.USDTMarginedFutures: - accData, err := e.UAccountBalanceV2(ctx) + resp, err := e.UAccountBalanceV2(ctx) if err != nil { - return info, err + return nil, err } - accountCurrencyDetails := make(map[string][]account.Balance) - for i := range accData { - currencyDetails := accountCurrencyDetails[accData[i].AccountAlias] - accountCurrencyDetails[accData[i].AccountAlias] = append( - currencyDetails, account.Balance{ - Currency: currency.NewCode(accData[i].Asset), - Total: accData[i].Balance, - Hold: accData[i].Balance - accData[i].AvailableBalance, - Free: accData[i].AvailableBalance, - }, - ) - } - - if info.Accounts, err = account.CollectBalances(accountCurrencyDetails, assetType); err != nil { - return account.Holdings{}, err + subAccts = make(accounts.SubAccounts, 0, len(resp)) + for i := range resp { + a := accounts.NewSubAccount(assetType, resp[i].AccountAlias) + a.Balances.Set(resp[i].Asset, accounts.Balance{ + Total: resp[i].Balance, + Hold: resp[i].Balance - resp[i].AvailableBalance, + Free: resp[i].AvailableBalance, + }) + subAccts = subAccts.Merge(a) } case asset.Margin: - accData, err := e.GetMarginAccount(ctx) + resp, err := e.GetMarginAccount(ctx) if err != nil { - return info, err + return nil, err } - var currencyDetails []account.Balance - for i := range accData.UserAssets { - currencyDetails = append(currencyDetails, account.Balance{ - Currency: currency.NewCode(accData.UserAssets[i].Asset), - Total: accData.UserAssets[i].Free + accData.UserAssets[i].Locked, - Hold: accData.UserAssets[i].Locked, - Free: accData.UserAssets[i].Free, - AvailableWithoutBorrow: accData.UserAssets[i].Free - accData.UserAssets[i].Borrowed, - Borrowed: accData.UserAssets[i].Borrowed, + subAccts = accounts.SubAccounts{accounts.NewSubAccount(assetType, "")} + for i := range resp.UserAssets { + subAccts[0].Balances.Set(resp.UserAssets[i].Asset, accounts.Balance{ + Total: resp.UserAssets[i].Free + resp.UserAssets[i].Locked, + Hold: resp.UserAssets[i].Locked, + Free: resp.UserAssets[i].Free, + AvailableWithoutBorrow: resp.UserAssets[i].Free - resp.UserAssets[i].Borrowed, + Borrowed: resp.UserAssets[i].Borrowed, }) } - - acc.Currencies = currencyDetails - default: - return info, fmt.Errorf("%w %v", asset.ErrNotSupported, assetType) + return nil, fmt.Errorf("%w %v", asset.ErrNotSupported, assetType) } - 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 account.Holdings{}, err - } - return info, nil + return subAccts, e.Accounts.Save(ctx, subAccts, true) } // GetAccountFundingHistory returns funding history, deposits and @@ -1589,10 +1555,9 @@ func (e *Exchange) GetOrderHistory(ctx context.Context, req *order.MultiOrderReq return req.Filter(e.Name, orders), 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) } @@ -2520,9 +2485,8 @@ func (e *Exchange) GetFuturesPositionSummary(ctx context.Context, req *futures.P } var accountAsset *FuturesAccountAsset for i := range ai.Assets { - // TODO: utilise contract data to discern the underlying currency - // instead of having a user provide it - if ai.Assets[i].Asset != req.UnderlyingPair.Base.Upper().String() { + // TODO: utilise contract data to discern the underlying currency instead of having a user provide it + if !ai.Assets[i].Asset.Equal(req.UnderlyingPair.Base) { continue } accountAsset = &ai.Assets[i] @@ -2599,7 +2563,7 @@ func (e *Exchange) GetFuturesPositionSummary(ctx context.Context, req *futures.P MarginType: marginType, CollateralMode: collateralMode, ContractSettlementType: contractSettlementType, - Currency: currency.NewCode(accountAsset.Asset), + Currency: accountAsset.Asset, IsolatedMargin: decimal.NewFromFloat(isolatedMargin), NotionalSize: decimal.NewFromFloat(positionSize).Mul(decimal.NewFromFloat(markPrice)), Leverage: decimal.NewFromFloat(leverage), diff --git a/exchanges/binance/cfutures_types.go b/exchanges/binance/cfutures_types.go index 448d4694..c7aaedf0 100644 --- a/exchanges/binance/cfutures_types.go +++ b/exchanges/binance/cfutures_types.go @@ -396,18 +396,18 @@ type FuturesAccountInformation struct { // FuturesAccountAsset holds account asset information type FuturesAccountAsset struct { - Asset string `json:"asset"` - WalletBalance float64 `json:"walletBalance,string"` - UnrealizedProfit float64 `json:"unrealizedProfit,string"` - MarginBalance float64 `json:"marginBalance,string"` - MaintenanceMargin float64 `json:"maintMargin,string"` - InitialMargin float64 `json:"initialMargin,string"` - PositionInitialMargin float64 `json:"positionInitialMargin,string"` - OpenOrderInitialMargin float64 `json:"openOrderInitialMargin,string"` - MaxWithdrawAmount float64 `json:"maxWithdrawAmount,string"` - CrossWalletBalance float64 `json:"crossWalletBalance,string"` - CrossUnPNL float64 `json:"crossUnPnl,string"` - AvailableBalance float64 `json:"availableBalance,string"` + Asset currency.Code `json:"asset"` + WalletBalance float64 `json:"walletBalance,string"` + UnrealizedProfit float64 `json:"unrealizedProfit,string"` + MarginBalance float64 `json:"marginBalance,string"` + MaintenanceMargin float64 `json:"maintMargin,string"` + InitialMargin float64 `json:"initialMargin,string"` + PositionInitialMargin float64 `json:"positionInitialMargin,string"` + OpenOrderInitialMargin float64 `json:"openOrderInitialMargin,string"` + MaxWithdrawAmount float64 `json:"maxWithdrawAmount,string"` + CrossWalletBalance float64 `json:"crossWalletBalance,string"` + CrossUnPNL float64 `json:"crossUnPnl,string"` + AvailableBalance float64 `json:"availableBalance,string"` } // GenericAuthResponse is a general data response for a post auth request diff --git a/exchanges/binance/ufutures_types.go b/exchanges/binance/ufutures_types.go index 469f7b31..347580a6 100644 --- a/exchanges/binance/ufutures_types.go +++ b/exchanges/binance/ufutures_types.go @@ -239,13 +239,13 @@ type UFuturesOrderData struct { // UAccountBalanceV2Data stores account balance data for ufutures type UAccountBalanceV2Data struct { - AccountAlias string `json:"accountAlias"` - Asset string `json:"asset"` - Balance float64 `json:"balance,string"` - CrossWalletBalance float64 `json:"crossWalletBalance,string"` - CrossUnrealizedPNL float64 `json:"crossUnPnl,string"` - AvailableBalance float64 `json:"availableBalance,string"` - MaxWithdrawAmount float64 `json:"maxWithdrawAmount,string"` + AccountAlias string `json:"accountAlias"` + Asset currency.Code `json:"asset"` + Balance float64 `json:"balance,string"` + CrossWalletBalance float64 `json:"crossWalletBalance,string"` + CrossUnrealizedPNL float64 `json:"crossUnPnl,string"` + AvailableBalance float64 `json:"availableBalance,string"` + MaxWithdrawAmount float64 `json:"maxWithdrawAmount,string"` } // UAccountInformationV2Data stores account info for ufutures diff --git a/exchanges/binanceus/binanceus_test.go b/exchanges/binanceus/binanceus_test.go index ce7f6b3c..b92f7211 100644 --- a/exchanges/binanceus/binanceus_test.go +++ b/exchanges/binanceus/binanceus_test.go @@ -121,13 +121,11 @@ func TestUpdateTradablePairs(t *testing.T) { } } -func TestUpdateAccountInfo(t *testing.T) { +func TestUpdateAccountBalances(t *testing.T) { t.Parallel() sharedtestvalues.SkipTestIfCredentialsUnset(t, e) - _, err := e.UpdateAccountInfo(t.Context(), asset.Spot) - if err != nil { - t.Error("Binanceus UpdateAccountInfo() error", err) - } + _, err := e.UpdateAccountBalances(t.Context(), asset.Spot) + require.NoError(t, err) } func TestGetRecentTrades(t *testing.T) { diff --git a/exchanges/binanceus/binanceus_types.go b/exchanges/binanceus/binanceus_types.go index 57e4a993..30bd21cc 100644 --- a/exchanges/binanceus/binanceus_types.go +++ b/exchanges/binanceus/binanceus_types.go @@ -278,7 +278,7 @@ type Account struct { // Balance holds query order data type Balance struct { - Asset string `json:"asset"` + Asset currency.Code `json:"asset"` Free decimal.Decimal `json:"free"` Locked decimal.Decimal `json:"locked"` } diff --git a/exchanges/binanceus/binanceus_wrapper.go b/exchanges/binanceus/binanceus_wrapper.go index 5db52241..cd0b4e2c 100644 --- a/exchanges/binanceus/binanceus_wrapper.go +++ b/exchanges/binanceus/binanceus_wrapper.go @@ -11,10 +11,10 @@ import ( "github.com/thrasher-corp/gocryptotrader/common" "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" "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/deposit" "github.com/thrasher-corp/gocryptotrader/exchanges/fundingrate" @@ -335,41 +335,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) { - 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) (accounts.SubAccounts, error) { if assetType != asset.Spot { - return info, fmt.Errorf("%w %v", asset.ErrNotSupported, assetType) + return nil, fmt.Errorf("%w %v", asset.ErrNotSupported, assetType) } - theAccount, err := e.GetAccount(ctx) + resp, err := e.GetAccount(ctx) if err != nil { - return info, err + return nil, err } - currencyBalance := make([]account.Balance, len(theAccount.Balances)) - for i := range theAccount.Balances { - freeBalance := theAccount.Balances[i].Free.InexactFloat64() - locked := theAccount.Balances[i].Locked.InexactFloat64() - - currencyBalance[i] = account.Balance{ - Currency: currency.NewCode(theAccount.Balances[i].Asset), - Total: freeBalance + locked, - Hold: locked, - Free: freeBalance, - } + subAccts := accounts.SubAccounts{accounts.NewSubAccount(assetType, "")} + for i := range resp.Balances { + freeBalance := resp.Balances[i].Free.InexactFloat64() + locked := resp.Balances[i].Locked.InexactFloat64() + subAccts[0].Balances.Set(resp.Balances[i].Asset, accounts.Balance{ + Total: freeBalance + locked, + Hold: locked, + Free: freeBalance, + }) } - acc.Currencies = currencyBalance - 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 @@ -777,7 +762,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) } diff --git a/exchanges/bitfinex/bitfinex_test.go b/exchanges/bitfinex/bitfinex_test.go index 06dc5e72..e9f1af93 100644 --- a/exchanges/bitfinex/bitfinex_test.go +++ b/exchanges/bitfinex/bitfinex_test.go @@ -407,11 +407,8 @@ func TestGetLeaderboard(t *testing.T) { func TestGetAccountFees(t *testing.T) { t.Parallel() sharedtestvalues.SkipTestIfCredentialsUnset(t, e) - - _, err := e.UpdateAccountInfo(t.Context(), asset.Spot) - if err != nil { - t.Error("GetAccountInfo error", err) - } + _, err := e.UpdateAccountBalances(t.Context(), asset.Spot) + assert.NoError(t, err) } func TestGetWithdrawalFee(t *testing.T) { diff --git a/exchanges/bitfinex/bitfinex_types.go b/exchanges/bitfinex/bitfinex_types.go index 78cc5da2..14bdfa52 100644 --- a/exchanges/bitfinex/bitfinex_types.go +++ b/exchanges/bitfinex/bitfinex_types.go @@ -341,10 +341,10 @@ type MarginLimits struct { // Balance holds current balance data type Balance struct { - Type string `json:"type"` - Currency string `json:"currency"` - Amount float64 `json:"amount,string"` - Available float64 `json:"available,string"` + Type string `json:"type"` + Currency currency.Code `json:"currency"` + Amount float64 `json:"amount,string"` + Available float64 `json:"available,string"` } // WalletTransfer holds status of wallet to wallet content transfer on exchange diff --git a/exchanges/bitfinex/bitfinex_wrapper.go b/exchanges/bitfinex/bitfinex_wrapper.go index 7921cb0b..280611ea 100644 --- a/exchanges/bitfinex/bitfinex_wrapper.go +++ b/exchanges/bitfinex/bitfinex_wrapper.go @@ -14,10 +14,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/deposit" "github.com/thrasher-corp/gocryptotrader/exchanges/fundingrate" @@ -404,50 +404,23 @@ func (e *Exchange) UpdateOrderbook(ctx context.Context, p currency.Pair, assetTy return orderbook.Get(e.Name, fPair, assetType) } -// UpdateAccountInfo retrieves balances for all enabled currencies on the -// Bitfinex exchange -func (e *Exchange) UpdateAccountInfo(ctx context.Context, assetType asset.Item) (account.Holdings, error) { - var response account.Holdings - response.Exchange = e.Name - - accountBalance, err := e.GetAccountBalance(ctx) +// UpdateAccountBalances retrieves currency balances +func (e *Exchange) UpdateAccountBalances(ctx context.Context, assetType asset.Item) (accounts.SubAccounts, error) { + resp, err := e.GetAccountBalance(ctx) if err != nil { - return response, err + return nil, err } - - Accounts := []account.SubAccount{ - {ID: "deposit", AssetType: assetType}, - {ID: "exchange", AssetType: assetType}, - {ID: "trading", AssetType: assetType}, - {ID: "margin", AssetType: assetType}, - {ID: "funding", AssetType: assetType}, + subAccts := accounts.SubAccounts{} + for i := range resp { + a := accounts.NewSubAccount(assetType, resp[i].Type) + a.Balances.Set(resp[i].Currency, accounts.Balance{ + Total: resp[i].Amount, + Hold: resp[i].Amount - resp[i].Available, + Free: resp[i].Available, + }) + subAccts = subAccts.Merge(a) } - - for x := range accountBalance { - for i := range Accounts { - if Accounts[i].ID == accountBalance[x].Type { - Accounts[i].Currencies = append(Accounts[i].Currencies, - account.Balance{ - Currency: currency.NewCode(accountBalance[x].Currency), - Total: accountBalance[x].Amount, - Hold: accountBalance[x].Amount - accountBalance[x].Available, - Free: accountBalance[x].Available, - }) - } - } - } - - response.Accounts = Accounts - creds, err := e.GetCredentials(ctx) - if err != nil { - return account.Holdings{}, err - } - err = account.Process(&response, creds) - if err != nil { - return account.Holdings{}, err - } - - return response, nil + return subAccts, e.Accounts.Save(ctx, subAccts, true) } // GetAccountFundingHistory returns funding history, deposits and @@ -992,10 +965,9 @@ func (e *Exchange) appendOptionalDelimiter(p *currency.Pair) { } } -// 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) } diff --git a/exchanges/bitflyer/bitflyer_wrapper.go b/exchanges/bitflyer/bitflyer_wrapper.go index 7eb6643d..75c60439 100644 --- a/exchanges/bitflyer/bitflyer_wrapper.go +++ b/exchanges/bitflyer/bitflyer_wrapper.go @@ -11,8 +11,8 @@ import ( "github.com/thrasher-corp/gocryptotrader/common" "github.com/thrasher-corp/gocryptotrader/config" "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/account" "github.com/thrasher-corp/gocryptotrader/exchanges/asset" "github.com/thrasher-corp/gocryptotrader/exchanges/deposit" "github.com/thrasher-corp/gocryptotrader/exchanges/fundingrate" @@ -240,10 +240,9 @@ func (e *Exchange) UpdateOrderbook(ctx context.Context, p currency.Pair, assetTy return orderbook.Get(e.Name, fPair, assetType) } -// UpdateAccountInfo retrieves balances for all enabled currencies on the -// Bitflyer exchange -func (e *Exchange) UpdateAccountInfo(_ context.Context, _ asset.Item) (account.Holdings, error) { - return account.Holdings{}, common.ErrNotYetImplemented +// UpdateAccountBalances retrieves currency balances +func (e *Exchange) UpdateAccountBalances(_ context.Context, _ asset.Item) (accounts.SubAccounts, error) { + return accounts.SubAccounts{}, common.ErrNotYetImplemented } // GetAccountFundingHistory returns funding history, deposits and @@ -382,7 +381,7 @@ func (e *Exchange) GetFeeByType(ctx context.Context, feeBuilder *exchange.FeeBui // 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) } diff --git a/exchanges/bithumb/bithumb_test.go b/exchanges/bithumb/bithumb_test.go index 56b4cc4a..81db1afe 100644 --- a/exchanges/bithumb/bithumb_test.go +++ b/exchanges/bithumb/bithumb_test.go @@ -418,12 +418,11 @@ func TestCancelAllExchangeOrders(t *testing.T) { assert.Emptyf(t, resp.Status, "%v orders failed to cancel", len(resp.Status)) } -func TestGetAccountInfo(t *testing.T) { +func TestUpdateAccountBalances(t *testing.T) { t.Parallel() sharedtestvalues.SkipTestIfCredentialsUnset(t, e) - - _, err := e.UpdateAccountInfo(t.Context(), asset.Spot) - require.NoError(t, err, "UpdateAccountInfo must not error") + _, err := e.UpdateAccountBalances(t.Context(), asset.Spot) + require.NoError(t, err) } func TestModifyOrder(t *testing.T) { diff --git a/exchanges/bithumb/bithumb_wrapper.go b/exchanges/bithumb/bithumb_wrapper.go index ef936059..6ac11acd 100644 --- a/exchanges/bithumb/bithumb_wrapper.go +++ b/exchanges/bithumb/bithumb_wrapper.go @@ -13,10 +13,10 @@ import ( "github.com/thrasher-corp/gocryptotrader/common/convert" "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/currencystate" "github.com/thrasher-corp/gocryptotrader/exchanges/deposit" @@ -299,52 +299,29 @@ func (e *Exchange) UpdateOrderbook(ctx context.Context, p currency.Pair, assetTy return orderbook.Get(e.Name, p, assetType) } -// UpdateAccountInfo retrieves balances for all enabled currencies for the -// Bithumb exchange -func (e *Exchange) UpdateAccountInfo(ctx context.Context, assetType asset.Item) (account.Holdings, error) { - var info account.Holdings +// UpdateAccountBalances retrieves currency balances +func (e *Exchange) UpdateAccountBalances(ctx context.Context, assetType asset.Item) (accounts.SubAccounts, error) { bal, err := e.GetAccountBalance(ctx, "ALL") if err != nil { - return info, err + return nil, err } - - exchangeBalances := make([]account.Balance, 0, len(bal.Total)) - for key, totalAmount := range bal.Total { - hold, ok := bal.InUse[key] + subAccts := accounts.SubAccounts{accounts.NewSubAccount(assetType, "")} + for k, totalAmount := range bal.Total { + hold, ok := bal.InUse[k] if !ok { - return info, fmt.Errorf("getAccountInfo error - in use item not found for currency %s", - key) + return subAccts, fmt.Errorf("currency %s missing from InUse balances", k) } - - avail, ok := bal.Available[key] + avail, ok := bal.Available[k] if !ok { avail = totalAmount - hold } - - exchangeBalances = append(exchangeBalances, account.Balance{ - Currency: currency.NewCode(key), - Total: totalAmount, - Hold: hold, - Free: avail, + subAccts[0].Balances.Set(currency.NewCode(k), accounts.Balance{ + Total: totalAmount, + Hold: hold, + Free: avail, }) } - - info.Accounts = append(info.Accounts, account.SubAccount{ - Currencies: exchangeBalances, - AssetType: assetType, - }) - - info.Exchange = e.Name - creds, err := e.GetCredentials(ctx) - if err != nil { - return account.Holdings{}, err - } - err = account.Process(&info, creds) - if err != nil { - return account.Holdings{}, err - } - - return info, nil + return subAccts, e.Accounts.Save(ctx, subAccts, true) } // GetAccountFundingHistory returns funding history, deposits and @@ -731,10 +708,9 @@ func (e *Exchange) GetOrderHistory(ctx context.Context, req *order.MultiOrderReq return req.Filter(e.Name, orders), 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) } diff --git a/exchanges/bitmex/bitmex.go b/exchanges/bitmex/bitmex.go index ec72db43..104e4c84 100644 --- a/exchanges/bitmex/bitmex.go +++ b/exchanges/bitmex/bitmex.go @@ -673,7 +673,7 @@ func (e *Exchange) ConfirmEmail(ctx context.Context, token string) (ConfirmEmail &confirmation) } -// ConfirmTwoFactorAuth confirms 2FA for this account. +// ConfirmTwoFactorAuth confirms 2FA for this account func (e *Exchange) ConfirmTwoFactorAuth(ctx context.Context, token, typ string) (bool, error) { var working bool @@ -966,12 +966,14 @@ func calculateTradingFee(purchasePrice, amount float64, isMaker bool) float64 { return fee * purchasePrice * amount } +var xbtCurr = currency.NewCode("XBt") + // normalizeWalletInfo converts any non-standard currencies (eg. XBt -> BTC) func normalizeWalletInfo(w *WalletInfo) { - if w.Currency != "XBt" { + if !w.Currency.Equal(xbtCurr) { return } - w.Currency = "BTC" + w.Currency = currency.BTC w.Amount *= constSatoshiBTC } diff --git a/exchanges/bitmex/bitmex_test.go b/exchanges/bitmex/bitmex_test.go index 1aab2b25..7dc36def 100644 --- a/exchanges/bitmex/bitmex_test.go +++ b/exchanges/bitmex/bitmex_test.go @@ -639,19 +639,17 @@ func TestCancelAllExchangeOrders(t *testing.T) { } } -func TestUpdateAccountInfo(t *testing.T) { +func TestUpdateAccountBalances(t *testing.T) { t.Parallel() if sharedtestvalues.AreAPICredentialsSet(e) { - _, err := e.UpdateAccountInfo(t.Context(), asset.Spot) + _, err := e.UpdateAccountBalances(t.Context(), asset.Spot) require.NoError(t, err) - - _, err = e.UpdateAccountInfo(t.Context(), asset.Futures) + _, err = e.UpdateAccountBalances(t.Context(), asset.Futures) require.NoError(t, err) } else { - _, err := e.UpdateAccountInfo(t.Context(), asset.Spot) + _, err := e.UpdateAccountBalances(t.Context(), asset.Spot) require.Error(t, err) - - _, err = e.UpdateAccountInfo(t.Context(), asset.Futures) + _, err = e.UpdateAccountBalances(t.Context(), asset.Futures) require.Error(t, err) } } @@ -973,13 +971,13 @@ func TestUpdateTickers(t *testing.T) { func TestNormalizeWalletInfo(t *testing.T) { w := &WalletInfo{ - Currency: "XBt", + Currency: xbtCurr, Amount: 1e+08, } normalizeWalletInfo(w) - assert.Equal(t, "BTC", w.Currency, "Currency should be correct") + assert.Equal(t, currency.BTC, w.Currency, "Currency should be correct") assert.Equal(t, 1.0, w.Amount, "Amount should be correct") } diff --git a/exchanges/bitmex/bitmex_types.go b/exchanges/bitmex/bitmex_types.go index 93a5f138..bf891106 100644 --- a/exchanges/bitmex/bitmex_types.go +++ b/exchanges/bitmex/bitmex_types.go @@ -3,6 +3,7 @@ package bitmex import ( "time" + "github.com/thrasher-corp/gocryptotrader/currency" "github.com/thrasher-corp/gocryptotrader/exchanges/order" ) @@ -648,31 +649,31 @@ type MinWithdrawalFee struct { // WalletInfo wallet information type WalletInfo struct { - Account int64 `json:"account"` - Addr string `json:"addr"` - Amount float64 `json:"amount"` - ConfirmedDebit int64 `json:"confirmedDebit"` - Currency string `json:"currency"` - DeltaAmount int64 `json:"deltaAmount"` - DeltaDeposited int64 `json:"deltaDeposited"` - DeltaTransferIn int64 `json:"deltaTransferIn"` - DeltaTransferOut int64 `json:"deltaTransferOut"` - DeltaWithdrawn int64 `json:"deltaWithdrawn"` - Deposited int64 `json:"deposited"` - PendingCredit int64 `json:"pendingCredit"` - PendingDebit int64 `json:"pendingDebit"` - PrevAmount int64 `json:"prevAmount"` - PrevDeposited int64 `json:"prevDeposited"` - PrevTimestamp time.Time `json:"prevTimestamp"` - PrevTransferIn int64 `json:"prevTransferIn"` - PrevTransferOut int64 `json:"prevTransferOut"` - PrevWithdrawn int64 `json:"prevWithdrawn"` - Script string `json:"script"` - Timestamp time.Time `json:"timestamp"` - TransferIn int64 `json:"transferIn"` - TransferOut int64 `json:"transferOut"` - WithdrawalLock []string `json:"withdrawalLock"` - Withdrawn int64 `json:"withdrawn"` + Account int64 `json:"account"` + Addr string `json:"addr"` + Amount float64 `json:"amount"` + ConfirmedDebit int64 `json:"confirmedDebit"` + Currency currency.Code `json:"currency"` + DeltaAmount int64 `json:"deltaAmount"` + DeltaDeposited int64 `json:"deltaDeposited"` + DeltaTransferIn int64 `json:"deltaTransferIn"` + DeltaTransferOut int64 `json:"deltaTransferOut"` + DeltaWithdrawn int64 `json:"deltaWithdrawn"` + Deposited int64 `json:"deposited"` + PendingCredit int64 `json:"pendingCredit"` + PendingDebit int64 `json:"pendingDebit"` + PrevAmount int64 `json:"prevAmount"` + PrevDeposited int64 `json:"prevDeposited"` + PrevTimestamp time.Time `json:"prevTimestamp"` + PrevTransferIn int64 `json:"prevTransferIn"` + PrevTransferOut int64 `json:"prevTransferOut"` + PrevWithdrawn int64 `json:"prevWithdrawn"` + Script string `json:"script"` + Timestamp time.Time `json:"timestamp"` + TransferIn int64 `json:"transferIn"` + TransferOut int64 `json:"transferOut"` + WithdrawalLock []string `json:"withdrawalLock"` + Withdrawn int64 `json:"withdrawn"` } // orderTypeMap holds order type info based on Bitmex data diff --git a/exchanges/bitmex/bitmex_wrapper.go b/exchanges/bitmex/bitmex_wrapper.go index 7959a017..5376b86a 100644 --- a/exchanges/bitmex/bitmex_wrapper.go +++ b/exchanges/bitmex/bitmex_wrapper.go @@ -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" @@ -442,49 +442,24 @@ func (e *Exchange) UpdateOrderbook(ctx context.Context, p currency.Pair, assetTy return orderbook.Get(e.Name, p, assetType) } -// UpdateAccountInfo retrieves balances for all enabled currencies for the -// Bitmex exchange -func (e *Exchange) UpdateAccountInfo(ctx context.Context, assetType asset.Item) (account.Holdings, error) { - var info account.Holdings - +// UpdateAccountBalances retrieves currency balances +func (e *Exchange) UpdateAccountBalances(ctx context.Context, assetType asset.Item) (accounts.SubAccounts, error) { userMargins, err := e.GetAllUserMargin(ctx) if err != nil { - return info, err + return nil, err } - - accountBalances := make(map[string][]account.Balance) + var subAccts accounts.SubAccounts // Need to update to add Margin/Liquidity availability for i := range userMargins { - accountID := strconv.FormatInt(userMargins[i].Account, 10) - - var wallet WalletInfo - wallet, err = e.GetWalletInfo(ctx, userMargins[i].Currency) + wallet, err := e.GetWalletInfo(ctx, userMargins[i].Currency) if err != nil { continue } - - accountBalances[accountID] = append( - accountBalances[accountID], account.Balance{ - Currency: currency.NewCode(wallet.Currency), - Total: wallet.Amount, - }, - ) + a := accounts.NewSubAccount(assetType, strconv.FormatInt(userMargins[i].Account, 10)) + a.Balances.Set(wallet.Currency, accounts.Balance{Total: wallet.Amount}) + subAccts = subAccts.Merge(a) } - - if info.Accounts, err = account.CollectBalances(accountBalances, assetType); err != nil { - return account.Holdings{}, err - } - info.Exchange = e.Name - - creds, err := e.GetCredentials(ctx) - if err != nil { - return account.Holdings{}, 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 @@ -976,10 +951,9 @@ func (e *Exchange) AuthenticateWebsocket(ctx context.Context) error { return e.websocketSendAuth(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) } diff --git a/exchanges/bitstamp/bitstamp_wrapper.go b/exchanges/bitstamp/bitstamp_wrapper.go index cc7aa80a..9763e6f1 100644 --- a/exchanges/bitstamp/bitstamp_wrapper.go +++ b/exchanges/bitstamp/bitstamp_wrapper.go @@ -13,10 +13,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/deposit" "github.com/thrasher-corp/gocryptotrader/exchanges/fundingrate" @@ -336,40 +336,21 @@ func (e *Exchange) UpdateOrderbook(ctx context.Context, p currency.Pair, assetTy return orderbook.Get(e.Name, fPair, assetType) } -// UpdateAccountInfo retrieves balances for all enabled currencies for the -// Bitstamp exchange -func (e *Exchange) UpdateAccountInfo(ctx context.Context, assetType asset.Item) (account.Holdings, error) { - var response account.Holdings - response.Exchange = e.Name +// UpdateAccountBalances retrieves currency balances +func (e *Exchange) UpdateAccountBalances(ctx context.Context, assetType asset.Item) (accounts.SubAccounts, error) { accountBalance, err := e.GetBalance(ctx) if err != nil { - return response, err + return nil, err } - - currencies := make([]account.Balance, 0, len(accountBalance)) + subAccts := accounts.SubAccounts{accounts.NewSubAccount(assetType, "")} for k, v := range accountBalance { - currencies = append(currencies, account.Balance{ - Currency: currency.NewCode(k), - Total: v.Balance, - Hold: v.Reserved, - Free: v.Available, + subAccts[0].Balances.Set(currency.NewCode(k), accounts.Balance{ + Total: v.Balance, + Hold: v.Reserved, + Free: v.Available, }) } - response.Accounts = append(response.Accounts, account.SubAccount{ - AssetType: assetType, - Currencies: currencies, - }) - - creds, err := e.GetCredentials(ctx) - if err != nil { - return account.Holdings{}, err - } - err = account.Process(&response, creds) - if err != nil { - return account.Holdings{}, err - } - - return response, nil + return subAccts, e.Accounts.Save(ctx, subAccts, true) } // GetAccountFundingHistory returns funding history, deposits and @@ -772,10 +753,9 @@ func (e *Exchange) GetOrderHistory(ctx context.Context, req *order.MultiOrderReq return req.Filter(e.Name, orders), 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) } diff --git a/exchanges/btcmarkets/btcmarkets_types.go b/exchanges/btcmarkets/btcmarkets_types.go index 045799b8..177eff51 100644 --- a/exchanges/btcmarkets/btcmarkets_types.go +++ b/exchanges/btcmarkets/btcmarkets_types.go @@ -124,10 +124,10 @@ type TradeResponse struct { // AccountData stores account data type AccountData struct { - AssetName string `json:"assetName"` - Balance float64 `json:"balance,string"` - Available float64 `json:"available,string"` - Locked float64 `json:"locked,string"` + AssetName currency.Code `json:"assetName"` + Balance float64 `json:"balance,string"` + Available float64 `json:"available,string"` + Locked float64 `json:"locked,string"` } // TradeHistoryData stores data of past trades diff --git a/exchanges/btcmarkets/btcmarkets_wrapper.go b/exchanges/btcmarkets/btcmarkets_wrapper.go index 38b22d1e..21a001ea 100644 --- a/exchanges/btcmarkets/btcmarkets_wrapper.go +++ b/exchanges/btcmarkets/btcmarkets_wrapper.go @@ -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/deposit" "github.com/thrasher-corp/gocryptotrader/exchanges/fundingrate" @@ -283,36 +283,21 @@ func (e *Exchange) UpdateOrderbook(ctx context.Context, p currency.Pair, assetTy return orderbook.Get(e.Name, p, assetType) } -// UpdateAccountInfo retrieves balances for all enabled currencies -func (e *Exchange) UpdateAccountInfo(ctx context.Context, assetType asset.Item) (account.Holdings, error) { - var resp account.Holdings - data, err := e.GetAccountBalance(ctx) +// UpdateAccountBalances retrieves currency balances +func (e *Exchange) UpdateAccountBalances(ctx context.Context, assetType asset.Item) (accounts.SubAccounts, error) { + resp, err := e.GetAccountBalance(ctx) if err != nil { - return resp, err + return nil, err } - var acc account.SubAccount - acc.AssetType = assetType - for x := range data { - acc.Currencies = append(acc.Currencies, account.Balance{ - Currency: currency.NewCode(data[x].AssetName), - Total: data[x].Balance, - Hold: data[x].Locked, - Free: data[x].Available, + subAccts := accounts.SubAccounts{accounts.NewSubAccount(assetType, "")} + for i := range resp { + subAccts[0].Balances.Set(resp[i].AssetName, accounts.Balance{ + Total: resp[i].Balance, + Hold: resp[i].Locked, + Free: resp[i].Available, }) } - resp.Accounts = append(resp.Accounts, acc) - resp.Exchange = e.Name - - creds, err := e.GetCredentials(ctx) - if err != nil { - return account.Holdings{}, err - } - err = account.Process(&resp, creds) - if err != nil { - return account.Holdings{}, err - } - - return resp, nil + return subAccts, e.Accounts.Save(ctx, subAccts, true) } // GetAccountFundingHistory returns funding history, deposits and @@ -846,10 +831,9 @@ func (e *Exchange) GetOrderHistory(ctx context.Context, req *order.MultiOrderReq return req.Filter(e.Name, resp), 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) if err != nil { if e.CheckTransientError(err) == nil { return nil diff --git a/exchanges/btse/btse_types.go b/exchanges/btse/btse_types.go index b1738e7f..52bd572f 100644 --- a/exchanges/btse/btse_types.go +++ b/exchanges/btse/btse_types.go @@ -3,6 +3,7 @@ package btse import ( "time" + "github.com/thrasher-corp/gocryptotrader/currency" "github.com/thrasher-corp/gocryptotrader/exchanges/order" "github.com/thrasher-corp/gocryptotrader/types" ) @@ -177,9 +178,9 @@ type ServerTime struct { // CurrencyBalance stores the account info data type CurrencyBalance struct { - Currency string `json:"currency"` - Total float64 `json:"total"` - Available float64 `json:"available"` + Currency currency.Code `json:"currency"` + Total float64 `json:"total"` + Available float64 `json:"available"` } // AccountFees stores fee for each currency pair diff --git a/exchanges/btse/btse_wrapper.go b/exchanges/btse/btse_wrapper.go index 04c55d66..96a6ca35 100644 --- a/exchanges/btse/btse_wrapper.go +++ b/exchanges/btse/btse_wrapper.go @@ -14,10 +14,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/deposit" "github.com/thrasher-corp/gocryptotrader/exchanges/fundingrate" @@ -349,42 +349,21 @@ func (e *Exchange) UpdateOrderbook(ctx context.Context, p currency.Pair, assetTy return orderbook.Get(e.Name, p, assetType) } -// UpdateAccountInfo retrieves balances for all enabled currencies for the -// BTSE exchange -func (e *Exchange) UpdateAccountInfo(ctx context.Context, assetType asset.Item) (account.Holdings, error) { - var a account.Holdings - balance, err := e.GetWalletInformation(ctx) +// UpdateAccountBalances retrieves currency balances +func (e *Exchange) UpdateAccountBalances(ctx context.Context, assetType asset.Item) (accounts.SubAccounts, error) { + balances, err := e.GetWalletInformation(ctx) if err != nil { - return a, err + return nil, err } - - currencies := make([]account.Balance, len(balance)) - for b := range balance { - currencies[b] = account.Balance{ - Currency: currency.NewCode(balance[b].Currency), - Total: balance[b].Total, - Hold: balance[b].Total - balance[b].Available, - Free: balance[b].Available, - } + subAccts := accounts.SubAccounts{accounts.NewSubAccount(assetType, "")} + for i := range balances { + subAccts[0].Balances.Set(balances[i].Currency, accounts.Balance{ + Total: balances[i].Total, + Hold: balances[i].Total - balances[i].Available, + Free: balances[i].Available, + }) } - a.Exchange = e.Name - a.Accounts = []account.SubAccount{ - { - AssetType: assetType, - Currencies: currencies, - }, - } - - creds, err := e.GetCredentials(ctx) - if err != nil { - return account.Holdings{}, err - } - err = account.Process(&a, creds) - if err != nil { - return account.Holdings{}, err - } - - return a, nil + return subAccts, e.Accounts.Save(ctx, subAccts, true) } // GetAccountFundingHistory returns funding history, deposits and @@ -876,10 +855,9 @@ func (e *Exchange) GetFeeByType(ctx context.Context, feeBuilder *exchange.FeeBui return e.GetFee(ctx, feeBuilder) } -// 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) } diff --git a/exchanges/bybit/bybit_live_test.go b/exchanges/bybit/bybit_live_test.go index 5df09497..74d6eb45 100644 --- a/exchanges/bybit/bybit_live_test.go +++ b/exchanges/bybit/bybit_live_test.go @@ -18,17 +18,7 @@ import ( var mockTests = false func TestMain(m *testing.M) { - e = new(Exchange) - if err := testexch.Setup(e); err != nil { - log.Fatalf("Bybit Setup error: %s", err) - } - - if apiKey != "" && apiSecret != "" { - e.API.AuthenticatedSupport = true - e.API.AuthenticatedWebsocketSupport = true - e.SetCredentials(apiKey, apiSecret, "", "", "", "") - e.Websocket.SetCanUseAuthenticatedEndpoints(true) - } + e = testInstance() if e.API.AuthenticatedSupport { if _, err := e.FetchAccountType(context.Background()); err != nil { @@ -41,6 +31,21 @@ func TestMain(m *testing.M) { os.Exit(m.Run()) } +func testInstance() *Bybit { + e := new(Exchange) + if err := testexch.Setup(e); err != nil { + log.Fatalf("Bybit Setup error: %s", err) + } + + if apiKey != "" && apiSecret != "" { + e.API.AuthenticatedSupport = true + e.API.AuthenticatedWebsocketSupport = true + e.SetCredentials(apiKey, apiSecret, "", "", "", "") + e.Websocket.SetCanUseAuthenticatedEndpoints(true) + } + return e +} + func instantiateTradablePairs() { handleError := func(msg string, err error) { if err != nil { diff --git a/exchanges/bybit/bybit_mock_test.go b/exchanges/bybit/bybit_mock_test.go index f1098584..9e62942b 100644 --- a/exchanges/bybit/bybit_mock_test.go +++ b/exchanges/bybit/bybit_mock_test.go @@ -18,16 +18,7 @@ import ( var mockTests = true func TestMain(m *testing.M) { - e = new(Exchange) - if err := testexch.Setup(e); err != nil { - log.Fatalf("Bybit Setup error: %s", err) - } - - e.SetCredentials("mock", "tester", "", "", "", "") // Hack for UpdateAccountInfo test - - if err := testexch.MockHTTPInstance(e); err != nil { - log.Fatalf("Bybit MockHTTPInstance error: %s", err) - } + e = testInstance() if err := e.UpdateTradablePairs(context.Background()); err != nil { log.Fatalf("Bybit unable to UpdateTradablePairs: %s", err) @@ -57,3 +48,18 @@ func TestMain(m *testing.M) { os.Exit(m.Run()) } + +func testInstance() *Exchange { + b := new(Exchange) + if err := testexch.Setup(b); err != nil { + log.Fatalf("Bybit Setup error: %s", err) + } + + b.SetCredentials("mock", "tester", "", "", "", "") // Hack for UpdateAccountBalances test + + if err := testexch.MockHTTPInstance(b); err != nil { + log.Fatalf("Bybit MockHTTPInstance error: %s", err) + } + + return b +} diff --git a/exchanges/bybit/bybit_test.go b/exchanges/bybit/bybit_test.go index f4111258..118bf102 100644 --- a/exchanges/bybit/bybit_test.go +++ b/exchanges/bybit/bybit_test.go @@ -19,10 +19,10 @@ import ( "github.com/thrasher-corp/gocryptotrader/common/key" "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/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/fill" "github.com/thrasher-corp/gocryptotrader/exchanges/fundingrate" @@ -2778,44 +2778,49 @@ func TestGetBrokerEarning(t *testing.T) { } } -func TestUpdateAccountInfo(t *testing.T) { +func TestUpdateAccountBalances(t *testing.T) { t.Parallel() if !mockTests { sharedtestvalues.SkipTestIfCredentialsUnset(t, e) } - r, err := e.UpdateAccountInfo(t.Context(), asset.Spot) - require.NoError(t, err, "UpdateAccountInfo must not error") - require.NotEmpty(t, r, "UpdateAccountInfo must return account info") + e := testInstance() //nolint:govet // Intentional shadow to avoid future copy/paste mistakes + + subAccts, err := e.UpdateAccountBalances(t.Context(), asset.Spot) + require.NoError(t, err, "UpdateAccountBalances must not error") + require.NotEmpty(t, subAccts, "UpdateAccountBalances must return account info") if mockTests { - require.Len(t, r.Accounts, 1, "Accounts must have 1 item") - require.Len(t, r.Accounts[0].Currencies, 3, "Accounts currencies must have 3 currency items") + require.Len(t, subAccts, 1, "Accounts must have 1 item") + require.Len(t, subAccts[0].Balances, 3, "Accounts currencies must have 3 currency items") - for x := range r.Accounts[0].Currencies { - switch x { - case 0: - assert.Equal(t, currency.USDC, r.Accounts[0].Currencies[x].Currency, "Currency should be USDC") - assert.Equal(t, -30723.63021638, r.Accounts[0].Currencies[x].Total, "Total amount should be correct") - assert.Zero(t, r.Accounts[0].Currencies[x].Hold, "Hold amount should be zero") - assert.Equal(t, 30723.630216383711792744, r.Accounts[0].Currencies[x].Borrowed, "Borrowed amount should be correct") - assert.Zero(t, r.Accounts[0].Currencies[x].Free, "Free amount should be zero") - assert.Zero(t, r.Accounts[0].Currencies[x].AvailableWithoutBorrow, "AvailableWithoutBorrow amount should be zero") - case 1: - assert.Equal(t, currency.AVAX, r.Accounts[0].Currencies[x].Currency, "Currency should be AVAX") - assert.Equal(t, 2473.9, r.Accounts[0].Currencies[x].Total, "Total amount should be correct") - assert.Zero(t, r.Accounts[0].Currencies[x].Hold, "Hold amount should be zero") - assert.Zero(t, r.Accounts[0].Currencies[x].Borrowed, "Borrowed amount should be zero") - assert.Equal(t, 2473.9, r.Accounts[0].Currencies[x].Free, "Free amount should be correct") - assert.Equal(t, 1005.79191187, r.Accounts[0].Currencies[x].AvailableWithoutBorrow, "AvailableWithoutBorrow amount should be correct") - case 2: - assert.Equal(t, currency.USDT, r.Accounts[0].Currencies[x].Currency, "Currency should be USDT") - assert.Equal(t, 935.1415, r.Accounts[0].Currencies[x].Total, "Total amount should be correct") - assert.Zero(t, r.Accounts[0].Currencies[x].Borrowed, "Borrowed amount should be zero") - assert.Zero(t, r.Accounts[0].Currencies[x].Hold, "Hold amount should be zero") - assert.Equal(t, 935.1415, r.Accounts[0].Currencies[x].Free, "Free amount should be correct") - assert.Equal(t, 935.1415, r.Accounts[0].Currencies[x].AvailableWithoutBorrow, "AvailableWithoutBorrow amount should be correct") - } + for _, curr := range []currency.Code{currency.USDC, currency.AVAX, currency.USDT} { + t.Run(curr.String(), func(t *testing.T) { + t.Parallel() + require.Contains(t, subAccts[0].Balances, curr, "Balances must contain currency") + bal := subAccts[0].Balances[curr] + assert.Equal(t, curr, bal.Currency, "Balance Currency should be set") + switch curr { + case currency.USDC: + assert.Equal(t, -30723.63021638, bal.Total, "Total amount should be correct") + assert.Zero(t, bal.Hold, "Hold amount should be zero") + assert.Equal(t, 30723.630216383711792744, bal.Borrowed, "Borrowed amount should be correct") + assert.Zero(t, bal.Free, "Free amount should be zero") + assert.Zero(t, bal.AvailableWithoutBorrow, "AvailableWithoutBorrow amount should be zero") + case currency.AVAX: + assert.Equal(t, 2473.9, bal.Total, "Total amount should be correct") + assert.Zero(t, bal.Hold, "Hold amount should be zero") + assert.Zero(t, bal.Borrowed, "Borrowed amount should be zero") + assert.Equal(t, 2473.9, bal.Free, "Free amount should be correct") + assert.Equal(t, 1005.79191187, bal.AvailableWithoutBorrow, "AvailableWithoutBorrow amount should be correct") + case currency.USDT: + assert.Equal(t, 935.1415, bal.Total, "Total amount should be correct") + assert.Zero(t, bal.Borrowed, "Borrowed amount should be zero") + assert.Zero(t, bal.Hold, "Hold amount should be zero") + assert.Equal(t, 935.1415, bal.Free, "Free amount should be correct") + assert.Equal(t, 935.1415, bal.AvailableWithoutBorrow, "AvailableWithoutBorrow amount should be correct") + } + }) } } } @@ -2980,9 +2985,11 @@ var pushDataMap = map[string]string{ "unhandled": `{"topic": "unhandled"}`, } -func TestPushDataPublic(t *testing.T) { +func TestWSHandleData(t *testing.T) { t.Parallel() + e := testInstance() //nolint:govet // Intentional shadow to avoid future copy/paste mistakes + keys := slices.Collect(maps.Keys(pushDataMap)) slices.Sort(keys) for x := range keys { @@ -3009,14 +3016,21 @@ func TestWSHandleAuthenticatedData(t *testing.T) { e.API.AuthenticatedSupport = true e.API.AuthenticatedWebsocketSupport = true e.SetCredentials("test", "test", "", "", "", "") - testexch.FixtureToDataHandler(t, "testdata/wsAuth.json", func(ctx context.Context, r []byte) error { + fErrs := testexch.FixtureToDataHandlerWithErrors(t, "testdata/wsAuth.json", func(ctx context.Context, r []byte) error { if bytes.Contains(r, []byte("%s")) { r = fmt.Appendf(nil, string(r), optionsTradablePair.String()) } + if bytes.Contains(r, []byte("FANGLE-ACCOUNTS")) { + hold := e.Accounts + e.Accounts = nil + defer func() { e.Accounts = hold }() + } return e.wsHandleAuthenticatedData(ctx, &FixtureConnection{match: websocket.NewMatch()}, r) }) close(e.Websocket.DataHandler) require.Len(t, e.Websocket.DataHandler, 6, "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") i := 0 for data := range e.Websocket.DataHandler { @@ -3081,63 +3095,39 @@ func TestWSHandleAuthenticatedData(t *testing.T) { assert.Equal(t, 0.358635, v[0].Fee, "fee should be correct") assert.Equal(t, time.UnixMilli(1672364262444), v[0].Date, "Created time should be correct") assert.Equal(t, time.UnixMilli(1672364262457), v[0].LastUpdated, "Updated time should be correct") - case []account.Change: - require.Len(t, v, 6, "must see 6 items") - for i, change := range v { - assert.Empty(t, change.Account, "Account type should be empty") - assert.Equal(t, asset.Spot, change.AssetType, "Asset type should be Spot") - require.NotNil(t, change.Balance, "balance must not be nil") - switch i { - case 0: - assert.True(t, currency.USDC.Equal(change.Balance.Currency), "currency should match") - assert.Zero(t, change.Balance.AvailableWithoutBorrow, "AvailableWithoutBorrow should zero") - assert.Zero(t, change.Balance.Borrowed, "Borrowed should be 0") - assert.Equal(t, 201.34882644, change.Balance.Free, "Free should be correct") - assert.Zero(t, change.Balance.Hold, "Hold should be 0") - assert.Equal(t, 201.34882644, change.Balance.Total, "Total should be correct") - assert.Equal(t, time.UnixMilli(1672364262482), change.Balance.UpdatedAt, "Last updated should be correct") - case 1: - assert.True(t, currency.BTC.Equal(change.Balance.Currency), "currency should match") - assert.Equal(t, 0.06488393, change.Balance.Free, "Free should be correct") - assert.Zero(t, change.Balance.AvailableWithoutBorrow, "AvailableWithoutBorrow should zero") - assert.Zero(t, change.Balance.Borrowed, "Borrowed should be 0") - assert.Zero(t, change.Balance.Hold, "Hold should be 0") - assert.Equal(t, 0.06488393, change.Balance.Total, "Total should be correct") - assert.Equal(t, time.UnixMilli(1672364262482), change.Balance.UpdatedAt, "Last updated should be correct") - case 2: - assert.True(t, currency.ETH.Equal(change.Balance.Currency), "currency should match") - assert.Zero(t, change.Balance.Free, "Free should be 0") - assert.Zero(t, change.Balance.AvailableWithoutBorrow, "AvailableWithoutBorrow should zero") - assert.Zero(t, change.Balance.Borrowed, "Borrowed should be 0") - assert.Zero(t, change.Balance.Hold, "Hold should be 0") - assert.Zero(t, change.Balance.Total, "Total should be 0") - assert.Equal(t, time.UnixMilli(1672364262482), change.Balance.UpdatedAt, "Last updated should be correct") - case 3: - assert.True(t, currency.USDT.Equal(change.Balance.Currency), "currency should match") - assert.Equal(t, 11728.54414904, change.Balance.Free, "Free should be correct") - assert.Zero(t, change.Balance.AvailableWithoutBorrow, "AvailableWithoutBorrow should be 0") - assert.Zero(t, change.Balance.Borrowed, "Borrowed should be 0") - assert.Zero(t, change.Balance.Hold, "Hold should be 0") - assert.Equal(t, 11728.54414904, change.Balance.Total, "Total should be correct") - assert.Equal(t, time.UnixMilli(1672364262482), change.Balance.UpdatedAt, "Last updated should be correct") - case 4: - assert.True(t, currency.NewCode("EOS3L").Equal(change.Balance.Currency), "currency should match") - assert.Equal(t, 215.0570412, change.Balance.Free, "Free should be correct") - assert.Zero(t, change.Balance.AvailableWithoutBorrow, "AvailableWithoutBorrow should be 0") - assert.Zero(t, change.Balance.Borrowed, "Borrowed should be 0") - assert.Zero(t, change.Balance.Hold, "Hold should be 0") - assert.Equal(t, 215.0570412, change.Balance.Total, "Total should be correct") - assert.Equal(t, time.UnixMilli(1672364262482), change.Balance.UpdatedAt, "Last updated should be correct") - case 5: - assert.True(t, currency.BIT.Equal(change.Balance.Currency), "currency should match") - assert.Equal(t, 1.82, change.Balance.Free, "Free should be correct") - assert.Zero(t, change.Balance.AvailableWithoutBorrow, "AvailableWithoutBorrow should be 0") - assert.Zero(t, change.Balance.Borrowed, "Borrowed should be 0") - assert.Zero(t, change.Balance.Hold, "Hold should be 0") - assert.Equal(t, 1.82, change.Balance.Total, "Total should be correct") - assert.Equal(t, time.UnixMilli(1672364262482), change.Balance.UpdatedAt, "Last updated should be correct") - } - } + case accounts.SubAccounts: + require.Len(t, v, 1, "Must have correct number of SubAccounts") + assert.Equal(t, asset.Spot, v[0].AssetType, "Asset type should be correct") + exp := accounts.CurrencyBalances{} + exp.Set(currency.ETH, accounts.Balance{ + UpdatedAt: time.UnixMilli(1672364262482), + }) + exp.Set(currency.USDT, accounts.Balance{ + UpdatedAt: time.UnixMilli(1672364262482), + Total: 11728.54414904, + Free: 11728.54414904, + }) + exp.Set(currency.EOS3L, accounts.Balance{ + UpdatedAt: time.UnixMilli(1672364262482), + Total: 215.0570412, + Free: 215.0570412, + }) + exp.Set(currency.BIT, accounts.Balance{ + UpdatedAt: time.UnixMilli(1672364262482), + Total: 1.82, + Free: 1.82, + }) + exp.Set(currency.USDC, accounts.Balance{ + UpdatedAt: time.UnixMilli(1672364262482), + Total: 201.34882644, + Free: 201.34882644, + }) + exp.Set(currency.BTC, accounts.Balance{ + UpdatedAt: time.UnixMilli(1672364262482), + Total: 0.06488393, + Free: 0.06488393, + }) + assert.Equal(t, exp, v[0].Balances, "Balances should be correct") case *GreeksResponse: assert.Equal(t, "592324fa945a30-2603-49a5-b865-21668c29f2a6", v.ID, "ID should be correct") assert.Equal(t, "greeks", v.Topic, "Topic should be correct") @@ -3162,7 +3152,7 @@ func TestWSHandleAuthenticatedData(t *testing.T) { assert.Equal(t, 0.3374, v[0].Price, "price should be correct") assert.Equal(t, 25.0, v[0].Amount, "amount should be correct") default: - t.Errorf("Unexpected data received: %v", v) + t.Errorf("Unexpected data received: %T %v", v, v) } } } @@ -3170,11 +3160,11 @@ func TestWSHandleAuthenticatedData(t *testing.T) { func TestWsTicker(t *testing.T) { t.Parallel() e := new(Exchange) //nolint:govet // Intentional shadow + require.NoError(t, testexch.Setup(e), "Test instance Setup must not error") assetRouting := []asset.Item{ asset.Spot, asset.Options, asset.USDTMarginedFutures, asset.USDTMarginedFutures, asset.USDCMarginedFutures, asset.USDCMarginedFutures, asset.CoinMarginedFutures, asset.CoinMarginedFutures, } - require.NoError(t, testexch.Setup(e), "Test instance Setup must not error") testexch.FixtureToDataHandler(t, "testdata/wsTicker.json", func(_ context.Context, r []byte) error { defer slices.Delete(assetRouting, 0, 1) return e.wsHandleData(nil, assetRouting[0], r) @@ -3730,7 +3720,7 @@ func TestWebsocketAuthenticatePrivateConnection(t *testing.T) { e.API.AuthenticatedSupport = true e.API.AuthenticatedWebsocketSupport = true e.Websocket.SetCanUseAuthenticatedEndpoints(true) - ctx := account.DeployCredentialsToContext(t.Context(), &account.Credentials{Key: "dummy", Secret: "dummy"}) + ctx := accounts.DeployCredentialsToContext(t.Context(), &accounts.Credentials{Key: "dummy", Secret: "dummy"}) err = e.WebsocketAuthenticatePrivateConnection(ctx, &FixtureConnection{}) require.NoError(t, err) err = e.WebsocketAuthenticatePrivateConnection(ctx, &FixtureConnection{sendMessageReturnResponseOverride: []byte(`{"success":false,"ret_msg":"failed auth","conn_id":"5758770c-8152-4545-a84f-dae089e56499","req_id":"1","op":"subscribe"}`)}) @@ -3749,7 +3739,7 @@ func TestWebsocketAuthenticateTradeConnection(t *testing.T) { e.API.AuthenticatedSupport = true e.API.AuthenticatedWebsocketSupport = true e.Websocket.SetCanUseAuthenticatedEndpoints(true) - ctx := account.DeployCredentialsToContext(t.Context(), &account.Credentials{Key: "dummy", Secret: "dummy"}) + ctx := accounts.DeployCredentialsToContext(t.Context(), &accounts.Credentials{Key: "dummy", Secret: "dummy"}) err = e.WebsocketAuthenticateTradeConnection(ctx, &FixtureConnection{sendMessageReturnResponseOverride: []byte(`{"retCode":0,"retMsg":"OK","op":"auth","connId":"d2a641kgcg7ab33b7mdg-4x6a"}`)}) require.NoError(t, err) err = e.WebsocketAuthenticateTradeConnection(ctx, &FixtureConnection{sendMessageReturnResponseOverride: []byte(`{"retCode":10004,"retMsg":"Invalid sign","op":"auth","connId":"d2a63t6p49kk82nefh90-4ye8"}`)}) diff --git a/exchanges/bybit/bybit_websocket.go b/exchanges/bybit/bybit_websocket.go index a7bdfde2..a247e346 100644 --- a/exchanges/bybit/bybit_websocket.go +++ b/exchanges/bybit/bybit_websocket.go @@ -17,8 +17,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/fill" "github.com/thrasher-corp/gocryptotrader/exchanges/kline" @@ -315,26 +315,21 @@ func (e *Exchange) wsProcessWalletPushData(ctx context.Context, resp []byte) err if err := json.Unmarshal(resp, &result); 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, "")} for x := range result.Data { for y := range result.Data[x].Coin { - changes = append(changes, account.Change{ - AssetType: asset.Spot, - Balance: &account.Balance{ - Currency: result.Data[x].Coin[y].Coin, - Total: result.Data[x].Coin[y].WalletBalance.Float64(), - Free: result.Data[x].Coin[y].WalletBalance.Float64(), - UpdatedAt: result.CreationTime.Time(), - }, + subAccts[0].Balances.Set(result.Data[x].Coin[y].Coin, accounts.Balance{ + Total: result.Data[x].Coin[y].WalletBalance.Float64(), + Free: result.Data[x].Coin[y].WalletBalance.Float64(), + UpdatedAt: result.CreationTime.Time(), }) } } - 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 } // wsProcessOrder the order stream to see changes to your orders in real-time. diff --git a/exchanges/bybit/bybit_wrapper.go b/exchanges/bybit/bybit_wrapper.go index cdf94c82..ed2dad1b 100644 --- a/exchanges/bybit/bybit_wrapper.go +++ b/exchanges/bybit/bybit_wrapper.go @@ -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/deposit" "github.com/thrasher-corp/gocryptotrader/exchanges/fundingrate" @@ -653,16 +653,13 @@ func (e *Exchange) UpdateOrderbook(ctx context.Context, p currency.Pair, assetTy return orderbook.Get(e.Name, p, assetType) } -// UpdateAccountInfo retrieves balances for all enabled currencies -func (e *Exchange) UpdateAccountInfo(ctx context.Context, assetType asset.Item) (account.Holdings, error) { - var info account.Holdings - var acc account.SubAccount - var accountType string - info.Exchange = e.Name +// UpdateAccountBalances retrieves currency balances +func (e *Exchange) UpdateAccountBalances(ctx context.Context, assetType asset.Item) (accounts.SubAccounts, error) { at, err := e.FetchAccountType(ctx) if err != nil { - return info, err + return nil, err } + var accountType string switch assetType { case asset.Spot, asset.Options, asset.USDCMarginedFutures, asset.USDTMarginedFutures: switch at { @@ -678,15 +675,15 @@ func (e *Exchange) UpdateAccountInfo(ctx context.Context, assetType asset.Item) case asset.CoinMarginedFutures: accountType = "CONTRACT" default: - return info, fmt.Errorf("%s %w", assetType, asset.ErrNotSupported) + return nil, fmt.Errorf("%s %w", assetType, asset.ErrNotSupported) } - balances, err := e.GetWalletBalance(ctx, accountType, "") + resp, err := e.GetWalletBalance(ctx, accountType, "") if err != nil { - return info, err + return nil, err } - currencyBalance := []account.Balance{} - for i := range balances.List { - for _, c := range balances.List[i].Coin { + subAccts := accounts.SubAccounts{accounts.NewSubAccount(assetType, "")} + for i := range resp.List { + for _, c := range resp.List[i].Coin { // borrow amounts get truncated to 8 dec places when total and equity are calculated on the exchange truncBorrow := c.BorrowAmount.Decimal().Truncate(8).InexactFloat64() @@ -699,8 +696,7 @@ func (e *Exchange) UpdateAccountInfo(ctx context.Context, assetType asset.Item) freeBalance = c.AvailableBalanceForSpot.Float64() } - currencyBalance = append(currencyBalance, account.Balance{ - Currency: c.Coin, + subAccts[0].Balances.Set(c.Coin, accounts.Balance{ Total: c.WalletBalance.Float64(), Free: freeBalance, Borrowed: c.BorrowAmount.Float64(), @@ -709,18 +705,7 @@ func (e *Exchange) UpdateAccountInfo(ctx context.Context, assetType asset.Item) }) } } - acc.Currencies = currencyBalance - acc.AssetType = assetType - info.Accounts = append(info.Accounts, acc) - creds, err := e.GetCredentials(ctx) - if err != nil { - return account.Holdings{}, err - } - err = account.Process(&info, creds) - if err != nil { - return account.Holdings{}, err - } - return info, nil + return subAccts, e.Accounts.Save(ctx, subAccts, true) } // GetAccountFundingHistory returns funding history, deposits and @@ -1474,7 +1459,7 @@ func (e *Exchange) getCategoryFromPair(pair currency.Pair) []asset.Item { // 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) } diff --git a/exchanges/bybit/testdata/wsAuth.json b/exchanges/bybit/testdata/wsAuth.json index ee94dbcf..cbca69be 100644 --- a/exchanges/bybit/testdata/wsAuth.json +++ b/exchanges/bybit/testdata/wsAuth.json @@ -3,4 +3,5 @@ { "id": "5923242c464be9-25ca-483d-a743-c60101fc656f", "topic": "wallet", "creationTime": 1672364262482, "data": [ { "accountIMRate": "0.016", "accountMMRate": "0.003", "totalEquity": "12837.78330098", "totalWalletBalance": "12840.4045924", "totalMarginBalance": "12837.78330188", "totalAvailableBalance": "12632.05767702", "totalPerpUPL": "-2.62129051", "totalInitialMargin": "205.72562486", "totalMaintenanceMargin": "39.42876721", "coin": [ { "coin": "USDC", "equity": "200.62572554", "usdValue": "200.62572554", "walletBalance": "201.34882644", "availableToWithdraw": "0", "availableToBorrow": "1500000", "borrowAmount": "0", "accruedInterest": "0", "totalOrderIM": "0", "totalPositionIM": "202.99874213", "totalPositionMM": "39.14289747", "unrealisedPnl": "74.2768991", "cumRealisedPnl": "-209.1544627", "bonus": "0" }, { "coin": "BTC", "equity": "0.06488393", "usdValue": "1023.08402268", "walletBalance": "0.06488393", "availableToWithdraw": "0.06488393", "availableToBorrow": "2.5", "borrowAmount": "0", "accruedInterest": "0", "totalOrderIM": "0", "totalPositionIM": "0", "totalPositionMM": "0", "unrealisedPnl": "0", "cumRealisedPnl": "0", "bonus": "0" }, { "coin": "ETH", "equity": "0", "usdValue": "0", "walletBalance": "0", "availableToWithdraw": "0", "availableToBorrow": "26", "borrowAmount": "0", "accruedInterest": "0", "totalOrderIM": "0", "totalPositionIM": "0", "totalPositionMM": "0", "unrealisedPnl": "0", "cumRealisedPnl": "0", "bonus": "0" }, { "coin": "USDT", "equity": "11726.64664904", "usdValue": "11613.58597018", "walletBalance": "11728.54414904", "availableToWithdraw": "11723.92075829", "availableToBorrow": "2500000", "borrowAmount": "0", "accruedInterest": "0", "totalOrderIM": "0", "totalPositionIM": "2.72589075", "totalPositionMM": "0.28576575", "unrealisedPnl": "-1.8975", "cumRealisedPnl": "0.64782276", "bonus": "0" }, { "coin": "EOS3L", "equity": "215.0570412", "usdValue": "0", "walletBalance": "215.0570412", "availableToWithdraw": "215.0570412", "availableToBorrow": "0", "borrowAmount": "0", "accruedInterest": "", "totalOrderIM": "0", "totalPositionIM": "0", "totalPositionMM": "0", "unrealisedPnl": "0", "cumRealisedPnl": "0", "bonus": "0" }, { "coin": "BIT", "equity": "1.82", "usdValue": "0.48758257", "walletBalance": "1.82", "availableToWithdraw": "1.82", "availableToBorrow": "0", "borrowAmount": "0", "accruedInterest": "", "totalOrderIM": "0", "totalPositionIM": "0", "totalPositionMM": "0", "unrealisedPnl": "0", "cumRealisedPnl": "0", "bonus": "0" } ], "accountType": "UNIFIED", "accountLTV": "0.017" } ] } { "id": "592324fa945a30-2603-49a5-b865-21668c29f2a6", "topic": "greeks", "creationTime": 1672364262482, "data": [ { "baseCoin": "ETH", "totalDelta": "0.06999986", "totalGamma": "-0.00000001", "totalVega": "-0.00000024", "totalTheta": "0.00001314" } ] } {"id": "592324803b2785-26fa-4214-9963-bdd4727f07be", "topic": "execution", "creationTime": 1672364174455, "data": [ { "category": "linear", "symbol": "XRPUSDT", "execFee": "0.005061", "execId": "7e2ae69c-4edf-5800-a352-893d52b446aa", "execPrice": "0.3374", "execQty": "25", "execType": "Trade", "execValue": "8.435", "isMaker": false, "feeRate": "0.0006", "tradeIv": "", "markIv": "", "blockTradeId": "", "markPrice": "0.3391", "indexPrice": "", "underlyingPrice": "", "leavesQty": "0", "orderId": "f6e324ff-99c2-4e89-9739-3086e47f9381", "orderLinkId": "", "orderPrice": "0.3207", "orderQty":"25","orderType":"Market","stopOrderType":"UNKNOWN","side":"Sell","execTime":"1672364174443","isLeverage": "0","closedSize": "","seq":4688002127}]} -{ "id": "someID", "topic": "order", "creationTime": 1672364262474, "data": [{"category":"linear","symbol":"BTCUSDT","orderId":"c1956690-b731-4191-97c0-94b00422231b","orderLinkId":"","blockTradeId":"","side":"Sell","positionIdx":0,"orderStatus":"Filled","cancelType":"UNKNOWN","rejectReason":"EC_NoError","timeInForce":"IOC","isLeverage":"","price":"4.033","qty":"1.7","avgPrice":"4.24","leavesQty":"0","leavesValue":"0","cumExecQty":"1.7","cumExecValue":"7.2086","cumExecFee":"0.00288344","orderType":"Market","stopOrderType":"","orderIv":"","triggerPrice":"","takeProfit":"","stopLoss":"","triggerBy":"","tpTriggerBy":"","slTriggerBy":"","triggerDirection":0,"placeType":"","lastPriceOnCreated":"4.245","closeOnTrigger":false,"reduceOnly":false,"smpGroup":0,"smpType":"None","smpOrderId":"","slLimitPrice":"0","tpLimitPrice":"0","tpslMode":"UNKNOWN","createType":"CreateByUser","marketUnit":"","createdTime":"1733778525913","updatedTime":"1733778525917","feeCurrency":"","closedPnl":"0"}]} \ No newline at end of file +{ "id": "someID", "topic": "order", "creationTime": 1672364262474, "data": [{"category":"linear","symbol":"BTCUSDT","orderId":"c1956690-b731-4191-97c0-94b00422231b","orderLinkId":"","blockTradeId":"","side":"Sell","positionIdx":0,"orderStatus":"Filled","cancelType":"UNKNOWN","rejectReason":"EC_NoError","timeInForce":"IOC","isLeverage":"","price":"4.033","qty":"1.7","avgPrice":"4.24","leavesQty":"0","leavesValue":"0","cumExecQty":"1.7","cumExecValue":"7.2086","cumExecFee":"0.00288344","orderType":"Market","stopOrderType":"","orderIv":"","triggerPrice":"","takeProfit":"","stopLoss":"","triggerBy":"","tpTriggerBy":"","slTriggerBy":"","triggerDirection":0,"placeType":"","lastPriceOnCreated":"4.245","closeOnTrigger":false,"reduceOnly":false,"smpGroup":0,"smpType":"None","smpOrderId":"","slLimitPrice":"0","tpLimitPrice":"0","tpslMode":"UNKNOWN","createType":"CreateByUser","marketUnit":"","createdTime":"1733778525913","updatedTime":"1733778525917","feeCurrency":"","closedPnl":"0"}]} +{ "id": "FANGLE-ACCOUNTS", "topic": "wallet", "creationTime": 1672364262483, "data": [ { "coin": [ { "coin": "BREAK", "walletBalance": "201.34882644"}]}]} diff --git a/exchanges/coinbase/coinbase_test.go b/exchanges/coinbase/coinbase_test.go index 14b72d26..676e2e9c 100644 --- a/exchanges/coinbase/coinbase_test.go +++ b/exchanges/coinbase/coinbase_test.go @@ -121,13 +121,11 @@ func TestGetAccountByID(t *testing.T) { assert.ErrorIs(t, err, errAccountIDEmpty) sharedtestvalues.SkipTestIfCredentialsUnset(t, e) longResp, err := e.ListAccounts(t.Context(), 49, 0) - assert.NoError(t, err) + require.NoError(t, err) require.True(t, longResp != nil && len(longResp.Accounts) > 0, errExpectedNonEmpty) shortResp, err := e.GetAccountByID(t.Context(), longResp.Accounts[0].UUID) - assert.NoError(t, err) - if *shortResp != longResp.Accounts[0] { - t.Errorf(errExpectMismatch, shortResp, longResp.Accounts[0]) - } + require.NoError(t, err) + assert.Equal(t, shortResp, longResp.Accounts[0]) } func TestListAccounts(t *testing.T) { @@ -1136,10 +1134,10 @@ func TestUpdateTradablePairs(t *testing.T) { testexch.UpdatePairsOnce(t, e) } -func TestUpdateAccountInfo(t *testing.T) { +func TestUpdateAccountBalances(t *testing.T) { t.Parallel() sharedtestvalues.SkipTestIfCredentialsUnset(t, e) - resp, err := e.UpdateAccountInfo(t.Context(), asset.Spot) + resp, err := e.UpdateAccountBalances(t.Context(), asset.Spot) require.NoError(t, err) assert.NotEmpty(t, resp, errExpectedNonEmpty) } @@ -1916,10 +1914,10 @@ func convertTestHelper(t *testing.T) (fromAccID, toAccID string) { t.Fatal(errExpectedNonEmpty) } for x := range accIDs.Accounts { - if accIDs.Accounts[x].Currency == testStable.String() { + if accIDs.Accounts[x].Currency == testStable { fromAccID = accIDs.Accounts[x].UUID } - if accIDs.Accounts[x].Currency == testFiat.String() { + if accIDs.Accounts[x].Currency == testFiat { toAccID = accIDs.Accounts[x].UUID } if fromAccID != "" && toAccID != "" { diff --git a/exchanges/coinbase/coinbase_types.go b/exchanges/coinbase/coinbase_types.go index 583efdeb..8d741c12 100644 --- a/exchanges/coinbase/coinbase_types.go +++ b/exchanges/coinbase/coinbase_types.go @@ -46,11 +46,11 @@ type CurrencyAmount struct { Currency currency.Code `json:"currency"` } -// Account holds details for a trading account, returned by GetAccountByID and used as a sub-struct in the type AllAccountsResponse +// Account holds details for a trading account type Account struct { UUID string `json:"uuid"` Name string `json:"name"` - Currency string `json:"currency"` + Currency currency.Code `json:"currency"` AvailableBalance CurrencyAmount `json:"available_balance"` Default bool `json:"default"` Active bool `json:"active"` @@ -66,10 +66,10 @@ type Account struct { // AllAccountsResponse holds many Account structs, as well as pagination information, returned by ListAccounts type AllAccountsResponse struct { - Accounts []Account `json:"accounts"` - HasNext bool `json:"has_next"` - Cursor Integer `json:"cursor"` - Size uint8 `json:"size"` + Accounts []*Account `json:"accounts"` + HasNext bool `json:"has_next"` + Cursor Integer `json:"cursor"` + Size uint8 `json:"size"` } // PermissionsResponse holds information on the permissions of a user, returned by GetPermissions diff --git a/exchanges/coinbase/coinbase_wrapper.go b/exchanges/coinbase/coinbase_wrapper.go index 6de9795b..3e5acd55 100644 --- a/exchanges/coinbase/coinbase_wrapper.go +++ b/exchanges/coinbase/coinbase_wrapper.go @@ -12,11 +12,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/deposit" "github.com/thrasher-corp/gocryptotrader/exchanges/fundingrate" @@ -211,48 +211,29 @@ func (e *Exchange) UpdateTradablePairs(ctx context.Context) error { return e.EnsureOnePairEnabled() } -// UpdateAccountInfo retrieves balances for all enabled currencies for the coinbase exchange -func (e *Exchange) UpdateAccountInfo(ctx context.Context, assetType asset.Item) (account.Holdings, error) { - var ( - response account.Holdings - accountBalance []Account - done bool - err error - cursor int64 - accountResp *AllAccountsResponse - ) - response.Exchange = e.Name - for !done { - if accountResp, err = e.ListAccounts(ctx, 250, cursor); err != nil { - return response, err +// UpdateAccountBalances retrieves currency balances +func (e *Exchange) UpdateAccountBalances(ctx context.Context, assetType asset.Item) (subAccts accounts.SubAccounts, err error) { + for cursor := int64(0); ; { + resp, err := e.ListAccounts(ctx, 250, cursor) + if err != nil { + return subAccts, err } - accountBalance = append(accountBalance, accountResp.Accounts...) - done = !accountResp.HasNext - cursor = int64(accountResp.Cursor) + for _, subAcct := range resp.Accounts { + a := accounts.NewSubAccount(assetType, subAcct.UUID) + a.Balances.Set(subAcct.Currency, accounts.Balance{ + Total: subAcct.AvailableBalance.Value.Float64(), + Hold: subAcct.Hold.Value.Float64(), + Free: subAcct.AvailableBalance.Value.Float64() - subAcct.Hold.Value.Float64(), + AvailableWithoutBorrow: subAcct.AvailableBalance.Value.Float64(), + }) + subAccts = subAccts.Merge(a) + } + if !resp.HasNext { + break + } + cursor = int64(resp.Cursor) } - accountCurrencies := make(map[string][]account.Balance) - for i := range accountBalance { - profileID := accountBalance[i].UUID - currencies := accountCurrencies[profileID] - accountCurrencies[profileID] = append(currencies, account.Balance{ - Currency: currency.NewCode(accountBalance[i].Currency), - Total: accountBalance[i].AvailableBalance.Value.Float64(), - Hold: accountBalance[i].Hold.Value.Float64(), - Free: accountBalance[i].AvailableBalance.Value.Float64() - accountBalance[i].Hold.Value.Float64(), - AvailableWithoutBorrow: accountBalance[i].AvailableBalance.Value.Float64(), - }) - } - if response.Accounts, err = account.CollectBalances(accountCurrencies, assetType); err != nil { - return account.Holdings{}, err - } - creds, err := e.GetCredentials(ctx) - if err != nil { - return account.Holdings{}, err - } - if err := account.Process(&response, creds); err != nil { - return account.Holdings{}, err - } - return response, nil + return subAccts, e.Accounts.Save(ctx, subAccts, true) } // UpdateTickers updates all currency pairs of a given asset type @@ -831,7 +812,7 @@ func (e *Exchange) GetHistoricCandlesExtended(ctx context.Context, pair currency // 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) } diff --git a/exchanges/coinut/coinut.go b/exchanges/coinut/coinut.go index 598d5ebc..63942661 100644 --- a/exchanges/coinut/coinut.go +++ b/exchanges/coinut/coinut.go @@ -15,7 +15,6 @@ import ( "github.com/thrasher-corp/gocryptotrader/currency" "github.com/thrasher-corp/gocryptotrader/encoding/json" 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/order" "github.com/thrasher-corp/gocryptotrader/exchanges/request" @@ -277,16 +276,13 @@ func (e *Exchange) SendHTTPRequest(ctx context.Context, ep exchange.URL, apiRequ headers := make(map[string]string) if authenticated { - var creds *account.Credentials - creds, err = e.GetCredentials(ctx) + creds, err := e.GetCredentials(ctx) if err != nil { return nil, err } headers["X-USER"] = creds.ClientID var hmac []byte - hmac, err = crypto.GetHMAC(crypto.HashSHA256, - payload, - []byte(creds.Key)) + hmac, err = crypto.GetHMAC(crypto.HashSHA256, payload, []byte(creds.Key)) if err != nil { return nil, err } diff --git a/exchanges/coinut/coinut_test.go b/exchanges/coinut/coinut_test.go index 0ab0b699..3089cb97 100644 --- a/exchanges/coinut/coinut_test.go +++ b/exchanges/coinut/coinut_test.go @@ -14,9 +14,9 @@ import ( "github.com/thrasher-corp/gocryptotrader/common" "github.com/thrasher-corp/gocryptotrader/core" "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/order" "github.com/thrasher-corp/gocryptotrader/exchanges/sharedtestvalues" @@ -347,15 +347,11 @@ func TestCancelAllExchangeOrders(t *testing.T) { func TestGetAccountInfo(t *testing.T) { t.Parallel() if apiKey != "" || clientID != "" { - _, err := e.UpdateAccountInfo(t.Context(), asset.Spot) - if err != nil { - t.Error("GetAccountInfo() error", err) - } + _, err := e.UpdateAccountBalances(t.Context(), asset.Spot) + require.NoError(t, err, "UpdateAccountBalances must not error") } else { - _, err := e.UpdateAccountInfo(t.Context(), asset.Spot) - if err == nil { - t.Error("GetAccountInfo() Expected error") - } + _, err := e.UpdateAccountBalances(t.Context(), asset.Spot) + require.Error(t, err, "UpdateAccountBalances must error") } } @@ -780,8 +776,8 @@ func TestWsLogin(t *testing.T) { "unverified_email":"", "username":"test" }`) - ctx := account.DeployCredentialsToContext(t.Context(), - &account.Credentials{Key: "b46e658f-d4c4-433c-b032-093423b1aaa4", ClientID: "dummy"}) + ctx := accounts.DeployCredentialsToContext(t.Context(), + &accounts.Credentials{Key: "b46e658f-d4c4-433c-b032-093423b1aaa4", ClientID: "dummy"}) err := e.wsHandleData(ctx, pressXToJSON) if err != nil { t.Error(err) diff --git a/exchanges/coinut/coinut_wrapper.go b/exchanges/coinut/coinut_wrapper.go index 30c7b6aa..e043fb6f 100644 --- a/exchanges/coinut/coinut_wrapper.go +++ b/exchanges/coinut/coinut_wrapper.go @@ -12,10 +12,10 @@ import ( "github.com/thrasher-corp/gocryptotrader/common" "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" "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/deposit" "github.com/thrasher-corp/gocryptotrader/exchanges/fundingrate" @@ -197,100 +197,38 @@ func (e *Exchange) UpdateTradablePairs(ctx context.Context) error { return e.EnsureOnePairEnabled() } -// UpdateAccountInfo retrieves balances for all enabled currencies for the -// COINUT exchange -func (e *Exchange) UpdateAccountInfo(ctx context.Context, assetType asset.Item) (account.Holdings, error) { - var info account.Holdings +// UpdateAccountBalances retrieves currency balances +func (e *Exchange) UpdateAccountBalances(ctx context.Context, assetType asset.Item) (subAccts accounts.SubAccounts, err error) { var bal *UserBalance - var err error if e.Websocket.CanUseAuthenticatedWebsocketForWrapper() { - var resp *UserBalance - resp, err = e.wsGetAccountBalance(ctx) - if err != nil { - return info, err + if bal, err = e.wsGetAccountBalance(ctx); err != nil { + return nil, err } - bal = resp } else { - bal, err = e.GetUserBalance(ctx) - if err != nil { - return info, err + if bal, err = e.GetUserBalance(ctx); err != nil { + return nil, err } } - - balances := []account.Balance{ - { - Currency: currency.BCH, - Total: bal.BCH, + subAccts = accounts.SubAccounts{&accounts.SubAccount{ + AssetType: assetType, + Balances: accounts.CurrencyBalances{ + currency.BCH: {Currency: currency.BCH, Total: bal.BCH}, + currency.BTC: {Currency: currency.BTC, Total: bal.BTC}, + currency.BTG: {Currency: currency.BTG, Total: bal.BTG}, + currency.CAD: {Currency: currency.CAD, Total: bal.CAD}, + currency.ETC: {Currency: currency.ETC, Total: bal.ETC}, + currency.ETH: {Currency: currency.ETH, Total: bal.ETH}, + currency.LCH: {Currency: currency.LCH, Total: bal.LCH}, + currency.LTC: {Currency: currency.LTC, Total: bal.LTC}, + currency.MYR: {Currency: currency.MYR, Total: bal.MYR}, + currency.SGD: {Currency: currency.SGD, Total: bal.SGD}, + currency.USD: {Currency: currency.USD, Total: bal.USD}, + currency.XMR: {Currency: currency.XMR, Total: bal.XMR}, + currency.ZEC: {Currency: currency.ZEC, Total: bal.ZEC}, + currency.USDT: {Currency: currency.USDT, Total: bal.USDT}, }, - { - Currency: currency.BTC, - Total: bal.BTC, - }, - { - Currency: currency.BTG, - Total: bal.BTG, - }, - { - Currency: currency.CAD, - Total: bal.CAD, - }, - { - Currency: currency.ETC, - Total: bal.ETC, - }, - { - Currency: currency.ETH, - Total: bal.ETH, - }, - { - Currency: currency.LCH, - Total: bal.LCH, - }, - { - Currency: currency.LTC, - Total: bal.LTC, - }, - { - Currency: currency.MYR, - Total: bal.MYR, - }, - { - Currency: currency.SGD, - Total: bal.SGD, - }, - { - Currency: currency.USD, - Total: bal.USD, - }, - { - Currency: currency.USDT, - Total: bal.USDT, - }, - { - Currency: currency.XMR, - Total: bal.XMR, - }, - { - Currency: currency.ZEC, - Total: bal.ZEC, - }, - } - info.Exchange = e.Name - info.Accounts = append(info.Accounts, account.SubAccount{ - AssetType: assetType, - Currencies: balances, - }) - - creds, err := e.GetCredentials(ctx) - if err != nil { - return account.Holdings{}, err - } - err = account.Process(&info, creds) - if err != nil { - return account.Holdings{}, err - } - - return info, nil + }} + return subAccts, e.Accounts.Save(ctx, subAccts, true) } // UpdateTickers updates the ticker for all currency pairs of a given asset type @@ -1035,10 +973,9 @@ func (e *Exchange) loadInstrumentsIfNotLoaded(ctx context.Context) error { return 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) } diff --git a/exchanges/credentials.go b/exchanges/credentials.go index acd93fbb..dd48b62c 100644 --- a/exchanges/credentials.go +++ b/exchanges/credentials.go @@ -9,7 +9,7 @@ import ( "github.com/thrasher-corp/gocryptotrader/common" "github.com/thrasher-corp/gocryptotrader/config" - "github.com/thrasher-corp/gocryptotrader/exchanges/account" + "github.com/thrasher-corp/gocryptotrader/exchange/accounts" "github.com/thrasher-corp/gocryptotrader/log" ) @@ -67,7 +67,7 @@ func (a *API) SetSubAccount(sub string) { // CheckCredentials checks to see if the required fields have been set before // sending an authenticated API request -func (b *Base) CheckCredentials(creds *account.Credentials, isContext bool) error { +func (b *Base) CheckCredentials(creds *accounts.Credentials, isContext bool) error { if b.SkipAuthCheck { return nil } @@ -98,10 +98,10 @@ func (b *Base) AreCredentialsValid(ctx context.Context) bool { // GetDefaultCredentials returns the exchange.Base api credentials loaded by // config.json -func (b *Base) GetDefaultCredentials() *account.Credentials { +func (b *Base) GetDefaultCredentials() *accounts.Credentials { b.API.credMu.RLock() defer b.API.credMu.RUnlock() - if b.API.credentials == (account.Credentials{}) { + if b.API.credentials == (accounts.Credentials{}) { return nil } creds := b.API.credentials @@ -110,12 +110,12 @@ func (b *Base) GetDefaultCredentials() *account.Credentials { // GetCredentials checks and validates current credentials, context credentials // override default credentials, if no credentials found, will return an error. -func (b *Base) GetCredentials(ctx context.Context) (*account.Credentials, error) { - value := ctx.Value(account.ContextCredentialsFlag) +func (b *Base) GetCredentials(ctx context.Context) (*accounts.Credentials, error) { + value := ctx.Value(accounts.ContextCredentialsFlag) if value != nil { - ctxCredStore, ok := value.(*account.ContextCredentialsStore) + ctxCredStore, ok := value.(*accounts.ContextCredentialsStore) if !ok { - return nil, common.GetTypeAssertError("*account.ContextCredentialsStore", value) + return nil, common.GetTypeAssertError("*accounts.ContextCredentialsStore", value) } creds := ctxCredStore.Get() @@ -133,7 +133,7 @@ func (b *Base) GetCredentials(ctx context.Context) (*account.Credentials, error) return nil, fmt.Errorf("error checking credentials: %w", err) } - if subAccountOverride, ok := ctx.Value(account.ContextSubAccountFlag).(string); ok { + if subAccountOverride, ok := ctx.Value(accounts.ContextSubAccountFlag).(string); ok { creds.SubAccount = subAccountOverride } @@ -141,7 +141,7 @@ func (b *Base) GetCredentials(ctx context.Context) (*account.Credentials, error) } // VerifyAPICredentials verifies the exchanges API credentials -func (b *Base) VerifyAPICredentials(creds *account.Credentials) error { +func (b *Base) VerifyAPICredentials(creds *accounts.Credentials) error { b.API.credMu.RLock() defer b.API.credMu.RUnlock() if creds.IsEmpty() { diff --git a/exchanges/credentials_test.go b/exchanges/credentials_test.go index 13cfa5c4..72b4c61a 100644 --- a/exchanges/credentials_test.go +++ b/exchanges/credentials_test.go @@ -8,7 +8,7 @@ import ( "github.com/stretchr/testify/require" "github.com/thrasher-corp/gocryptotrader/common" "github.com/thrasher-corp/gocryptotrader/config" - "github.com/thrasher-corp/gocryptotrader/exchanges/account" + "github.com/thrasher-corp/gocryptotrader/exchange/accounts" ) func TestGetCredentials(t *testing.T) { @@ -18,17 +18,17 @@ func TestGetCredentials(t *testing.T) { require.ErrorIs(t, err, ErrCredentialsAreEmpty) b.API.CredentialsValidator.RequiresKey = true - ctx := account.DeployCredentialsToContext(t.Context(), &account.Credentials{Secret: "wow"}) + ctx := accounts.DeployCredentialsToContext(t.Context(), &accounts.Credentials{Secret: "wow"}) _, err = b.GetCredentials(ctx) require.ErrorIs(t, err, errRequiresAPIKey) b.API.CredentialsValidator.RequiresSecret = true - ctx = account.DeployCredentialsToContext(t.Context(), &account.Credentials{Key: "wow"}) + ctx = accounts.DeployCredentialsToContext(t.Context(), &accounts.Credentials{Key: "wow"}) _, err = b.GetCredentials(ctx) require.ErrorIs(t, err, errRequiresAPISecret) b.API.CredentialsValidator.RequiresBase64DecodeSecret = true - ctx = account.DeployCredentialsToContext(t.Context(), &account.Credentials{ + ctx = accounts.DeployCredentialsToContext(t.Context(), &accounts.Credentials{ Key: "meow", Secret: "invalidb64", }) @@ -36,7 +36,7 @@ func TestGetCredentials(t *testing.T) { require.ErrorIs(t, err, errBase64DecodeFailure) const expectedBase64DecodedOutput = "hello world" - ctx = account.DeployCredentialsToContext(t.Context(), &account.Credentials{ + ctx = accounts.DeployCredentialsToContext(t.Context(), &accounts.Credentials{ Key: "meow", Secret: "aGVsbG8gd29ybGQ=", }) @@ -47,12 +47,12 @@ func TestGetCredentials(t *testing.T) { t.Fatalf("received: %v but expected: %v", creds.Secret, expectedBase64DecodedOutput) } - ctx = context.WithValue(t.Context(), account.ContextCredentialsFlag, "pewpew") + ctx = context.WithValue(t.Context(), accounts.ContextCredentialsFlag, "pewpew") _, err = b.GetCredentials(ctx) require.ErrorIs(t, err, common.ErrTypeAssertFailure) b.API.CredentialsValidator.RequiresBase64DecodeSecret = false - fullCred := &account.Credentials{ + fullCred := &accounts.Credentials{ Key: "superkey", Secret: "supersecret", SubAccount: "supersub", @@ -61,7 +61,7 @@ func TestGetCredentials(t *testing.T) { OneTimePassword: "superOneTimePasssssss", } - ctx = account.DeployCredentialsToContext(t.Context(), fullCred) + ctx = accounts.DeployCredentialsToContext(t.Context(), fullCred) creds, err = b.GetCredentials(ctx) require.NoError(t, err) @@ -74,7 +74,7 @@ func TestGetCredentials(t *testing.T) { t.Fatal("unexpected values") } - lonelyCred := &account.Credentials{ + lonelyCred := &accounts.Credentials{ Key: "superkey", Secret: "supersecret", SubAccount: "supersub", @@ -82,7 +82,7 @@ func TestGetCredentials(t *testing.T) { OneTimePassword: "superOneTimePasssssss", } - ctx = account.DeployCredentialsToContext(t.Context(), lonelyCred) + ctx = accounts.DeployCredentialsToContext(t.Context(), lonelyCred) b.API.CredentialsValidator.RequiresClientID = true _, err = b.GetCredentials(ctx) require.ErrorIs(t, err, errRequiresAPIClientID) @@ -91,7 +91,7 @@ func TestGetCredentials(t *testing.T) { b.API.SetSecret("sir") b.API.SetClientID("1337") - ctx = context.WithValue(t.Context(), account.ContextSubAccountFlag, "superaccount") + ctx = context.WithValue(t.Context(), accounts.ContextSubAccountFlag, "superaccount") overridedSA, err := b.GetCredentials(ctx) require.NoError(t, err) @@ -119,7 +119,7 @@ func TestAreCredentialsValid(t *testing.T) { if b.AreCredentialsValid(t.Context()) { t.Fatal("should not be valid") } - ctx := account.DeployCredentialsToContext(t.Context(), &account.Credentials{Key: "hello"}) + ctx := accounts.DeployCredentialsToContext(t.Context(), &accounts.Credentials{Key: "hello"}) if !b.AreCredentialsValid(ctx) { t.Fatal("should be valid") } @@ -226,7 +226,7 @@ func TestCheckCredentials(t *testing.T) { base: &Base{ API: API{ CredentialsValidator: config.APICredentialsValidatorConfig{RequiresKey: true}, - credentials: account.Credentials{OneTimePassword: "wow"}, + credentials: accounts.Credentials{OneTimePassword: "wow"}, }, }, expectedErr: errRequiresAPIKey, @@ -237,7 +237,7 @@ func TestCheckCredentials(t *testing.T) { LoadedByConfig: true, API: API{ CredentialsValidator: config.APICredentialsValidatorConfig{RequiresKey: true}, - credentials: account.Credentials{Key: "k3y"}, + credentials: accounts.Credentials{Key: "k3y"}, }, }, expectedErr: ErrAuthenticationSupportNotEnabled, @@ -249,7 +249,7 @@ func TestCheckCredentials(t *testing.T) { API: API{ AuthenticatedSupport: true, CredentialsValidator: config.APICredentialsValidatorConfig{RequiresKey: true}, - credentials: account.Credentials{}, + credentials: accounts.Credentials{}, }, }, expectedErr: ErrCredentialsAreEmpty, @@ -259,7 +259,7 @@ func TestCheckCredentials(t *testing.T) { base: &Base{ API: API{ CredentialsValidator: config.APICredentialsValidatorConfig{RequiresBase64DecodeSecret: true}, - credentials: account.Credentials{Secret: "invalid"}, + credentials: accounts.Credentials{Secret: "invalid"}, }, }, expectedErr: errBase64DecodeFailure, @@ -269,7 +269,7 @@ func TestCheckCredentials(t *testing.T) { base: &Base{ API: API{ CredentialsValidator: config.APICredentialsValidatorConfig{RequiresBase64DecodeSecret: true}, - credentials: account.Credentials{Secret: "aGVsbG8gd29ybGQ="}, + credentials: accounts.Credentials{Secret: "aGVsbG8gd29ybGQ="}, }, }, checkBase64Output: true, @@ -281,7 +281,7 @@ func TestCheckCredentials(t *testing.T) { API: API{ AuthenticatedSupport: true, CredentialsValidator: config.APICredentialsValidatorConfig{RequiresKey: true}, - credentials: account.Credentials{Key: "k3y"}, + credentials: accounts.Credentials{Key: "k3y"}, }, }, expectedErr: nil, @@ -308,32 +308,32 @@ func TestCheckCredentials(t *testing.T) { func TestAPISetters(t *testing.T) { t.Parallel() api := API{} - api.SetKey(account.Key) - if api.credentials.Key != account.Key { + api.SetKey(accounts.Key) + if api.credentials.Key != accounts.Key { t.Fatal("unexpected value") } api = API{} - api.SetSecret(account.Secret) - if api.credentials.Secret != account.Secret { + api.SetSecret(accounts.Secret) + if api.credentials.Secret != accounts.Secret { t.Fatal("unexpected value") } api = API{} - api.SetClientID(account.ClientID) - if api.credentials.ClientID != account.ClientID { + api.SetClientID(accounts.ClientID) + if api.credentials.ClientID != accounts.ClientID { t.Fatal("unexpected value") } api = API{} - api.SetPEMKey(account.PEMKey) - if api.credentials.PEMKey != account.PEMKey { + api.SetPEMKey(accounts.PEMKey) + if api.credentials.PEMKey != accounts.PEMKey { t.Fatal("unexpected value") } api = API{} - api.SetSubAccount(account.SubAccountSTR) - if api.credentials.SubAccount != account.SubAccountSTR { + api.SetSubAccount(accounts.SubAccountSTR) + if api.credentials.SubAccount != accounts.SubAccountSTR { t.Fatal("unexpected value") } } diff --git a/exchanges/deribit/deribit_test.go b/exchanges/deribit/deribit_test.go index 3021546e..9df90d8e 100644 --- a/exchanges/deribit/deribit_test.go +++ b/exchanges/deribit/deribit_test.go @@ -3385,10 +3385,10 @@ func TestChannelName(t *testing.T) { assert.Panics(t, func() { channelName(&subscription.Subscription{Channel: "wibble"}) }, "Unknown channels should panic") } -func TestUpdateAccountInfo(t *testing.T) { +func TestUpdateAccountBalances(t *testing.T) { t.Parallel() sharedtestvalues.SkipTestIfCredentialsUnset(t, e) - result, err := e.UpdateAccountInfo(t.Context(), asset.Futures) + result, err := e.UpdateAccountBalances(t.Context(), asset.Futures) require.NoError(t, err) assert.NotNil(t, result) } diff --git a/exchanges/deribit/deribit_types.go b/exchanges/deribit/deribit_types.go index 930efcde..a4c26264 100644 --- a/exchanges/deribit/deribit_types.go +++ b/exchanges/deribit/deribit_types.go @@ -5,6 +5,7 @@ import ( "regexp" "time" + "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/types" @@ -120,13 +121,13 @@ type ContractSizeData struct { // CurrencyData stores data for currencies type CurrencyData struct { - CoinType string `json:"coin_type"` - Currency string `json:"currency"` // TODO: change to currency.Code - CurrencyLong string `json:"currency_long"` - FeePrecision int64 `json:"fee_precision"` - MinConfirmations int64 `json:"min_confirmations"` - MinWithdrawalFee float64 `json:"min_withdrawal_fee"` - WithdrawalFee float64 `json:"withdrawal_fee"` + CoinType string `json:"coin_type"` + Currency currency.Code `json:"currency"` + CurrencyLong string `json:"currency_long"` + FeePrecision int64 `json:"fee_precision"` + MinConfirmations int64 `json:"min_confirmations"` + MinWithdrawalFee float64 `json:"min_withdrawal_fee"` + WithdrawalFee float64 `json:"withdrawal_fee"` WithdrawalPriorities []struct { Value float64 `json:"value"` Name string `json:"name"` diff --git a/exchanges/deribit/deribit_wrapper.go b/exchanges/deribit/deribit_wrapper.go index 49e2f817..46f54065 100644 --- a/exchanges/deribit/deribit_wrapper.go +++ b/exchanges/deribit/deribit_wrapper.go @@ -14,11 +14,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/deposit" "github.com/thrasher-corp/gocryptotrader/exchanges/fundingrate" @@ -337,34 +337,29 @@ func (e *Exchange) UpdateOrderbook(ctx context.Context, p currency.Pair, assetTy return orderbook.Get(e.Name, p, assetType) } -// UpdateAccountInfo retrieves balances for all enabled currencies -func (e *Exchange) UpdateAccountInfo(ctx context.Context, _ asset.Item) (account.Holdings, error) { - var resp account.Holdings - resp.Exchange = e.Name +// UpdateAccountBalances retrieves currency balances +func (e *Exchange) UpdateAccountBalances(ctx context.Context, _ asset.Item) (accounts.SubAccounts, error) { currencies, err := e.GetCurrencies(ctx) if err != nil { - return resp, err + return nil, err } - resp.Accounts = make([]account.SubAccount, len(currencies)) - for x := range currencies { - var data *AccountSummaryData + subAccts := accounts.SubAccounts{accounts.NewSubAccount(asset.All, "")} + for i := range currencies { + var resp *AccountSummaryData if e.Websocket.IsConnected() && e.Websocket.CanUseAuthenticatedWebsocketForWrapper() { - data, err = e.WSRetrieveAccountSummary(ctx, currency.NewCode(currencies[x].Currency), false) + resp, err = e.WSRetrieveAccountSummary(ctx, currencies[i].Currency, false) } else { - data, err = e.GetAccountSummary(ctx, currency.NewCode(currencies[x].Currency), false) + resp, err = e.GetAccountSummary(ctx, currencies[i].Currency, false) } if err != nil { - return resp, err + return nil, err } - var subAcc account.SubAccount - subAcc.Currencies = append(subAcc.Currencies, account.Balance{ - Currency: currency.NewCode(currencies[x].Currency), - Total: data.Balance, - Hold: data.Balance - data.AvailableFunds, + subAccts[0].Balances.Set(currencies[i].Currency, accounts.Balance{ + Total: resp.Balance, + Hold: resp.Balance - resp.AvailableFunds, }) - resp.Accounts[x] = subAcc } - return resp, nil + return subAccts, e.Accounts.Save(ctx, subAccts, true) } // GetAccountFundingHistory returns funding history, deposits and withdrawals @@ -383,9 +378,9 @@ func (e *Exchange) GetAccountFundingHistory(ctx context.Context) ([]exchange.Fun for x := range currencies { var deposits *DepositsData if e.Websocket.IsConnected() && e.Websocket.CanUseAuthenticatedWebsocketForWrapper() { - deposits, err = e.WSRetrieveDeposits(ctx, currency.NewCode(currencies[x].Currency), 100, 0) + deposits, err = e.WSRetrieveDeposits(ctx, currencies[x].Currency, 100, 0) } else { - deposits, err = e.GetDeposits(ctx, currency.NewCode(currencies[x].Currency), 100, 0) + deposits, err = e.GetDeposits(ctx, currencies[x].Currency, 100, 0) } if err != nil { return nil, err @@ -396,7 +391,7 @@ func (e *Exchange) GetAccountFundingHistory(ctx context.Context) ([]exchange.Fun Status: deposits.Data[y].State, TransferID: deposits.Data[y].TransactionID, Timestamp: deposits.Data[y].UpdatedTimestamp.Time(), - Currency: currencies[x].Currency, + Currency: currencies[x].Currency.String(), Amount: deposits.Data[y].Amount, CryptoToAddress: deposits.Data[y].Address, TransferType: "deposit", @@ -404,9 +399,9 @@ func (e *Exchange) GetAccountFundingHistory(ctx context.Context) ([]exchange.Fun } var withdrawalData *WithdrawalsData if e.Websocket.IsConnected() && e.Websocket.CanUseAuthenticatedWebsocketForWrapper() { - withdrawalData, err = e.WSRetrieveWithdrawals(ctx, currency.NewCode(currencies[x].Currency), 100, 0) + withdrawalData, err = e.WSRetrieveWithdrawals(ctx, currencies[x].Currency, 100, 0) } else { - withdrawalData, err = e.GetWithdrawals(ctx, currency.NewCode(currencies[x].Currency), 100, 0) + withdrawalData, err = e.GetWithdrawals(ctx, currencies[x].Currency, 100, 0) } if err != nil { return nil, err @@ -417,7 +412,7 @@ func (e *Exchange) GetAccountFundingHistory(ctx context.Context) ([]exchange.Fun Status: withdrawalData.Data[z].State, TransferID: withdrawalData.Data[z].TransactionID, Timestamp: withdrawalData.Data[z].UpdatedTimestamp.Time(), - Currency: currencies[x].Currency, + Currency: currencies[x].Currency.String(), Amount: withdrawalData.Data[z].Amount, CryptoToAddress: withdrawalData.Data[z].Address, TransferType: "withdrawal", @@ -441,14 +436,14 @@ func (e *Exchange) GetWithdrawalsHistory(ctx context.Context, c currency.Code, _ } resp := []exchange.WithdrawalHistory{} for x := range currencies { - if !strings.EqualFold(currencies[x].Currency, c.String()) { + if !currencies[x].Currency.Equal(c) { continue } var withdrawalData *WithdrawalsData if e.Websocket.IsConnected() && e.Websocket.CanUseAuthenticatedWebsocketForWrapper() { - withdrawalData, err = e.WSRetrieveWithdrawals(ctx, currency.NewCode(currencies[x].Currency), 100, 0) + withdrawalData, err = e.WSRetrieveWithdrawals(ctx, currencies[x].Currency, 100, 0) } else { - withdrawalData, err = e.GetWithdrawals(ctx, currency.NewCode(currencies[x].Currency), 100, 0) + withdrawalData, err = e.GetWithdrawals(ctx, currencies[x].Currency, 100, 0) } if err != nil { return nil, err @@ -458,7 +453,7 @@ func (e *Exchange) GetWithdrawalsHistory(ctx context.Context, c currency.Code, _ Status: withdrawalData.Data[y].State, TransferID: withdrawalData.Data[y].TransactionID, Timestamp: withdrawalData.Data[y].UpdatedTimestamp.Time(), - Currency: currencies[x].Currency, + Currency: currencies[x].Currency.String(), Amount: withdrawalData.Data[y].Amount, CryptoToAddress: withdrawalData.Data[y].Address, TransferType: "deposit", @@ -1027,10 +1022,9 @@ func (e *Exchange) GetFeeByType(ctx context.Context, feeBuilder *exchange.FeeBui return fee, 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) } diff --git a/exchanges/exchange.go b/exchanges/exchange.go index 1067e428..44abcb04 100644 --- a/exchanges/exchange.go +++ b/exchanges/exchange.go @@ -20,9 +20,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/dispatch" + "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/exchanges/account" "github.com/thrasher-corp/gocryptotrader/exchanges/asset" "github.com/thrasher-corp/gocryptotrader/exchanges/collateral" "github.com/thrasher-corp/gocryptotrader/exchanges/currencystate" @@ -264,8 +265,7 @@ func (b *Base) GetClientBankAccounts(exchangeName, withdrawalCurrency string) (* return cfg.GetClientBankAccounts(exchangeName, withdrawalCurrency) } -// GetExchangeBankAccounts returns banking details associated with an -// exchange for funding purposes +// GetExchangeBankAccounts returns banking details associated with an exchange for funding purposes func (b *Base) GetExchangeBankAccounts(id, depositCurrency string) (*banking.Account, error) { cfg := config.GetConfig() return cfg.GetExchangeBankAccounts(b.Name, id, depositCurrency) @@ -600,7 +600,15 @@ func (b *Base) SetupDefaults(exch *config.Exchange) error { log.Warnf(log.ExchangeSys, "%s orderbook verification has been bypassed via config.", b.Name) } + if b.Accounts == nil { + var err error + if b.Accounts, err = accounts.GetStore().GetExchangeAccounts(b); err != nil { + return err + } + } + b.ValidateOrderbook = !exch.Orderbook.VerificationBypass + b.States = currencystate.NewCurrencyStates() return nil @@ -1910,14 +1918,24 @@ func (b *Base) GetCachedOrderbook(p currency.Pair, assetType asset.Item) (*order return orderbook.Get(b.Name, p, assetType) } -// GetCachedAccountInfo retrieves balances for all enabled currencies -// NOTE: UpdateAccountInfo method must be called first to update the account info map -func (b *Base) GetCachedAccountInfo(ctx context.Context, assetType asset.Item) (account.Holdings, error) { +// GetCachedSubAccounts retrieves all cached SubAccounts, filtered by credentials and asset +// NOTE: Accounts.Save method should be called first to populate the local cache +func (b *Base) GetCachedSubAccounts(ctx context.Context, assetType asset.Item) (accounts.SubAccounts, error) { creds, err := b.GetCredentials(ctx) if err != nil { - return account.Holdings{}, err + return nil, err } - return account.GetHoldings(b.Name, creds, assetType) + return b.Accounts.SubAccounts(creds, assetType) +} + +// GetCachedCurrencyBalances retrieves cached balances for all SubAccounts grouped by currency +// NOTE: Accounts.Save method should be called first to populate the local cache +func (b *Base) GetCachedCurrencyBalances(ctx context.Context, assetType asset.Item) (accounts.CurrencyBalances, error) { + creds, err := b.GetCredentials(ctx) + if err != nil { + return nil, err + } + return b.Accounts.CurrencyBalances(creds, assetType) } // GetOrderExecutionLimits returns a limit based on the exchange, asset and pair from storage @@ -1966,3 +1984,8 @@ func (b *Base) MessageID() string { func (b *Base) MessageSequence() int64 { return b.messageSequence.IncrementAndGet() } + +// SubscribeAccountBalances returns a pipe to stream account holding updates +func (b *Base) SubscribeAccountBalances() (dispatch.Pipe, error) { + return b.Accounts.Subscribe() +} diff --git a/exchanges/exchange_test.go b/exchanges/exchange_test.go index 03293fd9..4824a093 100644 --- a/exchanges/exchange_test.go +++ b/exchanges/exchange_test.go @@ -14,9 +14,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/dispatch" + "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/exchanges/account" "github.com/thrasher-corp/gocryptotrader/exchanges/asset" "github.com/thrasher-corp/gocryptotrader/exchanges/collateral" "github.com/thrasher-corp/gocryptotrader/exchanges/deposit" @@ -769,11 +770,8 @@ func TestIsEnabled(t *testing.T) { func TestSetupDefaults(t *testing.T) { t.Parallel() - newRequester, err := request.New("testSetupDefaults", - common.NewHTTPClientWithTimeout(0)) - if err != nil { - t.Fatal(err) - } + newRequester, err := request.New("testSetupDefaults", common.NewHTTPClientWithTimeout(0)) + require.NoError(t, err, "request.New must not error") b := Base{ Name: "awesomeTest", @@ -787,62 +785,35 @@ func TestSetupDefaults(t *testing.T) { ConnectionMonitorDelay: time.Second * 5, } - err = b.SetupDefaults(&cfg) - if err != nil { - t.Fatal(err) - } - if cfg.HTTPTimeout.String() != "15s" { - t.Error("HTTP timeout should be set to 15s") - } + accountsStore := accounts.GetStore() + require.NoError(t, b.SetupDefaults(&cfg)) + // If this fails, something raced and changed accounts.global under us. Probably accounts.TestGetStore. + // Highly unlikely, but this check will clarify what happened + require.Same(t, accountsStore, accounts.GetStore(), "Global accounts Store must not change during SetupDefaults") + + assert.Equal(t, 15*time.Second, cfg.HTTPTimeout, "config.HTTPTimeout should default correctly") - // Test custom HTTP timeout is set cfg.HTTPTimeout = time.Second * 30 - err = b.SetupDefaults(&cfg) - if err != nil { - t.Fatal(err) - } - if cfg.HTTPTimeout.String() != "30s" { - t.Error("HTTP timeout should be set to 30s") - } + require.NoError(t, b.SetupDefaults(&cfg)) + require.NoError(t, err) + assert.Equal(t, 30*time.Second, cfg.HTTPTimeout, "config.HTTPTimeout should respect override") // Test asset types err = b.CurrencyPairs.Store(asset.Spot, ¤cy.PairStore{Enabled: currency.Pairs{btcusdPair}}) require.NoError(t, err, "Store must not error") - require.NoError(t, b.SetupDefaults(&cfg), "SetupDefaults must not error") - ps, err := cfg.CurrencyPairs.Get(asset.Spot) - if err != nil { - t.Fatal(err) - } - if !ps.Enabled.Contains(btcusdPair, true) { - t.Error("default pair should be stored in the configs pair store") - } + require.NoError(t, b.SetupDefaults(&cfg)) - // Test websocket support - b.Websocket = websocket.NewManager() - b.Features.Supports.Websocket = true - err = b.Websocket.Setup(&websocket.ManagerSetup{ - ExchangeConfig: &config.Exchange{ - WebsocketTrafficTimeout: time.Second * 30, - Name: "test", - Features: &config.FeaturesConfig{}, - }, - Features: &protocol.Features{}, - DefaultURL: "ws://something.com", - RunningURL: "ws://something.com", - Connector: func() error { return nil }, - GenerateSubscriptions: func() (subscription.List, error) { return subscription.List{}, nil }, - Subscriber: func(subscription.List) error { return nil }, - }) - if err != nil { - t.Fatal(err) - } - err = b.Websocket.Enable() - if err != nil { - t.Fatal(err) - } - if !b.IsWebsocketEnabled() { - t.Error("websocket should be enabled") - } + ps, err := cfg.CurrencyPairs.Get(asset.Spot) + require.NoError(t, err, "CurrencyPairs.Get must not error") + assert.True(t, ps.Enabled.Contains(btcusdPair, true), "default pair should be stored in the configs pair store") + + exp, err := accountsStore.GetExchangeAccounts(&b) + require.NoError(t, err, "GetExchangeAccounts must not error") + assert.Same(t, exp, b.Accounts, "SetupDefaults should default accounts from the global accounts store") + b.Accounts = accounts.MustNewAccounts(&b) + a := b.Accounts + require.NoError(t, b.SetupDefaults(&cfg)) + assert.Same(t, a, b.Accounts, "SetDefaults should not overwrite Accounts override") } func TestSetPairs(t *testing.T) { @@ -1145,9 +1116,7 @@ func TestIsWebsocketEnabled(t *testing.T) { t.Parallel() var b Base - if b.IsWebsocketEnabled() { - t.Error("exchange doesn't support websocket") - } + require.False(t, b.IsWebsocketEnabled(), "IsWebsocketEnabled must return false on an empty Base") b.Websocket = websocket.NewManager() err := b.Websocket.Setup(&websocket.ManagerSetup{ @@ -1168,12 +1137,10 @@ func TestIsWebsocketEnabled(t *testing.T) { GenerateSubscriptions: func() (subscription.List, error) { return nil, nil }, Subscriber: func(subscription.List) error { return nil }, }) - if err != nil { - t.Error(err) - } - if !b.IsWebsocketEnabled() { - t.Error("websocket should be enabled") - } + require.NoError(t, err, "Websocket.Setup must not error") + assert.True(t, b.IsWebsocketEnabled(), "websocket should be enabled") + require.NoError(t, b.Websocket.Disable(), "Websocket.Disable must not error") + assert.False(t, b.IsWebsocketEnabled(), "websocket should not be enabled") } func TestSupportsWithdrawPermissions(t *testing.T) { @@ -2636,30 +2603,92 @@ func TestGetCachedOrderbook(t *testing.T) { assert.Equal(t, pair, ob.Pair) } -func TestGetCachedAccountInfo(t *testing.T) { +func TestGetCachedSubAccounts(t *testing.T) { t.Parallel() b := Base{Name: "test"} - creds := &account.Credentials{ - Key: "test", - Secret: "test", - } - ctx := account.DeployCredentialsToContext(t.Context(), &account.Credentials{ + ctx := accounts.DeployCredentialsToContext(t.Context(), &accounts.Credentials{ Key: "test", Secret: "test", }) - _, err := b.GetCachedAccountInfo(ctx, asset.Spot) - assert.ErrorIs(t, err, account.ErrExchangeHoldingsNotFound) + _, err := b.GetCachedSubAccounts(ctx, asset.Spot) + assert.ErrorIs(t, err, common.ErrNilPointer) - err = account.Process(&account.Holdings{Exchange: "test", Accounts: []account.SubAccount{ - {AssetType: asset.Spot, Currencies: []account.Balance{{Currency: currency.BTC, Total: 1}}}, - }}, creds) - require.NoError(t, err, "account.Process must not error") + b.Accounts = accounts.MustNewAccounts(&b) + _, err = b.GetCachedSubAccounts(ctx, asset.Spot) + assert.ErrorIs(t, err, accounts.ErrNoSubAccounts) - _, err = b.GetCachedAccountInfo(ctx, asset.Spot) + err = b.Accounts.Save(ctx, accounts.SubAccounts{ + {AssetType: asset.Spot, Balances: accounts.CurrencyBalances{currency.BTC: {Total: 1}}}, + }, true) + require.NoError(t, err, "b.Accounts.Save must not error") + + _, err = b.GetCachedSubAccounts(ctx, asset.Spot) assert.NoError(t, err) } +func TestGetCurrencyBalances(t *testing.T) { + t.Parallel() + b := Base{Name: "test"} + + _, err := b.GetCachedCurrencyBalances(t.Context(), asset.Spot) + assert.ErrorIs(t, err, ErrCredentialsAreEmpty) + + ctx := accounts.DeployCredentialsToContext(t.Context(), &accounts.Credentials{ + Key: "test", + Secret: "test", + }) + _, err = b.GetCachedCurrencyBalances(ctx, asset.Spot) + assert.ErrorIs(t, err, common.ErrNilPointer) + + b.Accounts = accounts.MustNewAccounts(&b) + _, err = b.GetCachedCurrencyBalances(ctx, asset.Spot) + assert.ErrorIs(t, err, accounts.ErrNoBalances) + + err = b.Accounts.Save(ctx, accounts.SubAccounts{ + {AssetType: asset.Spot, Balances: accounts.CurrencyBalances{currency.BTC: {Total: 1.4}}}, + }, true) + require.NoError(t, err, "b.Accounts.Save must not error") + + a, err := b.GetCachedCurrencyBalances(ctx, asset.Spot) + require.NoError(t, err) + require.Contains(t, a, currency.BTC) + assert.Equal(t, 1.4, a[currency.BTC].Total, "BTC Total should be correct") +} + +func TestSubscribeAccountBalances(t *testing.T) { + t.Parallel() + b := Base{Name: "test"} + + _, err := b.SubscribeAccountBalances() + assert.ErrorIs(t, err, common.ErrNilPointer) + + err = dispatch.EnsureRunning(dispatch.DefaultMaxWorkers, dispatch.DefaultJobsLimit) + require.NoError(t, err, "dispatch.EnsureRunning must not error") + + b.Accounts = accounts.MustNewAccounts(&b) + p, err := b.SubscribeAccountBalances() + require.NoError(t, err) + + ctx := accounts.DeployCredentialsToContext(t.Context(), &accounts.Credentials{ + Key: "test", + Secret: "test", + }) + exp := &accounts.SubAccount{AssetType: asset.Spot, Balances: accounts.CurrencyBalances{currency.BTC: {Total: 1.4}}} + err = b.Accounts.Save(ctx, accounts.SubAccounts{exp}, true) + require.NoError(t, err, "b.Accounts.Save must not error") + require.EventuallyWithT(t, func(c *assert.CollectT) { + select { + case a := <-p.Channel(): + require.IsType(c, &accounts.SubAccount{}, a, "Save must publish *SubAccount") + subAcct, _ := a.(*accounts.SubAccount) + assert.Equal(c, exp, subAcct, "Save should publish the same update") + default: + require.Fail(c, "Data must eventually arrive") + } + }, time.Second, time.Millisecond, "Publish must eventually send to Channel") +} + // FakeBase is used to override functions type FakeBase struct{ Base } @@ -2695,8 +2724,8 @@ func (f *FakeBase) CancelOrder(context.Context, *order.Cancel) error { return nil } -func (f *FakeBase) GetCachedAccountInfo(context.Context, asset.Item) (account.Holdings, error) { - return account.Holdings{}, nil +func (f *FakeBase) GetCachedSubAccounts(context.Context, asset.Item) (accounts.SubAccounts, error) { + return accounts.SubAccounts{}, nil } func (f *FakeBase) GetCachedOrderbook(currency.Pair, asset.Item) (*orderbook.Book, error) { @@ -2731,8 +2760,8 @@ func (f *FakeBase) UpdateOrderbook(context.Context, currency.Pair, asset.Item) ( return nil, nil } -func (f *FakeBase) UpdateAccountInfo(context.Context, asset.Item) (account.Holdings, error) { - return account.Holdings{}, nil +func (f *FakeBase) UpdateAccountBalances(context.Context, asset.Item) (accounts.SubAccounts, error) { + return accounts.SubAccounts{}, nil } func (f *FakeBase) GetRecentTrades(context.Context, currency.Pair, asset.Item) ([]trade.Data, error) { diff --git a/exchanges/exchange_types.go b/exchanges/exchange_types.go index 5d4b092e..da605d73 100644 --- a/exchanges/exchange_types.go +++ b/exchanges/exchange_types.go @@ -7,8 +7,8 @@ import ( "github.com/thrasher-corp/gocryptotrader/common" "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" - "github.com/thrasher-corp/gocryptotrader/exchanges/account" "github.com/thrasher-corp/gocryptotrader/exchanges/asset" "github.com/thrasher-corp/gocryptotrader/exchanges/currencystate" "github.com/thrasher-corp/gocryptotrader/exchanges/kline" @@ -222,7 +222,7 @@ type API struct { Endpoints *Endpoints - credentials account.Credentials + credentials accounts.Credentials credMu sync.RWMutex CredentialsValidator config.APICredentialsValidatorConfig @@ -248,6 +248,7 @@ type Base struct { WebsocketResponseMaxLimit time.Duration WebsocketOrderbookBufferLimit int64 Websocket *websocket.Manager + Accounts *accounts.Accounts *request.Requester Config *config.Exchange settingsMutex sync.RWMutex diff --git a/exchanges/exmo/exmo_wrapper.go b/exchanges/exmo/exmo_wrapper.go index 18502931..170b7a78 100644 --- a/exchanges/exmo/exmo_wrapper.go +++ b/exchanges/exmo/exmo_wrapper.go @@ -12,8 +12,8 @@ import ( "github.com/thrasher-corp/gocryptotrader/common" "github.com/thrasher-corp/gocryptotrader/config" "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/account" "github.com/thrasher-corp/gocryptotrader/exchanges/asset" "github.com/thrasher-corp/gocryptotrader/exchanges/deposit" "github.com/thrasher-corp/gocryptotrader/exchanges/fundingrate" @@ -268,49 +268,26 @@ func (e *Exchange) UpdateOrderbook(ctx context.Context, p currency.Pair, assetTy return orderbook.Get(e.Name, p, assetType) } -// UpdateAccountInfo retrieves balances for all enabled currencies for the -// Exmo exchange -func (e *Exchange) UpdateAccountInfo(ctx context.Context, assetType asset.Item) (account.Holdings, error) { - result, err := e.GetUserInfo(ctx) +// UpdateAccountBalances retrieves currency balances +func (e *Exchange) UpdateAccountBalances(ctx context.Context, assetType asset.Item) (accounts.SubAccounts, error) { + resp, err := e.GetUserInfo(ctx) if err != nil { - return account.Holdings{}, err + return nil, err } - - response := account.Holdings{ - Exchange: e.Name, - } - - currencies := make([]account.Balance, 0, len(result.Balances)) - for x, y := range result.Balances { - var exchangeCurrency account.Balance - exchangeCurrency.Currency = currency.NewCode(x) - for z, w := range result.Reserved { - if z != x { - continue - } - avail, reserved := y.Float64(), w.Float64() - exchangeCurrency.Total = avail + reserved - exchangeCurrency.Hold = reserved - exchangeCurrency.Free = avail + subAccts := accounts.SubAccounts{accounts.NewSubAccount(assetType, "")} + for k, bal := range resp.Balances { + avail := bal.Float64() + reserved := 0.0 + if r, ok := resp.Reserved[k]; ok { + reserved = r.Float64() } - currencies = append(currencies, exchangeCurrency) + subAccts[0].Balances.Set(currency.NewCode(k), accounts.Balance{ + Total: avail + reserved, + Hold: reserved, + Free: avail, + }) } - - response.Accounts = append(response.Accounts, account.SubAccount{ - AssetType: assetType, - Currencies: currencies, - }) - - creds, err := e.GetCredentials(ctx) - if err != nil { - return account.Holdings{}, err - } - err = account.Process(&response, creds) - if err != nil { - return account.Holdings{}, err - } - - return response, nil + return subAccts, e.Accounts.Save(ctx, subAccts, true) } // GetAccountFundingHistory returns funding history, deposits and @@ -668,7 +645,7 @@ func (e *Exchange) GetOrderHistory(ctx context.Context, req *order.MultiOrderReq // 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) } diff --git a/exchanges/gateio/gateio.go b/exchanges/gateio/gateio.go index 77aaa558..d9b2fe05 100644 --- a/exchanges/gateio/gateio.go +++ b/exchanges/gateio/gateio.go @@ -629,7 +629,7 @@ func (e *Exchange) GetSpotOrders(ctx context.Context, currencyPair currency.Pair } // CancelAllOpenOrdersSpecifiedCurrencyPair cancel all open orders in specified currency pair -func (e *Exchange) CancelAllOpenOrdersSpecifiedCurrencyPair(ctx context.Context, currencyPair currency.Pair, side order.Side, account asset.Item) ([]SpotOrder, error) { +func (e *Exchange) CancelAllOpenOrdersSpecifiedCurrencyPair(ctx context.Context, currencyPair currency.Pair, side order.Side, a asset.Item) ([]SpotOrder, error) { if currencyPair.IsEmpty() { return nil, currency.ErrCurrencyPairEmpty } @@ -638,8 +638,8 @@ func (e *Exchange) CancelAllOpenOrdersSpecifiedCurrencyPair(ctx context.Context, if side == order.Buy || side == order.Sell { params.Set("side", strings.ToLower(side.Title())) } - if account == asset.Spot || account == asset.Margin || account == asset.CrossMargin { - params.Set("account", account.String()) + if a == asset.Spot || a == asset.Margin || a == asset.CrossMargin { + params.Set("account", a.String()) } var response []SpotOrder return response, e.SendAuthenticatedHTTPRequest(ctx, exchange.RestSpot, spotCancelAllOpenOrdersEPL, http.MethodDelete, gateioSpotOrders, params, nil, &response) @@ -663,7 +663,7 @@ func (e *Exchange) CancelBatchOrdersWithIDList(ctx context.Context, args []Cance } // GetSpotOrder retrieves a single spot order using the order id and currency pair information. -func (e *Exchange) GetSpotOrder(ctx context.Context, orderID string, currencyPair currency.Pair, account asset.Item) (*SpotOrder, error) { +func (e *Exchange) GetSpotOrder(ctx context.Context, orderID string, currencyPair currency.Pair, a asset.Item) (*SpotOrder, error) { if orderID == "" { return nil, errInvalidOrderID } @@ -672,7 +672,7 @@ func (e *Exchange) GetSpotOrder(ctx context.Context, orderID string, currencyPai } params := url.Values{} params.Set("currency_pair", currencyPair.String()) - if accountType := account.String(); accountType != "" { + if accountType := a.String(); accountType != "" { params.Set("account", accountType) } var response *SpotOrder @@ -817,7 +817,7 @@ func (e *Exchange) CreatePriceTriggeredOrder(ctx context.Context, arg *PriceTrig } // GetPriceTriggeredOrderList retrieves price orders created with an order detail and trigger price information. -func (e *Exchange) GetPriceTriggeredOrderList(ctx context.Context, status string, market currency.Pair, account asset.Item, offset, limit uint64) ([]SpotPriceTriggeredOrder, error) { +func (e *Exchange) GetPriceTriggeredOrderList(ctx context.Context, status string, market currency.Pair, a asset.Item, offset, limit uint64) ([]SpotPriceTriggeredOrder, error) { if status != statusOpen && status != statusFinished { return nil, fmt.Errorf("%w status %s", errInvalidOrderStatus, status) } @@ -826,8 +826,8 @@ func (e *Exchange) GetPriceTriggeredOrderList(ctx context.Context, status string if market.IsPopulated() { params.Set("market", market.String()) } - if account == asset.CrossMargin { - params.Set("account", account.String()) + if a == asset.CrossMargin { + params.Set("account", a.String()) } if limit > 0 { params.Set("limit", strconv.FormatUint(limit, 10)) @@ -840,18 +840,18 @@ func (e *Exchange) GetPriceTriggeredOrderList(ctx context.Context, status string } // CancelMultipleSpotOpenOrders deletes price triggered orders. -func (e *Exchange) CancelMultipleSpotOpenOrders(ctx context.Context, currencyPair currency.Pair, account asset.Item) ([]SpotPriceTriggeredOrder, error) { +func (e *Exchange) CancelMultipleSpotOpenOrders(ctx context.Context, currencyPair currency.Pair, a asset.Item) ([]SpotPriceTriggeredOrder, error) { params := url.Values{} if currencyPair.IsPopulated() { params.Set("market", currencyPair.String()) } - switch account { + switch a { case asset.Empty: return nil, asset.ErrNotSupported case asset.Spot: params.Set("account", "normal") default: - params.Set("account", account.String()) + params.Set("account", a.String()) } var response []SpotPriceTriggeredOrder return response, e.SendAuthenticatedHTTPRequest(ctx, exchange.RestSpot, spotCancelTriggerOrdersEPL, http.MethodDelete, gateioSpotPriceOrders, params, nil, &response) diff --git a/exchanges/gateio/gateio_test.go b/exchanges/gateio/gateio_test.go index 490b5830..8884756d 100644 --- a/exchanges/gateio/gateio_test.go +++ b/exchanges/gateio/gateio_test.go @@ -20,8 +20,8 @@ import ( "github.com/thrasher-corp/gocryptotrader/core" "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/fundingrate" "github.com/thrasher-corp/gocryptotrader/exchanges/futures" @@ -66,12 +66,35 @@ func TestUpdateTradablePairs(t *testing.T) { testexch.UpdatePairsOnce(t, e) } -func TestGetAccountInfo(t *testing.T) { +func TestCancelAllExchangeOrders(t *testing.T) { + t.Parallel() + sharedtestvalues.SkipTestIfCredentialsUnset(t, e, canManipulateRealOrders) + _, err := e.CancelAllOrders(t.Context(), nil) + require.ErrorIs(t, err, order.ErrCancelOrderIsNil) + + r := &order.Cancel{ + OrderID: "1", + AccountID: "1", + } + + for _, a := range e.GetAssetTypes(false) { + r.AssetType = a + r.Pair = currency.EMPTYPAIR + _, err = e.CancelAllOrders(t.Context(), r) + assert.ErrorIs(t, err, currency.ErrCurrencyPairEmpty) + + r.Pair = getPair(t, a) + _, err = e.CancelAllOrders(t.Context(), r) + require.NoError(t, err) + } +} + +func TestGetAccountBalances(t *testing.T) { t.Parallel() sharedtestvalues.SkipTestIfCredentialsUnset(t, e) for _, a := range e.GetAssetTypes(false) { - _, 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) } } @@ -2028,7 +2051,7 @@ const wsBalancesPushDataJSON = `{"time": 1605248616, "channel": "spot.balances", func TestBalancesPushData(t *testing.T) { t.Parallel() - ctx := account.DeployCredentialsToContext(t.Context(), &account.Credentials{Key: "test", Secret: "test"}) + ctx := accounts.DeployCredentialsToContext(t.Context(), &accounts.Credentials{Key: "test", Secret: "test"}) if err := e.WsHandleSpotData(ctx, nil, []byte(wsBalancesPushDataJSON)); err != nil { t.Errorf("%s websocket balances push data error: %v", e.Name, err) } @@ -2047,7 +2070,7 @@ const wsCrossMarginBalancePushDataJSON = `{"time": 1605248616,"channel": "spot.c func TestCrossMarginBalancePushData(t *testing.T) { t.Parallel() - ctx := account.DeployCredentialsToContext(t.Context(), &account.Credentials{Key: "test", Secret: "test"}) + ctx := accounts.DeployCredentialsToContext(t.Context(), &accounts.Credentials{Key: "test", Secret: "test"}) if err := e.WsHandleSpotData(ctx, nil, []byte(wsCrossMarginBalancePushDataJSON)); err != nil { t.Errorf("%s websocket cross margin balance push data error: %v", e.Name, err) } @@ -2069,7 +2092,7 @@ func TestFuturesDataHandler(t *testing.T) { require.NoError(t, testexch.Setup(e), "Test instance Setup must not error") testexch.FixtureToDataHandler(t, "testdata/wsFutures.json", func(ctx context.Context, m []byte) error { if strings.Contains(string(m), "futures.balances") { - ctx = account.DeployCredentialsToContext(ctx, &account.Credentials{Key: "test", Secret: "test"}) + ctx = accounts.DeployCredentialsToContext(ctx, &accounts.Credentials{Key: "test", Secret: "test"}) } return e.WsHandleFuturesData(ctx, nil, m, asset.CoinMarginedFutures) }) @@ -2236,7 +2259,7 @@ const optionsBalancePushDataJSON = `{ "channel": "options.balances", "event": "u func TestOptionsBalancePushData(t *testing.T) { t.Parallel() - ctx := account.DeployCredentialsToContext(t.Context(), &account.Credentials{Key: "test", Secret: "test"}) + ctx := accounts.DeployCredentialsToContext(t.Context(), &accounts.Credentials{Key: "test", Secret: "test"}) if err := e.WsHandleOptionsData(ctx, nil, []byte(optionsBalancePushDataJSON)); err != nil { t.Errorf("%s websocket options balance push data error: %v", e.Name, err) } diff --git a/exchanges/gateio/gateio_types.go b/exchanges/gateio/gateio_types.go index 5b6da7af..693b84e4 100644 --- a/exchanges/gateio/gateio_types.go +++ b/exchanges/gateio/gateio_types.go @@ -949,18 +949,18 @@ type OptionsUnderlyingTicker struct { IndexPrice types.Number `json:"index_price"` } -// OptionAccount represents option account. +// OptionAccount represents an option account. type OptionAccount struct { - User int64 `json:"user"` - Currency string `json:"currency"` - ShortEnabled bool `json:"short_enabled"` - Total types.Number `json:"total"` - UnrealisedPnl string `json:"unrealised_pnl"` - InitMargin string `json:"init_margin"` - MaintMargin string `json:"maint_margin"` - OrderMargin string `json:"order_margin"` - Available types.Number `json:"available"` - Point string `json:"point"` + User int64 `json:"user"` + Currency currency.Code `json:"currency"` + ShortEnabled bool `json:"short_enabled"` + Total types.Number `json:"total"` + UnrealisedPnl string `json:"unrealised_pnl"` + InitMargin string `json:"init_margin"` + MaintMargin string `json:"maint_margin"` + OrderMargin string `json:"order_margin"` + Available types.Number `json:"available"` + Point string `json:"point"` } // AccountBook represents account changing history item @@ -1218,11 +1218,11 @@ type MarginAccountItem struct { // AccountBalanceInformation represents currency account balance information. type AccountBalanceInformation struct { - Available types.Number `json:"available"` - Borrowed types.Number `json:"borrowed"` - Interest types.Number `json:"interest"` - Currency string `json:"currency"` - LockedAmount types.Number `json:"locked"` + Available types.Number `json:"available"` + Borrowed types.Number `json:"borrowed"` + Interest types.Number `json:"interest"` + Currency currency.Code `json:"currency"` + LockedAmount types.Number `json:"locked"` } // MarginAccountBalanceChangeInfo represents margin account balance @@ -1371,9 +1371,9 @@ type SpotTradingFeeRate struct { // SpotAccount represents spot account type SpotAccount struct { - Currency string `json:"currency"` - Available types.Number `json:"available"` - Locked types.Number `json:"locked"` + Currency currency.Code `json:"currency"` + Available types.Number `json:"available"` + Locked types.Number `json:"locked"` } // CreateOrderRequest represents a single order creation param. @@ -1710,26 +1710,26 @@ type InitFlashSwapOrderPreviewResponse struct { // FuturesAccount represents futures account detail type FuturesAccount struct { - User int64 `json:"user"` - Currency string `json:"currency"` - Total types.Number `json:"total"` // total = position_margin + order_margin + available - UnrealisedPnl types.Number `json:"unrealised_pnl"` - PositionMargin types.Number `json:"position_margin"` - OrderMargin types.Number `json:"order_margin"` // Order margin of unfinished orders - Available types.Number `json:"available"` // The available balance for transferring or trading - Point types.Number `json:"point"` - Bonus string `json:"bonus"` - EnabledCredit bool `json:"enable_credit"` - InDualMode bool `json:"in_dual_mode"` // Whether dual mode is enabled - UpdateTime types.Time `json:"update_time"` - UpdateID int64 `json:"update_id"` - PositionInitialMargine types.Number `json:"position_initial_margin"` // applicable to the portfolio margin account model - MaintenanceMargin types.Number `json:"maintenance_margin"` - MarginMode int64 `json:"margin_mode"` // Margin mode: 1-cross margin, 2-isolated margin, 3-portfolio margin - EnabledEvolvedClassic bool `json:"enable_evolved_classic"` - CrossInitialMargin types.Number `json:"cross_initial_margin"` - CrossUnrealisedPnl types.Number `json:"cross_unrealised_pnl"` - IsolatedPositionMargin types.Number `json:"isolated_position_margin"` + User int64 `json:"user"` + Currency currency.Code `json:"currency"` + Total types.Number `json:"total"` // total = position_margin + order_margin + available + UnrealisedPnl types.Number `json:"unrealised_pnl"` + PositionMargin types.Number `json:"position_margin"` + OrderMargin types.Number `json:"order_margin"` // Order margin of unfinished orders + Available types.Number `json:"available"` // The available balance for transferring or trading + Point types.Number `json:"point"` + Bonus string `json:"bonus"` + EnabledCredit bool `json:"enable_credit"` + InDualMode bool `json:"in_dual_mode"` // Whether dual mode is enabled + UpdateTime types.Time `json:"update_time"` + UpdateID int64 `json:"update_id"` + PositionInitialMargine types.Number `json:"position_initial_margin"` // applicable to the portfolio margin account model + MaintenanceMargin types.Number `json:"maintenance_margin"` + MarginMode int64 `json:"margin_mode"` // Margin mode: 1-cross margin, 2-isolated margin, 3-portfolio margin + EnabledEvolvedClassic bool `json:"enable_evolved_classic"` + CrossInitialMargin types.Number `json:"cross_initial_margin"` + CrossUnrealisedPnl types.Number `json:"cross_unrealised_pnl"` + IsolatedPositionMargin types.Number `json:"isolated_position_margin"` History struct { DepositAndWithdrawal string `json:"dnw"` // total amount of deposit and withdraw ProfitAndLoss types.Number `json:"pnl"` // total amount of trading profit and loss @@ -2159,15 +2159,15 @@ type WsSpotBalance struct { // WsMarginBalance represents margin account balance push data type WsMarginBalance struct { - Timestamp types.Time `json:"timestamp_ms"` - User string `json:"user"` - CurrencyPair string `json:"currency_pair"` - Currency string `json:"currency"` - Change types.Number `json:"change"` - Available types.Number `json:"available"` - Freeze types.Number `json:"freeze"` - Borrowed string `json:"borrowed"` - Interest string `json:"interest"` + Timestamp types.Time `json:"timestamp_ms"` + User string `json:"user"` + CurrencyPair string `json:"currency_pair"` + Currency currency.Code `json:"currency"` + Change types.Number `json:"change"` + Available types.Number `json:"available"` + Freeze types.Number `json:"freeze"` + Borrowed string `json:"borrowed"` + Interest string `json:"interest"` } // WsFundingBalance represents funding balance push data. @@ -2182,12 +2182,12 @@ type WsFundingBalance struct { // WsCrossMarginBalance represents a cross margin balance detail type WsCrossMarginBalance struct { - Timestamp types.Time `json:"timestamp_ms"` - User string `json:"user"` - Currency string `json:"currency"` - Change string `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 string `json:"change"` + Total types.Number `json:"total"` + Available types.Number `json:"available"` } // WsCrossMarginLoan represents a cross margin loan push data diff --git a/exchanges/gateio/gateio_websocket.go b/exchanges/gateio/gateio_websocket.go index 23173658..426b687e 100644 --- a/exchanges/gateio/gateio_websocket.go +++ b/exchanges/gateio/gateio_websocket.go @@ -21,8 +21,8 @@ import ( "github.com/thrasher-corp/gocryptotrader/common/key" "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/fill" "github.com/thrasher-corp/gocryptotrader/exchanges/kline" @@ -489,65 +489,55 @@ func (e *Exchange) processUserPersonalTrades(data []byte) error { } func (e *Exchange) processSpotBalances(ctx context.Context, data []byte) error { - var resp []WsSpotBalance + var resp []*WsSpotBalance if err := json.Unmarshal(data, &resp); err != nil { return err } - - creds, err := e.GetCredentials(ctx) - if err != nil { + subAccts := accounts.SubAccounts{} + for _, bal := range resp { + a := accounts.NewSubAccount(asset.Spot, bal.User) + a.Balances.Set(bal.Currency, accounts.Balance{ + Total: bal.Total.Float64(), + Free: bal.Available.Float64(), + Hold: bal.Freeze.Float64(), + AvailableWithoutBorrow: bal.Available.Float64(), + UpdatedAt: bal.Timestamp.Time(), + }) + subAccts = subAccts.Merge(a) + } + if err := e.Accounts.Save(ctx, subAccts, false); err != nil { return err } - - changes := make([]account.Change, len(resp)) - for i := range resp { - changes[i] = account.Change{ - Account: resp[i].User, - AssetType: asset.Spot, - Balance: &account.Balance{ - 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(), - }, - } - } - e.Websocket.DataHandler <- changes - return account.ProcessChange(e.Name, changes, creds) + e.Websocket.DataHandler <- subAccts + return nil } func (e *Exchange) processMarginBalances(ctx context.Context, data []byte) error { resp := struct { - Time types.Time `json:"time"` - Channel string `json:"channel"` - Event string `json:"event"` - Result []WsMarginBalance `json:"result"` + Time types.Time `json:"time"` + Channel string `json:"channel"` + Event string `json:"event"` + Result []*WsMarginBalance `json:"result"` }{} - err := json.Unmarshal(data, &resp) - if err != nil { + if err := json.Unmarshal(data, &resp); err != nil { return err } - creds, err := e.GetCredentials(ctx) - if err != nil { + subAccts := accounts.SubAccounts{} + for _, bal := range resp.Result { + a := accounts.NewSubAccount(asset.Margin, bal.User) + a.Balances.Set(bal.Currency, accounts.Balance{ + Total: bal.Available.Float64() + bal.Freeze.Float64(), + Free: bal.Available.Float64(), + Hold: bal.Freeze.Float64(), + UpdatedAt: bal.Timestamp.Time(), + }) + subAccts = subAccts.Merge(a) + } + if err := e.Accounts.Save(ctx, subAccts, false); err != nil { return err } - changes := make([]account.Change, len(resp.Result)) - for x := range resp.Result { - changes[x] = account.Change{ - AssetType: asset.Margin, - Balance: &account.Balance{ - Currency: currency.NewCode(resp.Result[x].Currency), - Total: resp.Result[x].Available.Float64() + resp.Result[x].Freeze.Float64(), - Free: resp.Result[x].Available.Float64(), - Hold: resp.Result[x].Freeze.Float64(), - UpdatedAt: resp.Result[x].Timestamp.Time(), - }, - } - } - e.Websocket.DataHandler <- changes - return account.ProcessChange(e.Name, changes, creds) + e.Websocket.DataHandler <- subAccts + return nil } func (e *Exchange) processFundingBalances(data []byte) error { @@ -567,34 +557,30 @@ func (e *Exchange) processFundingBalances(data []byte) error { func (e *Exchange) processCrossMarginBalance(ctx context.Context, data []byte) error { resp := struct { - Time types.Time `json:"time"` - Channel string `json:"channel"` - Event string `json:"event"` - Result []WsCrossMarginBalance `json:"result"` + Time types.Time `json:"time"` + Channel string `json:"channel"` + Event string `json:"event"` + Result []*WsCrossMarginBalance `json:"result"` }{} err := json.Unmarshal(data, &resp) if err != nil { return err } - creds, err := e.GetCredentials(ctx) - if err != nil { + subAccts := accounts.SubAccounts{} + for _, bal := range resp.Result { + a := accounts.NewSubAccount(asset.CrossMargin, bal.User) + a.Balances.Set(bal.Currency, accounts.Balance{ + Total: bal.Total.Float64(), + Free: bal.Available.Float64(), + UpdatedAt: bal.Timestamp.Time(), + }) + subAccts = subAccts.Merge(a) + } + if err := e.Accounts.Save(ctx, subAccts, false); err != nil { return err } - changes := make([]account.Change, len(resp.Result)) - for x := range resp.Result { - changes[x] = account.Change{ - Account: resp.Result[x].User, - AssetType: asset.Margin, - Balance: &account.Balance{ - Currency: currency.NewCode(resp.Result[x].Currency), - Total: resp.Result[x].Total.Float64(), - Free: resp.Result[x].Available.Float64(), - UpdatedAt: resp.Result[x].Timestamp.Time(), - }, - } - } - e.Websocket.DataHandler <- changes - return account.ProcessChange(e.Name, changes, creds) + e.Websocket.DataHandler <- subAccts + return nil } func (e *Exchange) processCrossMarginLoans(data []byte) error { diff --git a/exchanges/gateio/gateio_websocket_delivery_futures.go b/exchanges/gateio/gateio_websocket_delivery_futures.go index e26bcc53..1795d7e9 100644 --- a/exchanges/gateio/gateio_websocket_delivery_futures.go +++ b/exchanges/gateio/gateio_websocket_delivery_futures.go @@ -9,8 +9,8 @@ import ( gws "github.com/gorilla/websocket" "github.com/thrasher-corp/gocryptotrader/currency" + "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/subscription" @@ -115,7 +115,7 @@ func (e *Exchange) generateDeliveryFuturesPayload(ctx context.Context, event str if len(channelsToSubscribe) == 0 { return nil, errors.New("cannot generate payload, no channels supplied") } - var creds *account.Credentials + var creds *accounts.Credentials var err error if e.Websocket.CanUseAuthenticatedEndpoints() { creds, err = e.GetCredentials(ctx) diff --git a/exchanges/gateio/gateio_websocket_futures.go b/exchanges/gateio/gateio_websocket_futures.go index 61bd28a7..4c010fbe 100644 --- a/exchanges/gateio/gateio_websocket_futures.go +++ b/exchanges/gateio/gateio_websocket_futures.go @@ -11,8 +11,8 @@ import ( gws "github.com/gorilla/websocket" "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/fill" "github.com/thrasher-corp/gocryptotrader/exchanges/kline" @@ -198,7 +198,7 @@ func (e *Exchange) generateFuturesPayload(ctx context.Context, event string, cha if len(channelsToSubscribe) == 0 { return nil, errors.New("cannot generate payload, no channels supplied") } - var creds *account.Credentials + var creds *accounts.Credentials var err error if e.Websocket.CanUseAuthenticatedEndpoints() { creds, err = e.GetCredentials(ctx) @@ -632,36 +632,31 @@ func (e *Exchange) processPositionCloseData(data []byte) error { } func (e *Exchange) processBalancePushData(ctx context.Context, data []byte, assetType asset.Item) error { - var resp []WsBalance + 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)) - for x, bal := range resp { + subAccts := accounts.SubAccounts{} + for _, 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: c, - Total: bal.Balance, - Free: bal.Balance, - AvailableWithoutBorrow: bal.Balance, - UpdatedAt: bal.Time.Time(), - }, - } + a := accounts.NewSubAccount(assetType, bal.User) + a.Balances.Set(c, accounts.Balance{ + Total: bal.Balance, + Free: bal.Balance, + AvailableWithoutBorrow: bal.Balance, + UpdatedAt: bal.Time.Time(), + }) + subAccts = subAccts.Merge(a) } - e.Websocket.DataHandler <- changes - return account.ProcessChange(e.Name, changes, creds) + err := e.Accounts.Save(ctx, subAccts, false) + if err == nil { + e.Websocket.DataHandler <- subAccts + } + return err } func (e *Exchange) processFuturesReduceRiskLimitNotification(data []byte) error { diff --git a/exchanges/gateio/gateio_websocket_option.go b/exchanges/gateio/gateio_websocket_option.go index 7c8c55e2..735ed88c 100644 --- a/exchanges/gateio/gateio_websocket_option.go +++ b/exchanges/gateio/gateio_websocket_option.go @@ -12,8 +12,8 @@ import ( gws "github.com/gorilla/websocket" "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/fill" "github.com/thrasher-corp/gocryptotrader/exchanges/kline" @@ -218,7 +218,7 @@ func (e *Exchange) generateOptionsPayload(ctx context.Context, event string, cha continue } params = append([]string{strconv.FormatInt(userID, 10)}, params...) - var creds *account.Credentials + var creds *accounts.Credentials creds, err = e.GetCredentials(ctx) if err != nil { return nil, err diff --git a/exchanges/gateio/gateio_websocket_test.go b/exchanges/gateio/gateio_websocket_test.go index e2fbca17..286f48a8 100644 --- a/exchanges/gateio/gateio_websocket_test.go +++ b/exchanges/gateio/gateio_websocket_test.go @@ -2,6 +2,9 @@ package gateio import ( "context" + "maps" + "slices" + "strconv" "testing" "time" @@ -9,8 +12,8 @@ import ( "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/account" "github.com/thrasher-corp/gocryptotrader/exchanges/asset" ) @@ -42,17 +45,17 @@ type websocketBalancesTest struct { input []byte err error deployCreds bool - expected []account.Change + expected accounts.SubAccounts } -func TestProcessSpotBalances(t *testing.T) { +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) //nolint:govet // Intentional shadow e.SetDefaults() e.Name = "ProcessSpotBalancesTest" + e.Accounts = accounts.MustNewAccounts(e) - // Sequential tests, do not use t.Run(); Some timestamps are deliberately identical from trading activity - for _, tc := range []websocketBalancesTest{ + for i, tc := range []websocketBalancesTest{ { input: []byte(`[{"timestamp":"1755718222"}]`), err: exchange.ErrCredentialsAreEmpty, @@ -60,17 +63,19 @@ func TestProcessSpotBalances(t *testing.T) { { 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{ + expected: accounts.SubAccounts{ { - Account: "12870774", + ID: "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), + 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), + }, }, }, }, @@ -78,44 +83,51 @@ func TestProcessSpotBalances(t *testing.T) { { 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{ + expected: accounts.SubAccounts{ { - Account: "12870774", + ID: "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), + 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), + }, }, }, }, }, } { - 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) + 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) { +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) //nolint:govet // Intentional shadow e.SetDefaults() e.Name = "ProcessFuturesBalancesTest" + e.Accounts = accounts.MustNewAccounts(e) - // Sequential tests, do not use t.Run(); Some timestamps are deliberately identical from trading activity - for _, tc := range []websocketBalancesTest{ + usdtLower := currency.USDT.Lower() + + for i, tc := range []websocketBalancesTest{ { input: []byte(`[{"timestamp":"1755718222"}]`), err: exchange.ErrCredentialsAreEmpty, @@ -123,16 +135,18 @@ func TestProcessBalancePushData(t *testing.T) { { 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{ + expected: accounts.SubAccounts{ { - Account: "12870774", + ID: "12870774", AssetType: asset.USDTMarginedFutures, - Balance: &account.Balance{ - Currency: currency.USDT, - Total: 2214.191673190433, - Free: 2214.191673190433, - AvailableWithoutBorrow: 2214.191673190433, - UpdatedAt: time.UnixMilli(1755738515671), + Balances: accounts.CurrencyBalances{ + usdtLower: accounts.Balance{ + Currency: usdtLower, + Total: 2214.191673190433, + Free: 2214.191673190433, + AvailableWithoutBorrow: 2214.191673190433, + UpdatedAt: time.UnixMilli(1755738515671), + }, }, }, }, @@ -140,33 +154,37 @@ func TestProcessBalancePushData(t *testing.T) { { 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{ + expected: accounts.SubAccounts{ { - Account: "12870774", + ID: "12870774", AssetType: asset.USDTMarginedFutures, - Balance: &account.Balance{ - Currency: currency.USDT, - Total: 2214.189114310433, - Free: 2214.189114310433, - AvailableWithoutBorrow: 2214.189114310433, - UpdatedAt: time.UnixMilli(1755738516430), + Balances: accounts.CurrencyBalances{ + usdtLower: accounts.Balance{ + Currency: usdtLower, + 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) + 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) + } + }) } } @@ -175,25 +193,19 @@ func checkAccountChange(ctx context.Context, t *testing.T, exch *Exchange, tc *w require.Len(t, exch.Websocket.DataHandler, 1) payload := <-exch.Websocket.DataHandler - received, ok := payload.([]account.Change) + 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)) - 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") + require.Equal(t, tc.expected, received) - 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) + 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, tc.expected[i].Balance.Free, stored.GetFree(), "free balance should equal with accounts stored value") + assert.Equal(t, bal.Free, stored.Free, "free balance should equal with accounts stored value") } } diff --git a/exchanges/gateio/gateio_wrapper.go b/exchanges/gateio/gateio_wrapper.go index 8c126d03..f09c06ba 100644 --- a/exchanges/gateio/gateio_wrapper.go +++ b/exchanges/gateio/gateio_wrapper.go @@ -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/deposit" "github.com/thrasher-corp/gocryptotrader/exchanges/fundingrate" @@ -675,56 +675,43 @@ func (e *Exchange) UpdateOrderbookWithLimit(ctx context.Context, p currency.Pair return orderbook.Get(e.Name, p, a) } -// UpdateAccountInfo retrieves balances for all enabled currencies for the -func (e *Exchange) UpdateAccountInfo(ctx context.Context, a asset.Item) (account.Holdings, error) { - info := account.Holdings{ - Exchange: e.Name, - Accounts: []account.SubAccount{{ - AssetType: a, - }}, - } +// UpdateAccountBalances retrieves currency balances +func (e *Exchange) UpdateAccountBalances(ctx context.Context, a asset.Item) (accounts.SubAccounts, error) { + subAccts := accounts.SubAccounts{accounts.NewSubAccount(a, "")} switch a { case asset.Spot: balances, err := e.GetSpotAccounts(ctx, currency.EMPTYCODE) if err != nil { - return info, err + return nil, err } - currencies := make([]account.Balance, len(balances)) for i := range balances { - currencies[i] = account.Balance{ - Currency: currency.NewCode(balances[i].Currency), - Total: balances[i].Available.Float64() + balances[i].Locked.Float64(), - Hold: balances[i].Locked.Float64(), - Free: balances[i].Available.Float64(), - } + subAccts[0].Balances.Set(balances[i].Currency, accounts.Balance{ + Total: balances[i].Available.Float64() + balances[i].Locked.Float64(), + Hold: balances[i].Locked.Float64(), + Free: balances[i].Available.Float64(), + }) } - info.Accounts[0].Currencies = currencies case asset.Margin, asset.CrossMargin: balances, err := e.GetMarginAccountList(ctx, currency.EMPTYPAIR) if err != nil { - return info, err + return nil, err } - currencies := make([]account.Balance, 0, 2*len(balances)) for i := range balances { - currencies = append(currencies, - account.Balance{ - Currency: currency.NewCode(balances[i].Base.Currency), - Total: balances[i].Base.Available.Float64() + balances[i].Base.LockedAmount.Float64(), - Hold: balances[i].Base.LockedAmount.Float64(), - Free: balances[i].Base.Available.Float64(), - }, - account.Balance{ - Currency: currency.NewCode(balances[i].Quote.Currency), - Total: balances[i].Quote.Available.Float64() + balances[i].Quote.LockedAmount.Float64(), - Hold: balances[i].Quote.LockedAmount.Float64(), - Free: balances[i].Quote.Available.Float64(), - }) + subAccts[0].Balances.Set(balances[i].Base.Currency, accounts.Balance{ + Total: balances[i].Base.Available.Float64() + balances[i].Base.LockedAmount.Float64(), + Hold: balances[i].Base.LockedAmount.Float64(), + Free: balances[i].Base.Available.Float64(), + }) + subAccts[0].Balances.Set(balances[i].Quote.Currency, accounts.Balance{ + Total: balances[i].Quote.Available.Float64() + balances[i].Quote.LockedAmount.Float64(), + Hold: balances[i].Quote.LockedAmount.Float64(), + Free: balances[i].Quote.Available.Float64(), + }) } - info.Accounts[0].Currencies = currencies case asset.CoinMarginedFutures, asset.USDTMarginedFutures, asset.DeliveryFutures: settle, err := getSettlementCurrency(currency.EMPTYPAIR, a) if err != nil { - return info, err + return nil, err } var acc *FuturesAccount if a == asset.DeliveryFutures { @@ -733,33 +720,27 @@ func (e *Exchange) UpdateAccountInfo(ctx context.Context, a asset.Item) (account acc, err = e.QueryFuturesAccount(ctx, settle) } if err != nil { - return info, err + return nil, err } - info.Accounts[0].Currencies = []account.Balance{{ - Currency: currency.NewCode(acc.Currency), - Total: acc.Total.Float64(), - Hold: acc.Total.Float64() - acc.Available.Float64(), - Free: acc.Available.Float64(), - }} + subAccts[0].Balances.Set(acc.Currency, accounts.Balance{ + Total: acc.Total.Float64(), + Hold: acc.Total.Float64() - acc.Available.Float64(), + Free: acc.Available.Float64(), + }) case asset.Options: balance, err := e.GetOptionAccounts(ctx) if err != nil { - return info, err + return nil, err } - info.Accounts[0].Currencies = []account.Balance{{ - Currency: currency.NewCode(balance.Currency), - Total: balance.Total.Float64(), - Hold: balance.Total.Float64() - balance.Available.Float64(), - Free: balance.Available.Float64(), - }} + subAccts[0].Balances.Set(balance.Currency, accounts.Balance{ + Total: balance.Total.Float64(), + Hold: balance.Total.Float64() - balance.Available.Float64(), + Free: balance.Available.Float64(), + }) default: - return info, fmt.Errorf("%w asset type: %v", asset.ErrNotSupported, a) + return nil, fmt.Errorf("%w asset type: %q", asset.ErrNotSupported, a) } - creds, err := e.GetCredentials(ctx) - if err == nil { - err = account.Process(&info, creds) - } - return info, err + return subAccts, e.Accounts.Save(ctx, subAccts, true) } // GetAccountFundingHistory returns funding history, deposits and @@ -1756,10 +1737,9 @@ func (e *Exchange) GetAvailableTransferChains(ctx context.Context, cryptocurrenc return availableChains, 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) } diff --git a/exchanges/gemini/gemini_types.go b/exchanges/gemini/gemini_types.go index 700ff458..03735498 100644 --- a/exchanges/gemini/gemini_types.go +++ b/exchanges/gemini/gemini_types.go @@ -224,9 +224,9 @@ type OneDayNotionalVolume struct { // Balance is a simple balance type type Balance struct { - Currency string `json:"currency"` - Amount float64 `json:"amount,string"` - Available float64 `json:"available,string"` + Currency currency.Code `json:"currency"` + Amount float64 `json:"amount,string"` + Available float64 `json:"available,string"` } // DepositAddress holds assigned deposit address for a specific currency diff --git a/exchanges/gemini/gemini_wrapper.go b/exchanges/gemini/gemini_wrapper.go index 45489de5..7e678865 100644 --- a/exchanges/gemini/gemini_wrapper.go +++ b/exchanges/gemini/gemini_wrapper.go @@ -14,10 +14,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/deposit" "github.com/thrasher-corp/gocryptotrader/exchanges/fundingrate" @@ -215,41 +215,21 @@ func (e *Exchange) UpdateTradablePairs(ctx context.Context) error { return e.EnsureOnePairEnabled() } -// UpdateAccountInfo Retrieves balances for all enabled currencies for the -// Gemini exchange -func (e *Exchange) UpdateAccountInfo(ctx context.Context, assetType asset.Item) (account.Holdings, error) { - var response account.Holdings - response.Exchange = e.Name - accountBalance, err := e.GetBalances(ctx) +// UpdateAccountBalances retrieves currency balances +func (e *Exchange) UpdateAccountBalances(ctx context.Context, assetType asset.Item) (accounts.SubAccounts, error) { + resp, err := e.GetBalances(ctx) if err != nil { - return response, err + return nil, err } - - currencies := make([]account.Balance, len(accountBalance)) - for i := range accountBalance { - currencies[i] = account.Balance{ - Currency: currency.NewCode(accountBalance[i].Currency), - Total: accountBalance[i].Amount, - Hold: accountBalance[i].Amount - accountBalance[i].Available, - Free: accountBalance[i].Available, - } + subAccts := accounts.SubAccounts{accounts.NewSubAccount(assetType, "")} + for i := range resp { + subAccts[0].Balances.Set(resp[i].Currency, accounts.Balance{ + Total: resp[i].Amount, + Hold: resp[i].Amount - resp[i].Available, + Free: resp[i].Available, + }) } - - response.Accounts = append(response.Accounts, account.SubAccount{ - AssetType: assetType, - Currencies: currencies, - }) - - creds, err := e.GetCredentials(ctx) - if err != nil { - return account.Holdings{}, err - } - err = account.Process(&response, creds) - if err != nil { - return account.Holdings{}, err - } - - return response, nil + return subAccts, e.Accounts.Save(ctx, subAccts, true) } // UpdateTickers updates the ticker for all currency pairs of a given asset type @@ -758,10 +738,9 @@ func (e *Exchange) GetOrderHistory(ctx context.Context, req *order.MultiOrderReq return req.Filter(e.Name, orders), 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) } diff --git a/exchanges/hitbtc/hitbtc.go b/exchanges/hitbtc/hitbtc.go index a382ddb1..8229b1dd 100644 --- a/exchanges/hitbtc/hitbtc.go +++ b/exchanges/hitbtc/hitbtc.go @@ -202,21 +202,13 @@ func (e *Exchange) GetCandles(ctx context.Context, currencyPair, limit, period s // https://api.hitbtc.com/?python#market-data // GetBalances returns full balance for your account -func (e *Exchange) GetBalances(ctx context.Context) (map[string]Balance, error) { +func (e *Exchange) GetBalances(ctx context.Context) (map[currency.Code]Balance, error) { var result []Balance - err := e.SendAuthenticatedHTTPRequest(ctx, - exchange.RestSpot, - http.MethodGet, - apiV2Balance, - url.Values{}, - otherRequests, - &result) - ret := make(map[string]Balance) - - if err != nil { - return ret, err + if err := e.SendAuthenticatedHTTPRequest(ctx, exchange.RestSpot, http.MethodGet, apiV2Balance, url.Values{}, otherRequests, &result); err != nil { + return nil, err } + ret := make(map[currency.Code]Balance) for _, item := range result { ret[item.Currency] = item } diff --git a/exchanges/hitbtc/hitbtc_types.go b/exchanges/hitbtc/hitbtc_types.go index 50bf6a82..463b8c2f 100644 --- a/exchanges/hitbtc/hitbtc_types.go +++ b/exchanges/hitbtc/hitbtc_types.go @@ -96,9 +96,9 @@ type LoanOrders struct { // Balance is a simple balance type type Balance struct { - Currency string `json:"currency"` - Available float64 `json:"available,string"` // Amount available for trading or transfer to main account - Reserved float64 `json:"reserved,string"` // Amount reserved for active orders or incomplete transfers to main account + Currency currency.Code `json:"currency"` + Available float64 `json:"available,string"` // Amount available for trading or transfer to main account + Reserved float64 `json:"reserved,string"` // Amount reserved for active orders or incomplete transfers to main account } // DepositCryptoAddresses contains address information diff --git a/exchanges/hitbtc/hitbtc_wrapper.go b/exchanges/hitbtc/hitbtc_wrapper.go index 977599a0..1c1565e6 100644 --- a/exchanges/hitbtc/hitbtc_wrapper.go +++ b/exchanges/hitbtc/hitbtc_wrapper.go @@ -12,10 +12,10 @@ import ( "github.com/thrasher-corp/gocryptotrader/common" "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" "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/deposit" "github.com/thrasher-corp/gocryptotrader/exchanges/fundingrate" @@ -305,41 +305,21 @@ func (e *Exchange) UpdateOrderbook(ctx context.Context, c currency.Pair, assetTy return orderbook.Get(e.Name, c, assetType) } -// UpdateAccountInfo retrieves balances for all enabled currencies for the -// HitBTC exchange -func (e *Exchange) UpdateAccountInfo(ctx context.Context, assetType asset.Item) (account.Holdings, error) { - var response account.Holdings - response.Exchange = e.Name - accountBalance, err := e.GetBalances(ctx) +// UpdateAccountBalances retrieves currency balances +func (e *Exchange) UpdateAccountBalances(ctx context.Context, assetType asset.Item) (accounts.SubAccounts, error) { + resp, err := e.GetBalances(ctx) if err != nil { - return response, err + return nil, err } - - currencies := make([]account.Balance, 0, len(accountBalance)) - for i := range accountBalance { - currencies = append(currencies, account.Balance{ - Currency: currency.NewCode(accountBalance[i].Currency), - Total: accountBalance[i].Available + accountBalance[i].Reserved, - Hold: accountBalance[i].Reserved, - Free: accountBalance[i].Available, + subAccts := accounts.SubAccounts{accounts.NewSubAccount(assetType, "")} + for i := range resp { + subAccts[0].Balances.Set(resp[i].Currency, accounts.Balance{ + Total: resp[i].Available + resp[i].Reserved, + Hold: resp[i].Reserved, + Free: resp[i].Available, }) } - - response.Accounts = append(response.Accounts, account.SubAccount{ - AssetType: assetType, - Currencies: currencies, - }) - - creds, err := e.GetCredentials(ctx) - if err != nil { - return account.Holdings{}, err - } - err = account.Process(&response, creds) - if err != nil { - return account.Holdings{}, err - } - - return response, nil + return subAccts, e.Accounts.Save(ctx, subAccts, true) } // GetAccountFundingHistory returns funding history, deposits and @@ -741,10 +721,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) } diff --git a/exchanges/huobi/cfutures_types.go b/exchanges/huobi/cfutures_types.go index 8764d687..f33a3e5e 100644 --- a/exchanges/huobi/cfutures_types.go +++ b/exchanges/huobi/cfutures_types.go @@ -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"` } diff --git a/exchanges/huobi/futures_types.go b/exchanges/huobi/futures_types.go index 9b8c34de..b9c82042 100644 --- a/exchanges/huobi/futures_types.go +++ b/exchanges/huobi/futures_types.go @@ -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"` } diff --git a/exchanges/huobi/huobi_test.go b/exchanges/huobi/huobi_test.go index 903bc85b..aa352ba6 100644 --- a/exchanges/huobi/huobi_test.go +++ b/exchanges/huobi/huobi_test.go @@ -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) } } diff --git a/exchanges/huobi/huobi_types.go b/exchanges/huobi/huobi_types.go index c4383a02..a5199160 100644 --- a/exchanges/huobi/huobi_types.go +++ b/exchanges/huobi/huobi_types.go @@ -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. diff --git a/exchanges/huobi/huobi_websocket.go b/exchanges/huobi/huobi_websocket.go index 79234d77..591f2af5 100644 --- a/exchanges/huobi/huobi_websocket.go +++ b/exchanges/huobi/huobi_websocket.go @@ -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) diff --git a/exchanges/huobi/huobi_wrapper.go b/exchanges/huobi/huobi_wrapper.go index 128f7afe..d46ee37f 100644 --- a/exchanges/huobi/huobi_wrapper.go +++ b/exchanges/huobi/huobi_wrapper.go @@ -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) } diff --git a/exchanges/interfaces.go b/exchanges/interfaces.go index f05c1d36..4f06139b 100644 --- a/exchanges/interfaces.go +++ b/exchanges/interfaces.go @@ -8,9 +8,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/dispatch" + "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/exchanges/account" "github.com/thrasher-corp/gocryptotrader/exchanges/asset" "github.com/thrasher-corp/gocryptotrader/exchanges/collateral" "github.com/thrasher-corp/gocryptotrader/exchanges/currencystate" @@ -86,7 +87,7 @@ type IBotExchange interface { GetOrderExecutionLimits(a asset.Item, cp currency.Pair) (limits.MinMaxLevel, error) CheckOrderExecutionLimits(a asset.Item, cp currency.Pair, price, amount float64, orderType order.Type) error UpdateOrderExecutionLimits(ctx context.Context, a asset.Item) error - GetCredentials(ctx context.Context) (*account.Credentials, error) + GetCredentials(ctx context.Context) (*accounts.Credentials, error) EnsureOnePairEnabled() error PrintEnabledPairs() IsVerbose() bool @@ -98,10 +99,10 @@ type IBotExchange interface { // VerifyAPICredentials determines if the credentials supplied have unset // required values. See exchanges/credentials.go Base method for // implementation. - VerifyAPICredentials(creds *account.Credentials) error + VerifyAPICredentials(creds *accounts.Credentials) error // GetDefaultCredentials returns the exchange.Base api credentials loaded by // config.json. See exchanges/credentials.go Base method for implementation. - GetDefaultCredentials() *account.Credentials + GetDefaultCredentials() *accounts.Credentials FunctionalityChecker AccountManagement @@ -151,9 +152,11 @@ type CurrencyStateManagement interface { // AccountManagement defines functionality for exchange account management type AccountManagement interface { - UpdateAccountInfo(ctx context.Context, a asset.Item) (account.Holdings, error) - GetCachedAccountInfo(ctx context.Context, a asset.Item) (account.Holdings, error) + UpdateAccountBalances(ctx context.Context, a asset.Item) (accounts.SubAccounts, error) + GetCachedSubAccounts(ctx context.Context, a asset.Item) (accounts.SubAccounts, error) + GetCachedCurrencyBalances(ctx context.Context, a asset.Item) (accounts.CurrencyBalances, error) HasAssetTypeAccountSegregation() bool + SubscribeAccountBalances() (dispatch.Pipe, error) } // FunctionalityChecker defines functionality for retrieving exchange diff --git a/exchanges/kraken/futures_types.go b/exchanges/kraken/futures_types.go index 6a011e4b..7a1a2ae2 100644 --- a/exchanges/kraken/futures_types.go +++ b/exchanges/kraken/futures_types.go @@ -421,8 +421,8 @@ type FuturesNotificationData struct { // FuturesAccountsData stores account data type FuturesAccountsData struct { - ServerTime string `json:"serverTime"` - Accounts map[string]AccountsData `json:"accounts"` + ServerTime string `json:"serverTime"` + Accounts map[string]*AccountsData `json:"accounts"` } // AccountsData stores data of an account diff --git a/exchanges/kraken/kraken_test.go b/exchanges/kraken/kraken_test.go index bdb71965..ec779973 100644 --- a/exchanges/kraken/kraken_test.go +++ b/exchanges/kraken/kraken_test.go @@ -729,17 +729,17 @@ func TestCancelAllExchangeOrders(t *testing.T) { assert.Empty(t, resp.Status, "CancelAllOrders Status should not contain any failed order errors") } -// TestUpdateAccountInfo exercises UpdateAccountInfo -func TestUpdateAccountInfo(t *testing.T) { +// TestUpdateAccountBalances exercises UpdateAccountBalances +func TestUpdateAccountBalances(t *testing.T) { t.Parallel() for _, a := range []asset.Item{asset.Spot, asset.Futures} { - _, err := e.UpdateAccountInfo(t.Context(), a) + _, err := e.UpdateAccountBalances(t.Context(), a) if sharedtestvalues.AreAPICredentialsSet(e) { - assert.NoErrorf(t, err, "UpdateAccountInfo should not error for asset %s", a) // Note Well: Spot and Futures have separate api keys + assert.NoErrorf(t, err, "UpdateAccountBalances should not error for asset %s", a) // Note Well: Spot and Futures have separate api keys } else { - assert.ErrorIsf(t, err, exchange.ErrAuthenticationSupportNotEnabled, "UpdateAccountInfo should error correctly for asset %s", a) + assert.ErrorIsf(t, err, exchange.ErrAuthenticationSupportNotEnabled, "UpdateAccountBalances should error correctly for asset %s", a) } } } diff --git a/exchanges/kraken/kraken_wrapper.go b/exchanges/kraken/kraken_wrapper.go index 62f68047..d4163514 100644 --- a/exchanges/kraken/kraken_wrapper.go +++ b/exchanges/kraken/kraken_wrapper.go @@ -14,11 +14,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/deposit" "github.com/thrasher-corp/gocryptotrader/exchanges/fundingrate" @@ -511,69 +511,46 @@ func (e *Exchange) UpdateOrderbook(ctx context.Context, p currency.Pair, assetTy return orderbook.Get(e.Name, p, assetType) } -// UpdateAccountInfo retrieves balances for all enabled currencies for the -// Kraken exchange - to-do -func (e *Exchange) UpdateAccountInfo(ctx context.Context, assetType asset.Item) (account.Holdings, error) { - var info account.Holdings - var balances []account.Balance - info.Exchange = e.Name +// UpdateAccountBalances retrieves currency balances +func (e *Exchange) UpdateAccountBalances(ctx context.Context, assetType asset.Item) (subAccts accounts.SubAccounts, err error) { if !assetTranslator.Seeded() { if err := e.SeedAssets(ctx); err != nil { - return info, err + return nil, err } } switch assetType { case asset.Spot: - bal, err := e.GetBalance(ctx) + resp, err := e.GetBalance(ctx) if err != nil { - return info, err + return nil, err } - for key := range bal { - translatedCurrency := assetTranslator.LookupAltName(key) - if translatedCurrency == "" { - log.Warnf(log.ExchangeSys, "%s unable to translate currency: %s\n", - e.Name, - key) + subAccts = accounts.SubAccounts{accounts.NewSubAccount(assetType, "")} + for key, bal := range resp { + c := assetTranslator.LookupAltName(key) + if c == "" { + log.Warnf(log.ExchangeSys, "%s unable to translate currency: %s", e.Name, key) continue } - balances = append(balances, account.Balance{ - Currency: currency.NewCode(translatedCurrency), - Total: bal[key].Total, - Hold: bal[key].Hold, - Free: bal[key].Total - bal[key].Hold, + subAccts[0].Balances.Set(currency.NewCode(c), accounts.Balance{ + Total: bal.Total, + Hold: bal.Hold, + Free: bal.Total - bal.Hold, }) } - info.Accounts = append(info.Accounts, account.SubAccount{ - Currencies: balances, - AssetType: assetType, - }) case asset.Futures: - bal, err := e.GetFuturesAccountData(ctx) + resp, err := e.GetFuturesAccountData(ctx) if err != nil { - return info, err + return nil, err } - for name := range bal.Accounts { - for code := range bal.Accounts[name].Balances { - balances = append(balances, account.Balance{ - Currency: currency.NewCode(code).Upper(), - Total: bal.Accounts[name].Balances[code], - }) + for name, v := range resp.Accounts { + a := accounts.NewSubAccount(assetType, name) + for curr, bal := range v.Balances { + a.Balances.Set(currency.NewCode(curr), accounts.Balance{Total: bal}) } - info.Accounts = append(info.Accounts, account.SubAccount{ - ID: name, - AssetType: asset.Futures, - Currencies: balances, - }) + subAccts = subAccts.Merge(a) } } - creds, err := e.GetCredentials(ctx) - if err != nil { - return account.Holdings{}, 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 @@ -1407,10 +1384,9 @@ func (e *Exchange) AuthenticateWebsocket(ctx context.Context) error { return 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) } diff --git a/exchanges/kucoin/kucoin_futures_types.go b/exchanges/kucoin/kucoin_futures_types.go index 324f2e3b..9400e445 100644 --- a/exchanges/kucoin/kucoin_futures_types.go +++ b/exchanges/kucoin/kucoin_futures_types.go @@ -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 diff --git a/exchanges/kucoin/kucoin_test.go b/exchanges/kucoin/kucoin_test.go index d15e384c..349ef46e 100644 --- a/exchanges/kucoin/kucoin_test.go +++ b/exchanges/kucoin/kucoin_test.go @@ -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) } diff --git a/exchanges/kucoin/kucoin_types.go b/exchanges/kucoin/kucoin_types.go index 59e5d1c5..af0af324 100644 --- a/exchanges/kucoin/kucoin_types.go +++ b/exchanges/kucoin/kucoin_types.go @@ -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:,main、trade、trade_hf、margin - 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 diff --git a/exchanges/kucoin/kucoin_websocket.go b/exchanges/kucoin/kucoin_websocket.go index f266e1c0..f7500847 100644 --- a/exchanges/kucoin/kucoin_websocket.go +++ b/exchanges/kucoin/kucoin_websocket.go @@ -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. diff --git a/exchanges/kucoin/kucoin_wrapper.go b/exchanges/kucoin/kucoin_wrapper.go index e57e359e..d43ddde5 100644 --- a/exchanges/kucoin/kucoin_wrapper.go +++ b/exchanges/kucoin/kucoin_wrapper.go @@ -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) } diff --git a/exchanges/kucoin/testdata/wsHandleData.json b/exchanges/kucoin/testdata/wsHandleData.json index 85d400b1..9275d4fb 100644 --- a/exchanges/kucoin/testdata/wsHandleData.json +++ b/exchanges/kucoin/testdata/wsHandleData.json @@ -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}} diff --git a/exchanges/lbank/lbank_test.go b/exchanges/lbank/lbank_test.go index dab9e589..f0feeca3 100644 --- a/exchanges/lbank/lbank_test.go +++ b/exchanges/lbank/lbank_test.go @@ -21,9 +21,9 @@ import ( "github.com/stretchr/testify/require" "github.com/thrasher-corp/gocryptotrader/common" "github.com/thrasher-corp/gocryptotrader/currency" + "github.com/thrasher-corp/gocryptotrader/exchange/accounts" "github.com/thrasher-corp/gocryptotrader/exchange/order/limits" 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" @@ -227,31 +227,30 @@ func TestLoadPrivKey(t *testing.T) { e.SetDefaults() require.ErrorIs(t, e.loadPrivKey(t.Context()), exchange.ErrCredentialsAreEmpty) - ctx := account.DeployCredentialsToContext(t.Context(), &account.Credentials{Key: "test", Secret: "errortest"}) + ctx := accounts.DeployCredentialsToContext(t.Context(), &accounts.Credentials{Key: "test", Secret: "errortest"}) assert.ErrorIs(t, e.loadPrivKey(ctx), errPEMBlockIsNil) key, err := rsa.GenerateKey(rand.Reader, 2048) require.NoError(t, err) der := x509.MarshalPKCS1PrivateKey(key) - ctx = account.DeployCredentialsToContext(t.Context(), &account.Credentials{Key: "test", Secret: base64.StdEncoding.EncodeToString(der)}) + ctx = accounts.DeployCredentialsToContext(t.Context(), &accounts.Credentials{Key: "test", Secret: base64.StdEncoding.EncodeToString(der)}) require.ErrorIs(t, e.loadPrivKey(ctx), errUnableToParsePrivateKey) ecdsaKey, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) require.NoError(t, err) der, err = x509.MarshalPKCS8PrivateKey(ecdsaKey) require.NoError(t, err) - ctx = account.DeployCredentialsToContext(t.Context(), &account.Credentials{Key: "test", Secret: base64.StdEncoding.EncodeToString(der)}) + ctx = accounts.DeployCredentialsToContext(t.Context(), &accounts.Credentials{Key: "test", Secret: base64.StdEncoding.EncodeToString(der)}) require.ErrorIs(t, e.loadPrivKey(ctx), common.ErrTypeAssertFailure) key, err = rsa.GenerateKey(rand.Reader, 2048) require.NoError(t, err) der, err = x509.MarshalPKCS8PrivateKey(key) require.NoError(t, err) - ctx = account.DeployCredentialsToContext(t.Context(), &account.Credentials{Key: "test", Secret: base64.StdEncoding.EncodeToString(der)}) + ctx = accounts.DeployCredentialsToContext(t.Context(), &accounts.Credentials{Key: "test", Secret: base64.StdEncoding.EncodeToString(der)}) assert.NoError(t, e.loadPrivKey(ctx), "loadPrivKey should not error") sharedtestvalues.SkipTestIfCredentialsUnset(t, e) - assert.NoError(t, e.loadPrivKey(t.Context()), "loadPrivKey should not error") } @@ -350,8 +349,8 @@ func TestGetAccountInfo(t *testing.T) { t.Parallel() sharedtestvalues.SkipTestIfCredentialsUnset(t, e) - _, err := e.UpdateAccountInfo(t.Context(), asset.Spot) - assert.NoError(t, err, "UpdateAccountInfo should not error") + _, err := e.UpdateAccountBalances(t.Context(), asset.Spot) + assert.NoError(t, err, "UpdateAccountBalances should not error") } func TestGetActiveOrders(t *testing.T) { diff --git a/exchanges/lbank/lbank_wrapper.go b/exchanges/lbank/lbank_wrapper.go index d5e4d15e..4f108b9f 100644 --- a/exchanges/lbank/lbank_wrapper.go +++ b/exchanges/lbank/lbank_wrapper.go @@ -11,8 +11,8 @@ import ( "github.com/thrasher-corp/gocryptotrader/common" "github.com/thrasher-corp/gocryptotrader/config" "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/account" "github.com/thrasher-corp/gocryptotrader/exchanges/asset" "github.com/thrasher-corp/gocryptotrader/exchanges/deposit" "github.com/thrasher-corp/gocryptotrader/exchanges/fundingrate" @@ -224,42 +224,27 @@ func (e *Exchange) UpdateOrderbook(ctx context.Context, p currency.Pair, assetTy return orderbook.Get(e.Name, p, assetType) } -// UpdateAccountInfo retrieves balances for all enabled currencies for the -// Lbank exchange -func (e *Exchange) UpdateAccountInfo(ctx context.Context, assetType asset.Item) (account.Holdings, error) { - var info account.Holdings - data, err := e.GetUserInfo(ctx) +// UpdateAccountBalances retrieves currency balances +func (e *Exchange) UpdateAccountBalances(ctx context.Context, assetType asset.Item) (accounts.SubAccounts, error) { + resp, err := e.GetUserInfo(ctx) if err != nil { - return info, err + return nil, err } - acc := account.SubAccount{AssetType: assetType} - for key, val := range data.Info.Asset { - hold, ok := data.Info.Freeze[key] + subAccts := accounts.SubAccounts{accounts.NewSubAccount(assetType, "")} + for k, val := range resp.Info.Asset { + hold, ok := resp.Info.Freeze[k] if !ok { - return info, fmt.Errorf("hold data not found with %s", key) + return nil, fmt.Errorf("hold data not found with %s", k) } totalVal := val.Float64() totalHold := hold.Float64() - acc.Currencies = append(acc.Currencies, account.Balance{ - Currency: currency.NewCode(key), - Total: totalVal, - Hold: totalHold, - Free: totalVal - totalHold, + subAccts[0].Balances.Set(currency.NewCode(k), accounts.Balance{ + Total: totalVal, + Hold: totalHold, + Free: totalVal - totalHold, }) } - - info.Accounts = append(info.Accounts, acc) - info.Exchange = e.Name - - creds, err := e.GetCredentials(ctx) - if err != nil { - return account.Holdings{}, err - } - err = account.Process(&info, creds) - if err != nil { - return account.Holdings{}, err - } - return info, nil + return subAccts, e.Accounts.Save(ctx, subAccts, true) } // GetAccountFundingHistory returns funding history, deposits and @@ -771,10 +756,9 @@ func (e *Exchange) getAllOpenOrderID(ctx context.Context) (map[string][]string, return resp, 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) } diff --git a/exchanges/okx/okx_test.go b/exchanges/okx/okx_test.go index 60a27458..d37bbfb4 100644 --- a/exchanges/okx/okx_test.go +++ b/exchanges/okx/okx_test.go @@ -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) + } } } diff --git a/exchanges/okx/okx_types.go b/exchanges/okx/okx_types.go index b950e9d8..683d998c 100644 --- a/exchanges/okx/okx_types.go +++ b/exchanges/okx/okx_types.go @@ -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 diff --git a/exchanges/okx/okx_websocket.go b/exchanges/okx/okx_websocket.go index f06ce55f..45adf526 100644 --- a/exchanges/okx/okx_websocket.go +++ b/exchanges/okx/okx_websocket.go @@ -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 diff --git a/exchanges/okx/okx_wrapper.go b/exchanges/okx/okx_wrapper.go index 45cc2b05..ce09087d 100644 --- a/exchanges/okx/okx_wrapper.go +++ b/exchanges/okx/okx_wrapper.go @@ -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) } diff --git a/exchanges/poloniex/poloniex.go b/exchanges/poloniex/poloniex.go index ca113576..ab01898f 100644 --- a/exchanges/poloniex/poloniex.go +++ b/exchanges/poloniex/poloniex.go @@ -223,35 +223,32 @@ func (e *Exchange) GetLoanOrders(ctx context.Context, ccy string) (LoanOrders, e } // GetBalances returns balances for your account. -func (e *Exchange) GetBalances(ctx context.Context) (Balance, error) { +func (e *Exchange) GetBalances(ctx context.Context) (map[currency.Code]float64, error) { var result any if err := e.SendAuthenticatedHTTPRequest(ctx, exchange.RestSpot, http.MethodPost, poloniexBalances, url.Values{}, &result); err != nil { - return Balance{}, err + return nil, err } data, ok := result.(map[string]any) if !ok { - return Balance{}, common.GetTypeAssertError("map[string]any", result, "balance result") + return nil, common.GetTypeAssertError("map[string]any", result, "balance result") } - balance := Balance{ - Currency: make(map[string]float64), - } + bals := make(map[currency.Code]float64, len(data)) for x, y := range data { bal, ok := y.(string) if !ok { - return Balance{}, common.GetTypeAssertError("string", y, "balance amount") + return nil, common.GetTypeAssertError("string", y, "balance amount") } var err error - balance.Currency[x], err = strconv.ParseFloat(bal, 64) - if err != nil { - return Balance{}, err + if bals[currency.NewCode(x)], err = strconv.ParseFloat(bal, 64); err != nil { + return nil, err } } - return balance, nil + return bals, nil } // GetCompleteBalances returns complete balances from your account. diff --git a/exchanges/poloniex/poloniex_test.go b/exchanges/poloniex/poloniex_test.go index dd009cf8..eea1b856 100644 --- a/exchanges/poloniex/poloniex_test.go +++ b/exchanges/poloniex/poloniex_test.go @@ -12,6 +12,7 @@ import ( "github.com/thrasher-corp/gocryptotrader/common" "github.com/thrasher-corp/gocryptotrader/core" "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/asset" @@ -550,7 +551,7 @@ func TestWsAuth(t *testing.T) { if err != nil { t.Fatal(err) } - go e.wsReadData() + go e.wsReadData(t.Context()) creds, err := e.GetCredentials(t.Context()) if err != nil { t.Fatal(err) @@ -570,7 +571,7 @@ func TestWsAuth(t *testing.T) { func TestWsSubAck(t *testing.T) { pressXToJSON := []byte(`[1002, 1]`) - err := e.wsHandleData(pressXToJSON) + err := e.wsHandleData(t.Context(), pressXToJSON) if err != nil { t.Error(err) } @@ -582,7 +583,7 @@ func TestWsTicker(t *testing.T) { t.Error(err) } pressXToJSON := []byte(`[1002, null, [ 50, "382.98901522", "381.99755898", "379.41296309", "-0.04312950", "14969820.94951828", "38859.58435407", 0, "412.25844455", "364.56122072" ] ]`) - err = e.wsHandleData(pressXToJSON) + err = e.wsHandleData(t.Context(), pressXToJSON) if err != nil { t.Error(err) } @@ -594,7 +595,7 @@ func TestWsExchangeVolume(t *testing.T) { t.Error(err) } pressXToJSON := []byte(`[1003,null,["2018-11-07 16:26",5804,{"BTC":"3418.409","ETH":"2645.921","USDT":"10832502.689","USDC":"1578020.908"}]]`) - err = e.wsHandleData(pressXToJSON) + err = e.wsHandleData(t.Context(), pressXToJSON) if err != nil { t.Error(err) } @@ -607,7 +608,7 @@ func TestWsTrades(t *testing.T) { t.Error(err) } pressXToJSON := []byte(`[14, 8768, [["t", "42706057", 1, "0.05567134", "0.00181421", 1522877119]]]`) - err = e.wsHandleData(pressXToJSON) + err = e.wsHandleData(t.Context(), pressXToJSON) if err != nil { t.Error(err) } @@ -619,13 +620,13 @@ func TestWsPriceAggregateOrderbook(t *testing.T) { t.Error(err) } pressXToJSON := []byte(`[50,141160924,[["i",{"currencyPair":"BTC_LTC","orderBook":[{"0.002784":"17.55","0.002786":"1.47","0.002792":"13.25","0.0028":"0.21","0.002804":"0.02","0.00281":"1.5","0.002811":"258.82","0.002812":"3.81","0.002817":"0.06","0.002824":"3","0.002825":"0.02","0.002836":"18.01","0.002837":"0.03","0.00284":"0.03","0.002842":"12.7","0.00285":"0.02","0.002852":"0.02","0.002855":"1.3","0.002857":"15.64","0.002864":"0.01"},{"0.002782":"45.93","0.002781":"1.46","0.002774":"13.34","0.002773":"0.04","0.002771":"0.05","0.002765":"6.21","0.002764":"3","0.00276":"10.77","0.002758":"3.11","0.002754":"0.02","0.002751":"288.94","0.00275":"24.06","0.002745":"187.27","0.002743":"0.04","0.002742":"0.96","0.002731":"0.06","0.00273":"12.13","0.002727":"0.02","0.002725":"0.03","0.002719":"1.09"}]}, "1692080077892"]]]`) - err = e.wsHandleData(pressXToJSON) + err = e.wsHandleData(t.Context(), pressXToJSON) if err != nil { t.Error(err) } pressXToJSON = []byte(`[50,141160925,[["o",1,"0.002742","0", "1692080078806"],["o",1,"0.002718","0.02", "1692080078806"]]]`) - err = e.wsHandleData(pressXToJSON) + err = e.wsHandleData(t.Context(), pressXToJSON) if err != nil { t.Error(err) } @@ -675,23 +676,23 @@ func TestProcessAccountMarginPosition(t *testing.T) { } margin := []byte(`[1000,"",[["m", 23432933, 28, "-0.06000000"]]]`) - err = e.wsHandleData(margin) + err = e.wsHandleData(t.Context(), margin) require.ErrorIs(t, err, errNotEnoughData) margin = []byte(`[1000,"",[["m", "23432933", 28, "-0.06000000", null]]]`) - err = e.wsHandleData(margin) + err = e.wsHandleData(t.Context(), margin) require.ErrorIs(t, err, errTypeAssertionFailure) margin = []byte(`[1000,"",[["m", 23432933, "28", "-0.06000000", null]]]`) - err = e.wsHandleData(margin) + err = e.wsHandleData(t.Context(), margin) require.ErrorIs(t, err, errTypeAssertionFailure) margin = []byte(`[1000,"",[["m", 23432933, 28, -0.06000000, null]]]`) - err = e.wsHandleData(margin) + err = e.wsHandleData(t.Context(), margin) require.ErrorIs(t, err, errTypeAssertionFailure) margin = []byte(`[1000,"",[["m", 23432933, 28, "-0.06000000", null]]]`) - err = e.wsHandleData(margin) + err = e.wsHandleData(t.Context(), margin) if err != nil { t.Fatal(err) } @@ -704,38 +705,38 @@ func TestProcessAccountPendingOrder(t *testing.T) { } pending := []byte(`[1000,"",[["p",431682155857,127,"1000.00000000","1.00000000","0"]]]`) - err = e.wsHandleData(pending) + err = e.wsHandleData(t.Context(), pending) require.ErrorIs(t, err, errNotEnoughData) pending = []byte(`[1000,"",[["p","431682155857",127,"1000.00000000","1.00000000","0",null]]]`) - err = e.wsHandleData(pending) + err = e.wsHandleData(t.Context(), pending) require.ErrorIs(t, err, errTypeAssertionFailure) pending = []byte(`[1000,"",[["p",431682155857,"127","1000.00000000","1.00000000","0",null]]]`) - err = e.wsHandleData(pending) + err = e.wsHandleData(t.Context(), pending) require.ErrorIs(t, err, errTypeAssertionFailure) pending = []byte(`[1000,"",[["p",431682155857,127,1000.00000000,"1.00000000","0",null]]]`) - err = e.wsHandleData(pending) + err = e.wsHandleData(t.Context(), pending) require.ErrorIs(t, err, errTypeAssertionFailure) pending = []byte(`[1000,"",[["p",431682155857,127,"1000.00000000",1.00000000,"0",null]]]`) - err = e.wsHandleData(pending) + err = e.wsHandleData(t.Context(), pending) require.ErrorIs(t, err, errTypeAssertionFailure) pending = []byte(`[1000,"",[["p",431682155857,127,"1000.00000000","1.00000000",0,null]]]`) - err = e.wsHandleData(pending) + err = e.wsHandleData(t.Context(), pending) require.ErrorIs(t, err, errTypeAssertionFailure) pending = []byte(`[1000,"",[["p",431682155857,127,"1000.00000000","1.00000000","0",null]]]`) - err = e.wsHandleData(pending) + err = e.wsHandleData(t.Context(), pending) if err != nil { t.Fatal(err) } // Unmatched pair in system pending = []byte(`[1000,"",[["p",431682155857,666,"1000.00000000","1.00000000","0",null]]]`) - err = e.wsHandleData(pending) + err = e.wsHandleData(t.Context(), pending) if err != nil { t.Fatal(err) } @@ -743,45 +744,45 @@ func TestProcessAccountPendingOrder(t *testing.T) { func TestProcessAccountOrderUpdate(t *testing.T) { orderUpdate := []byte(`[1000,"",[["o",431682155857,"0.00000000","f"]]]`) - err := e.wsHandleData(orderUpdate) + err := e.wsHandleData(t.Context(), orderUpdate) require.ErrorIs(t, err, errNotEnoughData) orderUpdate = []byte(`[1000,"",[["o","431682155857","0.00000000","f",null]]]`) - err = e.wsHandleData(orderUpdate) + err = e.wsHandleData(t.Context(), orderUpdate) require.ErrorIs(t, err, errTypeAssertionFailure) orderUpdate = []byte(`[1000,"",[["o",431682155857,0.00000000,"f",null]]]`) - err = e.wsHandleData(orderUpdate) + err = e.wsHandleData(t.Context(), orderUpdate) require.ErrorIs(t, err, errTypeAssertionFailure) orderUpdate = []byte(`[1000,"",[["o",431682155857,"0.00000000",123,null]]]`) - err = e.wsHandleData(orderUpdate) + err = e.wsHandleData(t.Context(), orderUpdate) require.ErrorIs(t, err, errTypeAssertionFailure) orderUpdate = []byte(`[1000,"",[["o",431682155857,"0.00000000","c",null]]]`) - err = e.wsHandleData(orderUpdate) + err = e.wsHandleData(t.Context(), orderUpdate) require.ErrorIs(t, err, errNotEnoughData) orderUpdate = []byte(`[1000,"",[["o",431682155857,"0.50000000","c",null,"0.50000000"]]]`) - err = e.wsHandleData(orderUpdate) + err = e.wsHandleData(t.Context(), orderUpdate) if err != nil { t.Fatal(err) } orderUpdate = []byte(`[1000,"",[["o",431682155857,"0.00000000","c",null,"1.00000000"]]]`) - err = e.wsHandleData(orderUpdate) + err = e.wsHandleData(t.Context(), orderUpdate) if err != nil { t.Fatal(err) } orderUpdate = []byte(`[1000,"",[["o",431682155857,"0.50000000","f",null]]]`) - err = e.wsHandleData(orderUpdate) + err = e.wsHandleData(t.Context(), orderUpdate) if err != nil { t.Fatal(err) } orderUpdate = []byte(`[1000,"",[["o",431682155857,"0.00000000","s",null]]]`) - err = e.wsHandleData(orderUpdate) + err = e.wsHandleData(t.Context(), orderUpdate) if err != nil { t.Fatal(err) } @@ -794,39 +795,39 @@ func TestProcessAccountOrderLimit(t *testing.T) { } accountTrade := []byte(`[1000,"",[["n",127,431682155857,"0","1000.00000000","1.00000000","2021-04-13 07:19:56","1.00000000"]]]`) - err = e.wsHandleData(accountTrade) + err = e.wsHandleData(t.Context(), accountTrade) require.ErrorIs(t, err, errNotEnoughData) accountTrade = []byte(`[1000,"",[["n","127",431682155857,"0","1000.00000000","1.00000000","2021-04-13 07:19:56","1.00000000",null]]]`) - err = e.wsHandleData(accountTrade) + err = e.wsHandleData(t.Context(), accountTrade) require.ErrorIs(t, err, errTypeAssertionFailure) accountTrade = []byte(`[1000,"",[["n",127,"431682155857","0","1000.00000000","1.00000000","2021-04-13 07:19:56","1.00000000",null]]]`) - err = e.wsHandleData(accountTrade) + err = e.wsHandleData(t.Context(), accountTrade) require.ErrorIs(t, err, errTypeAssertionFailure) accountTrade = []byte(`[1000,"",[["n",127,431682155857,0,"1000.00000000","1.00000000","2021-04-13 07:19:56","1.00000000",null]]]`) - err = e.wsHandleData(accountTrade) + err = e.wsHandleData(t.Context(), accountTrade) require.ErrorIs(t, err, errTypeAssertionFailure) accountTrade = []byte(`[1000,"",[["n",127,431682155857,"0",1000.00000000,"1.00000000","2021-04-13 07:19:56","1.00000000",null]]]`) - err = e.wsHandleData(accountTrade) + err = e.wsHandleData(t.Context(), accountTrade) require.ErrorIs(t, err, errTypeAssertionFailure) accountTrade = []byte(`[1000,"",[["n",127,431682155857,"0","1000.00000000",1.00000000,"2021-04-13 07:19:56","1.00000000",null]]]`) - err = e.wsHandleData(accountTrade) + err = e.wsHandleData(t.Context(), accountTrade) require.ErrorIs(t, err, errTypeAssertionFailure) accountTrade = []byte(`[1000,"",[["n",127,431682155857,"0","1000.00000000","1.00000000",1234,"1.00000000",null]]]`) - err = e.wsHandleData(accountTrade) + err = e.wsHandleData(t.Context(), accountTrade) require.ErrorIs(t, err, errTypeAssertionFailure) accountTrade = []byte(`[1000,"",[["n",127,431682155857,"0","1000.00000000","1.00000000","2021-04-13 07:19:56",1.00000000,null]]]`) - err = e.wsHandleData(accountTrade) + err = e.wsHandleData(t.Context(), accountTrade) require.ErrorIs(t, err, errTypeAssertionFailure) accountTrade = []byte(`[1000,"",[["n",127,431682155857,"0","1000.00000000","1.00000000","2021-04-13 07:19:56","1.00000000",null]]]`) - err = e.wsHandleData(accountTrade) + err = e.wsHandleData(t.Context(), accountTrade) if err != nil { t.Fatal(err) } @@ -839,59 +840,58 @@ func TestProcessAccountBalanceUpdate(t *testing.T) { } balance := []byte(`[1000,"",[["b",243,"e"]]]`) - err = e.wsHandleData(balance) + err = e.wsHandleData(t.Context(), balance) require.ErrorIs(t, err, errNotEnoughData) balance = []byte(`[1000,"",[["b","243","e","-1.00000000"]]]`) - err = e.wsHandleData(balance) + err = e.wsHandleData(t.Context(), balance) require.ErrorIs(t, err, errTypeAssertionFailure) balance = []byte(`[1000,"",[["b",243,1234,"-1.00000000"]]]`) - err = e.wsHandleData(balance) + err = e.wsHandleData(t.Context(), balance) require.ErrorIs(t, err, errTypeAssertionFailure) balance = []byte(`[1000,"",[["b",243,"e",-1.00000000]]]`) - err = e.wsHandleData(balance) + err = e.wsHandleData(t.Context(), balance) require.ErrorIs(t, err, errTypeAssertionFailure) + ctx := accounts.DeployCredentialsToContext(t.Context(), &accounts.Credentials{Key: "test", Secret: "test"}) balance = []byte(`[1000,"",[["b",243,"e","-1.00000000"]]]`) - err = e.wsHandleData(balance) - if err != nil { - t.Fatal(err) - } + err = e.wsHandleData(ctx, balance) + require.NoError(t, err, "wsHandleData must not error") } func TestProcessAccountTrades(t *testing.T) { accountTrades := []byte(`[1000,"",[["t", 12345, "0.03000000", "0.50000000", "0.00250000", 0, 6083059, "0.00000375", "2018-09-08 05:54:09", "12345"]]]`) - err := e.wsHandleData(accountTrades) + err := e.wsHandleData(t.Context(), accountTrades) require.ErrorIs(t, err, errNotEnoughData) accountTrades = []byte(`[1000,"",[["t", "12345", "0.03000000", "0.50000000", "0.00250000", 0, 6083059, "0.00000375", "2018-09-08 05:54:09", "12345", "0.015"]]]`) - err = e.wsHandleData(accountTrades) + err = e.wsHandleData(t.Context(), accountTrades) require.ErrorIs(t, err, errTypeAssertionFailure) accountTrades = []byte(`[1000,"",[["t", 12345, 0.03000000, "0.50000000", "0.00250000", 0, 6083059, "0.00000375", "2018-09-08 05:54:09", "12345", "0.015"]]]`) - err = e.wsHandleData(accountTrades) + err = e.wsHandleData(t.Context(), accountTrades) require.ErrorIs(t, err, errTypeAssertionFailure) accountTrades = []byte(`[1000,"",[["t", 12345, "0.03000000", 0.50000000, "0.00250000", 0, 6083059, "0.00000375", "2018-09-08 05:54:09", "12345", "0.015"]]]`) - err = e.wsHandleData(accountTrades) + err = e.wsHandleData(t.Context(), accountTrades) require.ErrorIs(t, err, errTypeAssertionFailure) accountTrades = []byte(`[1000,"",[["t", 12345, "0.03000000", "0.50000000", "0.00250000", 0, 6083059, 0.00000375, "2018-09-08 05:54:09", "12345", "0.015"]]]`) - err = e.wsHandleData(accountTrades) + err = e.wsHandleData(t.Context(), accountTrades) require.ErrorIs(t, err, errTypeAssertionFailure) accountTrades = []byte(`[1000,"",[["t", 12345, "0.03000000", "0.50000000", "0.00250000", 0, 6083059, 0.0000037, "2018-09-08 05:54:09", "12345", "0.015"]]]`) - err = e.wsHandleData(accountTrades) + err = e.wsHandleData(t.Context(), accountTrades) require.ErrorIs(t, err, errTypeAssertionFailure) accountTrades = []byte(`[1000,"",[["t", 12345, "0.03000000", "0.50000000", "0.00250000", 0, 6083059, "0.00000375", 12345, "12345", 0.015]]]`) - err = e.wsHandleData(accountTrades) + err = e.wsHandleData(t.Context(), accountTrades) require.ErrorIs(t, err, errTypeAssertionFailure) accountTrades = []byte(`[1000,"",[["t", 12345, "0.03000000", "0.50000000", "0.00250000", 0, 6083059, "0.00000375", "2018-09-08 05:54:09", "12345", "0.015"]]]`) - err = e.wsHandleData(accountTrades) + err = e.wsHandleData(t.Context(), accountTrades) if err != nil { t.Fatal(err) } @@ -899,15 +899,15 @@ func TestProcessAccountTrades(t *testing.T) { func TestProcessAccountKilledOrder(t *testing.T) { kill := []byte(`[1000,"",[["k", 1337]]]`) - err := e.wsHandleData(kill) + err := e.wsHandleData(t.Context(), kill) require.ErrorIs(t, err, errNotEnoughData) kill = []byte(`[1000,"",[["k", "1337", null]]]`) - err = e.wsHandleData(kill) + err = e.wsHandleData(t.Context(), kill) require.ErrorIs(t, err, errTypeAssertionFailure) kill = []byte(`[1000,"",[["k", 1337, null]]]`) - err = e.wsHandleData(kill) + err = e.wsHandleData(t.Context(), kill) if err != nil { t.Fatal(err) } diff --git a/exchanges/poloniex/poloniex_types.go b/exchanges/poloniex/poloniex_types.go index 24cd16c2..e04eb7f0 100644 --- a/exchanges/poloniex/poloniex_types.go +++ b/exchanges/poloniex/poloniex_types.go @@ -144,11 +144,6 @@ type LoanOrders struct { Demands []LoanOrder `json:"demands"` } -// Balance holds data for a range of currencies -type Balance struct { - Currency map[string]float64 -} - // CompleteBalance contains the complete balance with a btcvalue type CompleteBalance struct { Available float64 `json:"available,string"` diff --git a/exchanges/poloniex/poloniex_websocket.go b/exchanges/poloniex/poloniex_websocket.go index cef38caa..688d222c 100644 --- a/exchanges/poloniex/poloniex_websocket.go +++ b/exchanges/poloniex/poloniex_websocket.go @@ -15,8 +15,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/order" "github.com/thrasher-corp/gocryptotrader/exchanges/orderbook" @@ -72,7 +72,7 @@ func (e *Exchange) WsConnect() error { } e.Websocket.Wg.Add(1) - go e.wsReadData() + go e.wsReadData(ctx) return nil } @@ -103,21 +103,21 @@ func (e *Exchange) loadCurrencyDetails(ctx context.Context) error { } // wsReadData handles data from the websocket connection -func (e *Exchange) wsReadData() { +func (e *Exchange) wsReadData(ctx context.Context) { defer e.Websocket.Wg.Done() for { resp := e.Websocket.Conn.ReadMessage() if resp.Raw == nil { return } - err := e.wsHandleData(resp.Raw) + err := e.wsHandleData(ctx, resp.Raw) if err != nil { e.Websocket.DataHandler <- fmt.Errorf("%s: %w", e.Name, err) } } } -func (e *Exchange) wsHandleData(respRaw []byte) error { +func (e *Exchange) wsHandleData(ctx context.Context, respRaw []byte) error { var result any err := json.Unmarshal(respRaw, &result) if err != nil { @@ -185,7 +185,7 @@ func (e *Exchange) wsHandleData(respRaw []byte) error { return fmt.Errorf("account notification limit order creation: %w", err) } case accountNotificationBalanceUpdate: - err = e.processAccountBalanceUpdate(notification) + err = e.processAccountBalanceUpdate(ctx, notification) if err != nil { return fmt.Errorf("account notification balance update: %w", err) } @@ -585,7 +585,7 @@ func (e *Exchange) Unsubscribe(subs subscription.List) error { } func (e *Exchange) manageSubs(ctx context.Context, subs subscription.List, op wsOp) error { - var creds *account.Credentials + var creds *accounts.Credentials if e.IsWebsocketAuthenticationSupported() { var err error creds, err = e.GetCredentials(ctx) @@ -918,7 +918,7 @@ func (e *Exchange) processAccountOrderLimit(notification []any) error { return nil } -func (e *Exchange) processAccountBalanceUpdate(notification []any) error { +func (e *Exchange) processAccountBalanceUpdate(ctx context.Context, notification []any) error { if len(notification) < 4 { return errNotEnoughData } @@ -927,7 +927,7 @@ func (e *Exchange) processAccountBalanceUpdate(notification []any) error { if !ok { return fmt.Errorf("%w currency ID not float64", errTypeAssertionFailure) } - code, err := e.details.GetCode(currencyID) + curr, err := e.details.GetCode(currencyID) if err != nil { return err } @@ -946,18 +946,20 @@ func (e *Exchange) processAccountBalanceUpdate(notification []any) error { return err } - // TODO: Integrate with exchange account system - // NOTES: This will affect free amount, a rest call might be needed to get - // locked and total amounts periodically. - e.Websocket.DataHandler <- account.Change{ - Account: deriveWalletType(walletType), - AssetType: asset.Spot, - Balance: &account.Balance{ - Currency: code, - Total: amount, - Free: amount, - }, + bal := accounts.Balance{ + Currency: curr, + Total: amount, + Free: amount, } + + id := deriveWalletType(walletType) + subAccts := accounts.SubAccounts{accounts.NewSubAccount(asset.Spot, id)} + subAccts[0].Balances.Set(curr, bal) + + if err := e.Accounts.Save(ctx, subAccts, true); err != nil { + return err + } + e.Websocket.DataHandler <- subAccts return nil } diff --git a/exchanges/poloniex/poloniex_wrapper.go b/exchanges/poloniex/poloniex_wrapper.go index 5c55f8f2..708b1c38 100644 --- a/exchanges/poloniex/poloniex_wrapper.go +++ b/exchanges/poloniex/poloniex_wrapper.go @@ -13,10 +13,10 @@ import ( "github.com/thrasher-corp/gocryptotrader/common" "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" "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/deposit" "github.com/thrasher-corp/gocryptotrader/exchanges/fundingrate" @@ -339,43 +339,20 @@ 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 for the -// Poloniex exchange -func (e *Exchange) UpdateAccountInfo(ctx context.Context, assetType asset.Item) (account.Holdings, error) { - var response account.Holdings - response.Exchange = e.Name - accountBalance, err := e.GetBalances(ctx) +// UpdateAccountBalances retrieves currency balances +func (e *Exchange) UpdateAccountBalances(ctx context.Context, assetType asset.Item) (accounts.SubAccounts, error) { + resp, err := e.GetBalances(ctx) if err != nil { - return response, err + return nil, err } - - currencies := make([]account.Balance, 0, len(accountBalance.Currency)) - for x, y := range accountBalance.Currency { - currencies = append(currencies, account.Balance{ - Currency: currency.NewCode(x), - Total: y, - }) + subAccts := accounts.SubAccounts{accounts.NewSubAccount(assetType, "")} + for curr, bal := range resp { + subAccts[0].Balances.Set(curr, accounts.Balance{Total: bal}) } - - response.Accounts = append(response.Accounts, account.SubAccount{ - AssetType: assetType, - Currencies: currencies, - }) - - creds, err := e.GetCredentials(ctx) - if err != nil { - return account.Holdings{}, err - } - err = account.Process(&response, creds) - if err != nil { - return account.Holdings{}, err - } - - return response, nil + return subAccts, e.Accounts.Save(ctx, subAccts, true) } -// GetAccountFundingHistory returns funding history, deposits and -// withdrawals +// GetAccountFundingHistory returns funding history, deposits and withdrawals func (e *Exchange) GetAccountFundingHistory(ctx context.Context) ([]exchange.FundingHistory, error) { end := time.Now() walletActivity, err := e.WalletActivity(ctx, end.Add(-time.Hour*24*365), end, "") @@ -932,10 +909,9 @@ func (e *Exchange) GetOrderHistory(ctx context.Context, req *order.MultiOrderReq return req.Filter(e.Name, orders), 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) } diff --git a/exchanges/sharedtestvalues/customex.go b/exchanges/sharedtestvalues/customex.go index fa8f0f34..db8aae92 100644 --- a/exchanges/sharedtestvalues/customex.go +++ b/exchanges/sharedtestvalues/customex.go @@ -7,10 +7,10 @@ import ( "github.com/thrasher-corp/gocryptotrader/common" "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/deposit" "github.com/thrasher-corp/gocryptotrader/exchanges/fundingrate" @@ -92,9 +92,9 @@ func (c *CustomEx) GetAvailablePairs(asset.Item) (currency.Pairs, error) { return nil, nil } -// UpdateAccountInfo is a mock method for CustomEx -func (c *CustomEx) UpdateAccountInfo(context.Context, asset.Item) (account.Holdings, error) { - return account.Holdings{}, nil +// UpdateAccountBalances is a mock method returning empty currency balances +func (c *CustomEx) UpdateAccountBalances(context.Context, asset.Item) (accounts.SubAccounts, error) { + return accounts.SubAccounts{}, nil } // SetPairs is a mock method for CustomEx diff --git a/exchanges/yobit/yobit_test.go b/exchanges/yobit/yobit_test.go index cb514213..62fa5d45 100644 --- a/exchanges/yobit/yobit_test.go +++ b/exchanges/yobit/yobit_test.go @@ -81,10 +81,9 @@ func TestGetTrades(t *testing.T) { func TestGetAccountInfo(t *testing.T) { t.Parallel() - _, err := e.UpdateAccountInfo(t.Context(), asset.Spot) - if err == nil { - t.Error("GetAccountInfo() Expected error") - } + sharedtestvalues.SkipTestIfCredentialsUnset(t, e) + _, err := e.UpdateAccountBalances(t.Context(), asset.Spot) + require.NoError(t, err) } func TestGetOpenOrders(t *testing.T) { diff --git a/exchanges/yobit/yobit_wrapper.go b/exchanges/yobit/yobit_wrapper.go index cc021c68..cfca5dce 100644 --- a/exchanges/yobit/yobit_wrapper.go +++ b/exchanges/yobit/yobit_wrapper.go @@ -12,8 +12,8 @@ import ( "github.com/thrasher-corp/gocryptotrader/common" "github.com/thrasher-corp/gocryptotrader/config" "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/account" "github.com/thrasher-corp/gocryptotrader/exchanges/asset" "github.com/thrasher-corp/gocryptotrader/exchanges/deposit" "github.com/thrasher-corp/gocryptotrader/exchanges/fundingrate" @@ -235,46 +235,28 @@ func (e *Exchange) UpdateOrderbook(ctx context.Context, p currency.Pair, assetTy return orderbook.Get(e.Name, p, assetType) } -// UpdateAccountInfo retrieves balances for all enabled currencies for the -// Yobit exchange -func (e *Exchange) UpdateAccountInfo(ctx context.Context, assetType asset.Item) (account.Holdings, error) { - var response account.Holdings - response.Exchange = e.Name - accountBalance, err := e.GetAccountInformation(ctx) +// UpdateAccountBalances retrieves currency balances +func (e *Exchange) UpdateAccountBalances(ctx context.Context, assetType asset.Item) (accounts.SubAccounts, error) { + resp, err := e.GetAccountInformation(ctx) if err != nil { - return response, err + return nil, err } - - currencies := make([]account.Balance, 0, len(accountBalance.FundsInclOrders)) - for x, y := range accountBalance.FundsInclOrders { - var exchangeCurrency account.Balance - exchangeCurrency.Currency = currency.NewCode(x) - exchangeCurrency.Total = y - for z, w := range accountBalance.Funds { - if z == x { - exchangeCurrency.Hold = y - w - exchangeCurrency.Free = w - } + subAccts := accounts.SubAccounts{accounts.NewSubAccount(assetType, "")} + for curr, bal := range resp.FundsInclOrders { + subAccts[0].Balances.Set(currency.NewCode(curr), accounts.Balance{ + Total: bal, + Hold: bal, // Hold = FundsInclOrders balance - Funds balance; So we Set total here and then subtract Funds below + }) + } + for curr, bal := range resp.Funds { + if err := subAccts[0].Balances.Add(currency.NewCode(curr), accounts.Balance{ + Free: bal, + Hold: -bal, // Hold = FundsInclOrders balance - Funds balance; so we Set total above and now subtract Funds here + }); err != nil { + return nil, err } - - currencies = append(currencies, exchangeCurrency) } - - response.Accounts = append(response.Accounts, account.SubAccount{ - AssetType: assetType, - Currencies: currencies, - }) - - creds, err := e.GetCredentials(ctx) - if err != nil { - return account.Holdings{}, err - } - err = account.Process(&response, creds) - if err != nil { - return account.Holdings{}, err - } - - return response, nil + return subAccts, e.Accounts.Save(ctx, subAccts, true) } // GetAccountFundingHistory returns funding history, deposits and @@ -646,10 +628,9 @@ func (e *Exchange) GetOrderHistory(ctx context.Context, req *order.MultiOrderReq return req.Filter(e.Name, orders), 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) } diff --git a/gctrpc/rpc.pb.go b/gctrpc/rpc.pb.go index ef838df9..2ed739ab 100644 --- a/gctrpc/rpc.pb.go +++ b/gctrpc/rpc.pb.go @@ -1719,7 +1719,7 @@ func (x *GetOrderbooksResponse) GetOrderbooks() []*Orderbooks { return nil } -type GetAccountInfoRequest struct { +type GetAccountBalancesRequest struct { state protoimpl.MessageState `protogen:"open.v1"` Exchange string `protobuf:"bytes,1,opt,name=exchange,proto3" json:"exchange,omitempty"` AssetType string `protobuf:"bytes,2,opt,name=asset_type,json=assetType,proto3" json:"asset_type,omitempty"` @@ -1727,20 +1727,20 @@ type GetAccountInfoRequest struct { sizeCache protoimpl.SizeCache } -func (x *GetAccountInfoRequest) Reset() { - *x = GetAccountInfoRequest{} +func (x *GetAccountBalancesRequest) Reset() { + *x = GetAccountBalancesRequest{} mi := &file_rpc_proto_msgTypes[32] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } -func (x *GetAccountInfoRequest) String() string { +func (x *GetAccountBalancesRequest) String() string { return protoimpl.X.MessageStringOf(x) } -func (*GetAccountInfoRequest) ProtoMessage() {} +func (*GetAccountBalancesRequest) ProtoMessage() {} -func (x *GetAccountInfoRequest) ProtoReflect() protoreflect.Message { +func (x *GetAccountBalancesRequest) ProtoReflect() protoreflect.Message { mi := &file_rpc_proto_msgTypes[32] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) @@ -1752,19 +1752,19 @@ func (x *GetAccountInfoRequest) ProtoReflect() protoreflect.Message { return mi.MessageOf(x) } -// Deprecated: Use GetAccountInfoRequest.ProtoReflect.Descriptor instead. -func (*GetAccountInfoRequest) Descriptor() ([]byte, []int) { +// Deprecated: Use GetAccountBalancesRequest.ProtoReflect.Descriptor instead. +func (*GetAccountBalancesRequest) Descriptor() ([]byte, []int) { return file_rpc_proto_rawDescGZIP(), []int{32} } -func (x *GetAccountInfoRequest) GetExchange() string { +func (x *GetAccountBalancesRequest) GetExchange() string { if x != nil { return x.Exchange } return "" } -func (x *GetAccountInfoRequest) GetAssetType() string { +func (x *GetAccountBalancesRequest) GetAssetType() string { if x != nil { return x.AssetType } @@ -1915,7 +1915,7 @@ func (x *AccountCurrencyInfo) GetUpdatedAt() *timestamppb.Timestamp { return nil } -type GetAccountInfoResponse struct { +type GetAccountBalancesResponse struct { state protoimpl.MessageState `protogen:"open.v1"` Exchange string `protobuf:"bytes,1,opt,name=exchange,proto3" json:"exchange,omitempty"` Accounts []*Account `protobuf:"bytes,2,rep,name=accounts,proto3" json:"accounts,omitempty"` @@ -1923,20 +1923,20 @@ type GetAccountInfoResponse struct { sizeCache protoimpl.SizeCache } -func (x *GetAccountInfoResponse) Reset() { - *x = GetAccountInfoResponse{} +func (x *GetAccountBalancesResponse) Reset() { + *x = GetAccountBalancesResponse{} mi := &file_rpc_proto_msgTypes[35] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } -func (x *GetAccountInfoResponse) String() string { +func (x *GetAccountBalancesResponse) String() string { return protoimpl.X.MessageStringOf(x) } -func (*GetAccountInfoResponse) ProtoMessage() {} +func (*GetAccountBalancesResponse) ProtoMessage() {} -func (x *GetAccountInfoResponse) ProtoReflect() protoreflect.Message { +func (x *GetAccountBalancesResponse) ProtoReflect() protoreflect.Message { mi := &file_rpc_proto_msgTypes[35] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) @@ -1948,19 +1948,19 @@ func (x *GetAccountInfoResponse) ProtoReflect() protoreflect.Message { return mi.MessageOf(x) } -// Deprecated: Use GetAccountInfoResponse.ProtoReflect.Descriptor instead. -func (*GetAccountInfoResponse) Descriptor() ([]byte, []int) { +// Deprecated: Use GetAccountBalancesResponse.ProtoReflect.Descriptor instead. +func (*GetAccountBalancesResponse) Descriptor() ([]byte, []int) { return file_rpc_proto_rawDescGZIP(), []int{35} } -func (x *GetAccountInfoResponse) GetExchange() string { +func (x *GetAccountBalancesResponse) GetExchange() string { if x != nil { return x.Exchange } return "" } -func (x *GetAccountInfoResponse) GetAccounts() []*Account { +func (x *GetAccountBalancesResponse) GetAccounts() []*Account { if x != nil { return x.Accounts } @@ -15231,8 +15231,8 @@ const file_rpc_proto_rawDesc = "" + "\x15GetOrderbooksResponse\x122\n" + "\n" + "orderbooks\x18\x01 \x03(\v2\x12.gctrpc.OrderbooksR\n" + - "orderbooks\"R\n" + - "\x15GetAccountInfoRequest\x12\x1a\n" + + "orderbooks\"V\n" + + "\x19GetAccountBalancesRequest\x12\x1a\n" + "\bexchange\x18\x01 \x01(\tR\bexchange\x12\x1d\n" + "\n" + "asset_type\x18\x02 \x01(\tR\tassetType\"V\n" + @@ -15250,8 +15250,8 @@ const file_rpc_proto_rawDesc = "" + "\x13free_without_borrow\x18\x05 \x01(\x01R\x11freeWithoutBorrow\x12\x1a\n" + "\bborrowed\x18\x06 \x01(\x01R\bborrowed\x129\n" + "\n" + - "updated_at\x18\a \x01(\v2\x1a.google.protobuf.TimestampR\tupdatedAt\"a\n" + - "\x16GetAccountInfoResponse\x12\x1a\n" + + "updated_at\x18\a \x01(\v2\x1a.google.protobuf.TimestampR\tupdatedAt\"e\n" + + "\x1aGetAccountBalancesResponse\x12\x1a\n" + "\bexchange\x18\x01 \x01(\tR\bexchange\x12+\n" + "\baccounts\x18\x02 \x03(\v2\x0f.gctrpc.AccountR\baccounts\"\x12\n" + "\x10GetConfigRequest\"'\n" + @@ -16367,7 +16367,7 @@ const file_rpc_proto_rawDesc = "" + "\x05asset\x18\x02 \x01(\tR\x05asset\x12(\n" + "\x04pair\x18\x03 \x01(\v2\x14.gctrpc.CurrencyPairR\x04pair\"/\n" + "\x1bGetCurrencyTradeURLResponse\x12\x10\n" + - "\x03url\x18\x01 \x01(\tR\x03url2\x9al\n" + + "\x03url\x18\x01 \x01(\tR\x03url2\xccl\n" + "\x15GoCryptoTraderService\x12O\n" + "\aGetInfo\x12\x16.gctrpc.GetInfoRequest\x1a\x17.gctrpc.GetInfoResponse\"\x13\x82\xd3\xe4\x93\x02\r\x12\v/v1/getinfo\x12g\n" + "\rGetSubsystems\x12\x1c.gctrpc.GetSubsystemsRequest\x1a\x1d.gctrpc.GetSusbsytemsResponse\"\x19\x82\xd3\xe4\x93\x02\x13\x12\x11/v1/getsubsystems\x12h\n" + @@ -16385,10 +16385,10 @@ const file_rpc_proto_rawDesc = "" + "\n" + "GetTickers\x12\x19.gctrpc.GetTickersRequest\x1a\x1a.gctrpc.GetTickersResponse\"\x16\x82\xd3\xe4\x93\x02\x10\x12\x0e/v1/gettickers\x12c\n" + "\fGetOrderbook\x12\x1b.gctrpc.GetOrderbookRequest\x1a\x19.gctrpc.OrderbookResponse\"\x1b\x82\xd3\xe4\x93\x02\x15:\x01*\"\x10/v1/getorderbook\x12g\n" + - "\rGetOrderbooks\x12\x1c.gctrpc.GetOrderbooksRequest\x1a\x1d.gctrpc.GetOrderbooksResponse\"\x19\x82\xd3\xe4\x93\x02\x13\x12\x11/v1/getorderbooks\x12k\n" + - "\x0eGetAccountInfo\x12\x1d.gctrpc.GetAccountInfoRequest\x1a\x1e.gctrpc.GetAccountInfoResponse\"\x1a\x82\xd3\xe4\x93\x02\x14\x12\x12/v1/getaccountinfo\x12q\n" + - "\x11UpdateAccountInfo\x12\x1d.gctrpc.GetAccountInfoRequest\x1a\x1e.gctrpc.GetAccountInfoResponse\"\x1d\x82\xd3\xe4\x93\x02\x17\x12\x15/v1/updateaccountinfo\x12y\n" + - "\x14GetAccountInfoStream\x12\x1d.gctrpc.GetAccountInfoRequest\x1a\x1e.gctrpc.GetAccountInfoResponse\" \x82\xd3\xe4\x93\x02\x1a\x12\x18/v1/getaccountinfostream0\x01\x12W\n" + + "\rGetOrderbooks\x12\x1c.gctrpc.GetOrderbooksRequest\x1a\x1d.gctrpc.GetOrderbooksResponse\"\x19\x82\xd3\xe4\x93\x02\x13\x12\x11/v1/getorderbooks\x12{\n" + + "\x12GetAccountBalances\x12!.gctrpc.GetAccountBalancesRequest\x1a\".gctrpc.GetAccountBalancesResponse\"\x1e\x82\xd3\xe4\x93\x02\x18\x12\x16/v1/getaccountbalances\x12\x81\x01\n" + + "\x15UpdateAccountBalances\x12!.gctrpc.GetAccountBalancesRequest\x1a\".gctrpc.GetAccountBalancesResponse\"!\x82\xd3\xe4\x93\x02\x1b\x12\x19/v1/updateaccountbalances\x12\x89\x01\n" + + "\x18GetAccountBalancesStream\x12!.gctrpc.GetAccountBalancesRequest\x1a\".gctrpc.GetAccountBalancesResponse\"$\x82\xd3\xe4\x93\x02\x1e\x12\x1c/v1/getaccountbalancesstream0\x01\x12W\n" + "\tGetConfig\x12\x18.gctrpc.GetConfigRequest\x1a\x19.gctrpc.GetConfigResponse\"\x15\x82\xd3\xe4\x93\x02\x0f\x12\r/v1/getconfig\x12c\n" + "\fGetPortfolio\x12\x1b.gctrpc.GetPortfolioRequest\x1a\x1c.gctrpc.GetPortfolioResponse\"\x18\x82\xd3\xe4\x93\x02\x12\x12\x10/v1/getportfolio\x12\x7f\n" + "\x13GetPortfolioSummary\x12\".gctrpc.GetPortfolioSummaryRequest\x1a#.gctrpc.GetPortfolioSummaryResponse\"\x1f\x82\xd3\xe4\x93\x02\x19\x12\x17/v1/getportfoliosummary\x12v\n" + @@ -16532,10 +16532,10 @@ var file_rpc_proto_goTypes = []any{ (*GetOrderbooksRequest)(nil), // 29: gctrpc.GetOrderbooksRequest (*Orderbooks)(nil), // 30: gctrpc.Orderbooks (*GetOrderbooksResponse)(nil), // 31: gctrpc.GetOrderbooksResponse - (*GetAccountInfoRequest)(nil), // 32: gctrpc.GetAccountInfoRequest + (*GetAccountBalancesRequest)(nil), // 32: gctrpc.GetAccountBalancesRequest (*Account)(nil), // 33: gctrpc.Account (*AccountCurrencyInfo)(nil), // 34: gctrpc.AccountCurrencyInfo - (*GetAccountInfoResponse)(nil), // 35: gctrpc.GetAccountInfoResponse + (*GetAccountBalancesResponse)(nil), // 35: gctrpc.GetAccountBalancesResponse (*GetConfigRequest)(nil), // 36: gctrpc.GetConfigRequest (*GetConfigResponse)(nil), // 37: gctrpc.GetConfigResponse (*PortfolioAddress)(nil), // 38: gctrpc.PortfolioAddress @@ -16762,7 +16762,7 @@ var file_rpc_proto_depIdxs = []int32{ 30, // 16: gctrpc.GetOrderbooksResponse.orderbooks:type_name -> gctrpc.Orderbooks 34, // 17: gctrpc.Account.currencies:type_name -> gctrpc.AccountCurrencyInfo 240, // 18: gctrpc.AccountCurrencyInfo.updated_at:type_name -> google.protobuf.Timestamp - 33, // 19: gctrpc.GetAccountInfoResponse.accounts:type_name -> gctrpc.Account + 33, // 19: gctrpc.GetAccountBalancesResponse.accounts:type_name -> gctrpc.Account 38, // 20: gctrpc.GetPortfolioResponse.portfolio:type_name -> gctrpc.PortfolioAddress 43, // 21: gctrpc.OfflineCoins.addresses:type_name -> gctrpc.OfflineCoinSummary 233, // 22: gctrpc.OnlineCoins.coins:type_name -> gctrpc.OnlineCoins.CoinsEntry @@ -16915,9 +16915,9 @@ var file_rpc_proto_depIdxs = []int32{ 23, // 169: gctrpc.GoCryptoTraderService.GetTickers:input_type -> gctrpc.GetTickersRequest 26, // 170: gctrpc.GoCryptoTraderService.GetOrderbook:input_type -> gctrpc.GetOrderbookRequest 29, // 171: gctrpc.GoCryptoTraderService.GetOrderbooks:input_type -> gctrpc.GetOrderbooksRequest - 32, // 172: gctrpc.GoCryptoTraderService.GetAccountInfo:input_type -> gctrpc.GetAccountInfoRequest - 32, // 173: gctrpc.GoCryptoTraderService.UpdateAccountInfo:input_type -> gctrpc.GetAccountInfoRequest - 32, // 174: gctrpc.GoCryptoTraderService.GetAccountInfoStream:input_type -> gctrpc.GetAccountInfoRequest + 32, // 172: gctrpc.GoCryptoTraderService.GetAccountBalances:input_type -> gctrpc.GetAccountBalancesRequest + 32, // 173: gctrpc.GoCryptoTraderService.UpdateAccountBalances:input_type -> gctrpc.GetAccountBalancesRequest + 32, // 174: gctrpc.GoCryptoTraderService.GetAccountBalancesStream:input_type -> gctrpc.GetAccountBalancesRequest 36, // 175: gctrpc.GoCryptoTraderService.GetConfig:input_type -> gctrpc.GetConfigRequest 39, // 176: gctrpc.GoCryptoTraderService.GetPortfolio:input_type -> gctrpc.GetPortfolioRequest 41, // 177: gctrpc.GoCryptoTraderService.GetPortfolioSummary:input_type -> gctrpc.GetPortfolioSummaryRequest @@ -17030,9 +17030,9 @@ var file_rpc_proto_depIdxs = []int32{ 25, // 284: gctrpc.GoCryptoTraderService.GetTickers:output_type -> gctrpc.GetTickersResponse 28, // 285: gctrpc.GoCryptoTraderService.GetOrderbook:output_type -> gctrpc.OrderbookResponse 31, // 286: gctrpc.GoCryptoTraderService.GetOrderbooks:output_type -> gctrpc.GetOrderbooksResponse - 35, // 287: gctrpc.GoCryptoTraderService.GetAccountInfo:output_type -> gctrpc.GetAccountInfoResponse - 35, // 288: gctrpc.GoCryptoTraderService.UpdateAccountInfo:output_type -> gctrpc.GetAccountInfoResponse - 35, // 289: gctrpc.GoCryptoTraderService.GetAccountInfoStream:output_type -> gctrpc.GetAccountInfoResponse + 35, // 287: gctrpc.GoCryptoTraderService.GetAccountBalances:output_type -> gctrpc.GetAccountBalancesResponse + 35, // 288: gctrpc.GoCryptoTraderService.UpdateAccountBalances:output_type -> gctrpc.GetAccountBalancesResponse + 35, // 289: gctrpc.GoCryptoTraderService.GetAccountBalancesStream:output_type -> gctrpc.GetAccountBalancesResponse 37, // 290: gctrpc.GoCryptoTraderService.GetConfig:output_type -> gctrpc.GetConfigResponse 40, // 291: gctrpc.GoCryptoTraderService.GetPortfolio:output_type -> gctrpc.GetPortfolioResponse 47, // 292: gctrpc.GoCryptoTraderService.GetPortfolioSummary:output_type -> gctrpc.GetPortfolioSummaryResponse diff --git a/gctrpc/rpc.pb.gw.go b/gctrpc/rpc.pb.gw.go index d2d8d5f1..3481c997 100644 --- a/gctrpc/rpc.pb.gw.go +++ b/gctrpc/rpc.pb.gw.go @@ -474,93 +474,93 @@ func local_request_GoCryptoTraderService_GetOrderbooks_0(ctx context.Context, ma } var ( - filter_GoCryptoTraderService_GetAccountInfo_0 = &utilities.DoubleArray{Encoding: map[string]int{}, Base: []int(nil), Check: []int(nil)} + filter_GoCryptoTraderService_GetAccountBalances_0 = &utilities.DoubleArray{Encoding: map[string]int{}, Base: []int(nil), Check: []int(nil)} ) -func request_GoCryptoTraderService_GetAccountInfo_0(ctx context.Context, marshaler runtime.Marshaler, client GoCryptoTraderServiceClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { - var protoReq GetAccountInfoRequest +func request_GoCryptoTraderService_GetAccountBalances_0(ctx context.Context, marshaler runtime.Marshaler, client GoCryptoTraderServiceClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { + var protoReq GetAccountBalancesRequest var metadata runtime.ServerMetadata if err := req.ParseForm(); err != nil { return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) } - if err := runtime.PopulateQueryParameters(&protoReq, req.Form, filter_GoCryptoTraderService_GetAccountInfo_0); err != nil { + if err := runtime.PopulateQueryParameters(&protoReq, req.Form, filter_GoCryptoTraderService_GetAccountBalances_0); err != nil { return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) } - msg, err := client.GetAccountInfo(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD)) + msg, err := client.GetAccountBalances(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD)) return msg, metadata, err } -func local_request_GoCryptoTraderService_GetAccountInfo_0(ctx context.Context, marshaler runtime.Marshaler, server GoCryptoTraderServiceServer, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { - var protoReq GetAccountInfoRequest +func local_request_GoCryptoTraderService_GetAccountBalances_0(ctx context.Context, marshaler runtime.Marshaler, server GoCryptoTraderServiceServer, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { + var protoReq GetAccountBalancesRequest var metadata runtime.ServerMetadata if err := req.ParseForm(); err != nil { return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) } - if err := runtime.PopulateQueryParameters(&protoReq, req.Form, filter_GoCryptoTraderService_GetAccountInfo_0); err != nil { + if err := runtime.PopulateQueryParameters(&protoReq, req.Form, filter_GoCryptoTraderService_GetAccountBalances_0); err != nil { return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) } - msg, err := server.GetAccountInfo(ctx, &protoReq) + msg, err := server.GetAccountBalances(ctx, &protoReq) return msg, metadata, err } var ( - filter_GoCryptoTraderService_UpdateAccountInfo_0 = &utilities.DoubleArray{Encoding: map[string]int{}, Base: []int(nil), Check: []int(nil)} + filter_GoCryptoTraderService_UpdateAccountBalances_0 = &utilities.DoubleArray{Encoding: map[string]int{}, Base: []int(nil), Check: []int(nil)} ) -func request_GoCryptoTraderService_UpdateAccountInfo_0(ctx context.Context, marshaler runtime.Marshaler, client GoCryptoTraderServiceClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { - var protoReq GetAccountInfoRequest +func request_GoCryptoTraderService_UpdateAccountBalances_0(ctx context.Context, marshaler runtime.Marshaler, client GoCryptoTraderServiceClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { + var protoReq GetAccountBalancesRequest var metadata runtime.ServerMetadata if err := req.ParseForm(); err != nil { return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) } - if err := runtime.PopulateQueryParameters(&protoReq, req.Form, filter_GoCryptoTraderService_UpdateAccountInfo_0); err != nil { + if err := runtime.PopulateQueryParameters(&protoReq, req.Form, filter_GoCryptoTraderService_UpdateAccountBalances_0); err != nil { return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) } - msg, err := client.UpdateAccountInfo(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD)) + msg, err := client.UpdateAccountBalances(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD)) return msg, metadata, err } -func local_request_GoCryptoTraderService_UpdateAccountInfo_0(ctx context.Context, marshaler runtime.Marshaler, server GoCryptoTraderServiceServer, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { - var protoReq GetAccountInfoRequest +func local_request_GoCryptoTraderService_UpdateAccountBalances_0(ctx context.Context, marshaler runtime.Marshaler, server GoCryptoTraderServiceServer, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { + var protoReq GetAccountBalancesRequest var metadata runtime.ServerMetadata if err := req.ParseForm(); err != nil { return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) } - if err := runtime.PopulateQueryParameters(&protoReq, req.Form, filter_GoCryptoTraderService_UpdateAccountInfo_0); err != nil { + if err := runtime.PopulateQueryParameters(&protoReq, req.Form, filter_GoCryptoTraderService_UpdateAccountBalances_0); err != nil { return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) } - msg, err := server.UpdateAccountInfo(ctx, &protoReq) + msg, err := server.UpdateAccountBalances(ctx, &protoReq) return msg, metadata, err } var ( - filter_GoCryptoTraderService_GetAccountInfoStream_0 = &utilities.DoubleArray{Encoding: map[string]int{}, Base: []int(nil), Check: []int(nil)} + filter_GoCryptoTraderService_GetAccountBalancesStream_0 = &utilities.DoubleArray{Encoding: map[string]int{}, Base: []int(nil), Check: []int(nil)} ) -func request_GoCryptoTraderService_GetAccountInfoStream_0(ctx context.Context, marshaler runtime.Marshaler, client GoCryptoTraderServiceClient, req *http.Request, pathParams map[string]string) (GoCryptoTraderService_GetAccountInfoStreamClient, runtime.ServerMetadata, error) { - var protoReq GetAccountInfoRequest +func request_GoCryptoTraderService_GetAccountBalancesStream_0(ctx context.Context, marshaler runtime.Marshaler, client GoCryptoTraderServiceClient, req *http.Request, pathParams map[string]string) (GoCryptoTraderService_GetAccountBalancesStreamClient, runtime.ServerMetadata, error) { + var protoReq GetAccountBalancesRequest var metadata runtime.ServerMetadata if err := req.ParseForm(); err != nil { return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) } - if err := runtime.PopulateQueryParameters(&protoReq, req.Form, filter_GoCryptoTraderService_GetAccountInfoStream_0); err != nil { + if err := runtime.PopulateQueryParameters(&protoReq, req.Form, filter_GoCryptoTraderService_GetAccountBalancesStream_0); err != nil { return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) } - stream, err := client.GetAccountInfoStream(ctx, &protoReq) + stream, err := client.GetAccountBalancesStream(ctx, &protoReq) if err != nil { return nil, metadata, err } @@ -4159,7 +4159,7 @@ func RegisterGoCryptoTraderServiceHandlerServer(ctx context.Context, mux *runtim }) - mux.Handle("GET", pattern_GoCryptoTraderService_GetAccountInfo_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { + mux.Handle("GET", pattern_GoCryptoTraderService_GetAccountBalances_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { ctx, cancel := context.WithCancel(req.Context()) defer cancel() var stream runtime.ServerTransportStream @@ -4167,12 +4167,12 @@ func RegisterGoCryptoTraderServiceHandlerServer(ctx context.Context, mux *runtim inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) var err error var annotatedContext context.Context - annotatedContext, err = runtime.AnnotateIncomingContext(ctx, mux, req, "/gctrpc.GoCryptoTraderService/GetAccountInfo", runtime.WithHTTPPathPattern("/v1/getaccountinfo")) + annotatedContext, err = runtime.AnnotateIncomingContext(ctx, mux, req, "/gctrpc.GoCryptoTraderService/GetAccountBalances", runtime.WithHTTPPathPattern("/v1/getaccountbalances")) if err != nil { runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) return } - resp, md, err := local_request_GoCryptoTraderService_GetAccountInfo_0(annotatedContext, inboundMarshaler, server, req, pathParams) + resp, md, err := local_request_GoCryptoTraderService_GetAccountBalances_0(annotatedContext, inboundMarshaler, server, req, pathParams) md.HeaderMD, md.TrailerMD = metadata.Join(md.HeaderMD, stream.Header()), metadata.Join(md.TrailerMD, stream.Trailer()) annotatedContext = runtime.NewServerMetadataContext(annotatedContext, md) if err != nil { @@ -4180,11 +4180,11 @@ func RegisterGoCryptoTraderServiceHandlerServer(ctx context.Context, mux *runtim return } - forward_GoCryptoTraderService_GetAccountInfo_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) + forward_GoCryptoTraderService_GetAccountBalances_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) }) - mux.Handle("GET", pattern_GoCryptoTraderService_UpdateAccountInfo_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { + mux.Handle("GET", pattern_GoCryptoTraderService_UpdateAccountBalances_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { ctx, cancel := context.WithCancel(req.Context()) defer cancel() var stream runtime.ServerTransportStream @@ -4192,12 +4192,12 @@ func RegisterGoCryptoTraderServiceHandlerServer(ctx context.Context, mux *runtim inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) var err error var annotatedContext context.Context - annotatedContext, err = runtime.AnnotateIncomingContext(ctx, mux, req, "/gctrpc.GoCryptoTraderService/UpdateAccountInfo", runtime.WithHTTPPathPattern("/v1/updateaccountinfo")) + annotatedContext, err = runtime.AnnotateIncomingContext(ctx, mux, req, "/gctrpc.GoCryptoTraderService/UpdateAccountBalances", runtime.WithHTTPPathPattern("/v1/updateaccountbalances")) if err != nil { runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) return } - resp, md, err := local_request_GoCryptoTraderService_UpdateAccountInfo_0(annotatedContext, inboundMarshaler, server, req, pathParams) + resp, md, err := local_request_GoCryptoTraderService_UpdateAccountBalances_0(annotatedContext, inboundMarshaler, server, req, pathParams) md.HeaderMD, md.TrailerMD = metadata.Join(md.HeaderMD, stream.Header()), metadata.Join(md.TrailerMD, stream.Trailer()) annotatedContext = runtime.NewServerMetadataContext(annotatedContext, md) if err != nil { @@ -4205,11 +4205,11 @@ func RegisterGoCryptoTraderServiceHandlerServer(ctx context.Context, mux *runtim return } - forward_GoCryptoTraderService_UpdateAccountInfo_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) + forward_GoCryptoTraderService_UpdateAccountBalances_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) }) - mux.Handle("GET", pattern_GoCryptoTraderService_GetAccountInfoStream_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { + mux.Handle("GET", pattern_GoCryptoTraderService_GetAccountBalancesStream_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { err := status.Error(codes.Unimplemented, "streaming calls are not yet supported in the in-process transport") _, outboundMarshaler := runtime.MarshalerForRequest(mux, req) runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) @@ -6919,69 +6919,69 @@ func RegisterGoCryptoTraderServiceHandlerClient(ctx context.Context, mux *runtim }) - mux.Handle("GET", pattern_GoCryptoTraderService_GetAccountInfo_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { + mux.Handle("GET", pattern_GoCryptoTraderService_GetAccountBalances_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { ctx, cancel := context.WithCancel(req.Context()) defer cancel() inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) var err error var annotatedContext context.Context - annotatedContext, err = runtime.AnnotateContext(ctx, mux, req, "/gctrpc.GoCryptoTraderService/GetAccountInfo", runtime.WithHTTPPathPattern("/v1/getaccountinfo")) + annotatedContext, err = runtime.AnnotateContext(ctx, mux, req, "/gctrpc.GoCryptoTraderService/GetAccountBalances", runtime.WithHTTPPathPattern("/v1/getaccountbalances")) if err != nil { runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) return } - resp, md, err := request_GoCryptoTraderService_GetAccountInfo_0(annotatedContext, inboundMarshaler, client, req, pathParams) + resp, md, err := request_GoCryptoTraderService_GetAccountBalances_0(annotatedContext, inboundMarshaler, client, req, pathParams) annotatedContext = runtime.NewServerMetadataContext(annotatedContext, md) if err != nil { runtime.HTTPError(annotatedContext, mux, outboundMarshaler, w, req, err) return } - forward_GoCryptoTraderService_GetAccountInfo_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) + forward_GoCryptoTraderService_GetAccountBalances_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) }) - mux.Handle("GET", pattern_GoCryptoTraderService_UpdateAccountInfo_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { + mux.Handle("GET", pattern_GoCryptoTraderService_UpdateAccountBalances_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { ctx, cancel := context.WithCancel(req.Context()) defer cancel() inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) var err error var annotatedContext context.Context - annotatedContext, err = runtime.AnnotateContext(ctx, mux, req, "/gctrpc.GoCryptoTraderService/UpdateAccountInfo", runtime.WithHTTPPathPattern("/v1/updateaccountinfo")) + annotatedContext, err = runtime.AnnotateContext(ctx, mux, req, "/gctrpc.GoCryptoTraderService/UpdateAccountBalances", runtime.WithHTTPPathPattern("/v1/updateaccountbalances")) if err != nil { runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) return } - resp, md, err := request_GoCryptoTraderService_UpdateAccountInfo_0(annotatedContext, inboundMarshaler, client, req, pathParams) + resp, md, err := request_GoCryptoTraderService_UpdateAccountBalances_0(annotatedContext, inboundMarshaler, client, req, pathParams) annotatedContext = runtime.NewServerMetadataContext(annotatedContext, md) if err != nil { runtime.HTTPError(annotatedContext, mux, outboundMarshaler, w, req, err) return } - forward_GoCryptoTraderService_UpdateAccountInfo_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) + forward_GoCryptoTraderService_UpdateAccountBalances_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) }) - mux.Handle("GET", pattern_GoCryptoTraderService_GetAccountInfoStream_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { + mux.Handle("GET", pattern_GoCryptoTraderService_GetAccountBalancesStream_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { ctx, cancel := context.WithCancel(req.Context()) defer cancel() inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) var err error var annotatedContext context.Context - annotatedContext, err = runtime.AnnotateContext(ctx, mux, req, "/gctrpc.GoCryptoTraderService/GetAccountInfoStream", runtime.WithHTTPPathPattern("/v1/getaccountinfostream")) + annotatedContext, err = runtime.AnnotateContext(ctx, mux, req, "/gctrpc.GoCryptoTraderService/GetAccountBalancesStream", runtime.WithHTTPPathPattern("/v1/getaccountbalancesstream")) if err != nil { runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) return } - resp, md, err := request_GoCryptoTraderService_GetAccountInfoStream_0(annotatedContext, inboundMarshaler, client, req, pathParams) + resp, md, err := request_GoCryptoTraderService_GetAccountBalancesStream_0(annotatedContext, inboundMarshaler, client, req, pathParams) annotatedContext = runtime.NewServerMetadataContext(annotatedContext, md) if err != nil { runtime.HTTPError(annotatedContext, mux, outboundMarshaler, w, req, err) return } - forward_GoCryptoTraderService_GetAccountInfoStream_0(annotatedContext, mux, outboundMarshaler, w, req, func() (proto.Message, error) { return resp.Recv() }, mux.GetForwardResponseOptions()...) + forward_GoCryptoTraderService_GetAccountBalancesStream_0(annotatedContext, mux, outboundMarshaler, w, req, func() (proto.Message, error) { return resp.Recv() }, mux.GetForwardResponseOptions()...) }) @@ -9133,11 +9133,11 @@ var ( pattern_GoCryptoTraderService_GetOrderbooks_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1}, []string{"v1", "getorderbooks"}, "")) - pattern_GoCryptoTraderService_GetAccountInfo_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1}, []string{"v1", "getaccountinfo"}, "")) + pattern_GoCryptoTraderService_GetAccountBalances_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1}, []string{"v1", "getaccountbalances"}, "")) - pattern_GoCryptoTraderService_UpdateAccountInfo_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1}, []string{"v1", "updateaccountinfo"}, "")) + pattern_GoCryptoTraderService_UpdateAccountBalances_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1}, []string{"v1", "updateaccountbalances"}, "")) - pattern_GoCryptoTraderService_GetAccountInfoStream_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1}, []string{"v1", "getaccountinfostream"}, "")) + pattern_GoCryptoTraderService_GetAccountBalancesStream_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1}, []string{"v1", "getaccountbalancesstream"}, "")) pattern_GoCryptoTraderService_GetConfig_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1}, []string{"v1", "getconfig"}, "")) @@ -9365,11 +9365,11 @@ var ( forward_GoCryptoTraderService_GetOrderbooks_0 = runtime.ForwardResponseMessage - forward_GoCryptoTraderService_GetAccountInfo_0 = runtime.ForwardResponseMessage + forward_GoCryptoTraderService_GetAccountBalances_0 = runtime.ForwardResponseMessage - forward_GoCryptoTraderService_UpdateAccountInfo_0 = runtime.ForwardResponseMessage + forward_GoCryptoTraderService_UpdateAccountBalances_0 = runtime.ForwardResponseMessage - forward_GoCryptoTraderService_GetAccountInfoStream_0 = runtime.ForwardResponseStream + forward_GoCryptoTraderService_GetAccountBalancesStream_0 = runtime.ForwardResponseStream forward_GoCryptoTraderService_GetConfig_0 = runtime.ForwardResponseMessage diff --git a/gctrpc/rpc.proto b/gctrpc/rpc.proto index 489488f2..e649d398 100644 --- a/gctrpc/rpc.proto +++ b/gctrpc/rpc.proto @@ -164,7 +164,7 @@ message GetOrderbooksResponse { repeated Orderbooks orderbooks = 1; } -message GetAccountInfoRequest { +message GetAccountBalancesRequest { string exchange = 1; string asset_type = 2; } @@ -184,7 +184,7 @@ message AccountCurrencyInfo { google.protobuf.Timestamp updated_at = 7; } -message GetAccountInfoResponse { +message GetAccountBalancesResponse { string exchange = 1; repeated Account accounts = 2; } @@ -1608,16 +1608,16 @@ service GoCryptoTraderService { option (google.api.http) = {get: "/v1/getorderbooks"}; } - rpc GetAccountInfo(GetAccountInfoRequest) returns (GetAccountInfoResponse) { - option (google.api.http) = {get: "/v1/getaccountinfo"}; + rpc GetAccountBalances(GetAccountBalancesRequest) returns (GetAccountBalancesResponse) { + option (google.api.http) = {get: "/v1/getaccountbalances"}; } - rpc UpdateAccountInfo(GetAccountInfoRequest) returns (GetAccountInfoResponse) { - option (google.api.http) = {get: "/v1/updateaccountinfo"}; + rpc UpdateAccountBalances(GetAccountBalancesRequest) returns (GetAccountBalancesResponse) { + option (google.api.http) = {get: "/v1/updateaccountbalances"}; } - rpc GetAccountInfoStream(GetAccountInfoRequest) returns (stream GetAccountInfoResponse) { - option (google.api.http) = {get: "/v1/getaccountinfostream"}; + rpc GetAccountBalancesStream(GetAccountBalancesRequest) returns (stream GetAccountBalancesResponse) { + option (google.api.http) = {get: "/v1/getaccountbalancesstream"}; } rpc GetConfig(GetConfigRequest) returns (GetConfigResponse) { diff --git a/gctrpc/rpc.swagger.json b/gctrpc/rpc.swagger.json index d582105e..35656ff8 100644 --- a/gctrpc/rpc.swagger.json +++ b/gctrpc/rpc.swagger.json @@ -1064,14 +1064,14 @@ ] } }, - "/v1/getaccountinfo": { + "/v1/getaccountbalances": { "get": { - "operationId": "GoCryptoTraderService_GetAccountInfo", + "operationId": "GoCryptoTraderService_GetAccountBalances", "responses": { "200": { "description": "A successful response.", "schema": { - "$ref": "#/definitions/gctrpcGetAccountInfoResponse" + "$ref": "#/definitions/gctrpcGetAccountBalancesResponse" } }, "default": { @@ -1100,9 +1100,9 @@ ] } }, - "/v1/getaccountinfostream": { + "/v1/getaccountbalancesstream": { "get": { - "operationId": "GoCryptoTraderService_GetAccountInfoStream", + "operationId": "GoCryptoTraderService_GetAccountBalancesStream", "responses": { "200": { "description": "A successful response.(streaming responses)", @@ -1110,13 +1110,13 @@ "type": "object", "properties": { "result": { - "$ref": "#/definitions/gctrpcGetAccountInfoResponse" + "$ref": "#/definitions/gctrpcGetAccountBalancesResponse" }, "error": { "$ref": "#/definitions/rpcStatus" } }, - "title": "Stream result of gctrpcGetAccountInfoResponse" + "title": "Stream result of gctrpcGetAccountBalancesResponse" } }, "default": { @@ -4283,14 +4283,14 @@ ] } }, - "/v1/updateaccountinfo": { + "/v1/updateaccountbalances": { "get": { - "operationId": "GoCryptoTraderService_UpdateAccountInfo", + "operationId": "GoCryptoTraderService_UpdateAccountBalances", "responses": { "200": { "description": "A successful response.", "schema": { - "$ref": "#/definitions/gctrpcGetAccountInfoResponse" + "$ref": "#/definitions/gctrpcGetAccountBalancesResponse" } }, "default": { @@ -5852,7 +5852,7 @@ } } }, - "gctrpcGetAccountInfoResponse": { + "gctrpcGetAccountBalancesResponse": { "type": "object", "properties": { "exchange": { diff --git a/gctrpc/rpc_grpc.pb.go b/gctrpc/rpc_grpc.pb.go index 460dd36e..a5052f3b 100644 --- a/gctrpc/rpc_grpc.pb.go +++ b/gctrpc/rpc_grpc.pb.go @@ -35,9 +35,9 @@ const ( GoCryptoTraderService_GetTickers_FullMethodName = "/gctrpc.GoCryptoTraderService/GetTickers" GoCryptoTraderService_GetOrderbook_FullMethodName = "/gctrpc.GoCryptoTraderService/GetOrderbook" GoCryptoTraderService_GetOrderbooks_FullMethodName = "/gctrpc.GoCryptoTraderService/GetOrderbooks" - GoCryptoTraderService_GetAccountInfo_FullMethodName = "/gctrpc.GoCryptoTraderService/GetAccountInfo" - GoCryptoTraderService_UpdateAccountInfo_FullMethodName = "/gctrpc.GoCryptoTraderService/UpdateAccountInfo" - GoCryptoTraderService_GetAccountInfoStream_FullMethodName = "/gctrpc.GoCryptoTraderService/GetAccountInfoStream" + GoCryptoTraderService_GetAccountBalances_FullMethodName = "/gctrpc.GoCryptoTraderService/GetAccountBalances" + GoCryptoTraderService_UpdateAccountBalances_FullMethodName = "/gctrpc.GoCryptoTraderService/UpdateAccountBalances" + GoCryptoTraderService_GetAccountBalancesStream_FullMethodName = "/gctrpc.GoCryptoTraderService/GetAccountBalancesStream" GoCryptoTraderService_GetConfig_FullMethodName = "/gctrpc.GoCryptoTraderService/GetConfig" GoCryptoTraderService_GetPortfolio_FullMethodName = "/gctrpc.GoCryptoTraderService/GetPortfolio" GoCryptoTraderService_GetPortfolioSummary_FullMethodName = "/gctrpc.GoCryptoTraderService/GetPortfolioSummary" @@ -156,9 +156,9 @@ type GoCryptoTraderServiceClient interface { GetTickers(ctx context.Context, in *GetTickersRequest, opts ...grpc.CallOption) (*GetTickersResponse, error) GetOrderbook(ctx context.Context, in *GetOrderbookRequest, opts ...grpc.CallOption) (*OrderbookResponse, error) GetOrderbooks(ctx context.Context, in *GetOrderbooksRequest, opts ...grpc.CallOption) (*GetOrderbooksResponse, error) - GetAccountInfo(ctx context.Context, in *GetAccountInfoRequest, opts ...grpc.CallOption) (*GetAccountInfoResponse, error) - UpdateAccountInfo(ctx context.Context, in *GetAccountInfoRequest, opts ...grpc.CallOption) (*GetAccountInfoResponse, error) - GetAccountInfoStream(ctx context.Context, in *GetAccountInfoRequest, opts ...grpc.CallOption) (GoCryptoTraderService_GetAccountInfoStreamClient, error) + GetAccountBalances(ctx context.Context, in *GetAccountBalancesRequest, opts ...grpc.CallOption) (*GetAccountBalancesResponse, error) + UpdateAccountBalances(ctx context.Context, in *GetAccountBalancesRequest, opts ...grpc.CallOption) (*GetAccountBalancesResponse, error) + GetAccountBalancesStream(ctx context.Context, in *GetAccountBalancesRequest, opts ...grpc.CallOption) (GoCryptoTraderService_GetAccountBalancesStreamClient, error) GetConfig(ctx context.Context, in *GetConfigRequest, opts ...grpc.CallOption) (*GetConfigResponse, error) GetPortfolio(ctx context.Context, in *GetPortfolioRequest, opts ...grpc.CallOption) (*GetPortfolioResponse, error) GetPortfolioSummary(ctx context.Context, in *GetPortfolioSummaryRequest, opts ...grpc.CallOption) (*GetPortfolioSummaryResponse, error) @@ -409,30 +409,30 @@ func (c *goCryptoTraderServiceClient) GetOrderbooks(ctx context.Context, in *Get return out, nil } -func (c *goCryptoTraderServiceClient) GetAccountInfo(ctx context.Context, in *GetAccountInfoRequest, opts ...grpc.CallOption) (*GetAccountInfoResponse, error) { - out := new(GetAccountInfoResponse) - err := c.cc.Invoke(ctx, GoCryptoTraderService_GetAccountInfo_FullMethodName, in, out, opts...) +func (c *goCryptoTraderServiceClient) GetAccountBalances(ctx context.Context, in *GetAccountBalancesRequest, opts ...grpc.CallOption) (*GetAccountBalancesResponse, error) { + out := new(GetAccountBalancesResponse) + err := c.cc.Invoke(ctx, GoCryptoTraderService_GetAccountBalances_FullMethodName, in, out, opts...) if err != nil { return nil, err } return out, nil } -func (c *goCryptoTraderServiceClient) UpdateAccountInfo(ctx context.Context, in *GetAccountInfoRequest, opts ...grpc.CallOption) (*GetAccountInfoResponse, error) { - out := new(GetAccountInfoResponse) - err := c.cc.Invoke(ctx, GoCryptoTraderService_UpdateAccountInfo_FullMethodName, in, out, opts...) +func (c *goCryptoTraderServiceClient) UpdateAccountBalances(ctx context.Context, in *GetAccountBalancesRequest, opts ...grpc.CallOption) (*GetAccountBalancesResponse, error) { + out := new(GetAccountBalancesResponse) + err := c.cc.Invoke(ctx, GoCryptoTraderService_UpdateAccountBalances_FullMethodName, in, out, opts...) if err != nil { return nil, err } return out, nil } -func (c *goCryptoTraderServiceClient) GetAccountInfoStream(ctx context.Context, in *GetAccountInfoRequest, opts ...grpc.CallOption) (GoCryptoTraderService_GetAccountInfoStreamClient, error) { - stream, err := c.cc.NewStream(ctx, &GoCryptoTraderService_ServiceDesc.Streams[0], GoCryptoTraderService_GetAccountInfoStream_FullMethodName, opts...) +func (c *goCryptoTraderServiceClient) GetAccountBalancesStream(ctx context.Context, in *GetAccountBalancesRequest, opts ...grpc.CallOption) (GoCryptoTraderService_GetAccountBalancesStreamClient, error) { + stream, err := c.cc.NewStream(ctx, &GoCryptoTraderService_ServiceDesc.Streams[0], GoCryptoTraderService_GetAccountBalancesStream_FullMethodName, opts...) if err != nil { return nil, err } - x := &goCryptoTraderServiceGetAccountInfoStreamClient{stream} + x := &goCryptoTraderServiceGetAccountBalancesStreamClient{stream} if err := x.ClientStream.SendMsg(in); err != nil { return nil, err } @@ -442,17 +442,17 @@ func (c *goCryptoTraderServiceClient) GetAccountInfoStream(ctx context.Context, return x, nil } -type GoCryptoTraderService_GetAccountInfoStreamClient interface { - Recv() (*GetAccountInfoResponse, error) +type GoCryptoTraderService_GetAccountBalancesStreamClient interface { + Recv() (*GetAccountBalancesResponse, error) grpc.ClientStream } -type goCryptoTraderServiceGetAccountInfoStreamClient struct { +type goCryptoTraderServiceGetAccountBalancesStreamClient struct { grpc.ClientStream } -func (x *goCryptoTraderServiceGetAccountInfoStreamClient) Recv() (*GetAccountInfoResponse, error) { - m := new(GetAccountInfoResponse) +func (x *goCryptoTraderServiceGetAccountBalancesStreamClient) Recv() (*GetAccountBalancesResponse, error) { + m := new(GetAccountBalancesResponse) if err := x.ClientStream.RecvMsg(m); err != nil { return nil, err } @@ -1458,9 +1458,9 @@ type GoCryptoTraderServiceServer interface { GetTickers(context.Context, *GetTickersRequest) (*GetTickersResponse, error) GetOrderbook(context.Context, *GetOrderbookRequest) (*OrderbookResponse, error) GetOrderbooks(context.Context, *GetOrderbooksRequest) (*GetOrderbooksResponse, error) - GetAccountInfo(context.Context, *GetAccountInfoRequest) (*GetAccountInfoResponse, error) - UpdateAccountInfo(context.Context, *GetAccountInfoRequest) (*GetAccountInfoResponse, error) - GetAccountInfoStream(*GetAccountInfoRequest, GoCryptoTraderService_GetAccountInfoStreamServer) error + GetAccountBalances(context.Context, *GetAccountBalancesRequest) (*GetAccountBalancesResponse, error) + UpdateAccountBalances(context.Context, *GetAccountBalancesRequest) (*GetAccountBalancesResponse, error) + GetAccountBalancesStream(*GetAccountBalancesRequest, GoCryptoTraderService_GetAccountBalancesStreamServer) error GetConfig(context.Context, *GetConfigRequest) (*GetConfigResponse, error) GetPortfolio(context.Context, *GetPortfolioRequest) (*GetPortfolioResponse, error) GetPortfolioSummary(context.Context, *GetPortfolioSummaryRequest) (*GetPortfolioSummaryResponse, error) @@ -1612,14 +1612,14 @@ func (UnimplementedGoCryptoTraderServiceServer) GetOrderbook(context.Context, *G func (UnimplementedGoCryptoTraderServiceServer) GetOrderbooks(context.Context, *GetOrderbooksRequest) (*GetOrderbooksResponse, error) { return nil, status.Errorf(codes.Unimplemented, "method GetOrderbooks not implemented") } -func (UnimplementedGoCryptoTraderServiceServer) GetAccountInfo(context.Context, *GetAccountInfoRequest) (*GetAccountInfoResponse, error) { - return nil, status.Errorf(codes.Unimplemented, "method GetAccountInfo not implemented") +func (UnimplementedGoCryptoTraderServiceServer) GetAccountBalances(context.Context, *GetAccountBalancesRequest) (*GetAccountBalancesResponse, error) { + return nil, status.Errorf(codes.Unimplemented, "method GetAccountBalances not implemented") } -func (UnimplementedGoCryptoTraderServiceServer) UpdateAccountInfo(context.Context, *GetAccountInfoRequest) (*GetAccountInfoResponse, error) { - return nil, status.Errorf(codes.Unimplemented, "method UpdateAccountInfo not implemented") +func (UnimplementedGoCryptoTraderServiceServer) UpdateAccountBalances(context.Context, *GetAccountBalancesRequest) (*GetAccountBalancesResponse, error) { + return nil, status.Errorf(codes.Unimplemented, "method UpdateAccountBalances not implemented") } -func (UnimplementedGoCryptoTraderServiceServer) GetAccountInfoStream(*GetAccountInfoRequest, GoCryptoTraderService_GetAccountInfoStreamServer) error { - return status.Errorf(codes.Unimplemented, "method GetAccountInfoStream not implemented") +func (UnimplementedGoCryptoTraderServiceServer) GetAccountBalancesStream(*GetAccountBalancesRequest, GoCryptoTraderService_GetAccountBalancesStreamServer) error { + return status.Errorf(codes.Unimplemented, "method GetAccountBalancesStream not implemented") } func (UnimplementedGoCryptoTraderServiceServer) GetConfig(context.Context, *GetConfigRequest) (*GetConfigResponse, error) { return nil, status.Errorf(codes.Unimplemented, "method GetConfig not implemented") @@ -2210,60 +2210,60 @@ func _GoCryptoTraderService_GetOrderbooks_Handler(srv interface{}, ctx context.C return interceptor(ctx, in, info, handler) } -func _GoCryptoTraderService_GetAccountInfo_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { - in := new(GetAccountInfoRequest) +func _GoCryptoTraderService_GetAccountBalances_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(GetAccountBalancesRequest) if err := dec(in); err != nil { return nil, err } if interceptor == nil { - return srv.(GoCryptoTraderServiceServer).GetAccountInfo(ctx, in) + return srv.(GoCryptoTraderServiceServer).GetAccountBalances(ctx, in) } info := &grpc.UnaryServerInfo{ Server: srv, - FullMethod: GoCryptoTraderService_GetAccountInfo_FullMethodName, + FullMethod: GoCryptoTraderService_GetAccountBalances_FullMethodName, } handler := func(ctx context.Context, req interface{}) (interface{}, error) { - return srv.(GoCryptoTraderServiceServer).GetAccountInfo(ctx, req.(*GetAccountInfoRequest)) + return srv.(GoCryptoTraderServiceServer).GetAccountBalances(ctx, req.(*GetAccountBalancesRequest)) } return interceptor(ctx, in, info, handler) } -func _GoCryptoTraderService_UpdateAccountInfo_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { - in := new(GetAccountInfoRequest) +func _GoCryptoTraderService_UpdateAccountBalances_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(GetAccountBalancesRequest) if err := dec(in); err != nil { return nil, err } if interceptor == nil { - return srv.(GoCryptoTraderServiceServer).UpdateAccountInfo(ctx, in) + return srv.(GoCryptoTraderServiceServer).UpdateAccountBalances(ctx, in) } info := &grpc.UnaryServerInfo{ Server: srv, - FullMethod: GoCryptoTraderService_UpdateAccountInfo_FullMethodName, + FullMethod: GoCryptoTraderService_UpdateAccountBalances_FullMethodName, } handler := func(ctx context.Context, req interface{}) (interface{}, error) { - return srv.(GoCryptoTraderServiceServer).UpdateAccountInfo(ctx, req.(*GetAccountInfoRequest)) + return srv.(GoCryptoTraderServiceServer).UpdateAccountBalances(ctx, req.(*GetAccountBalancesRequest)) } return interceptor(ctx, in, info, handler) } -func _GoCryptoTraderService_GetAccountInfoStream_Handler(srv interface{}, stream grpc.ServerStream) error { - m := new(GetAccountInfoRequest) +func _GoCryptoTraderService_GetAccountBalancesStream_Handler(srv interface{}, stream grpc.ServerStream) error { + m := new(GetAccountBalancesRequest) if err := stream.RecvMsg(m); err != nil { return err } - return srv.(GoCryptoTraderServiceServer).GetAccountInfoStream(m, &goCryptoTraderServiceGetAccountInfoStreamServer{stream}) + return srv.(GoCryptoTraderServiceServer).GetAccountBalancesStream(m, &goCryptoTraderServiceGetAccountBalancesStreamServer{stream}) } -type GoCryptoTraderService_GetAccountInfoStreamServer interface { - Send(*GetAccountInfoResponse) error +type GoCryptoTraderService_GetAccountBalancesStreamServer interface { + Send(*GetAccountBalancesResponse) error grpc.ServerStream } -type goCryptoTraderServiceGetAccountInfoStreamServer struct { +type goCryptoTraderServiceGetAccountBalancesStreamServer struct { grpc.ServerStream } -func (x *goCryptoTraderServiceGetAccountInfoStreamServer) Send(m *GetAccountInfoResponse) error { +func (x *goCryptoTraderServiceGetAccountBalancesStreamServer) Send(m *GetAccountBalancesResponse) error { return x.ServerStream.SendMsg(m) } @@ -4082,12 +4082,12 @@ var GoCryptoTraderService_ServiceDesc = grpc.ServiceDesc{ Handler: _GoCryptoTraderService_GetOrderbooks_Handler, }, { - MethodName: "GetAccountInfo", - Handler: _GoCryptoTraderService_GetAccountInfo_Handler, + MethodName: "GetAccountBalances", + Handler: _GoCryptoTraderService_GetAccountBalances_Handler, }, { - MethodName: "UpdateAccountInfo", - Handler: _GoCryptoTraderService_UpdateAccountInfo_Handler, + MethodName: "UpdateAccountBalances", + Handler: _GoCryptoTraderService_UpdateAccountBalances_Handler, }, { MethodName: "GetConfig", @@ -4456,8 +4456,8 @@ var GoCryptoTraderService_ServiceDesc = grpc.ServiceDesc{ }, Streams: []grpc.StreamDesc{ { - StreamName: "GetAccountInfoStream", - Handler: _GoCryptoTraderService_GetAccountInfoStream_Handler, + StreamName: "GetAccountBalancesStream", + Handler: _GoCryptoTraderService_GetAccountBalancesStream_Handler, ServerStreams: true, }, { diff --git a/gctscript/modules/gct/exchange.go b/gctscript/modules/gct/exchange.go index be6aa85f..2168ed31 100644 --- a/gctscript/modules/gct/exchange.go +++ b/gctscript/modules/gct/exchange.go @@ -16,33 +16,33 @@ import ( ) const ( - orderbookFunc = "orderbook" - tickerFunc = "ticker" - exchangesFunc = "exchanges" - pairsFunc = "pairs" - accountInfoFunc = "accountinfo" - depositAddressFunc = "depositaddress" - orderQueryFunc = "orderquery" - orderCancelFunc = "ordercancel" - orderSubmitFunc = "ordersubmit" - withdrawCryptoFunc = "withdrawcrypto" - withdrawFiatFunc = "withdrawfiat" - ohlcvFunc = "ohlcv" + orderbookFunc = "orderbook" + tickerFunc = "ticker" + exchangesFunc = "exchanges" + pairsFunc = "pairs" + accountBalancesFunc = "accountbalances" + depositAddressFunc = "depositaddress" + orderQueryFunc = "orderquery" + orderCancelFunc = "ordercancel" + orderSubmitFunc = "ordersubmit" + withdrawCryptoFunc = "withdrawcrypto" + withdrawFiatFunc = "withdrawfiat" + ohlcvFunc = "ohlcv" ) var exchangeModule = map[string]objects.Object{ - orderbookFunc: &objects.UserFunction{Name: orderbookFunc, Value: ExchangeOrderbook}, - tickerFunc: &objects.UserFunction{Name: tickerFunc, Value: ExchangeTicker}, - exchangesFunc: &objects.UserFunction{Name: exchangesFunc, Value: ExchangeExchanges}, - pairsFunc: &objects.UserFunction{Name: pairsFunc, Value: ExchangePairs}, - accountInfoFunc: &objects.UserFunction{Name: accountInfoFunc, Value: ExchangeAccountInfo}, - depositAddressFunc: &objects.UserFunction{Name: depositAddressFunc, Value: ExchangeDepositAddress}, - orderQueryFunc: &objects.UserFunction{Name: orderQueryFunc, Value: ExchangeOrderQuery}, - orderCancelFunc: &objects.UserFunction{Name: orderCancelFunc, Value: ExchangeOrderCancel}, - orderSubmitFunc: &objects.UserFunction{Name: orderSubmitFunc, Value: ExchangeOrderSubmit}, - withdrawCryptoFunc: &objects.UserFunction{Name: withdrawCryptoFunc, Value: ExchangeWithdrawCrypto}, - withdrawFiatFunc: &objects.UserFunction{Name: withdrawFiatFunc, Value: ExchangeWithdrawFiat}, - ohlcvFunc: &objects.UserFunction{Name: ohlcvFunc, Value: exchangeOHLCV}, + orderbookFunc: &objects.UserFunction{Name: orderbookFunc, Value: ExchangeOrderbook}, + tickerFunc: &objects.UserFunction{Name: tickerFunc, Value: ExchangeTicker}, + exchangesFunc: &objects.UserFunction{Name: exchangesFunc, Value: ExchangeExchanges}, + pairsFunc: &objects.UserFunction{Name: pairsFunc, Value: ExchangePairs}, + accountBalancesFunc: &objects.UserFunction{Name: accountBalancesFunc, Value: ExchangeAccountBalances}, + depositAddressFunc: &objects.UserFunction{Name: depositAddressFunc, Value: ExchangeDepositAddress}, + orderQueryFunc: &objects.UserFunction{Name: orderQueryFunc, Value: ExchangeOrderQuery}, + orderCancelFunc: &objects.UserFunction{Name: orderCancelFunc, Value: ExchangeOrderCancel}, + orderSubmitFunc: &objects.UserFunction{Name: orderSubmitFunc, Value: ExchangeOrderSubmit}, + withdrawCryptoFunc: &objects.UserFunction{Name: withdrawCryptoFunc, Value: ExchangeWithdrawCrypto}, + withdrawFiatFunc: &objects.UserFunction{Name: withdrawFiatFunc, Value: ExchangeWithdrawFiat}, + ohlcvFunc: &objects.UserFunction{Name: ohlcvFunc, Value: exchangeOHLCV}, } // ExchangeOrderbook returns orderbook for requested exchange & currencypair @@ -239,23 +239,23 @@ func ExchangePairs(args ...objects.Object) (objects.Object, error) { return &r, nil } -// ExchangeAccountInfo returns account information for requested exchange -func ExchangeAccountInfo(args ...objects.Object) (objects.Object, error) { +// ExchangeAccountBalances returns account balances for requested exchange +func ExchangeAccountBalances(args ...objects.Object) (objects.Object, error) { if len(args) != 3 { return nil, objects.ErrWrongNumArguments } scriptCtx, ok := objects.ToInterface(args[0]).(*Context) if !ok { - return nil, constructRuntimeError(1, accountInfoFunc, "*gct.Context", args[0]) + return nil, constructRuntimeError(1, accountBalancesFunc, "*gct.Context", args[0]) } exchangeName, ok := objects.ToString(args[1]) if !ok { - return nil, constructRuntimeError(2, accountInfoFunc, "string", args[1]) + return nil, constructRuntimeError(2, accountBalancesFunc, "string", args[1]) } assetString, ok := objects.ToString(args[2]) if !ok { - return nil, constructRuntimeError(3, accountInfoFunc, "string", args[2]) + return nil, constructRuntimeError(3, accountBalancesFunc, "string", args[2]) } assetType, err := asset.New(assetString) if err != nil { @@ -263,25 +263,24 @@ func ExchangeAccountInfo(args ...objects.Object) (objects.Object, error) { } ctx := processScriptContext(scriptCtx) - rtnValue, err := wrappers.GetWrapper(). - AccountInformation(ctx, exchangeName, assetType) + rtnValue, err := wrappers.GetWrapper().AccountBalances(ctx, exchangeName, assetType) if err != nil { return errorResponsef(standardFormatting, err) } var funds objects.Array - for x := range rtnValue.Accounts { - for y := range rtnValue.Accounts[x].Currencies { - temp := make(map[string]objects.Object, 3) - temp["name"] = &objects.String{Value: rtnValue.Accounts[x].Currencies[y].Currency.String()} - temp["total"] = &objects.Float{Value: rtnValue.Accounts[x].Currencies[y].Total} - temp["hold"] = &objects.Float{Value: rtnValue.Accounts[x].Currencies[y].Hold} - funds.Value = append(funds.Value, &objects.Map{Value: temp}) + for i := range rtnValue { + for curr, bal := range rtnValue[i].Balances { + funds.Value = append(funds.Value, &objects.Map{Value: map[string]objects.Object{ + "name": &objects.String{Value: curr.String()}, + "total": &objects.Float{Value: bal.Total}, + "hold": &objects.Float{Value: bal.Hold}, + }}) } } data := make(map[string]objects.Object, 2) - data["exchange"] = &objects.String{Value: rtnValue.Exchange} + data["exchange"] = &objects.String{Value: exchangeName} data["currencies"] = &funds return &objects.Map{Value: data}, nil } diff --git a/gctscript/modules/gct/gct.go b/gctscript/modules/gct/gct.go index 5460a82e..8b363b90 100644 --- a/gctscript/modules/gct/gct.go +++ b/gctscript/modules/gct/gct.go @@ -4,7 +4,7 @@ import ( "context" objects "github.com/d5/tengo/v2" - "github.com/thrasher-corp/gocryptotrader/exchanges/account" + "github.com/thrasher-corp/gocryptotrader/exchange/accounts" "github.com/thrasher-corp/gocryptotrader/exchanges/request" ) @@ -189,7 +189,7 @@ func processScriptContext(scriptCtx *Context) context.Context { otp, _ = objects.ToString(object) } - ctx = account.DeployCredentialsToContext(ctx, &account.Credentials{ + ctx = accounts.DeployCredentialsToContext(ctx, &accounts.Credentials{ Key: key, Secret: secret, SubAccount: subAccount, @@ -199,7 +199,7 @@ func processScriptContext(scriptCtx *Context) context.Context { }) } else if object = scriptCtx.Value["subaccount"]; object != nil { subAccount, _ := objects.ToString(object) - ctx = account.DeploySubAccountOverrideToContext(ctx, subAccount) + ctx = accounts.DeploySubAccountOverrideToContext(ctx, subAccount) } return ctx } diff --git a/gctscript/modules/gct/gct_test.go b/gctscript/modules/gct/gct_test.go index 2c1121d6..21a96704 100644 --- a/gctscript/modules/gct/gct_test.go +++ b/gctscript/modules/gct/gct_test.go @@ -10,7 +10,7 @@ import ( "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "github.com/thrasher-corp/gocryptotrader/common" - "github.com/thrasher-corp/gocryptotrader/exchanges/account" + "github.com/thrasher-corp/gocryptotrader/exchange/accounts" "github.com/thrasher-corp/gocryptotrader/exchanges/asset" "github.com/thrasher-corp/gocryptotrader/exchanges/kline" "github.com/thrasher-corp/gocryptotrader/gctscript/modules" @@ -103,16 +103,16 @@ func TestExchangePairs(t *testing.T) { assert.ErrorIs(t, err, objects.ErrWrongNumArguments) } -func TestAccountInfo(t *testing.T) { +func TestAccountBalances(t *testing.T) { t.Parallel() - _, err := ExchangeAccountInfo() + _, err := ExchangeAccountBalances() assert.ErrorIs(t, err, objects.ErrWrongNumArguments) - _, err = ExchangeAccountInfo(ctx, exch, assetType) + _, err = ExchangeAccountBalances(ctx, exch, assetType) assert.NoError(t, err) - _, err = ExchangeAccountInfo(ctx, exchError, assetType) + _, err = ExchangeAccountBalances(ctx, exchError, assetType) assert.NoError(t, err) } @@ -391,7 +391,7 @@ func TestSetSubAccount(t *testing.T) { t.Fatal("should not be nil") } - subaccount, ok := ctx.Value(account.ContextSubAccountFlag).(string) + subaccount, ok := ctx.Value(accounts.ContextSubAccountFlag).(string) if !ok { t.Fatal("wrong type") } diff --git a/gctscript/modules/wrapper_types.go b/gctscript/modules/wrapper_types.go index 1ae4f1fa..7ce468cd 100644 --- a/gctscript/modules/wrapper_types.go +++ b/gctscript/modules/wrapper_types.go @@ -5,7 +5,7 @@ import ( "time" "github.com/thrasher-corp/gocryptotrader/currency" - "github.com/thrasher-corp/gocryptotrader/exchanges/account" + "github.com/thrasher-corp/gocryptotrader/exchange/accounts" "github.com/thrasher-corp/gocryptotrader/exchanges/asset" "github.com/thrasher-corp/gocryptotrader/exchanges/deposit" "github.com/thrasher-corp/gocryptotrader/exchanges/kline" @@ -35,7 +35,7 @@ type GCTExchange interface { QueryOrder(ctx context.Context, exch, orderid string, pair currency.Pair, assetType asset.Item) (*order.Detail, error) SubmitOrder(ctx context.Context, submit *order.Submit) (*order.SubmitResponse, error) CancelOrder(ctx context.Context, exch, orderid string, pair currency.Pair, item asset.Item) (bool, error) - AccountInformation(ctx context.Context, exch string, assetType asset.Item) (account.Holdings, error) + AccountBalances(ctx context.Context, exch string, assetType asset.Item) (accounts.SubAccounts, error) DepositAddress(exch, chain string, currencyCode currency.Code) (*deposit.Address, error) WithdrawalFiatFunds(ctx context.Context, bankAccountID string, request *withdraw.Request) (out string, err error) WithdrawalCryptoFunds(ctx context.Context, request *withdraw.Request) (out string, err error) diff --git a/gctscript/wrappers/gct/exchange/exchange.go b/gctscript/wrappers/gct/exchange/exchange.go index bb95d2a0..e251dd4c 100644 --- a/gctscript/wrappers/gct/exchange/exchange.go +++ b/gctscript/wrappers/gct/exchange/exchange.go @@ -9,8 +9,8 @@ import ( "github.com/thrasher-corp/gocryptotrader/currency" "github.com/thrasher-corp/gocryptotrader/engine" + "github.com/thrasher-corp/gocryptotrader/exchange/accounts" 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/kline" @@ -130,16 +130,16 @@ func (e Exchange) CancelOrder(ctx context.Context, exch, orderID string, cp curr return true, nil } -// AccountInformation returns account information (balance etc) for requested exchange -func (e Exchange) AccountInformation(ctx context.Context, exch string, assetType asset.Item) (account.Holdings, error) { +// AccountBalances returns account balances for requested exchange +func (e Exchange) AccountBalances(ctx context.Context, exch string, assetType asset.Item) (accounts.SubAccounts, error) { ex, err := e.GetExchange(exch) if err != nil { - return account.Holdings{}, err + return accounts.SubAccounts{}, err } - accountInfo, err := ex.GetCachedAccountInfo(ctx, assetType) + accountInfo, err := ex.GetCachedSubAccounts(ctx, assetType) if err != nil { - return account.Holdings{}, err + return accounts.SubAccounts{}, err } return accountInfo, nil diff --git a/gctscript/wrappers/gct/exchange/exchange_test.go b/gctscript/wrappers/gct/exchange/exchange_test.go index 6de95a41..0f273d7d 100644 --- a/gctscript/wrappers/gct/exchange/exchange_test.go +++ b/gctscript/wrappers/gct/exchange/exchange_test.go @@ -119,11 +119,11 @@ func TestExchange_Pairs(t *testing.T) { } } -func TestExchange_AccountInformation(t *testing.T) { +func TestExchange_AccountBalances(t *testing.T) { if !configureExchangeKeys() { t.Skip("no exchange configured test skipped") } - _, err := exchangeTest.AccountInformation(t.Context(), + _, err := exchangeTest.AccountBalances(t.Context(), exchName, asset.Spot) if err != nil { t.Fatal(err) diff --git a/gctscript/wrappers/gct/gctwrapper_test.go b/gctscript/wrappers/gct/gctwrapper_test.go index 3a4a23f8..c05d6fc5 100644 --- a/gctscript/wrappers/gct/gctwrapper_test.go +++ b/gctscript/wrappers/gct/gctwrapper_test.go @@ -159,14 +159,14 @@ func TestExchangePairs(t *testing.T) { assert.ErrorIs(t, err, objects.ErrWrongNumArguments) } -func TestExchangeAccountInfo(t *testing.T) { +func TestExchangeAccountBalances(t *testing.T) { t.Parallel() - _, err := gct.ExchangeAccountInfo() + _, err := gct.ExchangeAccountBalances() require.ErrorIs(t, err, objects.ErrWrongNumArguments) - obj, err := gct.ExchangeAccountInfo(ctx, exch, assetType) + obj, err := gct.ExchangeAccountBalances(ctx, exch, assetType) require.NoError(t, err) rString, ok := objects.ToString(obj) - require.True(t, ok, "ExchangeAccountInfo return value must return correctly from objects.ToString") + require.True(t, ok, "ExchangeAccountBalances return value must return correctly from objects.ToString") require.Contains(t, rString, "Bitstamp REST or Websocket authentication support is not enabled") } diff --git a/gctscript/wrappers/validator/validator.go b/gctscript/wrappers/validator/validator.go index 185bb331..0872b763 100644 --- a/gctscript/wrappers/validator/validator.go +++ b/gctscript/wrappers/validator/validator.go @@ -7,7 +7,7 @@ import ( "github.com/thrasher-corp/gocryptotrader/core" "github.com/thrasher-corp/gocryptotrader/currency" - "github.com/thrasher-corp/gocryptotrader/exchanges/account" + "github.com/thrasher-corp/gocryptotrader/exchange/accounts" "github.com/thrasher-corp/gocryptotrader/exchanges/asset" "github.com/thrasher-corp/gocryptotrader/exchanges/deposit" "github.com/thrasher-corp/gocryptotrader/exchanges/kline" @@ -184,32 +184,29 @@ func (w Wrapper) CancelOrder(_ context.Context, exch, orderid string, cp currenc return true, nil } -// AccountInformation validator for test execution/scripts -func (w Wrapper) AccountInformation(_ context.Context, exch string, assetType asset.Item) (account.Holdings, error) { +// AccountBalances validator for test execution/scripts +func (w Wrapper) AccountBalances(_ context.Context, exch string, assetType asset.Item) (accounts.SubAccounts, error) { if exch == exchError.String() { - return account.Holdings{}, errTestFailed + return nil, errTestFailed } - - return account.Holdings{ - Exchange: exch, - Accounts: []account.SubAccount{ - { - ID: exch, - AssetType: assetType, - Currencies: []account.Balance{ - { - Currency: currency.Code{ - Item: ¤cy.Item{ - ID: 0, - FullName: "Bitcoin", - Symbol: "BTC", - Role: 1, - AssocChain: "", - }, - }, - Total: 100, - Hold: 0, - }, + c := currency.Code{ + Item: ¤cy.Item{ + ID: 0, + FullName: "Bitcoin", + Symbol: "BTC", + Role: 1, + AssocChain: "", + }, + } + return accounts.SubAccounts{ + { + ID: "subacct1", + AssetType: assetType, + Balances: accounts.CurrencyBalances{ + c: accounts.Balance{ + Currency: c, + Total: 100, + Hold: 0, }, }, }, diff --git a/gctscript/wrappers/validator/validator_test.go b/gctscript/wrappers/validator/validator_test.go index bc8af044..cdee5233 100644 --- a/gctscript/wrappers/validator/validator_test.go +++ b/gctscript/wrappers/validator/validator_test.go @@ -4,6 +4,8 @@ import ( "testing" "time" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" "github.com/thrasher-corp/gocryptotrader/currency" "github.com/thrasher-corp/gocryptotrader/exchanges/asset" "github.com/thrasher-corp/gocryptotrader/exchanges/kline" @@ -60,20 +62,14 @@ func TestWrapper_IsEnabled(t *testing.T) { } } -func TestWrapper_AccountInformation(t *testing.T) { +func TestWrapperAccountBalances(t *testing.T) { t.Parallel() - _, err := testWrapper.AccountInformation(t.Context(), - exchName, asset.Spot) - if err != nil { - t.Fatal(err) - } + _, err := testWrapper.AccountBalances(t.Context(), exchName, asset.Spot) + require.NoError(t, err) - _, err = testWrapper.AccountInformation(t.Context(), - exchError.String(), asset.Spot) - if err == nil { - t.Fatal("expected AccountInformation to return error on invalid name") - } + _, err = testWrapper.AccountBalances(t.Context(), exchError.String(), asset.Spot) + assert.ErrorIs(t, err, errTestFailed) } func TestWrapper_CancelOrder(t *testing.T) { diff --git a/internal/testing/exchange/exchange.go b/internal/testing/exchange/exchange.go index ada3ee8c..e2c60620 100644 --- a/internal/testing/exchange/exchange.go +++ b/internal/testing/exchange/exchange.go @@ -18,6 +18,7 @@ import ( "github.com/stretchr/testify/require" "github.com/thrasher-corp/gocryptotrader/config" "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/mock" "github.com/thrasher-corp/gocryptotrader/exchanges/sharedtestvalues" @@ -34,8 +35,7 @@ func Setup(e exchange.IBotExchange) error { return err } - err = cfg.LoadConfig(filepath.Join(root, "testdata", "configtest.json"), true) - if err != nil { + if err = cfg.LoadConfig(filepath.Join(root, "testdata", "configtest.json"), true); err != nil { return fmt.Errorf("LoadConfig() error: %w", err) } e.SetDefaults() @@ -47,10 +47,13 @@ func Setup(e exchange.IBotExchange) error { e.SetDefaults() b := e.GetBase() b.Websocket = sharedtestvalues.NewTestWebsocket() - err = e.Setup(exchConf) - if err != nil { + + if err = e.Setup(exchConf); err != nil { return fmt.Errorf("Setup() error: %w", err) } + + b.Accounts = accounts.MustNewAccounts(b) + return nil } diff --git a/portfolio/portfolio.go b/portfolio/portfolio.go index 1469e231..e595b001 100644 --- a/portfolio/portfolio.go +++ b/portfolio/portfolio.go @@ -141,8 +141,7 @@ func (b *Base) ExchangeExists(exchangeName string) bool { }) } -// AddressExists checks to see if there is an address associated with the -// portfolio base +// AddressExists checks to see if there is an address associated with the portfolio base func (b *Base) AddressExists(address string) bool { b.mtx.RLock() defer b.mtx.RUnlock() @@ -181,7 +180,7 @@ func (b *Base) AddExchangeAddress(exchangeName string, coinType currency.Code, b }) } -// UpdateAddressBalance updates the portfolio base balance +// UpdateAddressBalance updates the portfolio base balance. func (b *Base) UpdateAddressBalance(address string, amount float64) { b.mtx.Lock() defer b.mtx.Unlock() @@ -203,8 +202,7 @@ func (b *Base) RemoveExchangeAddress(exchangeName string, coinType currency.Code })) } -// UpdateExchangeAddressBalance updates the portfolio balance when checked -// against correct exchangeName and coinType. +// UpdateExchangeAddressBalance updates the portfolio balance when checked against correct exchangeName and coinType. func (b *Base) UpdateExchangeAddressBalance(exchangeName string, coinType currency.Code, balance float64) { b.mtx.Lock() defer b.mtx.Unlock() @@ -216,7 +214,7 @@ func (b *Base) UpdateExchangeAddressBalance(exchangeName string, coinType curren } } -// AddAddress adds an address to the portfolio base or updates its balance if it already exists +// AddAddress adds an address to the portfolio base or updates its balance if it already exists. func (b *Base) AddAddress(address, description string, coinType currency.Code, balance float64) error { if address == "" { return common.ErrAddressIsEmptyOrInvalid