Skip to content

Commit

Permalink
currency/exchanges: Add bespoke exchange translator and pair matching…
Browse files Browse the repository at this point in the history
… helper (#1556)

* currency: translation and matching pairs

* Update exchanges/exchange_types.go

Co-authored-by: Scott <[email protected]>

* 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 <[email protected]>
Co-authored-by: Scott <[email protected]>
  • Loading branch information
3 people authored Aug 16, 2024
1 parent 0becfbd commit facf291
Show file tree
Hide file tree
Showing 10 changed files with 251 additions and 10 deletions.
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
83 changes: 83 additions & 0 deletions currency/translation.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
116 changes: 116 additions & 0 deletions currency/translation_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,9 @@ package currency

import (
"testing"

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

func TestGetTranslation(t *testing.T) {
Expand Down Expand Up @@ -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})
}
}
26 changes: 26 additions & 0 deletions exchanges/bybit/bybit_wrapper.go
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
9 changes: 5 additions & 4 deletions exchanges/exchange_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
3 changes: 3 additions & 0 deletions exchanges/gateio/gateio_wrapper.go
Original file line number Diff line number Diff line change
Expand Up @@ -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,
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 @@ -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,
},
Expand Down
2 changes: 1 addition & 1 deletion exchanges/okx/okx.go
Original file line number Diff line number Diff line change
Expand Up @@ -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))
Expand All @@ -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
Expand Down
4 changes: 2 additions & 2 deletions exchanges/okx/okx_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"`
Expand Down Expand Up @@ -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
Expand Down
11 changes: 8 additions & 3 deletions exchanges/okx/okx_wrapper.go
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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(),
Expand Down Expand Up @@ -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()
Expand Down Expand Up @@ -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,
Expand Down

0 comments on commit facf291

Please sign in to comment.