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/exchanges: Add bespoke exchange translator and pair matching helper #1556

Merged
merged 18 commits into from
Aug 16, 2024
1 change: 1 addition & 0 deletions currency/code_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -3070,6 +3070,7 @@ var (
WIF = NewCode("WIF")
AIDOGE = NewCode("AIDOGE")
PEPE = NewCode("PEPE")
USDCM = NewCode("USDCM")
EURR = NewCode("EURR")

stables = Currencies{
Expand Down
80 changes: 80 additions & 0 deletions currency/translation.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,3 +20,83 @@ var translations = map[*Item]Code{
XDG.Item: DOGE,
USDT.Item: USD,
}

// NewTranslations returns a new translation map, the key indicates the exchange
// representation and the value indicates the internal representation/common/standard
// representation. e.g. XBT as key and BTC as value, this is useful for exchanges
// that use different naming conventions.
func NewTranslations(t map[Code]Code) Translations {
lookup := make(map[*Item]Code)
for k, v := range t {
lookup[k.Item] = v
}
return lookup
}

// Translations is a map of translations for a specific exchange implementation
type Translations map[*Item]Code

gloriousCode marked this conversation as resolved.
Show resolved Hide resolved
// Translate returns the translated currency code, usually used to convert
// exchange specific currency codes to common currency codes. If no translation
// is found it will return the original currency code.
func (t Translations) Translate(incoming Code) Code {
if len(t) == 0 {
return incoming
}
val, ok := (t)[incoming.Item]
if !ok {
return incoming
}
return val
}

// Translator is an interface for translating currency codes
type Translator interface {
// TODO: Add a asset.Item param so that we can translate for asset
// permutations. Also return error.
Translate(Code) Code
}

// PairsWithTranslation is a pair list with a translator for a specific exchange.
type PairsWithTranslation struct {
Pairs Pairs
Translator Translator
}

// keyPair defines an immutable pair for lookup purposes
type keyPair struct {
Base *Item
Quote *Item
}
gloriousCode marked this conversation as resolved.
Show resolved Hide resolved

// FindMatchingPairsBetween returns all pairs that match the incoming pairs.
// Translator is used to convert exchange specific currency codes to common
// currency codes used in lookup process. The pairs are not modified. So that
// the original pairs are returned for deployment to the specific exchange.
// NOTE: Translator is optional and can be nil. Translator can be obtained from
// the exchange implementation by calling Base() method and accessing Features
// and Translation fields.
func FindMatchingPairsBetween(this, that PairsWithTranslation) map[Pair]Pair {
gloriousCode marked this conversation as resolved.
Show resolved Hide resolved
lookup := make(map[keyPair]*Pair)
var k keyPair
for i := range this.Pairs {
if this.Translator != nil {
k = keyPair{Base: this.Translator.Translate(this.Pairs[i].Base).Item, Quote: this.Translator.Translate(this.Pairs[i].Quote).Item}
lookup[k] = &this.Pairs[i]
continue
}
lookup[keyPair{Base: this.Pairs[i].Base.Item, Quote: this.Pairs[i].Quote.Item}] = &this.Pairs[i]
}
outgoing := make(map[Pair]Pair)
for i := range that.Pairs {
if that.Translator != nil {
k = keyPair{Base: that.Translator.Translate(that.Pairs[i].Base).Item, Quote: that.Translator.Translate(that.Pairs[i].Quote).Item}
} else {
k = keyPair{Base: that.Pairs[i].Base.Item, Quote: that.Pairs[i].Quote.Item}
}
if p, ok := lookup[k]; ok {
outgoing[*p] = that.Pairs[i]
}
}
return outgoing
}
96 changes: 96 additions & 0 deletions currency/translation_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ package currency

import (
"testing"

"github.com/stretchr/testify/require"
)

func TestGetTranslation(t *testing.T) {
Expand Down Expand Up @@ -34,3 +36,97 @@ func TestGetTranslation(t *testing.T) {
t.Errorf("received: '%v', but expected: '%v'", actual, XBT)
}
}

func TestNewTranslations(t *testing.T) {
t.Parallel()
translations := NewTranslations(map[Code]Code{
XBT: BTC,
XETH: ETH,
XDG: DOGE,
USDM: USD,
})
require.NotNil(t, translations)

if !translations.Translate(XBT).Equal(BTC) {
t.Error("NewTranslations: translation failed")
}

if !translations.Translate(LTC).Equal(LTC) {
t.Error("NewTranslations: translation failed")
}
}

func TestFindMatchingPairsBetween(t *testing.T) {
t.Parallel()
ltcusd := NewPair(LTC, USD)

spotPairs := Pairs{
NewPair(BTC, USD),
NewPair(ETH, USD),
NewPair(ETH, BTC),
ltcusd,
}

futuresPairs := Pairs{
NewPair(XBT, USDM),
NewPair(XETH, USDM),
NewPair(XETH, BTCM),
ltcusd, // exact match
NewPair(XRP, USDM), // no match
}

matchingPairs := FindMatchingPairsBetween(PairsWithTranslation{spotPairs, nil}, PairsWithTranslation{futuresPairs, nil})
require.Len(t, matchingPairs, 1)

if !matchingPairs[ltcusd].Equal(ltcusd) {
thrasher- marked this conversation as resolved.
Show resolved Hide resolved
t.Error("FindMatchingPairsBetween: matching pair not found")
}

translations := NewTranslations(map[Code]Code{
gloriousCode marked this conversation as resolved.
Show resolved Hide resolved
XBT: BTC,
XETH: ETH,
XDG: DOGE,
USDM: USD,
BTCM: BTC,
})

matchingPairs = FindMatchingPairsBetween(PairsWithTranslation{spotPairs, nil}, PairsWithTranslation{futuresPairs, translations})
gloriousCode marked this conversation as resolved.
Show resolved Hide resolved
require.Len(t, matchingPairs, 4)

matchingPairs = FindMatchingPairsBetween(PairsWithTranslation{spotPairs, translations}, PairsWithTranslation{futuresPairs, translations})
require.Len(t, matchingPairs, 4)

matchingPairs = FindMatchingPairsBetween(PairsWithTranslation{spotPairs, translations}, PairsWithTranslation{futuresPairs, nil})
require.Len(t, matchingPairs, 1)
}

func BenchmarkFindMatchingPairsBetween(b *testing.B) {
ltcusd := NewPair(LTC, USD)

spotPairs := Pairs{
NewPair(BTC, USD),
NewPair(ETH, USD),
NewPair(ETH, BTC),
ltcusd,
}

futuresPairs := Pairs{
NewPair(XBT, USDM),
NewPair(XETH, USDM),
NewPair(XETH, BTCM),
ltcusd, // exact match
NewPair(XRP, USDM), // no match
}

translations := NewTranslations(map[Code]Code{
XBT: BTC,
XETH: ETH,
XDG: DOGE,
USDM: USD,
BTCM: BTC,
})

for i := 0; i < b.N; i++ {
_ = FindMatchingPairsBetween(PairsWithTranslation{spotPairs, translations}, PairsWithTranslation{futuresPairs, translations})
}
}
1 change: 1 addition & 0 deletions exchanges/exchange_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -153,6 +153,7 @@ type Features struct {
Supports FeaturesSupported
Enabled FeaturesEnabled
Subscriptions []*subscription.Subscription
Translation currency.Translations
shazbert marked this conversation as resolved.
Show resolved Hide resolved
}

// FeaturesEnabled stores the exchange enabled features
Expand Down
6 changes: 6 additions & 0 deletions exchanges/kucoin/kucoin_wrapper.go
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,12 @@ func (ku *Kucoin) SetDefaults() {
log.Errorln(log.ExchangeSys, err)
}
ku.Features = exchange.Features{
Translation: currency.NewTranslations(map[currency.Code]currency.Code{
currency.XBT: currency.BTC,
currency.USDTM: currency.USDT,
currency.USDM: currency.USD,
currency.USDCM: currency.USDC,
}),
Supports: exchange.FeaturesSupported{
REST: true,
Websocket: true,
Expand Down
Loading