Deribit: Fix options combo currency formatting and various other improvements (#2048)

* update getAssetFromInstrument

* Some fixes for deribit

* neaten and improve

* Update exchanges/deribit/deribit.go

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* Update exchanges/deribit/deribit.go

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* comment improvements to appease T-1000

* Update exchanges/deribit/deribit.go

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* Update exchanges/deribit/deribit_test.go

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* fix AI issues

* niteroos

* fix return order, min func footprint

* fix disgusting egregious crime

* >=( ">=5"

* dropping e all the time

---------

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
This commit is contained in:
Scott
2025-09-12 15:03:32 +10:00
committed by GitHub
parent b74888577c
commit 4ac0519a4c
4 changed files with 257 additions and 218 deletions

View File

@@ -2628,85 +2628,48 @@ func (e *Exchange) StringToAssetKind(assetType string) (asset.Item, error) {
// getAssetPairByInstrument is able to determine the asset type and currency pair
// based on the received instrument ID
func (e *Exchange) getAssetPairByInstrument(instrument string) (currency.Pair, asset.Item, error) {
func getAssetPairByInstrument(instrument string) (asset.Item, currency.Pair, error) {
if instrument == "" {
return currency.EMPTYPAIR, asset.Empty, errInvalidInstrumentName
return asset.Empty, currency.EMPTYPAIR, currency.ErrSymbolStringEmpty
}
var item asset.Item
// Find the first occurrence of the delimiter and split the instrument string accordingly
parts := strings.Split(instrument, currency.DashDelimiter)
switch {
case len(parts) == 1:
if i := strings.IndexAny(instrument, currency.UnderscoreDelimiter); i == -1 {
return currency.EMPTYPAIR, asset.Empty, fmt.Errorf("%w %s", errUnsupportedInstrumentFormat, instrument)
}
item = asset.Spot
case len(parts) == 2:
item = asset.Futures
case parts[len(parts)-1] == "C" || parts[len(parts)-1] == "P":
item = asset.Options
case len(parts) >= 3:
// Check for options or other types
switch parts[1] {
case "USDC", "USDT":
item = asset.Futures
case "FS":
item = asset.FutureCombo
default:
item = asset.OptionCombo
}
default:
return currency.EMPTYPAIR, asset.Empty, fmt.Errorf("%w %s", errUnsupportedInstrumentFormat, instrument)
item, err := getAssetFromInstrument(instrument)
if err != nil {
return asset.Empty, currency.EMPTYPAIR, err
}
cp, err := currency.NewPairFromString(instrument)
if err != nil {
return currency.EMPTYPAIR, asset.Empty, err
return asset.Empty, currency.EMPTYPAIR, err
}
return cp, item, nil
return item, cp, nil
}
func getAssetFromPair(currencyPair currency.Pair) (asset.Item, error) {
currencyPairString := currencyPair.String()
vals := strings.Split(currencyPairString, currency.DashDelimiter)
if strings.HasSuffix(currencyPairString, perpString) || len(vals) == 2 {
// getAssetFromInstrument extrapolates the asset type from the instrument formatting as each type is unique
func getAssetFromInstrument(instrument string) (asset.Item, error) {
currencyParts := strings.Split(instrument, currency.DashDelimiter)
partsLen := len(currencyParts)
currencySuffix := currencyParts[partsLen-1]
hasUnderscore := strings.Contains(instrument, currency.UnderscoreDelimiter)
switch {
case partsLen == 1 && !hasUnderscore: // no pair delimiter found
return asset.Empty, fmt.Errorf("%w %s", errUnsupportedInstrumentFormat, instrument)
case partsLen == 1: // spot pairs use underscore eg BTC_USDC
return asset.Spot, nil
case partsLen == 2: // futures pairs use single dash eg ETH_USDC-PERPETUAL, BTC-12SEP25
return asset.Futures, nil
} else if len(vals) == 1 {
if vals = strings.Split(vals[0], currency.UnderscoreDelimiter); len(vals) == 2 {
return asset.Spot, nil
}
}
added := false
if len(vals) >= 3 {
for a := range vals {
lastVals := strings.Split(vals[a], currency.UnderscoreDelimiter)
if len(lastVals) > 1 {
added = true
if a < len(vals)-1 {
lastVals = append(lastVals, vals[a+1:]...)
}
vals = append(vals[:a], lastVals...)
}
}
}
length := len(vals)
if strings.EqualFold(vals[length-1], "C") || strings.EqualFold(vals[length-1], "P") {
case currencySuffix == "C", currencySuffix == "P": // options end in P or C to denote puts or calls eg BTC-26SEP25-30000-C
return asset.Options, nil
}
if length == 4 {
if added {
return asset.FutureCombo, nil
}
return asset.OptionCombo, nil
} else if length >= 5 {
case partsLen == 3: // futures combos have 3 parts eg BTC-FS-12SEP25_PERP
return asset.FutureCombo, nil
case partsLen == 4: // option combos with more than 3 parts eg BTC_USDC-PS-19SEP25-113000_111000
return asset.OptionCombo, nil
default: // deribit has changed their format and needs a review
return asset.Empty, fmt.Errorf("%w %s", errUnsupportedInstrumentFormat, instrument)
}
return asset.Empty, fmt.Errorf("%w currency pair: %v", errUnsupportedInstrumentFormat, currencyPair)
}
func calculateTradingFee(feeBuilder *exchange.FeeBuilder) (float64, error) {
assetType, err := getAssetFromPair(feeBuilder.Pair)
assetType, err := getAssetFromInstrument(feeBuilder.Pair.String())
if err != nil {
return 0, err
}
@@ -2757,7 +2720,7 @@ func getOfflineTradeFee(price, amount float64) float64 {
return 0.0003 * price * amount
}
func (e *Exchange) formatFuturesTradablePair(pair currency.Pair) string {
func formatFuturesTradablePair(pair currency.Pair) string {
var instrumentID string
if result := strings.Split(pair.String(), currency.DashDelimiter); len(result) == 3 {
instrumentID = strings.Join(result[:2], currency.UnderscoreDelimiter) + currency.DashDelimiter + result[2]
@@ -2772,7 +2735,7 @@ func (e *Exchange) formatFuturesTradablePair(pair currency.Pair) string {
// EXPIRE is DDMMMYY
// STRIKE may include a d for decimal point in linear options
// TYPE is Call or Put
func (e *Exchange) optionPairToString(pair currency.Pair) string {
func optionPairToString(pair currency.Pair) string {
initialDelimiter := currency.DashDelimiter
q := pair.Quote.String()
if strings.HasPrefix(q, "USDC") && len(q) > 11 { // Linear option
@@ -2783,3 +2746,22 @@ func (e *Exchange) optionPairToString(pair currency.Pair) string {
}
return pair.Base.String() + initialDelimiter + q
}
// optionComboPairToString formats an option combo pair to deribit request format
// e.g. XRP-USDC-CS-26SEP25-3D3_3D5 -> XRP_USDC-CS-26SEP25-3d3_3d5
func optionComboPairToString(pair currency.Pair) string {
parts := strings.Split(pair.String(), "-")
// Deribit uses lowercase 'd' to represent the decimal point
lastIdx := len(parts) - 1
parts[lastIdx] = strings.ReplaceAll(parts[lastIdx], "D", "d")
// Leave unchanged when:
// * length <= 3 (not enough info to be a combo needing underscore)
// * length == 4 and second token is not USDC (original logic kept as-is)
if len(parts) <= 3 || (len(parts) == 4 && parts[1] != "USDC") {
return strings.Join(parts, "-")
}
// Otherwise insert underscore after base (covers:
// * any length > 4
// * length == 4 with USDC as second token)
return parts[0] + "_" + strings.Join(parts[1:], "-")
}

View File

@@ -23,6 +23,7 @@ import (
"github.com/thrasher-corp/gocryptotrader/exchanges/futures"
"github.com/thrasher-corp/gocryptotrader/exchanges/kline"
"github.com/thrasher-corp/gocryptotrader/exchanges/order"
"github.com/thrasher-corp/gocryptotrader/exchanges/request"
"github.com/thrasher-corp/gocryptotrader/exchanges/sharedtestvalues"
"github.com/thrasher-corp/gocryptotrader/exchanges/subscription"
"github.com/thrasher-corp/gocryptotrader/exchanges/trade"
@@ -219,7 +220,7 @@ func TestSubmitOrder(t *testing.T) {
var err error
var info *InstrumentData
for assetType, cp := range assetToPairStringMap {
info, err = e.GetInstrument(t.Context(), e.formatPairString(assetType, cp))
info, err = e.GetInstrument(t.Context(), formatPairString(assetType, cp))
require.NoErrorf(t, err, "expected nil, got %v for asset type %s pair %s", err, assetType, cp)
require.NotNilf(t, result, "expected result not to be nil for asset type %s pair %s", assetType, cp)
@@ -252,7 +253,7 @@ func TestGetMarkPriceHistory(t *testing.T) {
var result []MarkPriceHistory
for _, ps := range []string{
e.optionPairToString(optionsTradablePair),
optionPairToString(optionsTradablePair),
spotTradablePair.String(),
btcPerpInstrument,
futureComboTradablePair.String(),
@@ -270,7 +271,7 @@ func TestWSRetrieveMarkPriceHistory(t *testing.T) {
var result []MarkPriceHistory
for _, ps := range []string{
e.optionPairToString(optionsTradablePair),
optionPairToString(optionsTradablePair),
spotTradablePair.String(),
btcPerpInstrument,
futureComboTradablePair.String(),
@@ -311,17 +312,19 @@ func TestGetBookSummaryByInstrument(t *testing.T) {
_, err := e.GetBookSummaryByInstrument(t.Context(), "")
require.ErrorIs(t, err, errInvalidInstrumentName)
var result []BookSummaryData
for _, ps := range []string{
btcPerpInstrument,
spotTradablePair.String(),
futureComboTradablePair.String(),
e.optionPairToString(optionsTradablePair),
optionComboTradablePair.String(),
optionPairToString(optionsTradablePair),
optionComboPairToString(optionComboTradablePair),
} {
result, err = e.GetBookSummaryByInstrument(t.Context(), ps)
require.NoErrorf(t, err, "expected nil, got %v for pair %s", err, ps)
require.NotNilf(t, result, "expected result not to be nil for pair %s", ps)
t.Run(ps, func(t *testing.T) {
t.Parallel()
result, err := e.GetBookSummaryByInstrument(t.Context(), ps)
require.NoError(t, err, "GetBookSummaryByInstrument must not error")
require.NotNil(t, result, "result must not be nil")
})
}
}
@@ -334,8 +337,8 @@ func TestWSRetrieveBookSummaryByInstrument(t *testing.T) {
btcPerpInstrument,
spotTradablePair.String(),
futureComboTradablePair.String(),
e.optionPairToString(optionsTradablePair),
optionComboTradablePair.String(),
optionPairToString(optionsTradablePair),
optionComboPairToString(optionComboTradablePair),
} {
result, err = e.WSRetrieveBookSummaryByInstrument(t.Context(), ps)
require.NoErrorf(t, err, "expected nil, got %v for pair %s", err, ps)
@@ -548,7 +551,7 @@ func TestGetInstrumentData(t *testing.T) {
var result *InstrumentData
for assetType, cp := range assetTypeToPairsMap {
result, err = e.GetInstrument(t.Context(), e.formatPairString(assetType, cp))
result, err = e.GetInstrument(t.Context(), formatPairString(assetType, cp))
require.NoErrorf(t, err, "expected nil, got %v for asset type %s pair %s", err, assetType, cp)
require.NotNilf(t, result, "expected result not to be nil for asset type %s pair %s", assetType, cp)
}
@@ -558,12 +561,13 @@ func TestWSRetrieveInstrumentData(t *testing.T) {
t.Parallel()
_, err := e.WSRetrieveInstrumentData(t.Context(), "")
require.ErrorIs(t, err, errInvalidInstrumentName)
var result *InstrumentData
for assetType, cp := range assetTypeToPairsMap {
result, err = e.WSRetrieveInstrumentData(t.Context(), e.formatPairString(assetType, cp))
require.NoErrorf(t, err, "expected nil, got %v for asset type %s pair %s", err, assetType, cp)
require.NotNilf(t, result, "expected result not to be nil for asset type %s pair %s", assetType, cp)
t.Run(fmt.Sprintf("%s %s", assetType, cp), func(t *testing.T) {
t.Parallel()
result, err := e.WSRetrieveInstrumentData(request.WithVerbose(t.Context()), formatPairString(assetType, cp))
require.NoError(t, err)
require.NotNil(t, result, "result must not be nil")
})
}
}
@@ -617,7 +621,7 @@ func TestWSRetrieveLastSettlementsByInstrument(t *testing.T) {
_, err := e.WSRetrieveLastSettlementsByInstrument(t.Context(), "", "settlement", "5", 0, time.Now().Add(-2*time.Hour))
require.ErrorIs(t, err, errInvalidInstrumentName)
result, err := e.WSRetrieveLastSettlementsByInstrument(t.Context(), e.formatFuturesTradablePair(futuresTradablePair), "settlement", "5", 0, time.Now().Add(-2*time.Hour))
result, err := e.WSRetrieveLastSettlementsByInstrument(t.Context(), formatFuturesTradablePair(futuresTradablePair), "settlement", "5", 0, time.Now().Add(-2*time.Hour))
require.NoError(t, err)
assert.NotNil(t, result)
}
@@ -627,7 +631,7 @@ func TestGetLastSettlementsByInstrument(t *testing.T) {
_, err := e.GetLastSettlementsByInstrument(t.Context(), "", "settlement", "5", 0, time.Now().Add(-2*time.Hour))
require.ErrorIs(t, err, errInvalidInstrumentName)
result, err := e.GetLastSettlementsByInstrument(t.Context(), e.formatFuturesTradablePair(futuresTradablePair), "settlement", "5", 0, time.Now().Add(-2*time.Hour))
result, err := e.GetLastSettlementsByInstrument(t.Context(), formatFuturesTradablePair(futuresTradablePair), "settlement", "5", 0, time.Now().Add(-2*time.Hour))
require.NoError(t, err)
assert.NotNil(t, result)
}
@@ -684,7 +688,7 @@ func TestGetLastTradesByInstrument(t *testing.T) {
require.ErrorIs(t, err, errInvalidInstrumentName)
for assetType, cp := range assetTypeToPairsMap {
result, err := e.GetLastTradesByInstrument(t.Context(), e.formatPairString(assetType, cp), "30500", "31500", "desc", 0, true)
result, err := e.GetLastTradesByInstrument(t.Context(), formatPairString(assetType, cp), "30500", "31500", "desc", 0, true)
require.NoErrorf(t, err, "expected %v, got %v currency asset %v pair %v", nil, err, assetType, cp)
require.NotNilf(t, result, "expected value not to be nil for asset %v pair: %v", assetType, cp)
}
@@ -696,7 +700,7 @@ func TestWSRetrieveLastTradesByInstrument(t *testing.T) {
require.ErrorIs(t, err, errInvalidInstrumentName)
for assetType, cp := range assetTypeToPairsMap {
result, err := e.WSRetrieveLastTradesByInstrument(t.Context(), e.formatPairString(assetType, cp), "30500", "31500", "desc", 0, true)
result, err := e.WSRetrieveLastTradesByInstrument(t.Context(), formatPairString(assetType, cp), "30500", "31500", "desc", 0, true)
require.NoErrorf(t, err, "expected %v, got %v currency asset %v pair %v", nil, err, assetType, cp)
require.NotNilf(t, result, "expected value not to be nil for asset %v pair: %v", assetType, cp)
}
@@ -708,7 +712,7 @@ func TestGetLastTradesByInstrumentAndTime(t *testing.T) {
require.ErrorIs(t, err, errInvalidInstrumentName)
for assetType, cp := range assetTypeToPairsMap {
result, err := e.GetLastTradesByInstrumentAndTime(t.Context(), e.formatPairString(assetType, cp), "", 0, time.Now().Add(-8*time.Hour), time.Now())
result, err := e.GetLastTradesByInstrumentAndTime(t.Context(), formatPairString(assetType, cp), "", 0, time.Now().Add(-8*time.Hour), time.Now())
require.NoErrorf(t, err, "expected %v, got %v currency pair %v", nil, err, cp)
require.NotNilf(t, result, "expected value not to be nil for pair: %v", cp)
}
@@ -720,7 +724,7 @@ func TestWSRetrieveLastTradesByInstrumentAndTime(t *testing.T) {
require.ErrorIs(t, err, errInvalidInstrumentName)
for assetType, cp := range assetTypeToPairsMap {
result, err := e.WSRetrieveLastTradesByInstrumentAndTime(t.Context(), e.formatPairString(assetType, cp), "", 0, true, time.Now().Add(-8*time.Hour), time.Now())
result, err := e.WSRetrieveLastTradesByInstrumentAndTime(t.Context(), formatPairString(assetType, cp), "", 0, true, time.Now().Add(-8*time.Hour), time.Now())
require.NoErrorf(t, err, "expected %v, got %v currency pair %v", nil, err, cp)
require.NotNilf(t, result, "expected value not to be nil for pair: %v", cp)
}
@@ -734,7 +738,7 @@ func TestWSProcessTrades(t *testing.T) {
testexch.FixtureToDataHandler(t, "testdata/wsAllTrades.json", e.wsHandleData)
close(e.Websocket.DataHandler)
p, a, err := e.getAssetPairByInstrument("BTC-PERPETUAL")
a, p, err := getAssetPairByInstrument("BTC-PERPETUAL")
require.NoError(t, err, "getAssetPairByInstrument must not error")
exp := []trade.Data{
@@ -780,7 +784,7 @@ func TestGetOrderbookData(t *testing.T) {
var result *Orderbook
for assetType, cp := range assetTypeToPairsMap {
result, err = e.GetOrderbook(t.Context(), e.formatPairString(assetType, cp), 0)
result, err = e.GetOrderbook(t.Context(), formatPairString(assetType, cp), 0)
require.NoErrorf(t, err, "expected %v, got %v currency pair %v", nil, err, cp)
require.NotNilf(t, result, "expected value not to be nil for pair: %v", cp)
}
@@ -796,7 +800,7 @@ func TestWSRetrieveOrderbookData(t *testing.T) {
var result *Orderbook
for assetType, cp := range assetTypeToPairsMap {
result, err = e.WSRetrieveOrderbookData(t.Context(), e.formatPairString(assetType, cp), 0)
result, err = e.WSRetrieveOrderbookData(t.Context(), formatPairString(assetType, cp), 0)
require.NoErrorf(t, err, "expected %v, got %v currency pair %v", nil, err, cp)
require.NotNilf(t, result, "expected value not to be nil for pair: %v", cp)
}
@@ -2192,22 +2196,22 @@ func TestWSSubmitCancelByLabel(t *testing.T) {
func TestSubmitCancelQuotes(t *testing.T) {
t.Parallel()
_, err := e.SubmitCancelQuotes(t.Context(), currency.EMPTYCODE, 0, 0, "all", "", futuresTradablePair.String(), "future", true)
_, err := e.SubmitCancelQuotes(t.Context(), currency.EMPTYCODE, 0, 0, "all", "", formatFuturesTradablePair(futuresTradablePair), "future", true)
require.ErrorIs(t, err, currency.ErrCurrencyCodeEmpty)
sharedtestvalues.SkipTestIfCredentialsUnset(t, e, canManipulateRealOrders)
result, err := e.SubmitCancelQuotes(t.Context(), currency.BTC, 0, 0, "all", "", futuresTradablePair.String(), "future", true)
result, err := e.SubmitCancelQuotes(t.Context(), currency.BTC, 0, 0, "all", "", formatFuturesTradablePair(futuresTradablePair), "future", true)
require.NoError(t, err)
assert.NotNil(t, result)
}
func TestWSSubmitCancelQuotes(t *testing.T) {
t.Parallel()
_, err := e.WSSubmitCancelQuotes(t.Context(), currency.EMPTYCODE, 0, 0, "all", "", futuresTradablePair.String(), "future", true)
_, err := e.WSSubmitCancelQuotes(t.Context(), currency.EMPTYCODE, 0, 0, "all", "", formatFuturesTradablePair(futuresTradablePair), "future", true)
require.ErrorIs(t, err, currency.ErrCurrencyCodeEmpty)
sharedtestvalues.SkipTestIfCredentialsUnset(t, e, canManipulateRealOrders)
result, err := e.WSSubmitCancelQuotes(t.Context(), currency.BTC, 0, 0, "all", "", futuresTradablePair.String(), "future", true)
result, err := e.WSSubmitCancelQuotes(t.Context(), currency.BTC, 0, 0, "all", "", formatFuturesTradablePair(futuresTradablePair), "future", true)
require.NoError(t, err)
assert.NotNil(t, result)
}
@@ -2218,7 +2222,7 @@ func TestSubmitClosePosition(t *testing.T) {
require.ErrorIs(t, err, errInvalidInstrumentName)
sharedtestvalues.SkipTestIfCredentialsUnset(t, e, canManipulateRealOrders)
result, err := e.SubmitClosePosition(t.Context(), e.formatFuturesTradablePair(futuresTradablePair), "limit", 35000)
result, err := e.SubmitClosePosition(t.Context(), formatFuturesTradablePair(futuresTradablePair), "limit", 35000)
require.NoError(t, err)
assert.NotNil(t, result)
}
@@ -2229,7 +2233,7 @@ func TestWSSubmitClosePosition(t *testing.T) {
require.ErrorIs(t, err, errInvalidInstrumentName)
sharedtestvalues.SkipTestIfCredentialsUnset(t, e, canManipulateRealOrders)
result, err := e.WSSubmitClosePosition(t.Context(), e.formatFuturesTradablePair(futuresTradablePair), "limit", 35000)
result, err := e.WSSubmitClosePosition(t.Context(), formatFuturesTradablePair(futuresTradablePair), "limit", 35000)
require.NoError(t, err)
assert.NotNil(t, result)
}
@@ -2238,13 +2242,13 @@ func TestGetMargins(t *testing.T) {
t.Parallel()
_, err := e.GetMargins(t.Context(), "", 5, 35000)
require.ErrorIs(t, err, errInvalidInstrumentName)
_, err = e.GetMargins(t.Context(), e.formatFuturesTradablePair(futuresTradablePair), 0, 35000)
_, err = e.GetMargins(t.Context(), formatFuturesTradablePair(futuresTradablePair), 0, 35000)
require.ErrorIs(t, err, errInvalidAmount)
_, err = e.GetMargins(t.Context(), e.formatFuturesTradablePair(futuresTradablePair), 5, -1)
_, err = e.GetMargins(t.Context(), formatFuturesTradablePair(futuresTradablePair), 5, -1)
require.ErrorIs(t, err, errInvalidPrice)
sharedtestvalues.SkipTestIfCredentialsUnset(t, e)
result, err := e.GetMargins(t.Context(), e.formatFuturesTradablePair(futuresTradablePair), 5, 35000)
result, err := e.GetMargins(t.Context(), formatFuturesTradablePair(futuresTradablePair), 5, 35000)
require.NoError(t, err)
assert.NotNil(t, result)
}
@@ -2255,7 +2259,7 @@ func TestWSRetrieveMargins(t *testing.T) {
require.ErrorIs(t, err, errInvalidInstrumentName)
sharedtestvalues.SkipTestIfCredentialsUnset(t, e)
result, err := e.WSRetrieveMargins(t.Context(), e.formatFuturesTradablePair(futuresTradablePair), 5, 35000)
result, err := e.WSRetrieveMargins(t.Context(), formatFuturesTradablePair(futuresTradablePair), 5, 35000)
require.NoError(t, err)
assert.NotNil(t, result)
}
@@ -2626,7 +2630,7 @@ func TestSendRequestForQuote(t *testing.T) {
require.ErrorIs(t, err, errInvalidInstrumentName)
sharedtestvalues.SkipTestIfCredentialsUnset(t, e)
err = e.SendRequestForQuote(t.Context(), e.formatFuturesTradablePair(futuresTradablePair), 1000, order.Buy)
err = e.SendRequestForQuote(t.Context(), formatFuturesTradablePair(futuresTradablePair), 1000, order.Buy)
assert.NoError(t, err)
}
@@ -2636,7 +2640,7 @@ func TestWSSendRequestForQuote(t *testing.T) {
require.ErrorIs(t, err, errInvalidInstrumentName)
sharedtestvalues.SkipTestIfCredentialsUnset(t, e)
err = e.WSSendRequestForQuote(t.Context(), e.formatFuturesTradablePair(futuresTradablePair), 1000, order.Buy)
err = e.WSSendRequestForQuote(t.Context(), formatFuturesTradablePair(futuresTradablePair), 1000, order.Buy)
assert.NoError(t, err)
}
@@ -3445,12 +3449,13 @@ func TestGetWithdrawalsHistory(t *testing.T) {
func TestGetRecentTrades(t *testing.T) {
t.Parallel()
var result []trade.Data
var err error
for assetType, cp := range assetTypeToPairsMap {
result, err = e.GetRecentTrades(t.Context(), cp, assetType)
require.NoErrorf(t, err, "expected nil, got %v for asset type %s pair %s", err, assetType, cp)
require.NotNilf(t, result, "expected result not to be nil for asset type %s pair %s", assetType, cp)
t.Run(fmt.Sprintf("%s %s", assetType, cp), func(t *testing.T) {
t.Parallel()
result, err := e.GetRecentTrades(t.Context(), cp, assetType)
require.NoError(t, err, "GetRecentTrades must not error")
require.NotNil(t, result, "result must not be nil")
})
}
}
@@ -3539,30 +3544,6 @@ func TestGetOrderHistory(t *testing.T) {
}
}
func TestGetAssetFromPair(t *testing.T) {
var assetTypeNew asset.Item
for _, assetType := range []asset.Item{asset.Spot, asset.Futures, asset.Options, asset.OptionCombo, asset.FutureCombo} {
availablePairs, err := e.GetEnabledPairs(assetType)
require.NoErrorf(t, err, "expected nil, got %v for asset type %s", err, assetType)
require.NotNilf(t, availablePairs, "expected result not to be nil for asset type %s", assetType)
format, err := e.GetPairFormat(assetType, true)
require.NoError(t, err)
for id, cp := range availablePairs {
t.Run(strconv.Itoa(id), func(t *testing.T) {
assetTypeNew, err = getAssetFromPair(cp.Format(format))
require.Equalf(t, assetType, assetTypeNew, "expected %s, but found %s for pair string %s", assetType.String(), assetTypeNew.String(), cp.Format(format))
})
}
}
cp, err := currency.NewPairFromString("some_thing_else")
require.NoError(t, err)
_, err = getAssetFromPair(cp)
assert.ErrorIs(t, err, errUnsupportedInstrumentFormat)
}
func TestGetAssetPairByInstrument(t *testing.T) {
t.Parallel()
for _, assetType := range []asset.Item{asset.Spot, asset.Futures, asset.Options, asset.OptionCombo, asset.FutureCombo} {
@@ -3570,27 +3551,64 @@ func TestGetAssetPairByInstrument(t *testing.T) {
require.NoErrorf(t, err, "expected nil, got %v for asset type %s", err, assetType)
require.NotNilf(t, availablePairs, "expected result not to be nil for asset type %s", assetType)
for _, cp := range availablePairs {
t.Run(fmt.Sprintf("%s %s", assetType, cp), func(t *testing.T) {
instrument := formatPairString(assetType, cp)
t.Run(fmt.Sprintf("%s %s", assetType, instrument), func(t *testing.T) {
t.Parallel()
extractedPair, extractedAsset, err := e.getAssetPairByInstrument(cp.String())
extractedAsset, extractedPair, err := getAssetPairByInstrument(instrument)
assert.NoError(t, err)
assert.Equal(t, cp.String(), extractedPair.String())
assert.Equal(t, assetType.String(), extractedAsset.String())
fPair, err := e.FormatExchangeCurrency(extractedPair, assetType)
require.NoError(t, err, "FormatExchangeCurrency must not error")
assert.Equal(t, cp.String(), fPair.String())
assert.Equal(t, assetType.String(), extractedAsset.String(), "asset should match for")
})
}
}
t.Run("empty asset, empty pair", func(t *testing.T) {
t.Parallel()
_, _, err := e.getAssetPairByInstrument("")
assert.ErrorIs(t, err, errInvalidInstrumentName)
_, _, err := getAssetPairByInstrument("")
assert.ErrorIs(t, err, currency.ErrSymbolStringEmpty)
})
t.Run("thisIsAFakeCurrency", func(t *testing.T) {
t.Parallel()
_, _, err := e.getAssetPairByInstrument("thisIsAFakeCurrency")
_, _, err := getAssetPairByInstrument("thisIsAFakeCurrency")
assert.ErrorIs(t, err, errUnsupportedInstrumentFormat)
})
}
func TestGetAssetFromInstrument(t *testing.T) {
t.Parallel()
tc := []struct {
instrument string
expectedAsset asset.Item
expectedError error
}{
{"BNB_USDC", asset.Spot, nil},
{"BTC-30DEC22", asset.Futures, nil},
{"BTCDVOL_USDC-1OCT25", asset.Futures, nil},
{"ADA_USDC-PERPETUAL", asset.Futures, nil},
{"PAXG_USDC-12SEP25-3320-P", asset.Options, nil},
{"ETH-3OCT25-4800-P", asset.Options, nil},
{"ETH-FS-26JUN26_26DEC25", asset.FutureCombo, nil},
{"BTC_USDC-PBUT-31OCT25-90000_100000_102000", asset.OptionCombo, nil},
{"XRP_USDC-CBUT-26SEP25-2d9_3d2_3d4", asset.OptionCombo, nil},
{"ETH-CS-26SEP25-5000_5500", asset.OptionCombo, nil},
{"HELLOMOTO", asset.Empty, errUnsupportedInstrumentFormat},
{"hi-my-name-is-moto", asset.Empty, errUnsupportedInstrumentFormat},
}
for _, test := range tc {
t.Run(test.instrument, func(t *testing.T) {
t.Parallel()
a, err := getAssetFromInstrument(test.instrument)
if test.expectedError != nil {
assert.ErrorIs(t, err, test.expectedError)
} else {
assert.NoError(t, err)
assert.Equal(t, test.expectedAsset, a)
}
})
}
}
func TestGetFeeByTypeOfflineTradeFee(t *testing.T) {
feeBuilder := &exchange.FeeBuilder{
Amount: 1,
@@ -3666,7 +3684,7 @@ func TestCalculateTradingFee(t *testing.T) {
require.NoError(t, err)
require.NotNil(t, result)
_, err = calculateTradingFee(feeBuilder)
assert.ErrorIs(t, err, errUnsupportedInstrumentFormat)
assert.ErrorIs(t, err, asset.ErrNotSupported)
}
func TestGetTime(t *testing.T) {
@@ -3757,7 +3775,7 @@ func TestProcessPushData(t *testing.T) {
t.Run(k, func(t *testing.T) {
t.Parallel()
err := e.wsHandleData(t.Context(), []byte(v))
require.NoErrorf(t, err, "%s: Received unexpected error for", k)
require.NoError(t, err, "wsHandleData must not error")
})
}
}
@@ -3774,7 +3792,7 @@ func TestFormatFuturesTradablePair(t *testing.T) {
for pair, instrumentID := range futuresInstrumentsOutputList {
t.Run(instrumentID, func(t *testing.T) {
t.Parallel()
instrument := e.formatFuturesTradablePair(pair)
instrument := formatFuturesTradablePair(pair)
require.Equal(t, instrumentID, instrument)
})
}
@@ -3790,7 +3808,7 @@ func TestOptionPairToString(t *testing.T) {
{Delimiter: currency.DashDelimiter, Base: currency.MATIC, Quote: currency.NewCode("USDC-8JUN24-0D99-P")}: "MATIC_USDC-8JUN24-0d99-P",
{Delimiter: currency.DashDelimiter, Base: currency.MATIC, Quote: currency.NewCode("USDC-6DEC29-0D87-C")}: "MATIC_USDC-6DEC29-0d87-C",
} {
assert.Equal(t, exp, e.optionPairToString(pair), "optionPairToString should return correctly")
assert.Equal(t, exp, optionPairToString(pair), "optionPairToString should return correctly")
}
}
@@ -4036,8 +4054,8 @@ func TestIsPerpetualFutureCurrency(t *testing.T) {
t.Run(fmt.Sprintf("Asset: %s Pair: %s", assetType.String(), instances[i].Pair.String()), func(t *testing.T) {
t.Parallel()
is, err := e.IsPerpetualFutureCurrency(assetType, instances[i].Pair)
require.ErrorIsf(t, err, instances[i].Error, "expected %v, got %v for asset: %s pair: %s", instances[i].Error, err, assetType.String(), instances[i].Pair.String())
require.Equalf(t, is, instances[i].Response, "expected %v, got %v for asset: %s pair: %s", instances[i].Response, is, assetType.String(), instances[i].Pair.String())
require.ErrorIs(t, err, instances[i].Error)
require.Equal(t, is, instances[i].Response)
})
}
}
@@ -4192,3 +4210,53 @@ func TestTimeInForceFromString(t *testing.T) {
require.ErrorIs(t, err, timeInForceList[i].Error)
}
}
func TestOptionsComboFormatting(t *testing.T) {
t.Parallel()
availablePairs, err := e.GetAvailablePairs(asset.OptionCombo)
require.NoError(t, err, "GetAvailablePairs must not error")
require.GreaterOrEqual(t, len(availablePairs), 5, "availablePairs must be greater than or equal 5")
for _, cp := range availablePairs[:5] {
t.Run(cp.String(), func(t *testing.T) {
t.Parallel()
_, err := e.GetPublicTicker(t.Context(), optionComboPairToString(cp))
assert.NoError(t, err, "GetPublicTicker should not error")
})
}
}
func TestAppendCandles(t *testing.T) {
t.Parallel()
_, err := appendCandles(nil, time.Time{})
assert.ErrorIs(t, err, kline.ErrNoTimeSeriesDataToConvert)
candles := &TVChartData{
Ticks: []int64{1337},
}
_, err = appendCandles(candles, time.Time{})
assert.ErrorIs(t, err, kline.ErrInsufficientCandleData)
candles = &TVChartData{
Open: []float64{1337},
High: []float64{1337},
Low: []float64{1337},
Close: []float64{1337},
Volume: []float64{1337},
Ticks: []int64{1337},
}
resp, err := appendCandles(candles, time.Time{})
assert.NoError(t, err)
assert.Len(t, resp, 1)
candles = &TVChartData{
Open: []float64{1337},
High: []float64{1337},
Low: []float64{1337},
Close: []float64{1337},
Volume: []float64{1337},
Ticks: []int64{1337},
}
resp, err = appendCandles(candles, time.Unix(1338, 0))
assert.NoError(t, err)
assert.Empty(t, resp)
}

View File

@@ -334,7 +334,7 @@ func (e *Exchange) processUserOrders(respRaw []byte, channels []string) error {
}
orderDetails := make([]order.Detail, len(orderData))
for x := range orderData {
cp, a, err := e.getAssetPairByInstrument(orderData[x].InstrumentName)
a, cp, err := getAssetPairByInstrument(orderData[x].InstrumentName)
if err != nil {
return err
}
@@ -390,7 +390,7 @@ func (e *Exchange) processUserOrderChanges(respRaw []byte, channels []string) er
}
var cp currency.Pair
var a asset.Item
cp, a, err = e.getAssetPairByInstrument(changeData.Trades[x].InstrumentName)
a, cp, err = getAssetPairByInstrument(changeData.Trades[x].InstrumentName)
if err != nil {
return err
}
@@ -424,7 +424,7 @@ func (e *Exchange) processUserOrderChanges(respRaw []byte, channels []string) er
if err != nil {
return err
}
cp, a, err := e.getAssetPairByInstrument(changeData.Orders[x].InstrumentName)
a, cp, err := getAssetPairByInstrument(changeData.Orders[x].InstrumentName)
if err != nil {
return err
}
@@ -450,7 +450,7 @@ func (e *Exchange) processUserOrderChanges(respRaw []byte, channels []string) er
}
func (e *Exchange) processQuoteTicker(respRaw []byte, channels []string) error {
cp, a, err := e.getAssetPairByInstrument(channels[1])
a, cp, err := getAssetPairByInstrument(channels[1])
if err != nil {
return err
}
@@ -498,7 +498,7 @@ func (e *Exchange) processTrades(respRaw []byte, channels []string) error {
for x := range tradesData {
var cp currency.Pair
var a asset.Item
cp, a, err = e.getAssetPairByInstrument(tradeList[x].InstrumentName)
a, cp, err = getAssetPairByInstrument(tradeList[x].InstrumentName)
if err != nil {
return err
}
@@ -528,7 +528,7 @@ func (e *Exchange) processIncrementalTicker(respRaw []byte, channels []string) e
if len(channels) != 2 {
return fmt.Errorf("%w, expected format 'incremental_ticker.{instrument_name}', but found %s", errMalformedData, strings.Join(channels, "."))
}
cp, a, err := e.getAssetPairByInstrument(channels[1])
a, cp, err := getAssetPairByInstrument(channels[1])
if err != nil {
return err
}
@@ -564,7 +564,7 @@ func (e *Exchange) processInstrumentTicker(respRaw []byte, channels []string) er
}
func (e *Exchange) processTicker(respRaw []byte, channels []string) error {
cp, a, err := e.getAssetPairByInstrument(channels[1])
a, cp, err := getAssetPairByInstrument(channels[1])
if err != nil {
return err
}
@@ -615,7 +615,7 @@ func (e *Exchange) processCandleChart(respRaw []byte, channels []string) error {
if len(channels) != 4 {
return fmt.Errorf("%w, expected format 'chart.trades.{instrument_name}.{resolution}', but found %s", errMalformedData, strings.Join(channels, "."))
}
cp, a, err := e.getAssetPairByInstrument(channels[2])
a, cp, err := getAssetPairByInstrument(channels[2])
if err != nil {
return err
}
@@ -649,7 +649,7 @@ func (e *Exchange) processOrderbook(respRaw []byte, channels []string) error {
return err
}
if len(channels) == 3 {
cp, a, err := e.getAssetPairByInstrument(orderbookData.InstrumentName)
a, cp, err := getAssetPairByInstrument(orderbookData.InstrumentName)
if err != nil {
return err
}
@@ -718,7 +718,7 @@ func (e *Exchange) processOrderbook(respRaw []byte, channels []string) error {
})
}
} else if len(channels) == 5 {
cp, a, err := e.getAssetPairByInstrument(orderbookData.InstrumentName)
a, cp, err := getAssetPairByInstrument(orderbookData.InstrumentName)
if err != nil {
return err
}

View File

@@ -254,7 +254,7 @@ func (e *Exchange) UpdateTicker(ctx context.Context, p currency.Pair, assetType
if err != nil {
return nil, err
}
instrumentID := e.formatPairString(assetType, p)
instrumentID := formatPairString(assetType, p)
var tickerData *TickerData
if e.Websocket.IsConnected() {
tickerData, err = e.WSRetrievePublicTicker(ctx, instrumentID)
@@ -294,7 +294,7 @@ func (e *Exchange) UpdateOrderbook(ctx context.Context, p currency.Pair, assetTy
if err != nil {
return nil, err
}
instrumentID := e.formatPairString(assetType, p)
instrumentID := formatPairString(assetType, p)
var obData *Orderbook
if e.Websocket.IsConnected() {
obData, err = e.WSRetrieveOrderbookData(ctx, instrumentID, 50)
@@ -477,7 +477,7 @@ func (e *Exchange) GetRecentTrades(ctx context.Context, p currency.Pair, assetTy
if err != nil {
return nil, err
}
instrumentID := e.formatPairString(assetType, p)
instrumentID := formatPairString(assetType, p)
resp := []trade.Data{}
var trades *PublicTradesData
if e.Websocket.IsConnected() {
@@ -521,7 +521,7 @@ func (e *Exchange) GetHistoricTrades(ctx context.Context, p currency.Pair, asset
var instrumentID string
switch assetType {
case asset.Futures, asset.Options, asset.Spot:
instrumentID = e.formatPairString(assetType, p)
instrumentID = formatPairString(assetType, p)
default:
return nil, fmt.Errorf("%w asset type %v", asset.ErrNotSupported, assetType)
}
@@ -1050,45 +1050,52 @@ func (e *Exchange) GetHistoricCandles(ctx context.Context, pair currency.Pair, a
case asset.Futures, asset.Spot:
var tradingViewData *TVChartData
if e.Websocket.IsConnected() {
tradingViewData, err = e.WSRetrievesTradingViewChartData(ctx, e.formatFuturesTradablePair(req.RequestFormatted), intervalString, start, end)
tradingViewData, err = e.WSRetrievesTradingViewChartData(ctx, formatFuturesTradablePair(req.RequestFormatted), intervalString, start, end)
} else {
tradingViewData, err = e.GetTradingViewChart(ctx, e.formatFuturesTradablePair(req.RequestFormatted), intervalString, start, end)
tradingViewData, err = e.GetTradingViewChart(ctx, formatFuturesTradablePair(req.RequestFormatted), intervalString, start, end)
}
if err != nil {
return nil, err
} else if len(tradingViewData.Ticks) == 0 {
return nil, kline.ErrNoTimeSeriesDataToConvert
}
checkLen := len(tradingViewData.Ticks)
if len(tradingViewData.Open) != checkLen ||
len(tradingViewData.High) != checkLen ||
len(tradingViewData.Low) != checkLen ||
len(tradingViewData.Close) != checkLen ||
len(tradingViewData.Volume) != checkLen {
return nil, fmt.Errorf("%s - %v: invalid trading view chart data received", a, req.RequestFormatted)
}
listCandles := make([]kline.Candle, 0, len(tradingViewData.Ticks))
for x := range tradingViewData.Ticks {
timeInfo := time.UnixMilli(tradingViewData.Ticks[x]).UTC()
if timeInfo.Before(start) {
continue
}
listCandles = append(listCandles, kline.Candle{
Open: tradingViewData.Open[x],
High: tradingViewData.High[x],
Low: tradingViewData.Low[x],
Close: tradingViewData.Close[x],
Volume: tradingViewData.Volume[x],
Time: timeInfo,
})
listCandles, err := appendCandles(tradingViewData, start)
if err != nil {
return nil, err
}
return req.ProcessResponse(listCandles)
case asset.OptionCombo, asset.FutureCombo, asset.Options:
// TODO: candlestick data for asset item option_combo, future_combo, and option not supported yet
}
return nil, fmt.Errorf("%w candlestick data for asset type %v", asset.ErrNotSupported, a)
}
func appendCandles(tradingViewData *TVChartData, start time.Time) ([]kline.Candle, error) {
if tradingViewData == nil || len(tradingViewData.Ticks) == 0 {
return nil, kline.ErrNoTimeSeriesDataToConvert
}
checkLen := len(tradingViewData.Ticks)
if len(tradingViewData.Open) != checkLen ||
len(tradingViewData.High) != checkLen ||
len(tradingViewData.Low) != checkLen ||
len(tradingViewData.Close) != checkLen ||
len(tradingViewData.Volume) != checkLen {
return nil, fmt.Errorf("%w: ohlcv len must be equal", kline.ErrInsufficientCandleData)
}
listCandles := make([]kline.Candle, 0, len(tradingViewData.Ticks))
for x := range tradingViewData.Ticks {
timeInfo := time.UnixMilli(tradingViewData.Ticks[x]).UTC()
if timeInfo.Before(start) {
continue
}
listCandles = append(listCandles, kline.Candle{
Open: tradingViewData.Open[x],
High: tradingViewData.High[x],
Low: tradingViewData.Low[x],
Close: tradingViewData.Close[x],
Volume: tradingViewData.Volume[x],
Time: timeInfo,
})
}
return listCandles, nil
}
// GetHistoricCandlesExtended returns candles between a time period for a set time interval
func (e *Exchange) GetHistoricCandlesExtended(ctx context.Context, pair currency.Pair, a asset.Item, interval kline.Interval, start, end time.Time) (*kline.Item, error) {
req, err := e.GetKlineExtendedRequest(pair, a, interval, start, end)
@@ -1105,39 +1112,19 @@ func (e *Exchange) GetHistoricCandlesExtended(ctx context.Context, pair currency
return nil, err
}
if e.Websocket.IsConnected() {
tradingViewData, err = e.WSRetrievesTradingViewChartData(ctx, e.formatFuturesTradablePair(req.RequestFormatted), intervalString, req.RangeHolder.Ranges[x].Start.Time, req.RangeHolder.Ranges[x].End.Time)
tradingViewData, err = e.WSRetrievesTradingViewChartData(ctx, formatFuturesTradablePair(req.RequestFormatted), intervalString, req.RangeHolder.Ranges[x].Start.Time, req.RangeHolder.Ranges[x].End.Time)
} else {
tradingViewData, err = e.GetTradingViewChart(ctx, e.formatFuturesTradablePair(req.RequestFormatted), intervalString, req.RangeHolder.Ranges[x].Start.Time, req.RangeHolder.Ranges[x].End.Time)
tradingViewData, err = e.GetTradingViewChart(ctx, formatFuturesTradablePair(req.RequestFormatted), intervalString, req.RangeHolder.Ranges[x].Start.Time, req.RangeHolder.Ranges[x].End.Time)
}
if err != nil {
return nil, err
}
checkLen := len(tradingViewData.Ticks)
if len(tradingViewData.Open) != checkLen ||
len(tradingViewData.High) != checkLen ||
len(tradingViewData.Low) != checkLen ||
len(tradingViewData.Close) != checkLen ||
len(tradingViewData.Volume) != checkLen {
return nil, fmt.Errorf("%s - %v: invalid trading view chart data received", a, e.formatFuturesTradablePair(req.RequestFormatted))
}
for i := range tradingViewData.Ticks {
timeInfo := time.UnixMilli(tradingViewData.Ticks[i]).UTC()
if timeInfo.Before(start) {
continue
}
timeSeries = append(timeSeries, kline.Candle{
Open: tradingViewData.Open[i],
High: tradingViewData.High[i],
Low: tradingViewData.Low[i],
Close: tradingViewData.Close[i],
Volume: tradingViewData.Volume[i],
Time: timeInfo,
})
timeSeries, err = appendCandles(tradingViewData, start)
if err != nil {
return nil, err
}
}
return req.ProcessResponse(timeSeries)
case asset.OptionCombo, asset.FutureCombo, asset.Options:
// TODO: candlestick data for asset item option_combo, future_combo, and option not supported yet
}
return nil, fmt.Errorf("%w candlestick data for asset type %v", asset.ErrNotSupported, a)
}
@@ -1352,7 +1339,7 @@ func (e *Exchange) GetOpenInterest(ctx context.Context, k ...key.PairAsset) ([]f
return nil, err
}
cp := k[i].Pair().Format(pFmt)
p := e.formatPairString(k[i].Asset, cp)
p := formatPairString(k[i].Asset, cp)
var oi []BookSummaryData
if e.Websocket.IsConnected() {
oi, err = e.WSRetrieveBookSummaryByInstrument(ctx, p)
@@ -1446,7 +1433,7 @@ func (e *Exchange) GetLatestFundingRates(ctx context.Context, r *fundingrate.Lat
return nil, err
}
cp := r.Pair.Format(pFmt)
p := e.formatPairString(r.Asset, cp)
p := formatPairString(r.Asset, cp)
var fri []FundingRateHistory
fri, err = e.GetFundingRateHistory(ctx, p, time.Now().Add(-time.Hour*16), time.Now())
if err != nil {
@@ -1503,7 +1490,7 @@ func (e *Exchange) GetHistoricalFundingRates(ctx context.Context, r *fundingrate
return nil, err
}
cp := r.Pair.Format(pFmt)
p := e.formatPairString(r.Asset, cp)
p := formatPairString(r.Asset, cp)
ed := r.EndDate
var fundingRates []fundingrate.Rate
@@ -1555,12 +1542,14 @@ func (e *Exchange) GetHistoricalFundingRates(ctx context.Context, r *fundingrate
}, nil
}
func (e *Exchange) formatPairString(assetType asset.Item, pair currency.Pair) string {
func formatPairString(assetType asset.Item, pair currency.Pair) string {
switch assetType {
case asset.Futures:
return e.formatFuturesTradablePair(pair)
return formatFuturesTradablePair(pair)
case asset.Options:
return e.optionPairToString(pair)
return optionPairToString(pair)
case asset.OptionCombo:
return optionComboPairToString(pair)
}
return pair.String()
}