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 4 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
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,
}
53 changes: 46 additions & 7 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,27 @@ 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
}
p.m.RLock()
defer p.m.RUnlock()
if p.matcher == nil {
return EMPTYPAIR, errPairMatcherIsNil
}
assets, ok := p.matcher[a]
gloriousCode marked this conversation as resolved.
Show resolved Hide resolved
if !ok {
return EMPTYPAIR, fmt.Errorf("%w: %v", asset.ErrNotSupported, a)
}
pair, ok := assets[strings.ToLower(symbol)]
if !ok {
return EMPTYPAIR, ErrPairNotFound
gloriousCode marked this conversation as resolved.
Show resolved Hide resolved
}
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 @@ -70,11 +90,19 @@ func (p *PairsManager) Store(a asset.Item, ps *PairStore) error {
if err != nil {
return err
}
matcher := make(map[string]*Pair)
for x := range cpy.Available {
matcher[cpy.Available[x].Base.Lower().String()+cpy.Available[x].Quote.Lower().String()] = &cpy.Available[x]
}
p.m.Lock()
if p.Pairs == nil {
p.Pairs = make(map[asset.Item]*PairStore)
}
p.Pairs[a] = cpy
if p.matcher == nil {
p.matcher = make(map[asset.Item]map[string]*Pair)
}
p.matcher[a] = matcher
p.m.Unlock()
return nil
}
Expand All @@ -83,6 +111,7 @@ func (p *PairsManager) Store(a asset.Item, ps *PairStore) error {
func (p *PairsManager) Delete(a asset.Item) {
p.m.Lock()
delete(p.Pairs, a)
delete(p.matcher, a)
p.m.Unlock()
}

Expand Down Expand Up @@ -184,6 +213,16 @@ func (p *PairsManager) StorePairs(a asset.Item, pairs Pairs, enabled bool) error
pairStore.Enabled = cpy
} else {
pairStore.Available = cpy

matcher := make(map[string]*Pair)
for x := range pairStore.Available {
matcher[pairStore.Available[x].Base.Lower().String()+pairStore.Available[x].Quote.Lower().String()] = &pairStore.Available[x]
}

if p.matcher == nil {
p.matcher = make(map[asset.Item]map[string]*Pair)
}
p.matcher[a] = matcher
}

return nil
Expand Down
42 changes: 42 additions & 0 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, asset.ErrNotSupported) {
t.Fatalf("received: '%v' but expected: '%v'", err, asset.ErrNotSupported)
}

_, 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
13 changes: 7 additions & 6 deletions currency/manager_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,13 @@ 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"`
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[asset.Item]map[string]*Pair `json:"-"`
m sync.RWMutex
}

Expand Down
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
16 changes: 16 additions & 0 deletions exchanges/exchange.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import (
"strconv"
"strings"
"time"
"unicode"

"github.com/thrasher-corp/gocryptotrader/common"
"github.com/thrasher-corp/gocryptotrader/common/convert"
Expand Down Expand Up @@ -1659,3 +1660,18 @@ func (b *Base) Shutdown() error {
}
return b.Requester.Shutdown()
}

