diff --git a/exchanges/deribit/deribit.go b/exchanges/deribit/deribit.go index 00efc5ab5a3..f4787df6be4 100644 --- a/exchanges/deribit/deribit.go +++ b/exchanges/deribit/deribit.go @@ -28,11 +28,25 @@ type Deribit struct { exchange.Base } +var ( + // optionRegex compiles optionDecimalRegex at startup and is used to help set + // option currency lower-case d eg MATIC-USDC-3JUN24-0d64-P + optionRegex *regexp.Regexp +) + const ( deribitAPIVersion = "/api/v2" + tradeBaseURL = "https://www.deribit.com/" + tradeSpot = "spot/" + tradeFutures = "futures/" + tradeOptions = "options/" + tradeFuturesCombo = "futures-spreads/" + tradeOptionsCombo = "combos/" - // Public endpoints + perpString = "PERPETUAL" + optionDecimalRegex = `\d+(D)\d+` + // Public endpoints // Market Data getBookByCurrency = "public/get_book_summary_by_currency" getBookByInstrument = "public/get_book_summary_by_instrument" @@ -2682,10 +2696,51 @@ func (d *Deribit) StringToAssetKind(assetType string) (asset.Item, error) { } } -func guessAssetTypeFromInstrument(currencyPair currency.Pair) (asset.Item, error) { +// getAssetPairByInstrument is able to determine the asset type and currency pair +// based on the received instrument ID +func (d *Deribit) getAssetPairByInstrument(instrument string) (currency.Pair, asset.Item, error) { + if instrument == "" { + return currency.EMPTYPAIR, asset.Empty, errInvalidInstrumentName + } + + 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) + } + cp, err := currency.NewPairFromString(instrument) + if err != nil { + return currency.EMPTYPAIR, asset.Empty, err + } + + return cp, item, nil +} + +func getAssetFromPair(currencyPair currency.Pair) (asset.Item, error) { currencyPairString := currencyPair.String() vals := strings.Split(currencyPairString, currency.DashDelimiter) - if strings.HasSuffix(currencyPairString, "PERPETUAL") || len(vals) == 2 { + if strings.HasSuffix(currencyPairString, perpString) || len(vals) == 2 { return asset.Futures, nil } else if len(vals) == 1 { if vals = strings.Split(vals[0], currency.UnderscoreDelimiter); len(vals) == 2 { @@ -2721,7 +2776,7 @@ func guessAssetTypeFromInstrument(currencyPair currency.Pair) (asset.Item, error } func calculateTradingFee(feeBuilder *exchange.FeeBuilder) (float64, error) { - assetType, err := guessAssetTypeFromInstrument(feeBuilder.Pair) + assetType, err := getAssetFromPair(feeBuilder.Pair) if err != nil { return 0, err } @@ -2735,7 +2790,7 @@ func calculateTradingFee(feeBuilder *exchange.FeeBuilder) (float64, error) { return feeBuilder.Amount * feeBuilder.PurchasePrice * 0.0005, nil case strings.HasPrefix(feeBuilder.Pair.String(), currencyBTC), strings.HasPrefix(feeBuilder.Pair.String(), currencyETH): - if strings.HasSuffix(feeBuilder.Pair.String(), "PERPETUAL") { + if strings.HasSuffix(feeBuilder.Pair.String(), perpString) { if feeBuilder.IsMaker { return 0, nil } @@ -2786,10 +2841,16 @@ func (d *Deribit) formatFuturesTradablePair(pair currency.Pair) string { // it has both uppercase or lowercase characters, which we can not achieve with the Upper=true or Upper=false func (d *Deribit) optionPairToString(pair currency.Pair) string { subCodes := strings.Split(pair.Quote.String(), currency.DashDelimiter) - if len(subCodes) == 3 { - if match, err := regexp.MatchString(`^[a-zA-Z0-9_]*$`, subCodes[1]); match && err == nil { - subCodes[1] = strings.ToLower(subCodes[1]) + initialDelimiter := currency.DashDelimiter + if subCodes[0] == "USDC" { + initialDelimiter = currency.UnderscoreDelimiter + } + for i := range subCodes { + if match := optionRegex.MatchString(subCodes[i]); match { + subCodes[i] = strings.ToLower(subCodes[i]) + break } } - return pair.Base.String() + currency.DashDelimiter + strings.Join(subCodes, currency.DashDelimiter) + + return pair.Base.String() + initialDelimiter + strings.Join(subCodes, currency.DashDelimiter) } diff --git a/exchanges/deribit/deribit_test.go b/exchanges/deribit/deribit_test.go index b726cdee41a..84b92c7b2d2 100644 --- a/exchanges/deribit/deribit_test.go +++ b/exchanges/deribit/deribit_test.go @@ -3,7 +3,6 @@ package deribit import ( "context" "encoding/json" - "errors" "fmt" "log" "os" @@ -47,7 +46,7 @@ var ( d = &Deribit{} optionsTradablePair, optionComboTradablePair, futureComboTradablePair currency.Pair spotTradablePair = currency.NewPairWithDelimiter(currencyBTC, "USDC", "_") - futuresTradablePair = currency.NewPairWithDelimiter(currencyBTC, "PERPETUAL", "-") + futuresTradablePair = currency.NewPairWithDelimiter(currencyBTC, perpString, "-") assetTypeToPairsMap map[asset.Item]currency.Pair ) @@ -90,6 +89,39 @@ func TestMain(m *testing.M) { os.Exit(m.Run()) } +func instantiateTradablePairs() { + if err := d.UpdateTradablePairs(context.Background(), true); err != nil { + log.Fatalf("Failed to update tradable pairs. Error: %v", err) + } + + handleError := func(err error, msg string) { + if err != nil { + log.Fatalf("%s. Error: %v", msg, err) + } + } + + updateTradablePair := func(assetType asset.Item, tradablePair *currency.Pair) { + if d.CurrencyPairs.IsAssetEnabled(assetType) == nil { + pairs, err := d.GetEnabledPairs(assetType) + handleError(err, fmt.Sprintf("Failed to get enabled pairs for asset type %v", assetType)) + + if len(pairs) == 0 { + handleError(currency.ErrCurrencyPairsEmpty, fmt.Sprintf("No enabled pairs for asset type %v", assetType)) + } + + if assetType == asset.Options { + *tradablePair, err = d.FormatExchangeCurrency(pairs[0], assetType) + handleError(err, "Failed to format exchange currency for options pair") + } else { + *tradablePair = pairs[0] + } + } + } + updateTradablePair(asset.Options, &optionsTradablePair) + updateTradablePair(asset.OptionCombo, &optionComboTradablePair) + updateTradablePair(asset.FutureCombo, &futureComboTradablePair) +} + func TestUpdateTicker(t *testing.T) { t.Parallel() _, err := d.UpdateTicker(context.Background(), currency.Pair{}, asset.Margin) @@ -97,8 +129,8 @@ func TestUpdateTicker(t *testing.T) { for assetType, cp := range assetTypeToPairsMap { result, err := d.UpdateTicker(context.Background(), cp, assetType) - require.NoErrorf(t, err, "expected nil, got %v for asset type %s pair %s", err, assetType.String(), cp.String()) - require.NotNilf(t, result, "Expected result not to be nil for asset type %s pair %s", assetType.String(), cp.String()) + 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) } } @@ -106,7 +138,7 @@ func TestUpdateOrderbook(t *testing.T) { t.Parallel() for assetType, cp := range assetTypeToPairsMap { result, err := d.UpdateOrderbook(context.Background(), cp, assetType) - require.NoErrorf(t, err, "%w for asset type: %v", err, assetType) + require.NoErrorf(t, err, "asset type: %v", assetType) require.NotNil(t, result) } } @@ -115,11 +147,9 @@ func TestGetHistoricTrades(t *testing.T) { t.Parallel() _, err := d.GetHistoricTrades(context.Background(), futureComboTradablePair, asset.FutureCombo, time.Now().Add(-time.Minute*10), time.Now()) require.ErrorIs(t, err, asset.ErrNotSupported) - var result []trade.Data for assetType, cp := range map[asset.Item]currency.Pair{asset.Spot: spotTradablePair, asset.Futures: futuresTradablePair} { - result, err = d.GetHistoricTrades(context.Background(), cp, assetType, time.Now().Add(-time.Minute*10), time.Now()) - require.NoErrorf(t, err, "%w asset type: %v", err, assetType) - require.NotNilf(t, result, "Expected value not to be nil for asset type: %s", err, assetType.String()) + _, err = d.GetHistoricTrades(context.Background(), cp, assetType, time.Now().Add(-time.Minute*10), time.Now()) + require.NoErrorf(t, err, "asset type: %v", assetType) } } @@ -127,8 +157,8 @@ func TestFetchRecentTrades(t *testing.T) { t.Parallel() for assetType, cp := range assetTypeToPairsMap { result, err := d.GetRecentTrades(context.Background(), cp, assetType) - require.NoErrorf(t, err, "expected nil, got %v for asset type %s pair %s", err, assetType.String(), cp.String()) - require.NotNilf(t, result, "Expected result not to be nil for asset type %s pair %s", assetType.String(), cp.String()) + 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) } } @@ -191,8 +221,8 @@ func TestSubmitOrder(t *testing.T) { var info *InstrumentData for assetType, cp := range assetToPairStringMap { info, err = d.GetInstrument(context.Background(), d.formatPairString(assetType, cp)) - require.NoErrorf(t, err, "expected nil, got %v for asset type %s pair %s", err, assetType.String(), cp.String()) - require.NotNilf(t, result, "Expected result not to be nil for asset type %s pair %s", assetType.String(), cp.String()) + 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) result, err = d.SubmitOrder( context.Background(), @@ -206,8 +236,8 @@ func TestSubmitOrder(t *testing.T) { Pair: cp, }, ) - require.NoErrorf(t, err, "expected nil, got %v for asset type %s pair %s", err, assetType.String(), cp.String()) - require.NotNilf(t, result, "Expected result not to be nil for asset type %s pair %s", assetType.String(), cp.String()) + 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) } } @@ -228,7 +258,7 @@ func TestGetMarkPriceHistory(t *testing.T) { } { result, err = d.GetMarkPriceHistory(context.Background(), ps, time.Now().Add(-5*time.Minute), time.Now()) 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) + require.NotNilf(t, result, "expected result not to be nil for pair %s", ps) } } @@ -246,7 +276,7 @@ func TestWSRetrieveMarkPriceHistory(t *testing.T) { } { result, err = d.WSRetrieveMarkPriceHistory(ps, time.Now().Add(-4*time.Hour), time.Now()) require.NoErrorf(t, err, "expected %v, got %v currency pair %v", nil, err, ps) - require.NotNilf(t, result, "Expected value not to be nil for pair: %v", ps) + require.NotNilf(t, result, "expected value not to be nil for pair: %v", ps) } } @@ -290,7 +320,7 @@ func TestGetBookSummaryByInstrument(t *testing.T) { } { result, err = d.GetBookSummaryByInstrument(context.Background(), 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) + require.NotNilf(t, result, "expected result not to be nil for pair %s", ps) } } @@ -308,7 +338,7 @@ func TestWSRetrieveBookSummaryByInstrument(t *testing.T) { } { result, err = d.WSRetrieveBookSummaryByInstrument(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) + require.NotNilf(t, result, "expected result not to be nil for pair %s", ps) } } @@ -503,8 +533,8 @@ func TestGetInstrumentData(t *testing.T) { var result *InstrumentData for assetType, cp := range assetTypeToPairsMap { result, err = d.GetInstrument(context.Background(), d.formatPairString(assetType, cp)) - require.NoErrorf(t, err, "expected nil, got %v for asset type %s pair %s", err, assetType.String(), cp.String()) - require.NotNilf(t, result, "Expected result not to be nil for asset type %s pair %s", assetType.String(), cp.String()) + 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) } } @@ -516,8 +546,8 @@ func TestWSRetrieveInstrumentData(t *testing.T) { var result *InstrumentData for assetType, cp := range assetTypeToPairsMap { result, err = d.WSRetrieveInstrumentData(d.formatPairString(assetType, cp)) - require.NoErrorf(t, err, "expected nil, got %v for asset type %s pair %s", err, assetType.String(), cp.String()) - require.NotNilf(t, result, "Expected result not to be nil for asset type %s pair %s", assetType.String(), cp.String()) + 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) } } @@ -638,7 +668,7 @@ func TestGetLastTradesByInstrument(t *testing.T) { for assetType, cp := range assetTypeToPairsMap { result, err := d.GetLastTradesByInstrument(context.Background(), d.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) + require.NotNilf(t, result, "expected value not to be nil for asset %v pair: %v", assetType, cp) } } @@ -650,7 +680,7 @@ func TestWSRetrieveLastTradesByInstrument(t *testing.T) { for assetType, cp := range assetTypeToPairsMap { result, err := d.WSRetrieveLastTradesByInstrument(d.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) + require.NotNilf(t, result, "expected value not to be nil for asset %v pair: %v", assetType, cp) } } @@ -662,7 +692,7 @@ func TestGetLastTradesByInstrumentAndTime(t *testing.T) { for assetType, cp := range assetTypeToPairsMap { result, err := d.GetLastTradesByInstrumentAndTime(context.Background(), d.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) + require.NotNilf(t, result, "expected value not to be nil for pair: %v", cp) } } @@ -674,7 +704,7 @@ func TestWSRetrieveLastTradesByInstrumentAndTime(t *testing.T) { for assetType, cp := range assetTypeToPairsMap { result, err := d.WSRetrieveLastTradesByInstrumentAndTime(d.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) + require.NotNilf(t, result, "expected value not to be nil for pair: %v", cp) } } @@ -687,7 +717,7 @@ func TestGetOrderbookData(t *testing.T) { for assetType, cp := range assetTypeToPairsMap { result, err = d.GetOrderbook(context.Background(), d.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) + require.NotNilf(t, result, "expected value not to be nil for pair: %v", cp) } } @@ -703,7 +733,7 @@ func TestWSRetrieveOrderbookData(t *testing.T) { for assetType, cp := range assetTypeToPairsMap { result, err = d.WSRetrieveOrderbookData(d.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) + require.NotNilf(t, result, "expected value not to be nil for pair: %v", cp) } } @@ -3249,8 +3279,8 @@ func TestFetchTicker(t *testing.T) { var err error for assetType, cp := range assetTypeToPairsMap { result, err = d.FetchTicker(context.Background(), cp, assetType) - require.NoErrorf(t, err, "expected nil, got %v for asset type %s pair %s", err, assetType.String(), cp.String()) - require.NotNilf(t, result, "Expected result not to be nil for asset type %s pair %s", assetType.String(), cp.String()) + 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) } } @@ -3260,8 +3290,8 @@ func TestFetchOrderbook(t *testing.T) { var err error for assetType, cp := range assetTypeToPairsMap { result, err = d.FetchOrderbook(context.Background(), cp, assetType) - require.NoErrorf(t, err, "expected nil, got %v for asset type %s", err, assetType.String()) - require.NotNilf(t, result, "Expected result not to be nil for asset type %s", assetType.String()) + require.NoErrorf(t, err, "expected nil, got %v for asset type %s", err, assetType) + require.NotNilf(t, result, "expected result not to be nil for asset type %s", assetType) } } @@ -3279,8 +3309,8 @@ func TestFetchAccountInfo(t *testing.T) { assetTypes := d.GetAssetTypes(true) for _, assetType := range assetTypes { result, err := d.FetchAccountInfo(context.Background(), assetType) - require.NoErrorf(t, err, "expected nil, got %v for asset type %s", err, assetType.String()) - require.NotNilf(t, result, "Expected result not to be nil for asset type %s", assetType.String()) + require.NoErrorf(t, err, "expected nil, got %v for asset type %s", err, assetType) + require.NotNilf(t, result, "expected result not to be nil for asset type %s", assetType) } } @@ -3306,8 +3336,8 @@ func TestGetRecentTrades(t *testing.T) { var err error for assetType, cp := range assetTypeToPairsMap { result, err = d.GetRecentTrades(context.Background(), cp, assetType) - require.NoErrorf(t, err, "expected nil, got %v for asset type %s pair %s", err, assetType.String(), cp.String()) - require.NotNilf(t, result, "Expected result not to be nil for asset type %s pair %s", assetType.String(), cp.String()) + 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) } } @@ -3337,8 +3367,8 @@ func TestCancelAllOrders(t *testing.T) { orderCancellation.AssetType = assetType orderCancellation.Pair = cp result, err = d.CancelAllOrders(context.Background(), orderCancellation) - require.NoErrorf(t, err, "expected nil, got %v for asset type %s pair %s", err, assetType.String(), cp.String()) - require.NotNilf(t, result, "Expected result not to be nil for asset type %s pair %s", assetType.String(), cp.String()) + 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) } } @@ -3347,8 +3377,8 @@ func TestGetOrderInfo(t *testing.T) { sharedtestvalues.SkipTestIfCredentialsUnset(t, d) for assetType, cp := range assetTypeToPairsMap { result, err := d.GetOrderInfo(context.Background(), "1234", cp, assetType) - require.NoErrorf(t, err, "expected nil, got %v for asset type %s pair %s", err, assetType.String(), cp.String()) - require.NotNilf(t, result, "Expected result not to be nil for asset type %s pair %s", assetType.String(), cp.String()) + 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) } } @@ -3389,8 +3419,8 @@ func TestGetActiveOrders(t *testing.T) { getOrdersRequest.Pairs = []currency.Pair{cp} getOrdersRequest.AssetType = assetType result, err := d.GetActiveOrders(context.Background(), &getOrdersRequest) - require.NoErrorf(t, err, "expected nil, got %v for asset type %s pair %s", err, assetType.String(), cp.String()) - require.NotNilf(t, result, "Expected result not to be nil for asset type %s pair %s", assetType.String(), cp.String()) + 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) } } @@ -3402,24 +3432,24 @@ func TestGetOrderHistory(t *testing.T) { Type: order.AnyType, AssetType: assetType, Side: order.AnySide, Pairs: []currency.Pair{cp}, }) - require.NoErrorf(t, err, "expected nil, got %v for asset type %s pair %s", err, assetType.String(), cp.String()) - require.NotNilf(t, result, "Expected result not to be nil for asset type %s pair %s", assetType.String(), cp.String()) + 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) } } -func TestGuessAssetTypeFromInstrument(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 := d.GetEnabledPairs(assetType) - require.NoErrorf(t, err, "expected nil, got %v for asset type %s", err, assetType.String()) - require.NotNilf(t, availablePairs, "Expected result not to be nil for asset type %s", assetType.String()) + 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 := d.GetPairFormat(assetType, true) require.NoError(t, err) for id, cp := range availablePairs { t.Run(strconv.Itoa(id), func(t *testing.T) { - assetTypeNew, err = guessAssetTypeFromInstrument(cp.Format(format)) + 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)) }) } @@ -3427,10 +3457,38 @@ func TestGuessAssetTypeFromInstrument(t *testing.T) { cp, err := currency.NewPairFromString("some_thing_else") require.NoError(t, err) - _, err = guessAssetTypeFromInstrument(cp) + _, 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} { + availablePairs, err := d.GetAvailablePairs(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) + for _, cp := range availablePairs { + t.Run(fmt.Sprintf("%s %s", assetType, cp), func(t *testing.T) { + t.Parallel() + extractedPair, extractedAsset, err := d.getAssetPairByInstrument(cp.String()) + assert.NoError(t, err) + assert.Equal(t, cp.String(), extractedPair.String()) + assert.Equal(t, assetType.String(), extractedAsset.String()) + }) + } + } + t.Run("empty asset, empty pair", func(t *testing.T) { + t.Parallel() + _, _, err := d.getAssetPairByInstrument("") + assert.ErrorIs(t, err, errInvalidInstrumentName) + }) + t.Run("thisIsAFakeCurrency", func(t *testing.T) { + t.Parallel() + _, _, err := d.getAssetPairByInstrument("thisIsAFakeCurrency") + assert.ErrorIs(t, err, errUnsupportedInstrumentFormat) + }) +} + func TestGetFeeByTypeOfflineTradeFee(t *testing.T) { var feeBuilder = &exchange.FeeBuilder{ Amount: 1, @@ -3445,9 +3503,9 @@ func TestGetFeeByTypeOfflineTradeFee(t *testing.T) { require.NoError(t, err) require.NotNil(t, result) if !sharedtestvalues.AreAPICredentialsSet(d) { - assert.Equalf(t, exchange.OfflineTradeFee, feeBuilder.FeeType, "Expected %f, received %f", exchange.OfflineTradeFee, feeBuilder.FeeType) + assert.Equalf(t, exchange.OfflineTradeFee, feeBuilder.FeeType, "expected %v, received %v", exchange.OfflineTradeFee, feeBuilder.FeeType) } else { - assert.Equalf(t, exchange.CryptocurrencyTradeFee, feeBuilder.FeeType, "Expected %v, received %v", exchange.CryptocurrencyTradeFee, feeBuilder.FeeType) + assert.Equalf(t, exchange.CryptocurrencyTradeFee, feeBuilder.FeeType, "expected %v, received %v", exchange.CryptocurrencyTradeFee, feeBuilder.FeeType) } } @@ -3594,24 +3652,47 @@ var websocketPushData = map[string]string{ func TestProcessPushData(t *testing.T) { t.Parallel() - for x := range websocketPushData { - err := d.wsHandleData([]byte(websocketPushData[x])) - require.NoErrorf(t, err, "%s: Received unexpected error for", x) + for k, v := range websocketPushData { + t.Run(k, func(t *testing.T) { + t.Parallel() + err := d.wsHandleData([]byte(v)) + require.NoErrorf(t, err, "%s: Received unexpected error for", k) + }) } } func TestFormatFuturesTradablePair(t *testing.T) { t.Parallel() futuresInstrumentsOutputList := map[currency.Pair]string{ - {Delimiter: currency.DashDelimiter, Base: currency.BTC, Quote: currency.NewCode("PERPETUAL")}: "BTC-PERPETUAL", + {Delimiter: currency.DashDelimiter, Base: currency.BTC, Quote: currency.NewCode(perpString)}: "BTC-PERPETUAL", {Delimiter: currency.DashDelimiter, Base: currency.AVAX, Quote: currency.NewCode("USDC-PERPETUAL")}: "AVAX_USDC-PERPETUAL", {Delimiter: currency.DashDelimiter, Base: currency.ETH, Quote: currency.NewCode("30DEC22")}: "ETH-30DEC22", {Delimiter: currency.DashDelimiter, Base: currency.SOL, Quote: currency.NewCode("30DEC22")}: "SOL-30DEC22", {Delimiter: currency.DashDelimiter, Base: currency.NewCode("BTCDVOL"), Quote: currency.NewCode("USDC-28JUN23")}: "BTCDVOL_USDC-28JUN23", } for pair, instrumentID := range futuresInstrumentsOutputList { - instrument := d.formatFuturesTradablePair(pair) - require.Equal(t, instrumentID, instrument) + t.Run(instrumentID, func(t *testing.T) { + t.Parallel() + instrument := d.formatFuturesTradablePair(pair) + require.Equal(t, instrumentID, instrument) + }) + } +} + +func TestOptionPairToString(t *testing.T) { + t.Parallel() + optionsList := map[currency.Pair]string{ + {Delimiter: currency.DashDelimiter, Base: currency.BTC, Quote: currency.NewCode("30MAY24-61000-C")}: "BTC-30MAY24-61000-C", + {Delimiter: currency.DashDelimiter, Base: currency.ETH, Quote: currency.NewCode("1JUN24-3200-P")}: "ETH-1JUN24-3200-P", + {Delimiter: currency.DashDelimiter, Base: currency.SOL, Quote: currency.NewCode("USDC-31MAY24-162-P")}: "SOL_USDC-31MAY24-162-P", + {Delimiter: currency.DashDelimiter, Base: currency.MATIC, Quote: currency.NewCode("USDC-6APR24-0d98-P")}: "MATIC_USDC-6APR24-0d98-P", + } + for pair, instrumentID := range optionsList { + t.Run(instrumentID, func(t *testing.T) { + t.Parallel() + instrument := d.optionPairToString(pair) + require.Equal(t, instrumentID, instrument) + }) } } @@ -3625,39 +3706,6 @@ func TestWSRetrieveCombos(t *testing.T) { assert.NotNil(t, result) } -func instantiateTradablePairs() { - if err := d.UpdateTradablePairs(context.Background(), true); err != nil { - log.Fatalf("Failed to update tradable pairs. Error: %v", err) - } - - handleError := func(err error, msg string) { - if err != nil { - log.Fatalf("%s. Error: %v", msg, err) - } - } - - updateTradablePair := func(assetType asset.Item, tradablePair *currency.Pair) { - if d.CurrencyPairs.IsAssetEnabled(assetType) == nil { - pairs, err := d.GetEnabledPairs(assetType) - handleError(err, fmt.Sprintf("Failed to get enabled pairs for asset type %v", assetType)) - - if len(pairs) == 0 { - handleError(currency.ErrCurrencyPairsEmpty, fmt.Sprintf("No enabled pairs for asset type %v", assetType)) - } - - if assetType == asset.Options { - *tradablePair, err = d.FormatExchangeCurrency(pairs[0], assetType) - handleError(err, "Failed to format exchange currency for options pair") - } else { - *tradablePair = pairs[0] - } - } - } - updateTradablePair(asset.Options, &optionsTradablePair) - updateTradablePair(asset.OptionCombo, &optionComboTradablePair) - updateTradablePair(asset.FutureCombo, &futureComboTradablePair) -} - func TestGetLatestFundingRates(t *testing.T) { t.Parallel() _, err := d.GetLatestFundingRates(context.Background(), &fundingrate.LatestRateRequest{ @@ -3799,6 +3847,9 @@ func TestGetFuturesContractDetails(t *testing.T) { result, err := d.GetFuturesContractDetails(context.Background(), asset.Futures) require.NoError(t, err) assert.NotNil(t, result) + + _, err = d.GetFuturesContractDetails(context.Background(), asset.FutureCombo) + require.ErrorIs(t, err, asset.ErrNotSupported) } func TestGetFuturesPositionSummary(t *testing.T) { @@ -3817,7 +3868,7 @@ func TestGetFuturesPositionSummary(t *testing.T) { sharedtestvalues.SkipTestIfCredentialsUnset(t, d) req := &futures.PositionSummaryRequest{ Asset: asset.Futures, - Pair: currency.NewPair(currency.BTC, currency.NewCode("PERPETUAL")), + Pair: currency.NewPair(currency.BTC, currency.NewCode(perpString)), } result, err := d.GetFuturesPositionSummary(context.Background(), req) require.NoError(t, err) @@ -3834,23 +3885,32 @@ func TestGetOpenInterest(t *testing.T) { require.ErrorIs(t, err, asset.ErrNotSupported) _, err = d.GetOpenInterest(context.Background(), key.PairAsset{ - Base: currency.BTC.Item, + Base: optionsTradablePair.Base.Item, Quote: optionsTradablePair.Quote.Item, Asset: asset.Options, }) - require.True(t, err == nil || errors.Is(err, currency.ErrCurrencyNotFound)) - - var result []futures.OpenInterest - assetTypeToPairs := getAssetToPairMap(asset.Futures & asset.FutureCombo) - for assetType, cp := range assetTypeToPairs { - result, err = d.GetOpenInterest(context.Background(), key.PairAsset{ - Base: cp.Base.Item, - Quote: cp.Quote.Item, - Asset: assetType, - }) - require.NoErrorf(t, err, "expected nil, got %s for asset type %s pair %s", assetType.String(), cp.String()) - require.NotNilf(t, result, "Expected result not to be nil for asset type %s pair %s", assetType.String(), cp.String()) - } + require.NoError(t, err) + + _, err = d.GetOpenInterest(context.Background(), key.PairAsset{ + Base: currency.BTC.Item, + Quote: currency.NewCode(perpString).Item, + Asset: asset.Futures, + }) + require.NoError(t, err) + + _, err = d.GetOpenInterest(context.Background(), key.PairAsset{ + Base: currency.NewCode("XRP").Item, + Quote: currency.NewCode("USDC-PERPETUAL").Item, + Asset: asset.Futures, + }) + require.NoError(t, err) + + _, err = d.GetOpenInterest(context.Background(), key.PairAsset{ + Base: futureComboTradablePair.Base.Item, + Quote: futureComboTradablePair.Quote.Item, + Asset: asset.FutureCombo, + }) + require.NoError(t, err) } func TestIsPerpetualFutureCurrency(t *testing.T) { @@ -3861,26 +3921,27 @@ func TestIsPerpetualFutureCurrency(t *testing.T) { Response bool }{ asset.Spot: { - {Pair: currency.EMPTYPAIR, Error: futures.ErrNotPerpetualFuture}, + {Pair: currency.EMPTYPAIR, Error: currency.ErrCurrencyPairEmpty, Response: false}, + {Pair: spotTradablePair, Error: nil, Response: false}, }, asset.Futures: { - {Pair: currency.EMPTYPAIR, Error: currency.ErrCurrencyPairEmpty}, - {Pair: currency.NewPair(currency.BTC, currency.NewCode("PERPETUAL")), Response: true}, - {Pair: currency.NewPair(currency.NewCode("ETH"), currency.NewCode("FS-30DEC22_PERP")), Response: true}, + {Pair: currency.NewPair(currency.BTC, currency.NewCode(perpString)), Response: true}, }, asset.FutureCombo: { - {Pair: currency.NewPair(currency.NewCode("SOL"), currency.NewCode("FS-30DEC22_28OCT22"))}, + {Pair: currency.NewPair(currency.NewCode("BTC"), currency.NewCode("FS-27SEP24_PERP")), Response: false}, }, asset.OptionCombo: { - {Pair: currency.NewPair(currency.NewCode(currencyBTC), currency.NewCode("STRG-21OCT22")), Error: futures.ErrNotPerpetualFuture}, - {Pair: currency.EMPTYPAIR, Error: futures.ErrNotPerpetualFuture}, + {Pair: currency.NewPair(currency.NewCode(currencyBTC), currency.NewCode("STRG-21OCT22")), Error: nil, Response: false}, }, } for assetType, instances := range assetPairToErrorMap { for i := range instances { - is, err := d.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()) + t.Run(fmt.Sprintf("Asset: %s Pair: %s", assetType.String(), instances[i].Pair.String()), func(t *testing.T) { + t.Parallel() + is, err := d.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()) + }) } } } @@ -3952,24 +4013,14 @@ func TestGetResolutionFromInterval(t *testing.T) { } } -func getAssetToPairMap(items asset.Item) map[asset.Item]currency.Pair { - newMap := make(map[asset.Item]currency.Pair) - for a := range assetTypeToPairsMap { - if a&items == a { - newMap[a] = assetTypeToPairsMap[a] - } - } - return newMap -} - func TestGetValidatedCurrencyCode(t *testing.T) { t.Parallel() pairs := map[currency.Pair]string{ currency.NewPairWithDelimiter(currencySOL, "21OCT22-20-C", "-"): currencySOL, - currency.NewPairWithDelimiter(currencyBTC, "PERPETUAL", "-"): currencyBTC, - currency.NewPairWithDelimiter(currencyETH, "PERPETUAL", "-"): currencyETH, - currency.NewPairWithDelimiter(currencySOL, "PERPETUAL", "-"): currencySOL, - currency.NewPairWithDelimiter("AVAX_USDC", "PERPETUAL", "-"): currencyUSDC, + currency.NewPairWithDelimiter(currencyBTC, perpString, "-"): currencyBTC, + currency.NewPairWithDelimiter(currencyETH, perpString, "-"): currencyETH, + currency.NewPairWithDelimiter(currencySOL, perpString, "-"): currencySOL, + currency.NewPairWithDelimiter("AVAX_USDC", perpString, "-"): currencyUSDC, currency.NewPairWithDelimiter(currencyBTC, "USDC", "_"): currencyBTC, currency.NewPairWithDelimiter(currencyETH, "USDC", "_"): currencyETH, currency.NewPairWithDelimiter("DOT", "USDC-PERPETUAL", "_"): currencyUSDC, @@ -3981,3 +4032,30 @@ func TestGetValidatedCurrencyCode(t *testing.T) { require.Equal(t, pairs[x], result, "expected: %s actual : %s for currency pair: %v", x, result, pairs[x]) } } + +func TestGetCurrencyTradeURL(t *testing.T) { + t.Parallel() + _, err := d.GetCurrencyTradeURL(context.Background(), asset.Spot, currency.EMPTYPAIR) + require.ErrorIs(t, err, currency.ErrCurrencyPairEmpty) + + for _, a := range d.GetAssetTypes(false) { + var pairs currency.Pairs + pairs, err = d.CurrencyPairs.GetPairs(a, false) + require.NoError(t, err, "cannot get pairs for %s", a) + require.NotEmpty(t, pairs, "no pairs for %s", a) + var resp string + resp, err = d.GetCurrencyTradeURL(context.Background(), a, pairs[0]) + require.NoError(t, err) + assert.NotEmpty(t, resp) + } + // specific test to ensure perps work + cp := currency.NewPair(currency.BTC, currency.NewCode("USDC-PERPETUAL")) + resp, err := d.GetCurrencyTradeURL(context.Background(), asset.Futures, cp) + require.NoError(t, err) + assert.NotEmpty(t, resp) + // specific test to ensure options with dates work + cp = currency.NewPair(currency.BTC, currency.NewCode("14JUN24-62000-C")) + resp, err = d.GetCurrencyTradeURL(context.Background(), asset.Options, cp) + require.NoError(t, err) + assert.NotEmpty(t, resp) +} diff --git a/exchanges/deribit/deribit_websocket.go b/exchanges/deribit/deribit_websocket.go index fae7bf50cb8..8a4a3fce69f 100644 --- a/exchanges/deribit/deribit_websocket.go +++ b/exchanges/deribit/deribit_websocket.go @@ -255,7 +255,7 @@ func (d *Deribit) wsHandleData(respRaw []byte) error { accessLog := &wsAccessLog{} return d.processData(respRaw, accessLog) case "changes": - return d.processChanges(respRaw, channels) + return d.processUserOrderChanges(respRaw, channels) case "lock": userLock := &WsUserLock{} return d.processData(respRaw, userLock) @@ -265,7 +265,7 @@ func (d *Deribit) wsHandleData(respRaw []byte) error { } return d.processData(respRaw, data) case "orders": - return d.processOrders(respRaw, channels) + return d.processUserOrders(respRaw, channels) case "portfolio": portfolio := &wsUserPortfolio{} return d.processData(respRaw, portfolio) @@ -294,52 +294,32 @@ func (d *Deribit) wsHandleData(respRaw []byte) error { return nil } -func (d *Deribit) processOrders(respRaw []byte, channels []string) error { - var currencyPair currency.Pair - var err error - var a asset.Item - switch len(channels) { - case 4: - currencyPair, err = currency.NewPairFromString(channels[2]) - if err != nil { - return err - } - case 5: - a, err = d.StringToAssetKind(channels[2]) - if err != nil { - return err - } - default: +func (d *Deribit) processUserOrders(respRaw []byte, channels []string) error { + if len(channels) != 4 && len(channels) != 5 { return fmt.Errorf("%w, expected format 'user.orders.{instrument_name}.raw, user.orders.{instrument_name}.{interval}, user.orders.{kind}.{currency}.raw, or user.orders.{kind}.{currency}.{interval}', but found %s", errMalformedData, strings.Join(channels, ".")) } var response WsResponse orderData := []WsOrder{} response.Params.Data = orderData - err = json.Unmarshal(respRaw, &response) + err := json.Unmarshal(respRaw, &response) if err != nil { return err } orderDetails := make([]order.Detail, len(orderData)) for x := range orderData { - oType, err := order.StringToOrderType(orderData[x].OrderType) + cp, a, err := d.getAssetPairByInstrument(orderData[x].InstrumentName) if err != nil { return err } - side, err := order.StringToOrderSide(orderData[x].Direction) + oType, err := order.StringToOrderType(orderData[x].OrderType) if err != nil { return err } - status, err := order.StringToOrderStatus(orderData[x].OrderState) + side, err := order.StringToOrderSide(orderData[x].Direction) if err != nil { return err } - if a != asset.Empty { - currencyPair, err = currency.NewPairFromString(orderData[x].InstrumentName) - if err != nil { - return err - } - } - a, err = guessAssetTypeFromInstrument(currencyPair) + status, err := order.StringToOrderStatus(orderData[x].OrderState) if err != nil { return err } @@ -356,14 +336,17 @@ func (d *Deribit) processOrders(respRaw []byte, channels []string) error { AssetType: a, Date: orderData[x].CreationTimestamp.Time(), LastUpdated: orderData[x].LastUpdateTimestamp.Time(), - Pair: currencyPair, + Pair: cp, } } d.Websocket.DataHandler <- orderDetails return nil } -func (d *Deribit) processChanges(respRaw []byte, channels []string) error { +func (d *Deribit) processUserOrderChanges(respRaw []byte, channels []string) error { + if len(channels) < 4 || len(channels) > 5 { + return fmt.Errorf("%w, expected format 'trades.{instrument_name}.{interval} or trades.{kind}.{currency}.{interval}', but found %s", errMalformedData, strings.Join(channels, ".")) + } var response WsResponse changeData := &wsChanges{} response.Params.Data = changeData @@ -371,43 +354,22 @@ func (d *Deribit) processChanges(respRaw []byte, channels []string) error { if err != nil { return err } - var currencyPair currency.Pair - var a asset.Item - switch len(channels) { - case 4: - currencyPair, err = currency.NewPairFromString(channels[2]) - if err != nil { - return err - } - case 5: - a, err = d.StringToAssetKind(channels[2]) - if err != nil { - return err - } - default: - return fmt.Errorf("%w, expected format 'trades.{instrument_name}.{interval} or trades.{kind}.{currency}.{interval}', but found %s", errMalformedData, strings.Join(channels, ".")) - } - tradeDatas := make([]trade.Data, len(changeData.Trades)) + td := make([]trade.Data, len(changeData.Trades)) for x := range changeData.Trades { var side order.Side side, err = order.StringToOrderSide(changeData.Trades[x].Direction) if err != nil { return err } - if currencyPair.IsEmpty() { - currencyPair, err = currency.NewPairFromString(changeData.Trades[x].InstrumentName) - if err != nil { - return err - } - } - if a == asset.Empty { - a, err = guessAssetTypeFromInstrument(currencyPair) - if err != nil { - return err - } + var cp currency.Pair + var a asset.Item + cp, a, err = d.getAssetPairByInstrument(changeData.Trades[x].InstrumentName) + if err != nil { + return err } - tradeDatas[x] = trade.Data{ - CurrencyPair: currencyPair, + + td[x] = trade.Data{ + CurrencyPair: cp, Exchange: d.Name, Timestamp: changeData.Trades[x].Timestamp.Time(), Price: changeData.Trades[x].Price, @@ -417,7 +379,7 @@ func (d *Deribit) processChanges(respRaw []byte, channels []string) error { AssetType: a, } } - err = trade.AddTradesToBuffer(d.Name, tradeDatas...) + err = trade.AddTradesToBuffer(d.Name, td...) if err != nil { return err } @@ -435,16 +397,9 @@ func (d *Deribit) processChanges(respRaw []byte, channels []string) error { if err != nil { return err } - if a != asset.Empty { - currencyPair, err = currency.NewPairFromString(changeData.Orders[x].InstrumentName) - if err != nil { - return err - } - } else { - a, err = guessAssetTypeFromInstrument(currencyPair) - if err != nil { - return err - } + cp, a, err := d.getAssetPairByInstrument(changeData.Orders[x].InstrumentName) + if err != nil { + return err } orders[x] = order.Detail{ Price: changeData.Orders[x].Price, @@ -459,7 +414,7 @@ func (d *Deribit) processChanges(respRaw []byte, channels []string) error { AssetType: a, Date: changeData.Orders[x].CreationTimestamp.Time(), LastUpdated: changeData.Orders[x].LastUpdateTimestamp.Time(), - Pair: currencyPair, + Pair: cp, } } d.Websocket.DataHandler <- orders @@ -468,7 +423,7 @@ func (d *Deribit) processChanges(respRaw []byte, channels []string) error { } func (d *Deribit) processQuoteTicker(respRaw []byte, channels []string) error { - cp, err := currency.NewPairFromString(channels[1]) + cp, a, err := d.getAssetPairByInstrument(channels[1]) if err != nil { return err } @@ -479,10 +434,6 @@ func (d *Deribit) processQuoteTicker(respRaw []byte, channels []string) error { if err != nil { return err } - a, err := guessAssetTypeFromInstrument(cp) - if err != nil { - return err - } d.Websocket.DataHandler <- &ticker.Price{ ExchangeName: d.Name, Pair: cp, @@ -497,55 +448,33 @@ func (d *Deribit) processQuoteTicker(respRaw []byte, channels []string) error { } func (d *Deribit) processTrades(respRaw []byte, channels []string) error { - var err error - var currencyPair currency.Pair - var a asset.Item - switch { - case (len(channels) == 3 && channels[0] == "trades") || (len(channels) == 4 && channels[0] == "user"): - currencyPair, err = currency.NewPairFromString(channels[len(channels)-2]) - if err != nil { - return err - } - case (len(channels) == 4 && channels[0] == "trades") || (len(channels) == 5 && channels[0] == "user"): - a, err = d.StringToAssetKind(channels[len(channels)-3]) - if err != nil { - return err - } - default: + if len(channels) < 3 || len(channels) > 5 { return fmt.Errorf("%w, expected format 'trades.{instrument_name}.{interval} or trades.{kind}.{currency}.{interval}', but found %s", errMalformedData, strings.Join(channels, ".")) } var response WsResponse - tradeList := []wsTrade{} + var tradeList []wsTrade response.Params.Data = &tradeList - err = json.Unmarshal(respRaw, &response) + err := json.Unmarshal(respRaw, &response) if err != nil { return err } if len(tradeList) == 0 { return fmt.Errorf("%v, empty list of trades found", common.ErrNoResponse) } - if a == asset.Empty && currencyPair.IsEmpty() { - currencyPair, err = currency.NewPairFromString(tradeList[0].InstrumentName) - if err != nil { - return err - } - a, err = guessAssetTypeFromInstrument(currencyPair) - if err != nil { - return err - } - } tradeDatas := make([]trade.Data, len(tradeList)) for x := range tradeDatas { - side, err := order.StringToOrderSide(tradeList[x].Direction) + var cp currency.Pair + var a asset.Item + cp, a, err = d.getAssetPairByInstrument(tradeList[x].InstrumentName) if err != nil { return err } - currencyPair, err = currency.NewPairFromString(tradeList[x].InstrumentName) + side, err := order.StringToOrderSide(tradeList[x].Direction) if err != nil { return err } tradeDatas[x] = trade.Data{ - CurrencyPair: currencyPair, + CurrencyPair: cp, Exchange: d.Name, Timestamp: tradeList[x].Timestamp.Time(), Price: tradeList[x].Price, @@ -562,7 +491,7 @@ func (d *Deribit) processIncrementalTicker(respRaw []byte, channels []string) er if len(channels) != 2 { return fmt.Errorf("%w, expected format 'incremental_ticker.{instrument_name}', but found %s", errMalformedData, strings.Join(channels, ".")) } - cp, err := currency.NewPairFromString(channels[1]) + cp, a, err := d.getAssetPairByInstrument(channels[1]) if err != nil { return err } @@ -573,14 +502,10 @@ func (d *Deribit) processIncrementalTicker(respRaw []byte, channels []string) er if err != nil { return err } - assetType, err := guessAssetTypeFromInstrument(cp) - if err != nil { - return err - } d.Websocket.DataHandler <- &ticker.Price{ ExchangeName: d.Name, Pair: cp, - AssetType: assetType, + AssetType: a, LastUpdated: incrementalTicker.Timestamp.Time(), BidSize: incrementalTicker.BestBidAmount, AskSize: incrementalTicker.BestAskAmount, @@ -602,11 +527,10 @@ func (d *Deribit) processInstrumentTicker(respRaw []byte, channels []string) err } func (d *Deribit) processTicker(respRaw []byte, channels []string) error { - cp, err := currency.NewPairFromString(channels[1]) + cp, a, err := d.getAssetPairByInstrument(channels[1]) if err != nil { return err } - var a asset.Item var response WsResponse tickerPriceResponse := &wsTicker{} response.Params.Data = tickerPriceResponse @@ -614,10 +538,6 @@ func (d *Deribit) processTicker(respRaw []byte, channels []string) error { if err != nil { return err } - a, err = guessAssetTypeFromInstrument(cp) - if err != nil { - return err - } tickerPrice := &ticker.Price{ ExchangeName: d.Name, Pair: cp, @@ -658,22 +578,17 @@ func (d *Deribit) 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, err := currency.NewPairFromString(channels[2]) + cp, a, err := d.getAssetPairByInstrument(channels[2]) if err != nil { return err } var response WsResponse - var a asset.Item candleData := &wsCandlestickData{} response.Params.Data = candleData err = json.Unmarshal(respRaw, &response) if err != nil { return err } - a, err = guessAssetTypeFromInstrument(cp) - if err != nil { - return err - } d.Websocket.DataHandler <- stream.KlineData{ Timestamp: time.UnixMilli(candleData.Tick), Pair: cp, @@ -696,9 +611,8 @@ func (d *Deribit) processOrderbook(respRaw []byte, channels []string) error { if err != nil { return err } - var assetType asset.Item if len(channels) == 3 { - cp, err := currency.NewPairFromString(orderbookData.InstrumentName) + cp, a, err := d.getAssetPairByInstrument(orderbookData.InstrumentName) if err != nil { return err } @@ -743,10 +657,6 @@ func (d *Deribit) processOrderbook(respRaw []byte, channels []string) error { if len(asks) == 0 && len(bids) == 0 { return nil } - assetType, err = guessAssetTypeFromInstrument(cp) - if err != nil { - return err - } if orderbookData.Type == "snapshot" { return d.Websocket.Orderbook.LoadSnapshot(&orderbook.Base{ Exchange: d.Name, @@ -755,7 +665,7 @@ func (d *Deribit) processOrderbook(respRaw []byte, channels []string) error { Pair: cp, Asks: asks, Bids: bids, - Asset: assetType, + Asset: a, LastUpdateID: orderbookData.ChangeID, }) } else if orderbookData.Type == "change" { @@ -763,17 +673,13 @@ func (d *Deribit) processOrderbook(respRaw []byte, channels []string) error { Asks: asks, Bids: bids, Pair: cp, - Asset: assetType, + Asset: a, UpdateID: orderbookData.ChangeID, UpdateTime: orderbookData.Timestamp.Time(), }) } } else if len(channels) == 5 { - cp, err := currency.NewPairFromString(orderbookData.InstrumentName) - if err != nil { - return err - } - assetType, err = guessAssetTypeFromInstrument(cp) + cp, a, err := d.getAssetPairByInstrument(orderbookData.InstrumentName) if err != nil { return err } @@ -824,7 +730,7 @@ func (d *Deribit) processOrderbook(respRaw []byte, channels []string) error { Asks: asks, Bids: bids, Pair: cp, - Asset: assetType, + Asset: a, Exchange: d.Name, LastUpdateID: orderbookData.ChangeID, LastUpdated: orderbookData.Timestamp.Time(), @@ -864,8 +770,8 @@ func (d *Deribit) GenerateDefaultSubscriptions() ([]subscription.Subscription, e case chartTradesChannel: for _, a := range assets { for z := range assetPairs[a] { - if ((assetPairs[a][z].Quote.Upper().String() == "PERPETUAL" || - !strings.Contains(assetPairs[a][z].Quote.Upper().String(), "PERPETUAL")) && + if ((assetPairs[a][z].Quote.Upper().String() == perpString || + !strings.Contains(assetPairs[a][z].Quote.Upper().String(), perpString)) && a == asset.Futures) || (a != asset.Spot && a != asset.Futures) { continue } @@ -885,8 +791,8 @@ func (d *Deribit) GenerateDefaultSubscriptions() ([]subscription.Subscription, e rawUserOrdersChannel: for _, a := range assets { for z := range assetPairs[a] { - if ((assetPairs[a][z].Quote.Upper().String() == "PERPETUAL" || - !strings.Contains(assetPairs[a][z].Quote.Upper().String(), "PERPETUAL")) && + if ((assetPairs[a][z].Quote.Upper().String() == perpString || + !strings.Contains(assetPairs[a][z].Quote.Upper().String(), perpString)) && a == asset.Futures) || (a != asset.Spot && a != asset.Futures) { continue } @@ -900,8 +806,8 @@ func (d *Deribit) GenerateDefaultSubscriptions() ([]subscription.Subscription, e case orderbookChannel: for _, a := range assets { for z := range assetPairs[a] { - if ((assetPairs[a][z].Quote.Upper().String() == "PERPETUAL" || - !strings.Contains(assetPairs[a][z].Quote.Upper().String(), "PERPETUAL")) && + if ((assetPairs[a][z].Quote.Upper().String() == perpString || + !strings.Contains(assetPairs[a][z].Quote.Upper().String(), perpString)) && a == asset.Futures) || (a != asset.Spot && a != asset.Futures) { continue } @@ -936,8 +842,8 @@ func (d *Deribit) GenerateDefaultSubscriptions() ([]subscription.Subscription, e tradesChannel: for _, a := range assets { for z := range assetPairs[a] { - if ((assetPairs[a][z].Quote.Upper().String() != "PERPETUAL" && - !strings.Contains(assetPairs[a][z].Quote.Upper().String(), "PERPETUAL")) && + if ((assetPairs[a][z].Quote.Upper().String() != perpString && + !strings.Contains(assetPairs[a][z].Quote.Upper().String(), perpString)) && a == asset.Futures) || (a != asset.Spot && a != asset.Futures) { continue } @@ -955,7 +861,7 @@ func (d *Deribit) GenerateDefaultSubscriptions() ([]subscription.Subscription, e userTradesChannelByInstrument: for _, a := range assets { for z := range assetPairs[a] { - if subscriptionChannels[x] == perpetualChannel && !strings.Contains(assetPairs[a][z].Quote.Upper().String(), "PERPETUAL") { + if subscriptionChannels[x] == perpetualChannel && !strings.Contains(assetPairs[a][z].Quote.Upper().String(), perpString) { continue } subscriptions = append(subscriptions, diff --git a/exchanges/deribit/deribit_wrapper.go b/exchanges/deribit/deribit_wrapper.go index 3b3bef964d1..baea885aaa5 100644 --- a/exchanges/deribit/deribit_wrapper.go +++ b/exchanges/deribit/deribit_wrapper.go @@ -4,6 +4,7 @@ import ( "context" "errors" "fmt" + "regexp" "sort" "strconv" "strings" @@ -62,16 +63,14 @@ func (d *Deribit) SetDefaults() { d.API.CredentialsValidator.RequiresKey = true d.API.CredentialsValidator.RequiresSecret = true - requestFmt := ¤cy.PairFormat{Uppercase: true, Delimiter: currency.DashDelimiter} - configFmt := ¤cy.PairFormat{Uppercase: true, Delimiter: currency.DashDelimiter} - err := d.StoreAssetPairFormat(asset.Spot, currency.PairStore{ - RequestFormat: ¤cy.PairFormat{Uppercase: true, Delimiter: currency.UnderscoreDelimiter}, - ConfigFormat: ¤cy.PairFormat{Uppercase: true, Delimiter: currency.UnderscoreDelimiter}}) + dashFormat := ¤cy.PairFormat{Uppercase: true, Delimiter: currency.DashDelimiter} + underscoreFormat := ¤cy.PairFormat{Uppercase: true, Delimiter: currency.UnderscoreDelimiter} + err := d.StoreAssetPairFormat(asset.Spot, currency.PairStore{RequestFormat: underscoreFormat, ConfigFormat: underscoreFormat}) if err != nil { log.Errorln(log.ExchangeSys, err) } for _, assetType := range []asset.Item{asset.Futures, asset.Options, asset.OptionCombo, asset.FutureCombo} { - if err = d.StoreAssetPairFormat(assetType, currency.PairStore{RequestFormat: requestFmt, ConfigFormat: configFmt}); err != nil { + if err = d.StoreAssetPairFormat(assetType, currency.PairStore{RequestFormat: dashFormat, ConfigFormat: dashFormat}); err != nil { log.Errorln(log.ExchangeSys, err) } } @@ -208,6 +207,10 @@ func (d *Deribit) Setup(exch *config.Exchange) error { if err != nil { return err } + + // setup option decimal regex at startup to make constant checks more efficient + optionRegex = regexp.MustCompile(optionDecimalRegex) + return d.Websocket.SetupNewConnection(stream.ConnectionSetup{ URL: d.Websocket.GetWebsocketURL(), ResponseCheckTimeout: exch.WebsocketResponseCheckTimeout, @@ -238,16 +241,9 @@ func (d *Deribit) FetchTradablePairs(ctx context.Context, assetType asset.Item) continue } var cp currency.Pair - if assetType == asset.Options { - cp, err = currency.NewPairDelimiter(instrumentsData[y].InstrumentName, currency.DashDelimiter) - if err != nil { - return nil, err - } - } else { - cp, err = currency.NewPairFromString(instrumentsData[y].InstrumentName) - if err != nil { - return nil, err - } + cp, err = currency.NewPairFromString(instrumentsData[y].InstrumentName) + if err != nil { + return nil, err } resp = resp.Add(cp) } @@ -259,17 +255,19 @@ func (d *Deribit) FetchTradablePairs(ctx context.Context, assetType asset.Item) // them in the exchanges config func (d *Deribit) UpdateTradablePairs(ctx context.Context, forceUpdate bool) error { assets := d.GetAssetTypes(false) + errs := common.CollectErrors(len(assets)) for x := range assets { - pairs, err := d.FetchTradablePairs(ctx, assets[x]) - if err != nil { - return err - } - err = d.UpdatePairs(pairs, assets[x], false, forceUpdate) - if err != nil { - return err - } + go func(x int) { + defer errs.Wg.Done() + pairs, err := d.FetchTradablePairs(ctx, assets[x]) + if err != nil { + errs.C <- err + return + } + errs.C <- d.UpdatePairs(pairs, assets[x], false, forceUpdate) + }(x) } - return nil + return errs.Collect() } // UpdateTickers updates the ticker for all currency pairs of a given asset type @@ -1091,7 +1089,7 @@ func (d *Deribit) GetHistoricCandles(ctx context.Context, pair currency.Pair, a if err != nil { return nil, err } - intervalString, err := d.GetResolutionFromInterval(interval) + intervalString, err := d.GetResolutionFromInterval(req.ExchangeInterval) if err != nil { return nil, err } @@ -1149,7 +1147,7 @@ func (d *Deribit) GetHistoricCandlesExtended(ctx context.Context, pair currency. switch a { case asset.Futures, asset.Spot: for x := range req.RangeHolder.Ranges { - intervalString, err := d.GetResolutionFromInterval(interval) + intervalString, err := d.GetResolutionFromInterval(req.ExchangeInterval) if err != nil { return nil, err } @@ -1266,59 +1264,6 @@ func (d *Deribit) GetFuturesContractDetails(ctx context.Context, item asset.Item return resp, nil } -// GetLatestFundingRates returns the latest funding rates data -func (d *Deribit) GetLatestFundingRates(ctx context.Context, r *fundingrate.LatestRateRequest) ([]fundingrate.LatestRateResponse, error) { - if r == nil { - return nil, fmt.Errorf("%w LatestRateRequest", common.ErrNilPointer) - } - if !d.SupportsAsset(r.Asset) { - return nil, fmt.Errorf("%s %w", r.Asset, asset.ErrNotSupported) - } - isPerpetual, err := d.IsPerpetualFutureCurrency(r.Asset, r.Pair) - if !isPerpetual || err != nil { - return nil, futures.ErrNotPerpetualFuture - } - available, err := d.GetAvailablePairs(r.Asset) - if err != nil { - return nil, err - } - if !available.Contains(r.Pair, true) && r.Pair.Quote.String() != "PERPETUAL" && !strings.HasSuffix(r.Pair.String(), "PERP") { - return nil, fmt.Errorf("%w pair: %v", futures.ErrNotPerpetualFuture, r.Pair) - } - r.Pair, err = d.FormatExchangeCurrency(r.Pair, r.Asset) - if err != nil { - return nil, err - } - var fri []FundingRateHistory - fri, err = d.GetFundingRateHistory(ctx, r.Pair.String(), time.Now().Add(-time.Hour*16), time.Now()) - if err != nil { - return nil, err - } - - resp := make([]fundingrate.LatestRateResponse, 1) - latestTime := fri[0].Timestamp.Time() - for i := range fri { - if fri[i].Timestamp.Time().Before(latestTime) { - continue - } - resp[0] = fundingrate.LatestRateResponse{ - TimeChecked: time.Now(), - Exchange: d.Name, - Asset: r.Asset, - Pair: r.Pair, - LatestRate: fundingrate.Rate{ - Time: fri[i].Timestamp.Time(), - Rate: decimal.NewFromFloat(fri[i].Interest8H), - }, - } - latestTime = fri[i].Timestamp.Time() - } - if len(resp) == 0 { - return nil, fmt.Errorf("%w %v %v", futures.ErrNotPerpetualFuture, r.Asset, r.Pair) - } - return resp, nil -} - // UpdateOrderExecutionLimits sets exchange execution order limits for an asset type func (d *Deribit) UpdateOrderExecutionLimits(ctx context.Context, a asset.Item) error { if !d.SupportsAsset(a) { @@ -1449,26 +1394,23 @@ func (d *Deribit) GetOpenInterest(ctx context.Context, k ...key.PairAsset) ([]fu } } result := make([]futures.OpenInterest, 0, len(k)) - var err error - var pair currency.Pair for i := range k { - pair, err = d.FormatExchangeCurrency(k[i].Pair(), k[i].Asset) + pFmt, err := d.CurrencyPairs.GetFormat(k[i].Asset, true) if err != nil { return nil, err } + cp := k[i].Pair().Format(pFmt) + p := d.formatPairString(k[i].Asset, cp) var oi []BookSummaryData if d.Websocket.IsConnected() { - oi, err = d.WSRetrieveBookBySummary(pair.Base, d.GetAssetKind(k[i].Asset)) + oi, err = d.WSRetrieveBookSummaryByInstrument(p) } else { - oi, err = d.GetBookSummaryByCurrency(ctx, pair.Base, d.GetAssetKind(k[i].Asset)) + oi, err = d.GetBookSummaryByInstrument(ctx, p) } if err != nil { return nil, err } for a := range oi { - if oi[a].InstrumentName != pair.String() { - continue - } result = append(result, futures.OpenInterest{ Key: key.ExchangePairAsset{ Exchange: d.Name, @@ -1487,28 +1429,105 @@ func (d *Deribit) GetOpenInterest(ctx context.Context, k ...key.PairAsset) ([]fu return result, nil } +// GetCurrencyTradeURL returns the URL to the exchange's trade page for the given asset and currency pair +func (d *Deribit) GetCurrencyTradeURL(_ context.Context, a asset.Item, cp currency.Pair) (string, error) { + if cp.IsEmpty() { + return "", currency.ErrCurrencyPairEmpty + } + switch a { + case asset.Futures: + isPerp, err := d.IsPerpetualFutureCurrency(a, cp) + if err != nil { + return "", err + } + if isPerp { + return tradeBaseURL + tradeFutures + cp.Base.Upper().String() + currency.UnderscoreDelimiter + cp.Quote.Upper().String(), nil + } + return tradeBaseURL + tradeFutures + cp.Upper().String(), nil + case asset.Spot: + cp.Delimiter = currency.UnderscoreDelimiter + return tradeBaseURL + tradeSpot + cp.Upper().String(), nil + case asset.Options: + baseString := cp.Base.Upper().String() + quoteString := cp.Quote.Upper().String() + quoteSplit := strings.Split(quoteString, currency.DashDelimiter) + if len(quoteSplit) > 1 && + (quoteSplit[len(quoteSplit)-1] == "C" || quoteSplit[len(quoteSplit)-1] == "P") { + return tradeBaseURL + tradeOptions + baseString + "/" + baseString + currency.DashDelimiter + quoteSplit[0], nil + } + return tradeBaseURL + tradeOptions + baseString, nil + case asset.FutureCombo: + return tradeBaseURL + tradeFuturesCombo + cp.Upper().String(), nil + case asset.OptionCombo: + return tradeBaseURL + tradeOptionsCombo + cp.Base.Upper().String(), nil + default: + return "", fmt.Errorf("%w %v", asset.ErrNotSupported, a) + } +} + // IsPerpetualFutureCurrency ensures a given asset and currency is a perpetual future // differs by exchange func (d *Deribit) IsPerpetualFutureCurrency(assetType asset.Item, pair currency.Pair) (bool, error) { - if !assetType.IsFutures() { - return false, futures.ErrNotPerpetualFuture - } else if strings.EqualFold(pair.Quote.String(), "PERPETUAL") || strings.HasSuffix(pair.String(), "PERP") { - return true, nil + if pair.IsEmpty() { + return false, currency.ErrCurrencyPairEmpty + } + if assetType != asset.Futures { + // deribit considers future combo, even if ending in "PERP" to not be a perpetual + return false, nil } - pair, err := d.FormatExchangeCurrency(pair, assetType) + pqs := strings.Split(pair.Quote.Upper().String(), currency.DashDelimiter) + return pqs[len(pqs)-1] == perpString, nil +} + +// GetLatestFundingRates returns the latest funding rates data +func (d *Deribit) GetLatestFundingRates(ctx context.Context, r *fundingrate.LatestRateRequest) ([]fundingrate.LatestRateResponse, error) { + if r == nil { + return nil, fmt.Errorf("%w LatestRateRequest", common.ErrNilPointer) + } + if !d.SupportsAsset(r.Asset) { + return nil, fmt.Errorf("%s %w", r.Asset, asset.ErrNotSupported) + } + isPerpetual, err := d.IsPerpetualFutureCurrency(r.Asset, r.Pair) if err != nil { - return false, err + return nil, err } - var instrumentInfo *InstrumentData - if d.Websocket.IsConnected() { - instrumentInfo, err = d.WSRetrieveInstrumentData(pair.String()) - } else { - instrumentInfo, err = d.GetInstrument(context.Background(), pair.String()) + if !isPerpetual { + return nil, fmt.Errorf("%w '%s'", futures.ErrNotPerpetualFuture, r.Pair) + } + pFmt, err := d.CurrencyPairs.GetFormat(r.Asset, true) + if err != nil { + return nil, err } + cp := r.Pair.Format(pFmt) + p := d.formatPairString(r.Asset, cp) + var fri []FundingRateHistory + fri, err = d.GetFundingRateHistory(ctx, p, time.Now().Add(-time.Hour*16), time.Now()) if err != nil { - return false, err + return nil, err + } + + resp := make([]fundingrate.LatestRateResponse, 1) + latestTime := fri[0].Timestamp.Time() + for i := range fri { + if fri[i].Timestamp.Time().Before(latestTime) { + continue + } + resp[0] = fundingrate.LatestRateResponse{ + TimeChecked: time.Now(), + Exchange: d.Name, + Asset: r.Asset, + Pair: r.Pair, + LatestRate: fundingrate.Rate{ + Time: fri[i].Timestamp.Time(), + Rate: decimal.NewFromFloat(fri[i].Interest8H), + }, + } + latestTime = fri[i].Timestamp.Time() } - return strings.EqualFold(instrumentInfo.SettlementPeriod, "perpetual"), nil + if len(resp) == 0 { + return nil, fmt.Errorf("%w %v %v", futures.ErrNotPerpetualFuture, r.Asset, r.Pair) + } + return resp, nil } // GetHistoricalFundingRates returns historical funding rates for a future @@ -1532,10 +1551,12 @@ func (d *Deribit) GetHistoricalFundingRates(ctx context.Context, r *fundingrate. if r.IncludePayments { return nil, fmt.Errorf("include payments %w", common.ErrNotYetImplemented) } - fPair, err := d.FormatExchangeCurrency(r.Pair, r.Asset) + pFmt, err := d.CurrencyPairs.GetFormat(r.Asset, true) if err != nil { return nil, err } + cp := r.Pair.Format(pFmt) + p := d.formatPairString(r.Asset, cp) ed := r.EndDate var fundingRates []fundingrate.Rate @@ -1546,9 +1567,9 @@ func (d *Deribit) GetHistoricalFundingRates(ctx context.Context, r *fundingrate. } var records []FundingRateHistory if d.Websocket.IsConnected() { - records, err = d.WSRetrieveFundingRateHistory(fPair.String(), r.StartDate, ed) + records, err = d.WSRetrieveFundingRateHistory(p, r.StartDate, ed) } else { - records, err = d.GetFundingRateHistory(ctx, fPair.String(), r.StartDate, ed) + records, err = d.GetFundingRateHistory(ctx, p, r.StartDate, ed) } if err != nil { return nil, err diff --git a/exchanges/kline/kline.go b/exchanges/kline/kline.go index 2360b9bb5f9..5a8ea21ebb1 100644 --- a/exchanges/kline/kline.go +++ b/exchanges/kline/kline.go @@ -198,8 +198,9 @@ func (k *Item) addPadding(start, exclusiveEnd time.Time, purgeOnPartial bool) er padded[x].Time = start case !k.Candles[target].Time.Equal(start): if k.Candles[target].Time.Before(start) { - return fmt.Errorf("%w when it should be %s truncated at a %s interval", + return fmt.Errorf("%w '%s' should be '%s' at '%s' interval", errCandleOpenTimeIsNotUTCAligned, + k.Candles[target].Time, start.Add(k.Interval.Duration()), k.Interval) } diff --git a/exchanges/orderbook/orderbook.go b/exchanges/orderbook/orderbook.go index bd6afbceb67..60514d6cb53 100644 --- a/exchanges/orderbook/orderbook.go +++ b/exchanges/orderbook/orderbook.go @@ -216,7 +216,7 @@ func (b *Base) Verify() error { // level books. In the event that there is a massive liquidity change where // a book dries up, this will still update so we do not traverse potential // incorrect old data. - if len(b.Asks) == 0 || len(b.Bids) == 0 { + if (len(b.Asks) == 0 || len(b.Bids) == 0) && !b.Asset.IsOptions() { log.Warnf(log.OrderBook, bookLengthIssue, b.Exchange,