Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

currency: Adds matching lookup table built from available pairs. #1312

Merged
merged 25 commits into from
Oct 18, 2023
Merged
Show file tree
Hide file tree
Changes from 14 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -547,6 +547,7 @@ var acceptableErrors = []error{
context.DeadlineExceeded, // If the context deadline is exceeded, it is not an error as only blockedCIExchanges use expired contexts by design
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
currency.ErrPairNotFound, // Is thrown when a pair is not found in a pair matching function
gloriousCode marked this conversation as resolved.
Show resolved Hide resolved
}

// 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,
}
78 changes: 56 additions & 22 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 @@ -26,9 +27,11 @@ var (
// exchange for that asset type.
ErrPairNotContainedInAvailablePairs = errors.New("pair not contained in available pairs")

errPairStoreIsNil = errors.New("pair store is nil")
errPairFormatIsNil = errors.New("pair format is nil")
errAssetNotFound = errors.New("asset type not found")
errPairStoreIsNil = errors.New("pair store is nil")
errPairFormatIsNil = errors.New("pair format is nil")
errAssetNotFound = errors.New("asset type not found")
errPairMatcherIsNil = errors.New("pair matcher is nil")
errSymbolStringEmpty = errors.New("symbol string is empty")
)

// GetAssetTypes returns a list of stored asset types
Expand All @@ -47,10 +50,6 @@ func (p *PairsManager) GetAssetTypes(enabled bool) asset.Items {

// 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.m.RLock()
defer p.m.RUnlock()
c, ok := p.Pairs[a]
gloriousCode marked this conversation as resolved.
Show resolved Hide resolved
Expand All @@ -61,6 +60,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.m.RLock()
defer p.m.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 @@ -75,13 +92,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.m.Unlock()
return nil
}

// Delete deletes a map entry based on the supplied asset type
func (p *PairsManager) Delete(a asset.Item) {
p.m.Lock()
vals, ok := p.Pairs[a]
if !ok {
p.m.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.m.Unlock()
}
Expand Down Expand Up @@ -184,6 +217,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 @@ -282,35 +324,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.m.RLock()
defer p.m.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)
}
if !*pairStore.AssetEnabled {
return fmt.Errorf("%s %w", a, asset.ErrNotEnabled)
return false, fmt.Errorf("%s %w", a, ErrAssetIsNil)
}
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 @@ -90,6 +90,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 @@ -529,43 +571,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
7 changes: 7 additions & 0 deletions currency/manager_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ type PairsManager struct {
UseGlobalFormat bool `json:"useGlobalFormat,omitempty"`
LastUpdated int64 `json:"lastUpdated,omitempty"`
Pairs FullStore `json:"pairs"`
matcher map[key]*Pair
m sync.RWMutex
}

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 {
gloriousCode marked this conversation as resolved.
Show resolved Hide resolved
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 @@ -90,23 +91,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)
gloriousCode marked this conversation as resolved.
Show resolved Hide resolved
}

// MarshalJSON conforms type to the marshaler interface
Expand Down
2 changes: 2 additions & 0 deletions currency/pair_types.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
package currency

// Pair holds currency pair information
// NOTE: UnmarshalJSON allows string conversion to Pair type but only if there
// is a delimiter present in the string, otherwise it will return an error.
type Pair struct {
Delimiter string `json:"delimiter,omitempty"`
Base Code `json:"base,omitempty"`
Expand Down
Loading