currency/exchanges: Add bespoke exchange translator and pair matching helper (#1556)

* currency: translation and matching pairs

* Update exchanges/exchange_types.go

Co-authored-by: Scott <gloriousCode@users.noreply.github.com>

* glorious: nits

* linter: fix?

* translation

* fix cherry pick

* gateio: translation for mbabydoge with 1e6 divisor

* okx: add translation

* cherry-pick: fix

* glorious: todos

* thrasher: nits

---------

Co-authored-by: Ryan O'Hara-Reid <ryan.oharareid@thrasher.io>
Co-authored-by: Scott <gloriousCode@users.noreply.github.com>
This commit is contained in:
Ryan O'Hara-Reid
2024-08-16 16:47:17 +10:00
committed by GitHub
parent 0becfbd0a6
commit facf291069
10 changed files with 251 additions and 10 deletions

View File

@@ -3070,6 +3070,7 @@ var (
WIF = NewCode("WIF")
AIDOGE = NewCode("AIDOGE")
PEPE = NewCode("PEPE")
USDCM = NewCode("USDCM")
EURR = NewCode("EURR")
stables = Currencies{

View File

@@ -20,3 +20,86 @@ var translations = map[*Item]Code{
XDG.Item: DOGE,
USDT.Item: USD,
}
// Translations is a map of translations for a specific exchange implementation
type Translations map[*Item]Code
// 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.
// TODO: Expand for specific assets.
func NewTranslations(t map[Code]Code) Translations {
lookup := make(map[*Item]Code)
for k, v := range t {
lookup[k.Item] = v
}
return lookup
}
// 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.
// TODO: Add TranslateToCommon and TranslateToExchange methods to allow for
// translation to and from exchange specific currency codes.
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
}
// 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 {
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
}

View File

@@ -2,6 +2,9 @@ package currency
import (
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestGetTranslation(t *testing.T) {
@@ -34,3 +37,116 @@ func TestGetTranslation(t *testing.T) {
t.Errorf("received: '%v', but expected: '%v'", actual, XBT)
}
}
func TestNewTranslations(t *testing.T) {
t.Parallel()
translationsTest := NewTranslations(map[Code]Code{
XBT: BTC,
XETH: ETH,
XDG: DOGE,
USDM: USD,
})
require.NotNil(t, translations)
assert.Equal(t, BTC, translationsTest.Translate(XBT), "Translate should return BTC")
assert.Equal(t, LTC, translationsTest.Translate(LTC), "Translate should return LTC")
}
func TestFindMatchingPairsBetween(t *testing.T) {
t.Parallel()
ltcusd := NewPair(LTC, USD)
spotPairs := Pairs{
NewPair(BTC, USD).Format(PairFormat{Delimiter: "DELIMITER"}),
NewPair(ETH, USD),
NewPair(ETH, BTC).Format(PairFormat{Delimiter: "DELIMITER"}),
ltcusd,
}
futuresPairs := Pairs{
NewPair(XBT, USDM),
NewPair(XETH, USDM).Format(PairFormat{Delimiter: "DELIMITER"}),
NewPair(XETH, BTCM),
ltcusd.Format(PairFormat{Delimiter: "DELIMITER"}), // exact match
NewPair(XRP, USDM), // no match
}
matchingPairs := FindMatchingPairsBetween(PairsWithTranslation{spotPairs, nil}, PairsWithTranslation{futuresPairs, nil})
require.Len(t, matchingPairs, 1)
assert.True(t, ltcusd.Equal(matchingPairs[ltcusd]), "Pairs should match")
translationsTest := NewTranslations(map[Code]Code{
XBT: BTC,
XETH: ETH,
XDG: DOGE,
USDM: USD,
BTCM: BTC,
})
expected := map[keyPair]Pair{
NewPair(BTC, USD).keyPair(): NewPair(XBT, USDM),
NewPair(ETH, USD).keyPair(): NewPair(XETH, USDM),
NewPair(ETH, BTC).keyPair(): NewPair(XETH, BTCM),
ltcusd.keyPair(): ltcusd,
}
matchingPairs = FindMatchingPairsBetween(PairsWithTranslation{spotPairs, nil}, PairsWithTranslation{futuresPairs, translationsTest})
require.Len(t, matchingPairs, 4)
for k, v := range matchingPairs {
assert.True(t, v.Equal(expected[k.keyPair()]), "Pairs should match")
}
matchingPairs = FindMatchingPairsBetween(PairsWithTranslation{spotPairs, translationsTest}, PairsWithTranslation{futuresPairs, translationsTest})
require.Len(t, matchingPairs, 4)
for k, v := range matchingPairs {
assert.True(t, v.Equal(expected[k.keyPair()]), "Pairs should match")
}
expected = map[keyPair]Pair{
ltcusd.keyPair(): ltcusd,
}
matchingPairs = FindMatchingPairsBetween(PairsWithTranslation{spotPairs, translationsTest}, PairsWithTranslation{futuresPairs, nil})
require.Len(t, matchingPairs, 1)
for k, v := range matchingPairs {
assert.True(t, v.Equal(expected[k.keyPair()]), "Pairs should match")
}
}
func (p Pair) keyPair() keyPair {
return keyPair{Base: p.Base.Item, Quote: p.Quote.Item}
}
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})
}
}