From facf29106942dc5d25dcde54271c528cdd41c915 Mon Sep 17 00:00:00 2001 From: Ryan O'Hara-Reid Date: Fri, 16 Aug 2024 16:47:17 +1000 Subject: [PATCH] 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 * 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 Co-authored-by: Scott --- currency/code_types.go | 1 + currency/translation.go | 83 +++++++++++++++++++++ currency/translation_test.go | 116 +++++++++++++++++++++++++++++ exchanges/bybit/bybit_wrapper.go | 26 +++++++ exchanges/exchange_types.go | 9 ++- exchanges/gateio/gateio_wrapper.go | 3 + exchanges/kucoin/kucoin_wrapper.go | 6 ++ exchanges/okx/okx.go | 2 +- exchanges/okx/okx_types.go | 4 +- exchanges/okx/okx_wrapper.go | 11 ++- 10 files changed, 251 insertions(+), 10 deletions(-) diff --git a/currency/code_types.go b/currency/code_types.go index e9a1f0c295a..4f3b3273de8 100644 --- a/currency/code_types.go +++ b/currency/code_types.go @@ -3070,6 +3070,7 @@ var ( WIF = NewCode("WIF") AIDOGE = NewCode("AIDOGE") PEPE = NewCode("PEPE") + USDCM = NewCode("USDCM") EURR = NewCode("EURR") stables = Currencies{ diff --git a/currency/translation.go b/currency/translation.go index 7948ef085ee..eecf57576c3 100644 --- a/currency/translation.go +++ b/currency/translation.go @@ -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 +} diff --git a/currency/translation_test.go b/currency/translation_test.go index d1254bfdb3f..728f701eafa 100644 --- a/currency/translation_test.go +++ b/currency/translation_test.go @@ -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}) + } +} diff --git a/exchanges/bybit/bybit_wrapper.go b/exchanges/bybit/bybit_wrapper.go index bb0705f1dce..e0b1ea0b317 100644 --- a/exchanges/bybit/bybit_wrapper.go +++ b/exchanges/bybit/bybit_wrapper.go @@ -74,6 +74,32 @@ func (by *Bybit) SetDefaults() { } by.Features = exchange.Features{ + CurrencyTranslations: currency.NewTranslations( + map[currency.Code]currency.Code{ + currency.NewCode("10000000AIDOGE"): currency.AIDOGE, + currency.NewCode("1000000BABYDOGE"): currency.BABYDOGE, + currency.NewCode("1000000MOG"): currency.NewCode("MOG"), + currency.NewCode("10000COQ"): currency.NewCode("COQ"), + currency.NewCode("10000LADYS"): currency.NewCode("LADYS"), + currency.NewCode("10000NFT"): currency.NFT, + currency.NewCode("10000SATS"): currency.NewCode("SATS"), + currency.NewCode("10000STARL"): currency.STARL, + currency.NewCode("10000WEN"): currency.NewCode("WEN"), + currency.NewCode("1000APU"): currency.NewCode("APU"), + currency.NewCode("1000BEER"): currency.NewCode("BEER"), + currency.NewCode("1000BONK"): currency.BONK, + currency.NewCode("1000BTT"): currency.BTT, + currency.NewCode("1000FLOKI"): currency.FLOKI, + currency.NewCode("1000IQ50"): currency.NewCode("IQ50"), + currency.NewCode("1000LUNC"): currency.LUNC, + currency.NewCode("1000PEPE"): currency.PEPE, + currency.NewCode("1000RATS"): currency.NewCode("RATS"), + currency.NewCode("1000TURBO"): currency.NewCode("TURBO"), + currency.NewCode("1000XEC"): currency.XEC, + currency.NewCode("LUNA2"): currency.LUNA, + currency.NewCode("SHIB1000"): currency.SHIB, + }, + ), Supports: exchange.FeaturesSupported{ REST: true, Websocket: true, diff --git a/exchanges/exchange_types.go b/exchanges/exchange_types.go index 1cf0f52a388..dfd325af057 100644 --- a/exchanges/exchange_types.go +++ b/exchanges/exchange_types.go @@ -150,10 +150,11 @@ type WithdrawalHistory struct { // Features stores the supported and enabled features // for the exchange type Features struct { - Supports FeaturesSupported - Enabled FeaturesEnabled - Subscriptions subscription.List - TradingRequirements protocol.TradingRequirements + Supports FeaturesSupported + Enabled FeaturesEnabled + Subscriptions subscription.List + CurrencyTranslations currency.Translations + TradingRequirements protocol.TradingRequirements } // FeaturesEnabled stores the exchange enabled features diff --git a/exchanges/gateio/gateio_wrapper.go b/exchanges/gateio/gateio_wrapper.go index b16de6353fe..2f6f64b5789 100644 --- a/exchanges/gateio/gateio_wrapper.go +++ b/exchanges/gateio/gateio_wrapper.go @@ -55,6 +55,9 @@ func (g *Gateio) SetDefaults() { } g.Features = exchange.Features{ + CurrencyTranslations: currency.NewTranslations(map[currency.Code]currency.Code{ + currency.NewCode("MBABYDOGE"): currency.BABYDOGE, + }), TradingRequirements: protocol.TradingRequirements{ SpotMarketOrderAmountPurchaseQuotationOnly: true, SpotMarketOrderAmountSellBaseOnly: true, diff --git a/exchanges/kucoin/kucoin_wrapper.go b/exchanges/kucoin/kucoin_wrapper.go index 6cf00ffac5b..0d6c20680a2 100644 --- a/exchanges/kucoin/kucoin_wrapper.go +++ b/exchanges/kucoin/kucoin_wrapper.go @@ -65,6 +65,12 @@ func (ku *Kucoin) SetDefaults() { log.Errorln(log.ExchangeSys, err) } ku.Features = exchange.Features{ + CurrencyTranslations: currency.NewTranslations(map[currency.Code]currency.Code{ + currency.XBT: currency.BTC, + currency.USDTM: currency.USDT, + currency.USDM: currency.USD, + currency.USDCM: currency.USDC, + }), TradingRequirements: protocol.TradingRequirements{ ClientOrderID: true, }, diff --git a/exchanges/okx/okx.go b/exchanges/okx/okx.go index af782f4d2f4..a3bbe1a552c 100644 --- a/exchanges/okx/okx.go +++ b/exchanges/okx/okx.go @@ -3166,7 +3166,6 @@ func (ok *Okx) GetCandlestickData(ctx context.Context, instrumentID string, inte return nil, errMissingInstrumentID } params.Set("instId", instrumentID) - var resp [][7]string params.Set("limit", strconv.FormatInt(limit, 10)) if !before.IsZero() { params.Set("before", strconv.FormatInt(before.UnixMilli(), 10)) @@ -3178,6 +3177,7 @@ func (ok *Okx) GetCandlestickData(ctx context.Context, instrumentID string, inte if bar != "" { params.Set("bar", bar) } + var resp [][7]string err := ok.SendHTTPRequest(ctx, exchange.RestSpot, rateLimit, http.MethodGet, common.EncodeURLValues(route, params), nil, &resp, false) if err != nil { return nil, err diff --git a/exchanges/okx/okx_types.go b/exchanges/okx/okx_types.go index e87b1a68648..b7b0e1dc054 100644 --- a/exchanges/okx/okx_types.go +++ b/exchanges/okx/okx_types.go @@ -1241,7 +1241,7 @@ type AccountDetail struct { AvailableBalance types.Number `json:"availBal"` AvailableEquity types.Number `json:"availEq"` CashBalance types.Number `json:"cashBal"` // Cash Balance - Currency string `json:"ccy"` + Currency currency.Code `json:"ccy"` CrossLiab types.Number `json:"crossLiab"` DiscountEquity types.Number `json:"disEq"` EquityOfCurrency types.Number `json:"eq"` @@ -1270,7 +1270,7 @@ type AccountPosition struct { AvailablePosition string `json:"availPos"` // Position that can be closed Only applicable to MARGIN, FUTURES/SWAP in the long-short mode, OPTION in Simple and isolated OPTION in margin Account. AveragePrice types.Number `json:"avgPx"` CreationTime okxUnixMilliTime `json:"cTime"` - Currency string `json:"ccy"` + Currency currency.Code `json:"ccy"` DeltaBS string `json:"deltaBS"` // delta:Black-Scholes Greeks in dollars,only applicable to OPTION DeltaPA string `json:"deltaPA"` // delta:Greeks in coins,only applicable to OPTION GammaBS string `json:"gammaBS"` // gamma:Black-Scholes Greeks in dollars,only applicable to OPTION diff --git a/exchanges/okx/okx_wrapper.go b/exchanges/okx/okx_wrapper.go index d29b7285b9b..d4056f571b2 100644 --- a/exchanges/okx/okx_wrapper.go +++ b/exchanges/okx/okx_wrapper.go @@ -63,6 +63,11 @@ func (ok *Okx) SetDefaults() { // Fill out the capabilities/features that the exchange supports ok.Features = exchange.Features{ + CurrencyTranslations: currency.NewTranslations(map[currency.Code]currency.Code{ + currency.NewCode("USDT-SWAP"): currency.USDT, + currency.NewCode("USD-SWAP"): currency.USD, + currency.NewCode("USDC-SWAP"): currency.USDC, + }), Supports: exchange.FeaturesSupported{ REST: true, Websocket: true, @@ -518,7 +523,7 @@ func (ok *Okx) UpdateAccountInfo(ctx context.Context, assetType asset.Item) (acc for i := range accountBalances { for j := range accountBalances[i].Details { currencyBalances = append(currencyBalances, account.Balance{ - Currency: currency.NewCode(accountBalances[i].Details[j].Currency), + Currency: accountBalances[i].Details[j].Currency, Total: accountBalances[i].Details[j].EquityOfCurrency.Float64(), Hold: accountBalances[i].Details[j].FrozenBalance.Float64(), Free: accountBalances[i].Details[j].AvailableBalance.Float64(), @@ -1848,7 +1853,7 @@ func (ok *Okx) GetFuturesPositionSummary(ctx context.Context, req *futures.Posit ) for i := range acc[0].Details { - if acc[0].Details[i].Currency != positionSummary.Currency { + if !acc[0].Details[i].Currency.Equal(positionSummary.Currency) { continue } freeCollateral = acc[0].Details[i].AvailableBalance.Decimal() @@ -1877,7 +1882,7 @@ func (ok *Okx) GetFuturesPositionSummary(ctx context.Context, req *futures.Posit Asset: req.Asset, MarginType: marginMode, CollateralMode: collateralMode, - Currency: currency.NewCode(positionSummary.Currency), + Currency: positionSummary.Currency, AvailableEquity: availableEquity, CashBalance: cashBalance, DiscountEquity: discountEquity,