mirror of
https://github.com/d0zingcat/gocryptotrader.git
synced 2026-06-08 15:11:07 +00:00
* Bybit: Fix race in TestUpdateAccountInfo and TestWSHandleData * DriveBy rename TestWSHandleData * This doesn't address running with -race=2+ due to the singleton * Accounts: Add account.GetService() * exchange: Assertify TestSetupDefaults * Exchanges: Add account.Service override for testing * Exchanges: Remove duplicate IsWebsocketEnabled test from TestSetupDefaults * Dispatch: Replace nil checks with NilGuard * Engine: Remove deprecated printAccountHoldingsChangeSummary * Dispatcher: Add EnsureRunning method * Accounts: Move singleton accounts service to exchange Accounts * Move singleton accounts service to exchange Accounts This maintains the concept of a global store, whilst allowing exchanges to override it when needed, particularly for testing. APIServer: * Remove getAllActiveAccounts from apiserver Deprecated apiserver only thing using this, so remove it instead of updating it * Update comment for UpdateAccountBalances everywhere * Docs: Add punctuation to function comments * Bybit: Coverage for wsProcessWalletPushData Save
149 lines
4.5 KiB
Go
149 lines
4.5 KiB
Go
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
|
|
}
|