mirror of
https://github.com/d0zingcat/gocryptotrader.git
synced 2026-05-13 23:16:45 +00:00
* Bitstamp: Add auth channel subscription handling * Bitstamp: Avoid searching for asset type We've hardcoded asset.Spot in order to find the pair. Looking the asset up from the pair makes no sense. * Bitstamp: Add type for wsOrders * Bitstamp: Working test of Generic DataHandler * Bitstamp: WS Order chan tests and remove type orderType could be derived from status == New and Buy & price == 9+e9 or Sell & price == 0 But it would only be true for the first update and it really doesn't feel worth the risk Consumers are going to have to merge to original request anyway * Bitstamp: Linter fixes * Kraken: Switch to shared fixture test * Bitstamp: Fix lint on TestFixtureToDataHandler * Engine: Add Clone for PairsManager go-vet highlighted that the mutex here is a value when we copied the PairsManager in a test. Options to fix: * Add a deep clone method to PairsManager * Add a shallow clone method with a disclaimer * Make the mutex a pointer * Make the PairsManager itself a pointer Options 3 and 4 are too invasive to justify changing at this point. There's an inherent risk of PM being passed by value, but govet should catch the copylock. There's more risk in changing everything to use a pointer at this stage. * Engine: Fix linter again, ironically * Bitstamp: Rename OHLC const * Bitstamp: Minor fixes to syntax * Bitstamp: Simplify chanSymb=>pair * Bitstamp: Still process order updates without ID If there's a ClientOrderID we'll still process the order. It doesn't seem likely we'd have this happen, but if it does we still want consumers to get something. * Bitstamp: Replace Clone with Lock methods * Engine: Expose PairsManager's Mutex Makes more sense than wrapping functions * Bitstamp: Fix linter copylock (again) * fixup! Engine: Expose PairsManager's Mutex Omit Mutex from Json * fixup! Bitstamp: Add auth channel subscription handling Remove unused wsAuthToken * Bitstamp: Simplify OrderData Unmarshal * Bitstamp: Remove unused contexts I added these following best practices, but the reality is that when/if we get context awareness in GCT, there will be a lot more to fix and this will be a drop in the ocean anyway. * Bitstamp: Only call handleWSOrder for MyOrders * Bitstamp: Avoid allocating again in handleWSOrder * CurrencyPairs: Remove public mutex Simplified to a Load method to avoid making mutex public * Tests: Improve test readability and clarity * Bitstamp: Wrap errWSPairParsingError Co-authored-by: Scott <gloriousCode@users.noreply.github.com> * Bitstamp: FetchWSAuth mock and live test --------- Co-authored-by: Scott <gloriousCode@users.noreply.github.com>
486 lines
12 KiB
Go
486 lines
12 KiB
Go
package currency
|
|
|
|
import (
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
|
|
"github.com/thrasher-corp/gocryptotrader/common"
|
|
"github.com/thrasher-corp/gocryptotrader/common/convert"
|
|
"github.com/thrasher-corp/gocryptotrader/exchanges/asset"
|
|
)
|
|
|
|
var (
|
|
// ErrAssetAlreadyEnabled defines an error for the pairs management system
|
|
// that declares the asset is already enabled.
|
|
ErrAssetAlreadyEnabled = errors.New("asset already enabled")
|
|
// ErrPairAlreadyEnabled returns when enabling a pair that is already enabled
|
|
ErrPairAlreadyEnabled = errors.New("pair already enabled")
|
|
// ErrPairNotFound is returned when a currency pair is not found
|
|
ErrPairNotFound = errors.New("pair not found")
|
|
// ErrAssetIsNil is an error when the asset has not been populated by the
|
|
// configuration
|
|
ErrAssetIsNil = errors.New("asset is nil")
|
|
// ErrPairNotContainedInAvailablePairs defines an error when a pair is not
|
|
// contained in the available pairs list and is not supported by the
|
|
// exchange for that asset type.
|
|
ErrPairNotContainedInAvailablePairs = errors.New("pair not contained in available pairs")
|
|
// ErrPairManagerNotInitialised is returned when a pairs manager is requested, but has not been setup
|
|
ErrPairManagerNotInitialised = errors.New("pair manager not initialised")
|
|
// ErrAssetNotFound is returned when an asset does not exist in the pairstore
|
|
ErrAssetNotFound = errors.New("asset type not found in pair store")
|
|
|
|
errPairStoreIsNil = errors.New("pair store is nil")
|
|
errPairFormatIsNil = errors.New("pair format is nil")
|
|
)
|
|
|
|
// GetAssetTypes returns a list of stored asset types
|
|
func (p *PairsManager) GetAssetTypes(enabled bool) asset.Items {
|
|
p.mutex.RLock()
|
|
defer p.mutex.RUnlock()
|
|
assetTypes := make(asset.Items, 0, len(p.Pairs))
|
|
for k, ps := range p.Pairs {
|
|
if enabled && (ps.AssetEnabled == nil || !*ps.AssetEnabled) {
|
|
continue
|
|
}
|
|
assetTypes = append(assetTypes, k)
|
|
}
|
|
return assetTypes
|
|
}
|
|
|
|
// Get gets the currency pair config based on the asset type
|
|
func (p *PairsManager) Get(a asset.Item) (*PairStore, error) {
|
|
if !a.IsValid() {
|
|
return nil, fmt.Errorf("%s %w", a, asset.ErrNotSupported)
|
|
}
|
|
|
|
p.mutex.RLock()
|
|
defer p.mutex.RUnlock()
|
|
c, ok := p.Pairs[a]
|
|
if !ok {
|
|
return nil,
|
|
fmt.Errorf("cannot get pair store, %v %w", a, asset.ErrNotSupported)
|
|
}
|
|
return c.copy()
|
|
}
|
|
|
|
// Store stores a new currency pair config based on its asset type
|
|
func (p *PairsManager) Store(a asset.Item, ps *PairStore) error {
|
|
if !a.IsValid() {
|
|
return fmt.Errorf("%s %w", a, asset.ErrNotSupported)
|
|
}
|
|
cpy, err := ps.copy()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
p.mutex.Lock()
|
|
if p.Pairs == nil {
|
|
p.Pairs = make(map[asset.Item]*PairStore)
|
|
}
|
|
p.Pairs[a] = cpy
|
|
p.mutex.Unlock()
|
|
return nil
|
|
}
|
|
|
|
// Delete deletes a map entry based on the supplied asset type
|
|
func (p *PairsManager) Delete(a asset.Item) {
|
|
p.mutex.Lock()
|
|
delete(p.Pairs, a)
|
|
p.mutex.Unlock()
|
|
}
|
|
|
|
// GetPairs gets a list of stored pairs based on the asset type and whether
|
|
// they're enabled or not
|
|
func (p *PairsManager) GetPairs(a asset.Item, enabled bool) (Pairs, error) {
|
|
if !a.IsValid() {
|
|
return nil, fmt.Errorf("%s %w", a, asset.ErrNotSupported)
|
|
}
|
|
|
|
p.mutex.RLock()
|
|
defer p.mutex.RUnlock()
|
|
pairStore, ok := p.Pairs[a]
|
|
if !ok {
|
|
return nil, nil
|
|
}
|
|
|
|
if !enabled {
|
|
availPairs := make(Pairs, len(pairStore.Available))
|
|
copy(availPairs, pairStore.Available)
|
|
return availPairs, nil
|
|
}
|
|
|
|
lenCheck := len(pairStore.Enabled)
|
|
if lenCheck == 0 {
|
|
return nil, nil
|
|
}
|
|
|
|
// NOTE: enabledPairs is declared before the next check for comparison
|
|
// reasons within exchange update pairs functionality.
|
|
enabledPairs := make(Pairs, lenCheck)
|
|
copy(enabledPairs, pairStore.Enabled)
|
|
|
|
err := pairStore.Available.ContainsAll(pairStore.Enabled, true)
|
|
if err != nil {
|
|
err = fmt.Errorf("%w of asset type %s", err, a)
|
|
}
|
|
return enabledPairs, err
|
|
}
|
|
|
|
// StoreFormat stores a new format for request or config format.
|
|
func (p *PairsManager) StoreFormat(a asset.Item, pFmt *PairFormat, config bool) error {
|
|
if !a.IsValid() {
|
|
return fmt.Errorf("%s %w", a, asset.ErrNotSupported)
|
|
}
|
|
if pFmt == nil {
|
|
return errPairFormatIsNil
|
|
}
|
|
|
|
cpy := *pFmt
|
|
|
|
p.mutex.Lock()
|
|
defer p.mutex.Unlock()
|
|
|
|
if p.Pairs == nil {
|
|
p.Pairs = make(map[asset.Item]*PairStore)
|
|
}
|
|
|
|
pairStore, ok := p.Pairs[a]
|
|
if !ok {
|
|
pairStore = new(PairStore)
|
|
p.Pairs[a] = pairStore
|
|
}
|
|
|
|
if config {
|
|
pairStore.ConfigFormat = &cpy
|
|
} else {
|
|
pairStore.RequestFormat = &cpy
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// StorePairs stores a list of pairs based on the asset type and whether
|
|
// they're enabled or not
|
|
func (p *PairsManager) StorePairs(a asset.Item, pairs Pairs, enabled bool) error {
|
|
if !a.IsValid() {
|
|
return fmt.Errorf("%s %w", a, asset.ErrNotSupported)
|
|
}
|
|
|
|
// NOTE: Length check not needed in this scenario as it has the ability to
|
|
// remove the entire stored list if needed.
|
|
cpy := make(Pairs, len(pairs))
|
|
copy(cpy, pairs)
|
|
|
|
p.mutex.Lock()
|
|
defer p.mutex.Unlock()
|
|
|
|
if p.Pairs == nil {
|
|
p.Pairs = make(map[asset.Item]*PairStore)
|
|
}
|
|
|
|
pairStore, ok := p.Pairs[a]
|
|
if !ok {
|
|
pairStore = new(PairStore)
|
|
p.Pairs[a] = pairStore
|
|
}
|
|
|
|
if enabled {
|
|
pairStore.Enabled = cpy
|
|
} else {
|
|
pairStore.Available = cpy
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// EnsureOnePairEnabled not all assets have pairs, eg options
|
|
// search for an asset that does and enable one if none are enabled
|
|
// error if no currency pairs found for an entire exchange
|
|
// returns the asset and pair of a pair if it has been enabled
|
|
func (p *PairsManager) EnsureOnePairEnabled() (Pair, asset.Item, error) {
|
|
if p == nil {
|
|
return EMPTYPAIR, asset.Empty, common.ErrNilPointer
|
|
}
|
|
p.mutex.Lock()
|
|
defer p.mutex.Unlock()
|
|
for _, v := range p.Pairs {
|
|
if v.AssetEnabled == nil ||
|
|
!*v.AssetEnabled ||
|
|
len(v.Available) == 0 {
|
|
continue
|
|
}
|
|
if len(v.Enabled) > 0 {
|
|
return EMPTYPAIR, asset.Empty, nil
|
|
}
|
|
}
|
|
for k, v := range p.Pairs {
|
|
if v.AssetEnabled == nil ||
|
|
!*v.AssetEnabled ||
|
|
len(v.Available) == 0 {
|
|
continue
|
|
}
|
|
rp, err := v.Available.GetRandomPair()
|
|
if err != nil {
|
|
return EMPTYPAIR, asset.Empty, err
|
|
}
|
|
p.Pairs[k].Enabled = v.Enabled.Add(rp)
|
|
return rp, k, nil
|
|
}
|
|
return EMPTYPAIR, asset.Empty, ErrCurrencyPairsEmpty
|
|
}
|
|
|
|
// DisablePair removes the pair from the enabled pairs list if found
|
|
func (p *PairsManager) DisablePair(a asset.Item, pair Pair) error {
|
|
if !a.IsValid() {
|
|
return fmt.Errorf("%s %w", a, asset.ErrNotSupported)
|
|
}
|
|
|
|
if pair.IsEmpty() {
|
|
return ErrCurrencyPairEmpty
|
|
}
|
|
|
|
p.mutex.Lock()
|
|
defer p.mutex.Unlock()
|
|
|
|
pairStore, err := p.getPairStoreRequiresLock(a)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
enabled, err := pairStore.Enabled.Remove(pair)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
pairStore.Enabled = enabled
|
|
return nil
|
|
}
|
|
|
|
// EnablePair adds a pair to the list of enabled pairs if it exists in the list
|
|
// of available pairs and isn't already added
|
|
func (p *PairsManager) EnablePair(a asset.Item, pair Pair) error {
|
|
if !a.IsValid() {
|
|
return fmt.Errorf("%s %w", a, asset.ErrNotSupported)
|
|
}
|
|
|
|
if pair.IsEmpty() {
|
|
return ErrCurrencyPairEmpty
|
|
}
|
|
|
|
p.mutex.Lock()
|
|
defer p.mutex.Unlock()
|
|
|
|
pairStore, err := p.getPairStoreRequiresLock(a)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
if pairStore.Enabled.Contains(pair, true) {
|
|
return fmt.Errorf("%s %w", pair, ErrPairAlreadyEnabled)
|
|
}
|
|
|
|
if !pairStore.Available.Contains(pair, true) {
|
|
return fmt.Errorf("%s %w in the list of available pairs",
|
|
pair, ErrPairNotFound)
|
|
}
|
|
pairStore.Enabled = pairStore.Enabled.Add(pair)
|
|
return nil
|
|
}
|
|
|
|
// IsAssetPairEnabled checks if a pair is enabled for an enabled asset type
|
|
func (p *PairsManager) IsAssetPairEnabled(a asset.Item, pair Pair) error {
|
|
if !a.IsValid() {
|
|
return fmt.Errorf("%s %w", a, asset.ErrNotSupported)
|
|
}
|
|
|
|
if pair.IsEmpty() {
|
|
return ErrCurrencyPairEmpty
|
|
}
|
|
|
|
p.mutex.RLock()
|
|
defer p.mutex.RUnlock()
|
|
|
|
pairStore, err := p.getPairStoreRequiresLock(a)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
if pairStore.AssetEnabled == nil {
|
|
return fmt.Errorf("%s %w", a, ErrAssetIsNil)
|
|
}
|
|
if !*pairStore.AssetEnabled {
|
|
return fmt.Errorf("%s %w", a, asset.ErrNotEnabled)
|
|
}
|
|
if !pairStore.Enabled.Contains(pair, true) {
|
|
return fmt.Errorf("%s %w", pair, ErrPairNotFound)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// IsAssetEnabled checks to see if an asset is enabled
|
|
func (p *PairsManager) IsAssetEnabled(a asset.Item) error {
|
|
if !a.IsValid() {
|
|
return fmt.Errorf("%s %w", a, asset.ErrNotSupported)
|
|
}
|
|
|
|
p.mutex.RLock()
|
|
defer p.mutex.RUnlock()
|
|
|
|
pairStore, err := p.getPairStoreRequiresLock(a)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
if pairStore.AssetEnabled == nil {
|
|
return fmt.Errorf("%s %w", a, ErrAssetIsNil)
|
|
}
|
|
|
|
if !*pairStore.AssetEnabled {
|
|
return fmt.Errorf("%s %w", a, asset.ErrNotEnabled)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// SetAssetEnabled sets if an asset is enabled or disabled for first run
|
|
func (p *PairsManager) SetAssetEnabled(a asset.Item, enabled bool) error {
|
|
if !a.IsValid() {
|
|
return fmt.Errorf("%s %w", a, asset.ErrNotSupported)
|
|
}
|
|
|
|
p.mutex.Lock()
|
|
defer p.mutex.Unlock()
|
|
|
|
pairStore, err := p.getPairStoreRequiresLock(a)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
if pairStore.AssetEnabled == nil {
|
|
pairStore.AssetEnabled = convert.BoolPtr(enabled)
|
|
return nil
|
|
}
|
|
|
|
if !*pairStore.AssetEnabled && !enabled {
|
|
return errors.New("asset already disabled")
|
|
} else if *pairStore.AssetEnabled && enabled {
|
|
return ErrAssetAlreadyEnabled
|
|
}
|
|
|
|
*pairStore.AssetEnabled = enabled
|
|
return nil
|
|
}
|
|
|
|
// Load sets the pair manager from a seed without copying mutexes
|
|
func (p *PairsManager) Load(seed *PairsManager) error {
|
|
if seed == nil {
|
|
return fmt.Errorf("%w PairsManager", common.ErrNilPointer)
|
|
}
|
|
p.mutex.Lock()
|
|
defer p.mutex.Unlock()
|
|
seed.mutex.RLock()
|
|
defer seed.mutex.RUnlock()
|
|
|
|
var pN PairsManager
|
|
j, err := json.Marshal(seed)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
err = json.Unmarshal(j, &pN)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
p.BypassConfigFormatUpgrades = pN.BypassConfigFormatUpgrades
|
|
if pN.UseGlobalFormat {
|
|
p.UseGlobalFormat = pN.UseGlobalFormat
|
|
p.RequestFormat = pN.RequestFormat
|
|
p.ConfigFormat = pN.ConfigFormat
|
|
}
|
|
p.LastUpdated = pN.LastUpdated
|
|
p.Pairs = pN.Pairs
|
|
|
|
return nil
|
|
}
|
|
|
|
func (p *PairsManager) getPairStoreRequiresLock(a asset.Item) (*PairStore, error) {
|
|
if p.Pairs == nil {
|
|
return nil, fmt.Errorf("%w when requesting %v pairs", ErrPairManagerNotInitialised, a)
|
|
}
|
|
|
|
pairStore, ok := p.Pairs[a]
|
|
if !ok {
|
|
return nil, fmt.Errorf("%w %v", ErrAssetNotFound, a)
|
|
}
|
|
|
|
if pairStore == nil {
|
|
return nil, errors.New("currency store is nil")
|
|
}
|
|
|
|
return pairStore, nil
|
|
}
|
|
|
|
// UnmarshalJSON implements the unmarshal json interface so that the key can be
|
|
// correctly unmarshalled from a string into a uint.
|
|
func (fs *FullStore) UnmarshalJSON(d []byte) error {
|
|
var temp map[string]*PairStore
|
|
err := json.Unmarshal(d, &temp)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
*fs = make(FullStore, len(temp))
|
|
for key, val := range temp {
|
|
ai, err := asset.New(key)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
(*fs)[ai] = val
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// MarshalJSON implements the marshal json interface so that the key can be
|
|
// correctly marshalled from a uint.
|
|
func (fs FullStore) MarshalJSON() ([]byte, error) {
|
|
temp := make(map[string]*PairStore, len(fs))
|
|
for key, val := range fs {
|
|
temp[key.String()] = val
|
|
}
|
|
return json.Marshal(temp)
|
|
}
|
|
|
|
// copy copies and segregates pair store from internal and external calls.
|
|
func (ps *PairStore) copy() (*PairStore, error) {
|
|
if ps == nil {
|
|
return nil, errPairStoreIsNil
|
|
}
|
|
var assetEnabled *bool
|
|
if ps.AssetEnabled != nil {
|
|
assetEnabled = convert.BoolPtr(*ps.AssetEnabled)
|
|
}
|
|
|
|
enabled := make(Pairs, len(ps.Enabled))
|
|
copy(enabled, ps.Enabled)
|
|
|
|
avail := make(Pairs, len(ps.Available))
|
|
copy(avail, ps.Available)
|
|
|
|
var rFmt *PairFormat
|
|
if ps.RequestFormat != nil {
|
|
cpy := *ps.RequestFormat
|
|
rFmt = &cpy
|
|
}
|
|
|
|
var cFmt *PairFormat
|
|
if ps.ConfigFormat != nil {
|
|
cpy := *ps.ConfigFormat
|
|
cFmt = &cpy
|
|
}
|
|
|
|
return &PairStore{
|
|
AssetEnabled: assetEnabled,
|
|
Enabled: enabled,
|
|
Available: avail,
|
|
RequestFormat: rFmt,
|
|
ConfigFormat: cFmt,
|
|
}, nil
|
|
}
|