// MatchSymbolWithAvailablePairs returns a currency pair based on the supplied
// symbol and asset type. If the string is expected to have a delimiter this
// will attempt to screen it out.
func (b *Base) MatchSymbolWithAvailablePairs(symbol string, a asset.Item, hasDelimiter bool) (currency.Pair, error) {
Copy link
Collaborator

@gloriousCode gloriousCode Sep 13, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So utilising the same benchmarking as last time for comparison:

func BenchmarkNewPairFromString(b *testing.B) {
	b.ReportAllocs()
	for i := 0; i < b.N; i++ {
		_, _ = currency.NewPairFromString("ZTG_USDT")
	}
}

func BenchmarkDeriveFrom(b *testing.B) {
	b.ReportAllocs()
	for i := 0; i < b.N; i++ {
		avail, _ := g.GetAvailablePairs(asset.Spot)
		_, _ = avail.DeriveFrom(strings.Replace("ZTG_USDT", "_", "", 1))
	}
}

func BenchmarkDeriveFromCheating(b *testing.B) {
	b.ReportAllocs()
	avail, _ := g.GetAvailablePairs(asset.Spot)
	for i := 0; i < b.N; i++ {
		_, _ = avail.DeriveFrom(strings.Replace("ZTG_USDT", "_", "", 1))
	}
}

func BenchmarkMatchSymbolWithAvailablePairs(b *testing.B) {
	b.ReportAllocs()
	for i := 0; i < b.N; i++ {
		_, _ = g.MatchSymbolWithAvailablePairs("ZTG_USDT", asset.Spot, true)
	}
}
Benchmark times tested time taken Bytes per op Allocations
BenchmarkNewPairFromString-10 20982969 57.07 ns/op 4 B/op 1 allocs/op
BenchmarkDeriveFrom-10 17596 67605 ns/op 294928 B/op 4 allocs/op
BenchmarkDeriveFromCheating-10 156580 7163 ns/op 17 B/op 2 allocs/op
BenchmarkMatchSymbolWithAvailablePairs-10 15378223 76.50 ns/op 16 B/op 2 allocs/op

Does show a lovely improvement. Would you be considering the removal of Derivefrom? Because your new feature seems underutilised in this PR

Plus very nice work on the PairFromString improvements!

if hasDelimiter {
for x := range symbol {
if unicode.IsPunct(rune(symbol[x])) {
symbol = symbol[:x] + symbol[x+1:]
break
}
}
}
return b.CurrencyPairs.Match(symbol, a)
}
34 changes: 34 additions & 0 deletions exchanges/exchange_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2988,3 +2988,37 @@ func TestEnsureOnePairEnabled(t *testing.T) {
t.Fatalf("received: '%v' but expected: '%v'", len(b.CurrencyPairs.Pairs[asset.Spot].Enabled), 1)
}
}

func TestMatchSymbolWithAvailablePairs(t *testing.T) {
b := Base{Name: "test"}
whatIWant := currency.NewPair(currency.BTC, currency.USDT)
err := b.CurrencyPairs.Store(asset.Spot, &currency.PairStore{
AssetEnabled: convert.BoolPtr(true),
Available: []currency.Pair{whatIWant}})
if err != nil {
t.Fatal(err)
}

_, err = b.MatchSymbolWithAvailablePairs("sillBillies", asset.Futures, false)
if !errors.Is(err, asset.ErrNotSupported) {
t.Fatalf("received: '%v' but expected: '%v'", err, asset.ErrNotSupported)
}

whatIGot, err := b.MatchSymbolWithAvailablePairs("btcusdT", asset.Spot, false)
if !errors.Is(err, nil) {
t.Fatalf("received: '%v' but expected: '%v'", err, nil)
}

if !whatIGot.Equal(whatIWant) {
t.Fatalf("received: '%v' but expected: '%v'", whatIGot, whatIWant)
}

whatIGot, err = b.MatchSymbolWithAvailablePairs("btc-usdT", asset.Spot, true)
if !errors.Is(err, nil) {
t.Fatalf("received: '%v' but expected: '%v'", err, nil)
}

if !whatIGot.Equal(whatIWant) {
t.Fatalf("received: '%v' but expected: '%v'", whatIGot, whatIWant)
}
}
22 changes: 11 additions & 11 deletions exchanges/gemini/gemini_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -31,17 +31,17 @@ type Ticker struct {

// TickerV2 holds returned ticker data from the exchange
type TickerV2 struct {
Ask float64 `json:"ask,string"`
Bid float64 `json:"bid,string"`
Changes []string `json:"changes"`
Close float64 `json:"close,string"`
High float64 `json:"high,string"`
Low float64 `json:"low,string"`
Open float64 `json:"open,string"`
Message string `json:"message,omitempty"`
Reason string `json:"reason,omitempty"`
Result string `json:"result,omitempty"`
Symbol currency.Pair `json:"symbol"`
Ask float64 `json:"ask,string"`
Bid float64 `json:"bid,string"`
Changes []string `json:"changes"`
Close float64 `json:"close,string"`
High float64 `json:"high,string"`
Low float64 `json:"low,string"`
Open float64 `json:"open,string"`
Message string `json:"message,omitempty"`
Reason string `json:"reason,omitempty"`
Result string `json:"result,omitempty"`
Symbol string `json:"symbol"`
}

// Orderbook contains orderbook information for both bid and ask side
Expand Down