Deribit: bug fixes, test fixes and implement GetCurrencyTradeURL (#1558)

* initial

* fixes WS instrument parsing, adds new funcs

* lint, err restore, funding rate fixes

* tightens function

* fix breaking test, reimplement option string

* fixes klines

* enhance regex

* WHOOPS

* verbose whoops

* exchange interval for basic too

* Adds err processing, err channel, fix the others

* utilises concurrent error grabber

* minor shrinkage, its cold
This commit is contained in:
Scott
2024-06-07 15:56:17 +10:00
committed by GitHub
parent 1199f38546
commit e16e16b4a1
6 changed files with 462 additions and 395 deletions

View File

@@ -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/"
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)
}

View File

@@ -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))
require.NoError(t, err)
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())
}
_, 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)
}

View File

@@ -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,33 +294,23 @@ 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 {
cp, a, err := d.getAssetPairByInstrument(orderData[x].InstrumentName)
if err != nil {
return err
}
oType, err := order.StringToOrderType(orderData[x].OrderType)
if err != nil {
return err
@@ -333,16 +323,6 @@ func (d *Deribit) processOrders(respRaw []byte, channels []string) error {
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)
if err != nil {
return err
}
orderDetails[x] = order.Detail{
Price: orderData[x].Price,
Amount: orderData[x].Amount,
@@ -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
}
var cp currency.Pair
var a asset.Item
cp, a, err = d.getAssetPairByInstrument(changeData.Trades[x].InstrumentName)
if err != nil {
return err
}
if a == asset.Empty {
a, err = guessAssetTypeFromInstrument(currencyPair)
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 {
var cp currency.Pair
var a asset.Item
cp, a, err = d.getAssetPairByInstrument(tradeList[x].InstrumentName)
if err != nil {
return err
}
side, err := order.StringToOrderSide(tradeList[x].Direction)
if err != nil {
return err
}
currencyPair, err = currency.NewPairFromString(tradeList[x].InstrumentName)
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.List, error) {
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.List, error) {
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.List, error) {
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.List, error) {
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.List, error) {
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,

View File

@@ -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 := &currency.PairFormat{Uppercase: true, Delimiter: currency.DashDelimiter}
configFmt := &currency.PairFormat{Uppercase: true, Delimiter: currency.DashDelimiter}
err := d.StoreAssetPairFormat(asset.Spot, currency.PairStore{
RequestFormat: &currency.PairFormat{Uppercase: true, Delimiter: currency.UnderscoreDelimiter},
ConfigFormat: &currency.PairFormat{Uppercase: true, Delimiter: currency.UnderscoreDelimiter}})
dashFormat := &currency.PairFormat{Uppercase: true, Delimiter: currency.DashDelimiter}
underscoreFormat := &currency.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
}
pair, err := d.FormatExchangeCurrency(pair, assetType)
if assetType != asset.Futures {
// deribit considers future combo, even if ending in "PERP" to not be a perpetual
return false, nil
}
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 false, err
return nil, err
}
return strings.EqualFold(instrumentInfo.SettlementPeriod, "perpetual"), nil
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 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
}
// 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

View File

@@ -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)
}

View File

@@ -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,