Skip to content

Commit

Permalink
currency: Adds matching lookup table built from available pairs (thra…
Browse files Browse the repository at this point in the history
…sher-corp#1312)

* currency: Add pair matching update (cherry-pick)

* exchange/currency: Add tests and update func

* linter fix, also if using json unmarshal functionality stop usage of string conversion without delimiter

* gemini: fix test

* currency/manager: potential optimisation

* exchanges: purge derive from wrapper cases and add warning comment

* glorious: nits

* glorious: nits

* linter: fix

* glorious: nits

* whoops

* whoops

* glorious: nits continued

* glorious: diff THANKS!

* hitbtc: fix update tradable pairs strings splitting. continue if not enabled tickers update pair.

* glorious: nits

* linter: fix

* Update exchanges/exmo/exmo_wrapper.go

Co-authored-by: Scott <[email protected]>

* bitstamp: fix test when 32 biterinos architecturinos

* capture more strings for speed

* swapsies because whos running 32bit \0/?

---------

Co-authored-by: Ryan O'Hara-Reid <[email protected]>
Co-authored-by: Scott <[email protected]>
  • Loading branch information
3 people authored Oct 18, 2023
1 parent d3bf4a4 commit ceef7a1
Show file tree
Hide file tree
Showing 32 changed files with 616 additions and 259 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -300,6 +300,8 @@ func generateMethodArg(ctx context.Context, t *testing.T, argGenerator *MethodAr
// Crypto Chain
input = reflect.ValueOf(cryptoChainPerExchange[exchName])
}
case "MatchSymbolWithAvailablePairs", "MatchSymbolCheckEnabled":
input = reflect.ValueOf(argGenerator.AssetParams.Pair.Base.Lower().String() + argGenerator.AssetParams.Pair.Quote.Lower().String())
default:
// OrderID
input = reflect.ValueOf("1337")
Expand Down Expand Up @@ -602,6 +604,7 @@ var acceptableErrors = []error{
order.ErrPairIsEmpty, // Is thrown when the empty pair and asset scenario for an order submission is sent in the Validate() function
deposit.ErrAddressNotFound, // Is thrown when an address is not found due to the exchange requiring valid API keys
futures.ErrNotFuturesAsset, // Is thrown when a futures function receives a non-futures asset
currency.ErrSymbolStringEmpty, // Is thrown when a symbol string is empty for blank MatchSymbol func checks
}

// warningErrors will t.Log(err) when thrown to diagnose things, but not necessarily suggest
Expand Down
8 changes: 0 additions & 8 deletions currency/currency_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -81,11 +81,3 @@ const (
ForwardSlashDelimiter = "/"
ColonDelimiter = ":"
)

// delimiters is a delimiter list
var delimiters = []string{
DashDelimiter,
UnderscoreDelimiter,
ForwardSlashDelimiter,
ColonDelimiter,
}
73 changes: 56 additions & 17 deletions currency/manager.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import (
"encoding/json"
"errors"
"fmt"
"strings"

"github.com/thrasher-corp/gocryptotrader/common"
"github.com/thrasher-corp/gocryptotrader/common/convert"
Expand All @@ -29,9 +30,12 @@ var (
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")
// ErrSymbolStringEmpty is an error when a symbol string is empty
ErrSymbolStringEmpty = errors.New("symbol string is empty")

errPairStoreIsNil = errors.New("pair store is nil")
errPairFormatIsNil = errors.New("pair format is nil")
errPairStoreIsNil = errors.New("pair store is nil")
errPairFormatIsNil = errors.New("pair format is nil")
errPairMatcherIsNil = errors.New("pair matcher is nil")
)

// GetAssetTypes returns a list of stored asset types
Expand Down Expand Up @@ -64,6 +68,24 @@ func (p *PairsManager) Get(a asset.Item) (*PairStore, error) {
return c.copy()
}

// Match returns a currency pair based on the supplied symbol and asset type
func (p *PairsManager) Match(symbol string, a asset.Item) (Pair, error) {
if symbol == "" {
return EMPTYPAIR, ErrSymbolStringEmpty
}
symbol = strings.ToLower(symbol)
p.mutex.RLock()
defer p.mutex.RUnlock()
if p.matcher == nil {
return EMPTYPAIR, errPairMatcherIsNil
}
pair, ok := p.matcher[key{symbol, a}]
if !ok {
return EMPTYPAIR, fmt.Errorf("%w for %v %v", ErrPairNotFound, symbol, a)
}
return *pair, nil
}

// 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() {
Expand All @@ -78,13 +100,29 @@ func (p *PairsManager) Store(a asset.Item, ps *PairStore) error {
p.Pairs = make(map[asset.Item]*PairStore)
}
p.Pairs[a] = cpy
if p.matcher == nil {
p.matcher = make(map[key]*Pair)
}
for x := range cpy.Available {
p.matcher[key{
Symbol: cpy.Available[x].Base.Lower().String() + cpy.Available[x].Quote.Lower().String(),
Asset: a}] = &cpy.Available[x]
}
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()
vals, ok := p.Pairs[a]
if !ok {
p.mutex.Unlock()
return
}
for x := range vals.Available {
delete(p.matcher, key{Symbol: vals.Available[x].Base.Lower().String() + vals.Available[x].Quote.Lower().String(), Asset: a})
}
delete(p.Pairs, a)
p.mutex.Unlock()
}
Expand Down Expand Up @@ -187,6 +225,15 @@ func (p *PairsManager) StorePairs(a asset.Item, pairs Pairs, enabled bool) error
pairStore.Enabled = cpy
} else {
pairStore.Available = cpy

if p.matcher == nil {
p.matcher = make(map[key]*Pair)
}
for x := range pairStore.Available {
p.matcher[key{
Symbol: pairStore.Available[x].Base.Lower().String() + pairStore.Available[x].Quote.Lower().String(),
Asset: a}] = &pairStore.Available[x]
}
}

return nil
Expand Down Expand Up @@ -285,35 +332,27 @@ func (p *PairsManager) EnablePair(a asset.Item, pair Pair) error {
return nil
}

// IsAssetPairEnabled checks if a pair is enabled for an enabled asset type
func (p *PairsManager) IsAssetPairEnabled(a asset.Item, pair Pair) error {
// IsPairEnabled checks if a pair is enabled for an enabled asset type
func (p *PairsManager) IsPairEnabled(pair Pair, a asset.Item) (bool, error) {
if !a.IsValid() {
return fmt.Errorf("%s %w", a, asset.ErrNotSupported)
return false, fmt.Errorf("%s %w", a, asset.ErrNotSupported)
}

if pair.IsEmpty() {
return ErrCurrencyPairEmpty
return false, ErrCurrencyPairEmpty
}

p.mutex.RLock()
defer p.mutex.RUnlock()

pairStore, err := p.getPairStoreRequiresLock(a)
if err != nil {
return err
return false, err
}

if pairStore.AssetEnabled == nil {
return fmt.Errorf("%s %w", a, ErrAssetIsNil)
return false, 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
return *pairStore.AssetEnabled && pairStore.Enabled.Contains(pair, true), nil
}

// IsAssetEnabled checks to see if an asset is enabled
Expand Down
87 changes: 75 additions & 12 deletions currency/manager_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,48 @@ func TestGet(t *testing.T) {
}
}

func TestPairsManagerMatch(t *testing.T) {
t.Parallel()

p := &PairsManager{}

_, err := p.Match("", 1337)
if !errors.Is(err, ErrSymbolStringEmpty) {
t.Fatalf("received: '%v' but expected: '%v'", err, ErrSymbolStringEmpty)
}

_, err = p.Match("sillyBilly", 1337)
if !errors.Is(err, errPairMatcherIsNil) {
t.Fatalf("received: '%v' but expected: '%v'", err, errPairMatcherIsNil)
}

p = initTest(t)

_, err = p.Match("sillyBilly", 1337)
if !errors.Is(err, ErrPairNotFound) {
t.Fatalf("received: '%v' but expected: '%v'", err, ErrPairNotFound)
}

_, err = p.Match("sillyBilly", asset.Spot)
if !errors.Is(err, ErrPairNotFound) {
t.Fatalf("received: '%v' but expected: '%v'", err, ErrPairNotFound)
}

whatIgot, err := p.Match("bTCuSD", asset.Spot)
if !errors.Is(err, nil) {
t.Fatalf("received: '%v' but expected: '%v'", err, nil)
}

whatIwant, err := NewPairFromString("btc-usd")
if err != nil {
t.Fatal(err)
}

if !whatIgot.Equal(whatIwant) {
t.Fatal("expected btc-usd")
}
}

func TestStore(t *testing.T) {
t.Parallel()
availPairs, err := NewPairsFromStrings([]string{"BTC-USD", "LTC-USD"})
Expand Down Expand Up @@ -531,43 +573,64 @@ func TestUnmarshalMarshal(t *testing.T) {
}
}

func TestIsAssetPairEnabled(t *testing.T) {
func TestIsPairEnabled(t *testing.T) {
t.Parallel()
pm := initTest(t)
cp := NewPairWithDelimiter("BTC", "USD", "-")
err := pm.IsAssetPairEnabled(asset.Spot, cp)
enabled, err := pm.IsPairEnabled(cp, asset.Spot)
if !errors.Is(err, nil) {
t.Fatalf("received: '%v' but expected: '%v'", err, nil)
}

if !enabled {
t.Fatal("expected pair to be enabled")
}

enabled, err = pm.IsPairEnabled(NewPair(SAFE, MOONRISE), asset.Spot)
if !errors.Is(err, nil) {
t.Fatalf("received: '%v' but expected: '%v'", err, nil)
}

err = pm.IsAssetPairEnabled(asset.Futures, cp)
if !errors.Is(err, asset.ErrNotEnabled) {
t.Fatalf("received: '%v' but expected: '%v'", err, asset.ErrNotEnabled)
if enabled {
t.Fatal("expected pair to be disabled")
}

enabled, err = pm.IsPairEnabled(cp, asset.Futures)
if !errors.Is(err, nil) {
t.Fatalf("received: '%v' but expected: '%v'", err, nil)
}

if enabled {
t.Fatal("expected pair to be disabled because asset type is not enabled")
}

cp = NewPairWithDelimiter("XRP", "DOGE", "-")
err = pm.IsAssetPairEnabled(asset.Spot, cp)
if !errors.Is(err, ErrPairNotFound) {
t.Fatalf("received: '%v' but expected: '%v'", err, ErrPairNotFound)
enabled, err = pm.IsPairEnabled(cp, asset.Spot)
if !errors.Is(err, nil) {
t.Fatalf("received: '%v' but expected: '%v'", err, nil)
}

if enabled {
t.Fatal("expected pair to be disabled because pair not found in enabled list")
}

err = pm.IsAssetPairEnabled(asset.PerpetualSwap, cp)
_, err = pm.IsPairEnabled(cp, asset.PerpetualSwap)
if !errors.Is(err, ErrAssetNotFound) {
t.Fatalf("received: '%v' but expected: '%v'", err, ErrAssetNotFound)
}

err = pm.IsAssetPairEnabled(asset.Item(1337), cp)
_, err = pm.IsPairEnabled(cp, asset.Item(1337))
if !errors.Is(err, asset.ErrNotSupported) {
t.Fatalf("received: '%v' but expected: '%v'", err, asset.ErrNotSupported)
}

pm.Pairs[asset.PerpetualSwap] = &PairStore{}
err = pm.IsAssetPairEnabled(asset.PerpetualSwap, cp)
_, err = pm.IsPairEnabled(cp, asset.PerpetualSwap)
if !errors.Is(err, ErrAssetIsNil) {
t.Fatalf("received: '%v' but expected: '%v'", err, ErrAssetIsNil)
}

err = pm.IsAssetPairEnabled(asset.PerpetualSwap, EMPTYPAIR)
_, err = pm.IsPairEnabled(EMPTYPAIR, asset.PerpetualSwap)
if !errors.Is(err, ErrCurrencyPairEmpty) {
t.Fatalf("received: '%v' but expected: '%v'", err, ErrCurrencyPairEmpty)
}
Expand Down
21 changes: 14 additions & 7 deletions currency/manager_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,13 +8,14 @@ import (

// PairsManager manages asset pairs
type PairsManager struct {
BypassConfigFormatUpgrades bool `json:"bypassConfigFormatUpgrades"`
RequestFormat *PairFormat `json:"requestFormat,omitempty"`
ConfigFormat *PairFormat `json:"configFormat,omitempty"`
UseGlobalFormat bool `json:"useGlobalFormat,omitempty"`
LastUpdated int64 `json:"lastUpdated,omitempty"`
Pairs FullStore `json:"pairs"`
mutex sync.RWMutex `json:"-"`
BypassConfigFormatUpgrades bool `json:"bypassConfigFormatUpgrades"`
RequestFormat *PairFormat `json:"requestFormat,omitempty"`
ConfigFormat *PairFormat `json:"configFormat,omitempty"`
UseGlobalFormat bool `json:"useGlobalFormat,omitempty"`
LastUpdated int64 `json:"lastUpdated,omitempty"`
Pairs FullStore `json:"pairs"`
matcher map[key]*Pair
mutex sync.RWMutex
}

// FullStore holds all supported asset types with the enabled and available
Expand All @@ -37,3 +38,9 @@ type PairFormat struct {
Separator string `json:"separator,omitempty"`
Index string `json:"index,omitempty"`
}

// key is used to store the asset type and symbol in a map
type key struct {
Symbol string
Asset asset.Item
}
25 changes: 10 additions & 15 deletions currency/pair.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import (
"errors"
"fmt"
"strings"
"unicode"
)

var errCannotCreatePair = errors.New("cannot create currency pair")
Expand Down Expand Up @@ -100,23 +101,17 @@ func NewPairFromString(currencyPair string) (Pair, error) {
errCannotCreatePair,
currencyPair)
}
var delimiter string
pairStrings := []string{currencyPair}
for x := range delimiters {
if strings.Contains(pairStrings[0], delimiters[x]) {
values := strings.SplitN(pairStrings[0], delimiters[x], 2)
if delimiter != "" {
values[1] += delimiter + pairStrings[1]
pairStrings = values
} else {
pairStrings = values
}
delimiter = delimiters[x]

for x := range currencyPair {
if unicode.IsPunct(rune(currencyPair[x])) {
return Pair{
Base: NewCode(currencyPair[:x]),
Delimiter: string(currencyPair[x]),
Quote: NewCode(currencyPair[x+1:]),
}, nil
}
}
if delimiter != "" {
return Pair{Base: NewCode(pairStrings[0]), Delimiter: delimiter, Quote: NewCode(pairStrings[1])}, nil
}

return NewPairFromStrings(currencyPair[0:3], currencyPair[3:])
}

Expand Down
18 changes: 16 additions & 2 deletions currency/pair_methods.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import (
"encoding/json"
"errors"
"fmt"
"unicode"
)

// EMPTYFORMAT defines an empty pair format
Expand Down Expand Up @@ -45,8 +46,21 @@ func (p *Pair) UnmarshalJSON(d []byte) error {
return nil
}

*p, err = NewPairFromString(pair)
return err
// Check if pair is in the format of BTC-USD
for x := range pair {
if unicode.IsPunct(rune(pair[x])) {
p.Base = NewCode(pair[:x])
p.Delimiter = string(pair[x])
p.Quote = NewCode(pair[x+1:])
return nil
}
}

// NOTE: Pair string could be in format DUSKUSDT (Kucoin) which will be
// incorrectly converted to DUS-KUSDT, ELKRW (Bithumb) which will convert
// converted to ELK-RW and HTUSDT (Lbank) which will be incorrectly
// converted to HTU-SDT.
return fmt.Errorf("%w from %s cannot ensure pair is in correct format, please use exchange method MatchSymbolWithAvailablePairs", errCannotCreatePair, pair)
}

// MarshalJSON conforms type to the marshaler interface
Expand Down
Loading

0 comments on commit ceef7a1

Please sign in to comment.