Files
gocryptotrader/exchanges/deribit/deribit_wrapper.go
Gareth Kirwan 4fcee8489e Deribit: Add subscription configuration (#1636)
* Kline: Fix Raw Short, Marshal and Unmarshal

* Deribit: Rename GenerateDefaultSubs

* Deribit: Remove custom GetDefaultConfig

Moved to exchange base by #1472

* Deribit: Straight Rename of eps to endpoints

Since I had to ask what this abbreviation meant, I think we should
abandon it

* Deribit: Add Subscription configuration

* Deribit: Fix race on Setup with optionsRegex

Calling Setup twice would race on the assignment to this package var.

There was an option to just move the assignment to the package var declaration, but this
change improves the performance and allocations:
```
BenchmarkOptionPairToString-8            1000000              1239 ns/op             485 B/op         10 allocs/op
BenchmarkOptionPairToString2-8           3473804               656.2 ns/op           348 B/op          7 allocs/op
```

I've also removed the t.Run because even success the -v output from
tests would be very noisy, and I don't think we were getting any benefit
from it at all:
```
=== RUN   TestOptionPairToString
=== PAUSE TestOptionPairToString
=== CONT  TestOptionPairToString
=== RUN   TestOptionPairToString/BTC-30MAY24-61000-C
=== PAUSE TestOptionPairToString/BTC-30MAY24-61000-C
=== RUN   TestOptionPairToString/ETH-1JUN24-3200-P
=== PAUSE TestOptionPairToString/ETH-1JUN24-3200-P
=== RUN   TestOptionPairToString/SOL_USDC-31MAY24-162-P
=== PAUSE TestOptionPairToString/SOL_USDC-31MAY24-162-P
=== RUN   TestOptionPairToString/MATIC_USDC-6APR24-0d98-P
=== PAUSE TestOptionPairToString/MATIC_USDC-6APR24-0d98-P
=== CONT  TestOptionPairToString/BTC-30MAY24-61000-C
=== CONT  TestOptionPairToString/SOL_USDC-31MAY24-162-P
=== CONT  TestOptionPairToString/ETH-1JUN24-3200-P
=== CONT  TestOptionPairToString/MATIC_USDC-6APR24-0d98-P
--- PASS: TestOptionPairToString (0.00s)
    --- PASS: TestOptionPairToString/BTC-30MAY24-61000-C (0.00s)
    --- PASS: TestOptionPairToString/ETH-1JUN24-3200-P (0.00s)
    --- PASS: TestOptionPairToString/SOL_USDC-31MAY24-162-P (0.00s)
    --- PASS: TestOptionPairToString/MATIC_USDC-6APR24-0d98-P (0.00s)
```

( And that got worse with me adding more tests )
2025-01-03 08:55:58 +11:00

1599 lines
53 KiB
Go

package deribit
import (
"context"
"errors"
"fmt"
"sort"
"strconv"
"strings"
"time"
"github.com/shopspring/decimal"
"github.com/thrasher-corp/gocryptotrader/common"
"github.com/thrasher-corp/gocryptotrader/common/key"
"github.com/thrasher-corp/gocryptotrader/config"
"github.com/thrasher-corp/gocryptotrader/currency"
exchange "github.com/thrasher-corp/gocryptotrader/exchanges"
"github.com/thrasher-corp/gocryptotrader/exchanges/account"
"github.com/thrasher-corp/gocryptotrader/exchanges/asset"
"github.com/thrasher-corp/gocryptotrader/exchanges/deposit"
"github.com/thrasher-corp/gocryptotrader/exchanges/fundingrate"
"github.com/thrasher-corp/gocryptotrader/exchanges/futures"
"github.com/thrasher-corp/gocryptotrader/exchanges/kline"
"github.com/thrasher-corp/gocryptotrader/exchanges/order"
"github.com/thrasher-corp/gocryptotrader/exchanges/orderbook"
"github.com/thrasher-corp/gocryptotrader/exchanges/protocol"
"github.com/thrasher-corp/gocryptotrader/exchanges/request"
"github.com/thrasher-corp/gocryptotrader/exchanges/stream"
"github.com/thrasher-corp/gocryptotrader/exchanges/stream/buffer"
"github.com/thrasher-corp/gocryptotrader/exchanges/ticker"
"github.com/thrasher-corp/gocryptotrader/exchanges/trade"
"github.com/thrasher-corp/gocryptotrader/log"
"github.com/thrasher-corp/gocryptotrader/portfolio/withdraw"
)
// SetDefaults sets the basic defaults for Deribit
func (d *Deribit) SetDefaults() {
d.Name = "Deribit"
d.Enabled = true
d.Verbose = true
d.API.CredentialsValidator.RequiresKey = true
d.API.CredentialsValidator.RequiresSecret = true
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: dashFormat, ConfigFormat: dashFormat}); err != nil {
log.Errorln(log.ExchangeSys, err)
}
}
// Fill out the capabilities/features that the exchange supports
d.Features = exchange.Features{
Supports: exchange.FeaturesSupported{
REST: true,
Websocket: true,
RESTCapabilities: protocol.Features{
TickerFetching: true,
KlineFetching: true,
TradeFetching: true,
OrderbookFetching: true,
AutoPairUpdates: true,
AccountInfo: true,
GetOrder: true,
GetOrders: true,
CancelOrders: true,
CancelOrder: true,
SubmitOrder: true,
UserTradeHistory: true,
CryptoDeposit: true,
CryptoWithdrawal: true,
TradeFee: true,
CryptoWithdrawalFee: true,
MultiChainDeposits: true,
MultiChainWithdrawals: true,
},
WebsocketCapabilities: protocol.Features{
TickerFetching: true,
OrderbookFetching: true,
},
WithdrawPermissions: exchange.AutoWithdrawCrypto |
exchange.AutoWithdrawFiat,
Kline: kline.ExchangeCapabilitiesSupported{
Intervals: true,
},
FuturesCapabilities: exchange.FuturesCapabilities{
Positions: true,
Leverage: true,
FundingRates: true,
SupportedFundingRateFrequencies: map[kline.Interval]bool{
kline.OneHour: true,
kline.EightHour: true,
},
OpenInterest: exchange.OpenInterestSupport{
Supported: true,
},
},
},
Enabled: exchange.FeaturesEnabled{
AutoPairUpdates: true,
Kline: kline.ExchangeCapabilitiesEnabled{
Intervals: kline.DeployExchangeIntervals(
kline.IntervalCapacity{Interval: kline.HundredMilliseconds},
kline.IntervalCapacity{Interval: kline.OneMin},
kline.IntervalCapacity{Interval: kline.ThreeMin},
kline.IntervalCapacity{Interval: kline.FiveMin},
kline.IntervalCapacity{Interval: kline.TenMin},
kline.IntervalCapacity{Interval: kline.FifteenMin},
kline.IntervalCapacity{Interval: kline.ThirtyMin},
kline.IntervalCapacity{Interval: kline.OneHour},
kline.IntervalCapacity{Interval: kline.TwoHour},
// NOTE: The supported time intervals below are returned
// offset to +8 hours. This may lead to
// issues with candle quality and conversion as the
// intervals may be broken up. The below intervals
// are therefore constructed from the intervals above.
// kline.IntervalCapacity{Interval: kline.ThreeHour},
// kline.IntervalCapacity{Interval: kline.SixHour},
// kline.IntervalCapacity{Interval: kline.TwelveHour},
// kline.IntervalCapacity{Interval: kline.OneDay},
),
GlobalResultLimit: 500,
},
},
Subscriptions: defaultSubscriptions.Clone(),
}
d.Requester, err = request.New(d.Name,
common.NewHTTPClientWithTimeout(exchange.DefaultHTTPTimeout),
request.WithLimiter(GetRateLimits()),
)
if err != nil {
log.Errorln(log.ExchangeSys, err)
}
for _, assetType := range []asset.Item{asset.Options, asset.OptionCombo, asset.FutureCombo} {
if err = d.DisableAssetWebsocketSupport(assetType); err != nil {
log.Errorln(log.ExchangeSys, err)
}
}
d.API.Endpoints = d.NewEndpoints()
err = d.API.Endpoints.SetDefaultEndpoints(map[exchange.URL]string{
exchange.RestFutures: "https://www.deribit.com",
exchange.RestSpot: "https://www.deribit.com",
exchange.RestSpotSupplementary: "https://test.deribit.com",
})
if err != nil {
log.Errorln(log.ExchangeSys, err)
}
d.Websocket = stream.NewWebsocket()
d.WebsocketResponseMaxLimit = exchange.DefaultWebsocketResponseMaxLimit
d.WebsocketResponseCheckTimeout = exchange.DefaultWebsocketResponseCheckTimeout
d.WebsocketOrderbookBufferLimit = exchange.DefaultWebsocketOrderbookBufferLimit
}
// Setup takes in the supplied exchange configuration details and sets params
func (d *Deribit) Setup(exch *config.Exchange) error {
err := exch.Validate()
if err != nil {
return err
}
if !exch.Enabled {
d.SetEnabled(false)
return nil
}
err = d.SetupDefaults(exch)
if err != nil {
return err
}
err = d.Websocket.Setup(&stream.WebsocketSetup{
ExchangeConfig: exch,
DefaultURL: deribitWebsocketAddress,
RunningURL: deribitWebsocketAddress,
Connector: d.WsConnect,
Subscriber: d.Subscribe,
Unsubscriber: d.Unsubscribe,
GenerateSubscriptions: d.generateSubscriptions,
Features: &d.Features.Supports.WebsocketCapabilities,
OrderbookBufferConfig: buffer.Config{
SortBuffer: true,
SortBufferByUpdateIDs: true,
},
})
if err != nil {
return err
}
return d.Websocket.SetupNewConnection(&stream.ConnectionSetup{
URL: d.Websocket.GetWebsocketURL(),
ResponseCheckTimeout: exch.WebsocketResponseCheckTimeout,
ResponseMaxLimit: exch.WebsocketResponseMaxLimit,
})
}
// FetchTradablePairs returns a list of the exchanges tradable pairs
func (d *Deribit) FetchTradablePairs(ctx context.Context, assetType asset.Item) (currency.Pairs, error) {
if !d.SupportsAsset(assetType) {
return nil, fmt.Errorf("%s: %w - %v", d.Name, asset.ErrNotSupported, assetType)
}
var resp currency.Pairs
for _, x := range baseCurrencies {
var instrumentsData []InstrumentData
var err error
if d.Websocket.IsConnected() {
instrumentsData, err = d.WSRetrieveInstrumentsData(currency.NewCode(x), d.GetAssetKind(assetType), false)
} else {
instrumentsData, err = d.GetInstruments(ctx, currency.NewCode(x), d.GetAssetKind(assetType), false)
}
if err != nil {
return nil, err
}
for y := range instrumentsData {
if !instrumentsData[y].IsActive {
continue
}
var cp currency.Pair
cp, err = currency.NewPairFromString(instrumentsData[y].InstrumentName)
if err != nil {
return nil, err
}
resp = resp.Add(cp)
}
}
return resp, nil
}
// UpdateTradablePairs updates the exchanges available pairs and stores
// 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 {
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 errs.Collect()
}
// UpdateTickers updates the ticker for all currency pairs of a given asset type
func (d *Deribit) UpdateTickers(_ context.Context, _ asset.Item) error {
return common.ErrFunctionNotSupported
}
// UpdateTicker updates and returns the ticker for a currency pair
func (d *Deribit) UpdateTicker(ctx context.Context, p currency.Pair, assetType asset.Item) (*ticker.Price, error) {
if !d.SupportsAsset(assetType) {
return nil, fmt.Errorf("%s: %w - %s", d.Name, asset.ErrNotSupported, assetType)
}
p, err := d.FormatExchangeCurrency(p, assetType)
if err != nil {
return nil, err
}
instrumentID := d.formatPairString(assetType, p)
var tickerData *TickerData
if d.Websocket.IsConnected() {
tickerData, err = d.WSRetrievePublicTicker(instrumentID)
} else {
tickerData, err = d.GetPublicTicker(ctx, instrumentID)
}
if err != nil {
return nil, err
}
resp := ticker.Price{
ExchangeName: d.Name,
Pair: p,
AssetType: assetType,
Ask: tickerData.BestAskPrice,
AskSize: tickerData.BestAskAmount,
Bid: tickerData.BestBidPrice,
BidSize: tickerData.BestBidAmount,
High: tickerData.Stats.High,
Low: tickerData.Stats.Low,
Last: tickerData.LastPrice,
Volume: tickerData.Stats.Volume,
Close: tickerData.LastPrice,
IndexPrice: tickerData.IndexPrice,
MarkPrice: tickerData.MarkPrice,
QuoteVolume: tickerData.Stats.VolumeUSD,
}
err = ticker.ProcessTicker(&resp)
if err != nil {
return nil, err
}
return ticker.GetTicker(d.Name, p, assetType)
}
// FetchTicker returns the ticker for a currency pair
func (d *Deribit) FetchTicker(ctx context.Context, p currency.Pair, assetType asset.Item) (*ticker.Price, error) {
tickerNew, err := ticker.GetTicker(d.Name, p, assetType)
if err != nil {
return d.UpdateTicker(ctx, p, assetType)
}
return tickerNew, nil
}
// FetchOrderbook returns orderbook base on the currency pair
func (d *Deribit) FetchOrderbook(ctx context.Context, currencyPair currency.Pair, assetType asset.Item) (*orderbook.Base, error) {
ob, err := orderbook.Get(d.Name, currencyPair, assetType)
if err != nil {
return d.UpdateOrderbook(ctx, currencyPair, assetType)
}
return ob, nil
}
// UpdateOrderbook updates and returns the orderbook for a currency pair
func (d *Deribit) UpdateOrderbook(ctx context.Context, p currency.Pair, assetType asset.Item) (*orderbook.Base, error) {
p, err := d.FormatExchangeCurrency(p, assetType)
if err != nil {
return nil, err
}
instrumentID := d.formatPairString(assetType, p)
var obData *Orderbook
if d.Websocket.IsConnected() {
obData, err = d.WSRetrieveOrderbookData(instrumentID, 50)
} else {
obData, err = d.GetOrderbook(ctx, instrumentID, 50)
}
if err != nil {
return nil, err
}
book := &orderbook.Base{
Exchange: d.Name,
Pair: p,
Asset: assetType,
VerifyOrderbook: d.CanVerifyOrderbook,
}
book.Asks = make(orderbook.Tranches, 0, len(obData.Asks))
for x := range obData.Asks {
if obData.Asks[x][0] == 0 || obData.Asks[x][1] == 0 {
continue
}
book.Asks = append(book.Asks, orderbook.Tranche{
Price: obData.Asks[x][0],
Amount: obData.Asks[x][1],
})
}
book.Bids = make(orderbook.Tranches, 0, len(obData.Bids))
for x := range obData.Bids {
if obData.Bids[x][0] == 0 || obData.Bids[x][1] == 0 {
continue
}
book.Bids = append(book.Bids, orderbook.Tranche{
Price: obData.Bids[x][0],
Amount: obData.Bids[x][1],
})
}
err = book.Process()
if err != nil {
return book, err
}
return orderbook.Get(d.Name, p, assetType)
}
// UpdateAccountInfo retrieves balances for all enabled currencies
func (d *Deribit) UpdateAccountInfo(ctx context.Context, _ asset.Item) (account.Holdings, error) {
var resp account.Holdings
resp.Exchange = d.Name
currencies, err := d.GetCurrencies(ctx)
if err != nil {
return resp, err
}
resp.Accounts = make([]account.SubAccount, len(currencies))
for x := range currencies {
var data *AccountSummaryData
if d.Websocket.IsConnected() && d.Websocket.CanUseAuthenticatedWebsocketForWrapper() {
data, err = d.WSRetrieveAccountSummary(currency.NewCode(currencies[x].Currency), false)
} else {
data, err = d.GetAccountSummary(ctx, currency.NewCode(currencies[x].Currency), false)
}
if err != nil {
return resp, err
}
var subAcc account.SubAccount
subAcc.Currencies = append(subAcc.Currencies, account.Balance{
Currency: currency.NewCode(currencies[x].Currency),
Total: data.Balance,
Hold: data.Balance - data.AvailableFunds,
})
resp.Accounts[x] = subAcc
}
return resp, nil
}
// FetchAccountInfo retrieves balances for all enabled currencies
func (d *Deribit) FetchAccountInfo(ctx context.Context, assetType asset.Item) (account.Holdings, error) {
creds, err := d.GetCredentials(ctx)
if err != nil {
return account.Holdings{}, err
}
accountData, err := account.GetHoldings(d.Name, creds, assetType)
if err != nil {
return d.UpdateAccountInfo(ctx, assetType)
}
return accountData, nil
}
// GetAccountFundingHistory returns funding history, deposits and withdrawals
func (d *Deribit) GetAccountFundingHistory(ctx context.Context) ([]exchange.FundingHistory, error) {
var currencies []CurrencyData
var err error
if d.Websocket.IsConnected() {
currencies, err = d.WSRetrieveCurrencies()
} else {
currencies, err = d.GetCurrencies(ctx)
}
if err != nil {
return nil, err
}
var resp []exchange.FundingHistory
for x := range currencies {
var deposits *DepositsData
if d.Websocket.IsConnected() && d.Websocket.CanUseAuthenticatedWebsocketForWrapper() {
deposits, err = d.WSRetrieveDeposits(currency.NewCode(currencies[x].Currency), 100, 0)
} else {
deposits, err = d.GetDeposits(ctx, currency.NewCode(currencies[x].Currency), 100, 0)
}
if err != nil {
return nil, err
}
for y := range deposits.Data {
resp = append(resp, exchange.FundingHistory{
ExchangeName: d.Name,
Status: deposits.Data[y].State,
TransferID: deposits.Data[y].TransactionID,
Timestamp: deposits.Data[y].UpdatedTimestamp.Time(),
Currency: currencies[x].Currency,
Amount: deposits.Data[y].Amount,
CryptoToAddress: deposits.Data[y].Address,
TransferType: "deposit",
})
}
var withdrawalData *WithdrawalsData
if d.Websocket.IsConnected() && d.Websocket.CanUseAuthenticatedWebsocketForWrapper() {
withdrawalData, err = d.WSRetrieveWithdrawals(currency.NewCode(currencies[x].Currency), 100, 0)
} else {
withdrawalData, err = d.GetWithdrawals(ctx, currency.NewCode(currencies[x].Currency), 100, 0)
}
if err != nil {
return nil, err
}
for z := range withdrawalData.Data {
resp = append(resp, exchange.FundingHistory{
ExchangeName: d.Name,
Status: withdrawalData.Data[z].State,
TransferID: withdrawalData.Data[z].TransactionID,
Timestamp: withdrawalData.Data[z].UpdatedTimestamp.Time(),
Currency: currencies[x].Currency,
Amount: withdrawalData.Data[z].Amount,
CryptoToAddress: withdrawalData.Data[z].Address,
TransferType: "withdrawal",
})
}
}
return resp, nil
}
// GetWithdrawalsHistory returns previous withdrawals data
func (d *Deribit) GetWithdrawalsHistory(ctx context.Context, c currency.Code, _ asset.Item) ([]exchange.WithdrawalHistory, error) {
var currencies []CurrencyData
var err error
if d.Websocket.IsConnected() {
currencies, err = d.WSRetrieveCurrencies()
} else {
currencies, err = d.GetCurrencies(ctx)
}
if err != nil {
return nil, err
}
resp := []exchange.WithdrawalHistory{}
for x := range currencies {
if !strings.EqualFold(currencies[x].Currency, c.String()) {
continue
}
var withdrawalData *WithdrawalsData
if d.Websocket.IsConnected() && d.Websocket.CanUseAuthenticatedWebsocketForWrapper() {
withdrawalData, err = d.WSRetrieveWithdrawals(currency.NewCode(currencies[x].Currency), 100, 0)
} else {
withdrawalData, err = d.GetWithdrawals(ctx, currency.NewCode(currencies[x].Currency), 100, 0)
}
if err != nil {
return nil, err
}
for y := range withdrawalData.Data {
resp = append(resp, exchange.WithdrawalHistory{
Status: withdrawalData.Data[y].State,
TransferID: withdrawalData.Data[y].TransactionID,
Timestamp: withdrawalData.Data[y].UpdatedTimestamp.Time(),
Currency: currencies[x].Currency,
Amount: withdrawalData.Data[y].Amount,
CryptoToAddress: withdrawalData.Data[y].Address,
TransferType: "deposit",
})
}
}
return resp, nil
}
// GetRecentTrades returns the most recent trades for a currency and asset
func (d *Deribit) GetRecentTrades(ctx context.Context, p currency.Pair, assetType asset.Item) ([]trade.Data, error) {
if !d.SupportsAsset(assetType) {
return nil, fmt.Errorf("%s: %w - %s", d.Name, asset.ErrNotSupported, assetType)
}
p, err := d.FormatExchangeCurrency(p, assetType)
if err != nil {
return nil, err
}
instrumentID := d.formatPairString(assetType, p)
resp := []trade.Data{}
var trades *PublicTradesData
if d.Websocket.IsConnected() {
trades, err = d.WSRetrieveLastTradesByInstrument(
instrumentID, "", "", "", 0, false)
} else {
trades, err = d.GetLastTradesByInstrument(
ctx,
instrumentID, "", "", "", 0, false)
}
if err != nil {
return nil, err
}
for a := range trades.Trades {
sideData := order.Sell
if trades.Trades[a].Direction == sideBUY {
sideData = order.Buy
}
resp = append(resp, trade.Data{
TID: trades.Trades[a].TradeID,
Exchange: d.Name,
Price: trades.Trades[a].Price,
Amount: trades.Trades[a].Amount,
Timestamp: trades.Trades[a].Timestamp.Time(),
AssetType: assetType,
Side: sideData,
CurrencyPair: p,
})
}
return resp, nil
}
// GetHistoricTrades returns historic trade data within the timeframe provided
func (d *Deribit) GetHistoricTrades(ctx context.Context, p currency.Pair, assetType asset.Item, timestampStart, timestampEnd time.Time) ([]trade.Data, error) {
if common.StartEndTimeCheck(timestampStart, timestampEnd) != nil {
return nil, fmt.Errorf("invalid time range supplied. Start: %v End %v",
timestampStart,
timestampEnd)
}
p, err := d.FormatExchangeCurrency(p, assetType)
if err != nil {
return nil, err
}
var instrumentID string
switch assetType {
case asset.Futures, asset.Options, asset.Spot:
instrumentID = d.formatPairString(assetType, p)
default:
return nil, fmt.Errorf("%w asset type %v", asset.ErrNotSupported, assetType)
}
var resp []trade.Data
var tradesData *PublicTradesData
var hasMore = true
for hasMore {
if d.Websocket.IsConnected() {
tradesData, err = d.WSRetrieveLastTradesByInstrumentAndTime(instrumentID, "asc", 100, true, timestampStart, timestampEnd)
} else {
tradesData, err = d.GetLastTradesByInstrumentAndTime(ctx, instrumentID, "asc", 100, timestampStart, timestampEnd)
}
if err != nil {
return nil, err
}
if len(tradesData.Trades) != 100 {
hasMore = false
}
for t := range tradesData.Trades {
if t == 99 {
if timestampStart.Equal(tradesData.Trades[t].Timestamp.Time()) {
hasMore = false
}
timestampStart = tradesData.Trades[t].Timestamp.Time()
}
sideData := order.Sell
if tradesData.Trades[t].Direction == sideBUY {
sideData = order.Buy
}
resp = append(resp, trade.Data{
TID: tradesData.Trades[t].TradeID,
Exchange: d.Name,
Price: tradesData.Trades[t].Price,
Amount: tradesData.Trades[t].Amount,
Timestamp: tradesData.Trades[t].Timestamp.Time(),
AssetType: assetType,
Side: sideData,
CurrencyPair: p,
})
}
}
return resp, nil
}
// SubmitOrder submits a new order
func (d *Deribit) SubmitOrder(ctx context.Context, s *order.Submit) (*order.SubmitResponse, error) {
err := s.Validate(d.GetTradingRequirements())
if err != nil {
return nil, err
}
if !d.SupportsAsset(s.AssetType) {
return nil, fmt.Errorf("%s: orderType %v is not valid", d.Name, s.AssetType)
}
var orderID string
var fmtPair currency.Pair
status := order.New
fmtPair, err = d.FormatExchangeCurrency(s.Pair, s.AssetType)
if err != nil {
return nil, err
}
timeInForce := ""
if s.ImmediateOrCancel {
timeInForce = "immediate_or_cancel"
}
var data *PrivateTradeData
reqParams := &OrderBuyAndSellParams{
Instrument: fmtPair.String(),
OrderType: strings.ToLower(s.Type.String()),
Label: s.ClientOrderID,
TimeInForce: timeInForce,
Amount: s.Amount,
Price: s.Price,
TriggerPrice: s.TriggerPrice,
PostOnly: s.PostOnly,
ReduceOnly: s.ReduceOnly,
}
switch {
case s.Side.IsLong():
if d.Websocket.IsConnected() && d.Websocket.CanUseAuthenticatedWebsocketForWrapper() {
data, err = d.WSSubmitBuy(reqParams)
} else {
data, err = d.SubmitBuy(ctx, reqParams)
}
if err != nil {
return nil, err
}
if data == nil {
return nil, common.ErrNoResponse
}
orderID = data.Order.OrderID
case s.Side.IsShort():
if d.Websocket.IsConnected() && d.Websocket.CanUseAuthenticatedWebsocketForWrapper() {
data, err = d.WSSubmitSell(reqParams)
} else {
data, err = d.SubmitSell(ctx, reqParams)
}
if err != nil {
return nil, err
}
if data == nil {
return nil, common.ErrNoResponse
}
orderID = data.Order.OrderID
}
resp, err := s.DeriveSubmitResponse(orderID)
if err != nil {
return nil, err
}
resp.Status = status
return resp, nil
}
// ModifyOrder will allow of changing orderbook placement and limit to
// market conversion
func (d *Deribit) ModifyOrder(ctx context.Context, action *order.Modify) (*order.ModifyResponse, error) {
if err := action.Validate(); err != nil {
return nil, err
}
if !d.SupportsAsset(action.AssetType) {
return nil, fmt.Errorf("%s: %w - %v", d.Name, asset.ErrNotSupported, action.AssetType)
}
var modify *PrivateTradeData
var err error
reqParam := &OrderBuyAndSellParams{
TriggerPrice: action.TriggerPrice,
PostOnly: action.PostOnly,
Amount: action.Amount,
OrderID: action.OrderID,
Price: action.Price,
}
if d.Websocket.IsConnected() && d.Websocket.CanUseAuthenticatedWebsocketForWrapper() {
modify, err = d.WSSubmitEdit(reqParam)
} else {
modify, err = d.SubmitEdit(ctx, reqParam)
}
if err != nil {
return nil, err
}
resp, err := action.DeriveModifyResponse()
if err != nil {
return nil, err
}
resp.OrderID = modify.Order.OrderID
return resp, nil
}
// CancelOrder cancels an order by its corresponding ID number
func (d *Deribit) CancelOrder(ctx context.Context, ord *order.Cancel) error {
if !d.SupportsAsset(ord.AssetType) {
return fmt.Errorf("%s: %w - %s", d.Name, asset.ErrNotSupported, ord.AssetType)
}
err := ord.Validate(ord.StandardCancel())
if err != nil {
return err
}
if d.Websocket.IsConnected() && d.Websocket.CanUseAuthenticatedWebsocketForWrapper() {
_, err = d.WSSubmitCancel(ord.OrderID)
} else {
_, err = d.SubmitCancel(ctx, ord.OrderID)
}
if err != nil {
return err
}
return nil
}
// CancelBatchOrders cancels orders by their corresponding ID numbers
func (d *Deribit) CancelBatchOrders(_ context.Context, _ []order.Cancel) (*order.CancelBatchResponse, error) {
return nil, common.ErrFunctionNotSupported
}
// CancelAllOrders cancels all orders associated with a currency pair
func (d *Deribit) CancelAllOrders(ctx context.Context, orderCancellation *order.Cancel) (order.CancelAllResponse, error) {
if err := orderCancellation.Validate(); err != nil {
return order.CancelAllResponse{}, err
}
var cancelData *MultipleCancelResponse
pairFmt, err := d.GetPairFormat(orderCancellation.AssetType, true)
if err != nil {
return order.CancelAllResponse{}, err
}
var orderTypeStr string
switch orderCancellation.Type {
case order.Limit:
orderTypeStr = order.Limit.String()
case order.Market:
orderTypeStr = order.Market.String()
case order.AnyType, order.UnknownType:
orderTypeStr = "all"
default:
return order.CancelAllResponse{}, fmt.Errorf("%s: orderType %v is not valid", d.Name, orderCancellation.Type)
}
if d.Websocket.IsConnected() && d.Websocket.CanUseAuthenticatedWebsocketForWrapper() {
cancelData, err = d.WSSubmitCancelAllByInstrument(pairFmt.Format(orderCancellation.Pair), orderTypeStr, true, true)
} else {
cancelData, err = d.SubmitCancelAllByInstrument(ctx, pairFmt.Format(orderCancellation.Pair), orderTypeStr, true, true)
}
if err != nil {
return order.CancelAllResponse{}, err
}
response := order.CancelAllResponse{Count: cancelData.CancelCount}
if len(cancelData.CancelDetails) > 0 {
response.Status = make(map[string]string)
for a := range cancelData.CancelDetails {
for b := range cancelData.CancelDetails[a].Result {
response.Status[cancelData.CancelDetails[a].Result[b].OrderID] = cancelData.CancelDetails[a].Result[b].OrderState
}
}
}
return response, nil
}
// GetOrderInfo returns order information based on order ID
func (d *Deribit) GetOrderInfo(ctx context.Context, orderID string, _ currency.Pair, assetType asset.Item) (*order.Detail, error) {
if !d.SupportsAsset(assetType) {
return nil, fmt.Errorf("%w assetType %v", asset.ErrNotSupported, assetType)
}
var orderInfo *OrderData
var err error
if d.Websocket.IsConnected() && d.Websocket.CanUseAuthenticatedWebsocketForWrapper() {
orderInfo, err = d.WSRetrievesOrderState(orderID)
} else {
orderInfo, err = d.GetOrderState(ctx, orderID)
}
if err != nil {
return nil, err
}
orderSide := order.Sell
if orderInfo.Direction == sideBUY {
orderSide = order.Buy
}
orderType, err := order.StringToOrderType(orderInfo.OrderType)
if err != nil {
return nil, err
}
var pair currency.Pair
pair, err = currency.NewPairFromString(orderInfo.InstrumentName)
if err != nil {
return nil, err
}
var orderStatus order.Status
if orderInfo.OrderState == "untriggered" {
orderStatus = order.UnknownStatus
} else {
orderStatus, err = order.StringToOrderStatus(orderInfo.OrderState)
if err != nil {
return nil, fmt.Errorf("%v: orderStatus %s not supported", d.Name, orderInfo.OrderState)
}
}
return &order.Detail{
AssetType: assetType,
Exchange: d.Name,
PostOnly: orderInfo.PostOnly,
Price: orderInfo.Price,
Amount: orderInfo.Amount,
ExecutedAmount: orderInfo.FilledAmount,
Fee: orderInfo.Commission,
RemainingAmount: orderInfo.Amount - orderInfo.FilledAmount,
OrderID: orderInfo.OrderID,
Pair: pair,
LastUpdated: orderInfo.LastUpdateTimestamp.Time(),
Side: orderSide,
Type: orderType,
Status: orderStatus,
}, nil
}
// GetDepositAddress returns a deposit address for a specified currency
func (d *Deribit) GetDepositAddress(ctx context.Context, cryptocurrency currency.Code, _, _ string) (*deposit.Address, error) {
var addressData *DepositAddressData
var err error
if d.Websocket.IsConnected() && d.Websocket.CanUseAuthenticatedWebsocketForWrapper() {
addressData, err = d.WSRetrieveCurrentDepositAddress(cryptocurrency)
} else {
addressData, err = d.GetCurrentDepositAddress(ctx, cryptocurrency)
}
if err != nil {
return nil, err
}
return &deposit.Address{
Address: addressData.Address,
Chain: addressData.Currency,
}, nil
}
// WithdrawCryptocurrencyFunds returns a withdrawal ID when a withdrawal is
// submitted
func (d *Deribit) WithdrawCryptocurrencyFunds(ctx context.Context, withdrawRequest *withdraw.Request) (*withdraw.ExchangeResponse, error) {
err := withdrawRequest.Validate()
if err != nil {
return nil, err
}
var withdrawData *WithdrawData
if d.Websocket.IsConnected() && d.Websocket.CanUseAuthenticatedWebsocketForWrapper() {
withdrawData, err = d.WSSubmitWithdraw(withdrawRequest.Currency, withdrawRequest.Crypto.Address, "", withdrawRequest.Amount)
} else {
withdrawData, err = d.SubmitWithdraw(ctx, withdrawRequest.Currency, withdrawRequest.Crypto.Address, "", withdrawRequest.Amount)
}
if err != nil {
return nil, err
}
return &withdraw.ExchangeResponse{
ID: strconv.FormatInt(withdrawData.ID, 10),
Status: withdrawData.State,
}, err
}
// WithdrawFiatFunds returns a withdrawal ID when a withdrawal is submitted
func (d *Deribit) WithdrawFiatFunds(_ context.Context, _ *withdraw.Request) (*withdraw.ExchangeResponse, error) {
return nil, common.ErrFunctionNotSupported
}
// WithdrawFiatFundsToInternationalBank returns a withdrawal ID when a withdrawal is submitted
func (d *Deribit) WithdrawFiatFundsToInternationalBank(_ context.Context, _ *withdraw.Request) (*withdraw.ExchangeResponse, error) {
return nil, common.ErrFunctionNotSupported
}
// GetActiveOrders retrieves any orders that are active/open
func (d *Deribit) GetActiveOrders(ctx context.Context, getOrdersRequest *order.MultiOrderRequest) (order.FilteredOrders, error) {
if err := getOrdersRequest.Validate(); err != nil {
return nil, err
}
if !d.SupportsAsset(getOrdersRequest.AssetType) {
return nil, fmt.Errorf("%s: %w - %v", d.Name, asset.ErrNotSupported, getOrdersRequest.AssetType)
}
if len(getOrdersRequest.Pairs) == 0 {
return nil, currency.ErrCurrencyPairsEmpty
}
var resp = []order.Detail{}
for x := range getOrdersRequest.Pairs {
fmtPair, err := d.FormatExchangeCurrency(getOrdersRequest.Pairs[x], getOrdersRequest.AssetType)
if err != nil {
return nil, err
}
var oTypeString string
switch getOrdersRequest.Type {
case order.AnyType, order.UnknownType:
oTypeString = "all"
default:
oTypeString = getOrdersRequest.Type.Lower()
}
var ordersData []OrderData
if d.Websocket.IsConnected() && d.Websocket.CanUseAuthenticatedWebsocketForWrapper() {
ordersData, err = d.WSRetrieveOpenOrdersByInstrument(fmtPair.String(), oTypeString)
} else {
ordersData, err = d.GetOpenOrdersByInstrument(ctx, fmtPair.String(), oTypeString)
}
if err != nil {
return nil, err
}
for y := range ordersData {
orderSide := order.Sell
if ordersData[y].Direction == sideBUY {
orderSide = order.Buy
}
if getOrdersRequest.Side != orderSide && getOrdersRequest.Side != order.AnySide {
continue
}
orderType, err := order.StringToOrderType(ordersData[y].OrderType)
if err != nil {
return nil, err
}
if getOrdersRequest.Type != orderType && getOrdersRequest.Type != order.AnyType {
continue
}
var orderStatus order.Status
ordersData[y].OrderState = strings.ToLower(ordersData[y].OrderState)
if ordersData[y].OrderState != "open" {
continue
}
resp = append(resp, order.Detail{
AssetType: getOrdersRequest.AssetType,
Exchange: d.Name,
PostOnly: ordersData[y].PostOnly,
Price: ordersData[y].Price,
Amount: ordersData[y].Amount,
ExecutedAmount: ordersData[y].FilledAmount,
Fee: ordersData[y].Commission,
RemainingAmount: ordersData[y].Amount - ordersData[y].FilledAmount,
OrderID: ordersData[y].OrderID,
Pair: getOrdersRequest.Pairs[x],
LastUpdated: ordersData[y].LastUpdateTimestamp.Time(),
Side: orderSide,
Type: orderType,
Status: orderStatus,
})
}
}
return resp, nil
}
// GetOrderHistory retrieves account order information
// Can Limit response to specific order status
func (d *Deribit) GetOrderHistory(ctx context.Context, getOrdersRequest *order.MultiOrderRequest) (order.FilteredOrders, error) {
if err := getOrdersRequest.Validate(); err != nil {
return nil, err
}
if len(getOrdersRequest.Pairs) == 0 {
return nil, currency.ErrCurrencyPairsEmpty
}
var resp []order.Detail
for x := range getOrdersRequest.Pairs {
fmtPair, err := d.FormatExchangeCurrency(getOrdersRequest.Pairs[x], getOrdersRequest.AssetType)
if err != nil {
return nil, err
}
var ordersData []OrderData
if d.Websocket.IsConnected() && d.Websocket.CanUseAuthenticatedWebsocketForWrapper() {
ordersData, err = d.WSRetrieveOrderHistoryByInstrument(fmtPair.String(), 100, 0, true, true)
} else {
ordersData, err = d.GetOrderHistoryByInstrument(ctx, fmtPair.String(), 100, 0, true, true)
}
if err != nil {
return nil, err
}
for y := range ordersData {
orderSide := order.Sell
if ordersData[y].Direction == sideBUY {
orderSide = order.Buy
}
if getOrdersRequest.Side != orderSide && getOrdersRequest.Side != order.AnySide {
continue
}
orderType, err := order.StringToOrderType(ordersData[y].OrderType)
if err != nil {
return nil, err
}
if getOrdersRequest.Type != orderType && getOrdersRequest.Type != order.AnyType {
continue
}
var orderStatus order.Status
if ordersData[y].OrderState == "untriggered" {
orderStatus = order.UnknownStatus
} else {
orderStatus, err = order.StringToOrderStatus(ordersData[y].OrderState)
if err != nil {
return resp, fmt.Errorf("%v: orderStatus %s not supported", d.Name, ordersData[y].OrderState)
}
}
resp = append(resp, order.Detail{
AssetType: getOrdersRequest.AssetType,
Exchange: d.Name,
PostOnly: ordersData[y].PostOnly,
Price: ordersData[y].Price,
Amount: ordersData[y].Amount,
ExecutedAmount: ordersData[y].FilledAmount,
Fee: ordersData[y].Commission,
RemainingAmount: ordersData[y].Amount - ordersData[y].FilledAmount,
OrderID: ordersData[y].OrderID,
Pair: getOrdersRequest.Pairs[x],
LastUpdated: ordersData[y].LastUpdateTimestamp.Time(),
Side: orderSide,
Type: orderType,
Status: orderStatus,
})
}
}
return resp, nil
}
// GetFeeByType returns an estimate of fee based on the type of transaction
func (d *Deribit) GetFeeByType(ctx context.Context, feeBuilder *exchange.FeeBuilder) (float64, error) {
if feeBuilder == nil {
return 0, fmt.Errorf("%T %w", feeBuilder, common.ErrNilPointer)
}
if !d.AreCredentialsValid(ctx) && // Todo check connection status
feeBuilder.FeeType == exchange.CryptocurrencyTradeFee {
feeBuilder.FeeType = exchange.OfflineTradeFee
}
var fee float64
var err error
switch feeBuilder.FeeType {
case exchange.CryptocurrencyTradeFee:
fee, err = calculateTradingFee(feeBuilder)
if err != nil {
return 0, err
}
case exchange.CryptocurrencyDepositFee:
case exchange.CryptocurrencyWithdrawalFee:
// Withdrawals are processed instantly if the balance in our hot wallet permits so. We keep only a small percentage of coins in hot storage,
// therefore there is a chance that your withdrawal cannot be processed immediately. If needed, once a day we will replenish the balance of the hot wallet from the cold storage.
case exchange.OfflineTradeFee:
fee = getOfflineTradeFee(feeBuilder.PurchasePrice, feeBuilder.Amount)
}
if fee < 0 {
fee = 0
}
return fee, nil
}
// ValidateAPICredentials validates current credentials used for wrapper
// functionality
func (d *Deribit) ValidateAPICredentials(ctx context.Context, assetType asset.Item) error {
_, err := d.UpdateAccountInfo(ctx, assetType)
return d.CheckTransientError(err)
}
// GetHistoricCandles returns candles between a time period for a set time interval
func (d *Deribit) GetHistoricCandles(ctx context.Context, pair currency.Pair, a asset.Item, interval kline.Interval, start, end time.Time) (*kline.Item, error) {
req, err := d.GetKlineRequest(pair, a, interval, start, end, false)
if err != nil {
return nil, err
}
intervalString, err := d.GetResolutionFromInterval(req.ExchangeInterval)
if err != nil {
return nil, err
}
switch a {
case asset.Futures, asset.Spot:
var tradingViewData *TVChartData
if d.Websocket.IsConnected() {
tradingViewData, err = d.WSRetrievesTradingViewChartData(d.formatFuturesTradablePair(req.RequestFormatted), intervalString, start, end)
} else {
tradingViewData, err = d.GetTradingViewChart(ctx, d.formatFuturesTradablePair(req.RequestFormatted), intervalString, start, end)
}
if err != nil {
return nil, err
} else if len(tradingViewData.Ticks) == 0 {
return nil, kline.ErrNoTimeSeriesDataToConvert
}
checkLen := len(tradingViewData.Ticks)
if len(tradingViewData.Open) != checkLen ||
len(tradingViewData.High) != checkLen ||
len(tradingViewData.Low) != checkLen ||
len(tradingViewData.Close) != checkLen ||
len(tradingViewData.Volume) != checkLen {
return nil, fmt.Errorf("%s - %v: invalid trading view chart data received", a, req.RequestFormatted)
}
listCandles := make([]kline.Candle, 0, len(tradingViewData.Ticks))
for x := range tradingViewData.Ticks {
timeInfo := time.UnixMilli(tradingViewData.Ticks[x]).UTC()
if timeInfo.Before(start) {
continue
}
listCandles = append(listCandles, kline.Candle{
Open: tradingViewData.Open[x],
High: tradingViewData.High[x],
Low: tradingViewData.Low[x],
Close: tradingViewData.Close[x],
Volume: tradingViewData.Volume[x],
Time: timeInfo,
})
}
return req.ProcessResponse(listCandles)
case asset.OptionCombo, asset.FutureCombo, asset.Options:
// TODO: candlestick data for asset item option_combo, future_combo, and option not supported yet
}
return nil, fmt.Errorf("%w candlestick data for asset type %v", asset.ErrNotSupported, a)
}
// GetHistoricCandlesExtended returns candles between a time period for a set time interval
func (d *Deribit) GetHistoricCandlesExtended(ctx context.Context, pair currency.Pair, a asset.Item, interval kline.Interval, start, end time.Time) (*kline.Item, error) {
req, err := d.GetKlineExtendedRequest(pair, a, interval, start, end)
if err != nil {
return nil, err
}
var tradingViewData *TVChartData
timeSeries := make([]kline.Candle, 0, req.Size())
switch a {
case asset.Futures, asset.Spot:
for x := range req.RangeHolder.Ranges {
intervalString, err := d.GetResolutionFromInterval(req.ExchangeInterval)
if err != nil {
return nil, err
}
if d.Websocket.IsConnected() {
tradingViewData, err = d.WSRetrievesTradingViewChartData(d.formatFuturesTradablePair(req.RequestFormatted), intervalString, req.RangeHolder.Ranges[x].Start.Time, req.RangeHolder.Ranges[x].End.Time)
} else {
tradingViewData, err = d.GetTradingViewChart(ctx, d.formatFuturesTradablePair(req.RequestFormatted), intervalString, req.RangeHolder.Ranges[x].Start.Time, req.RangeHolder.Ranges[x].End.Time)
}
if err != nil {
return nil, err
}
checkLen := len(tradingViewData.Ticks)
if len(tradingViewData.Open) != checkLen ||
len(tradingViewData.High) != checkLen ||
len(tradingViewData.Low) != checkLen ||
len(tradingViewData.Close) != checkLen ||
len(tradingViewData.Volume) != checkLen {
return nil, fmt.Errorf("%s - %v: invalid trading view chart data received", a, d.formatFuturesTradablePair(req.RequestFormatted))
}
for i := range tradingViewData.Ticks {
timeInfo := time.UnixMilli(tradingViewData.Ticks[i]).UTC()
if timeInfo.Before(start) {
continue
}
timeSeries = append(timeSeries, kline.Candle{
Open: tradingViewData.Open[i],
High: tradingViewData.High[i],
Low: tradingViewData.Low[i],
Close: tradingViewData.Close[i],
Volume: tradingViewData.Volume[i],
Time: timeInfo,
})
}
}
return req.ProcessResponse(timeSeries)
case asset.OptionCombo, asset.FutureCombo, asset.Options:
// TODO: candlestick data for asset item option_combo, future_combo, and option not supported yet
}
return nil, fmt.Errorf("%w candlestick data for asset type %v", asset.ErrNotSupported, a)
}
// GetServerTime returns the current exchange server time.
func (d *Deribit) GetServerTime(ctx context.Context, _ asset.Item) (time.Time, error) {
return d.GetTime(ctx)
}
// AuthenticateWebsocket sends an authentication message to the websocket
func (d *Deribit) AuthenticateWebsocket(ctx context.Context) error {
return d.wsLogin(ctx)
}
// GetFuturesContractDetails returns all contracts from the exchange by asset type
func (d *Deribit) GetFuturesContractDetails(ctx context.Context, item asset.Item) ([]futures.Contract, error) {
if !item.IsFutures() {
return nil, futures.ErrNotFuturesAsset
}
if item != asset.Futures {
return nil, fmt.Errorf("%w %v", asset.ErrNotSupported, item)
}
resp := []futures.Contract{}
for _, ccy := range baseCurrencies {
var marketSummary []InstrumentData
var err error
if d.Websocket.IsConnected() {
marketSummary, err = d.WSRetrieveInstrumentsData(currency.NewCode(ccy), d.GetAssetKind(item), false)
} else {
marketSummary, err = d.GetInstruments(ctx, currency.NewCode(ccy), d.GetAssetKind(item), false)
}
if err != nil {
return nil, err
}
for i := range marketSummary {
if marketSummary[i].Kind != "future" && marketSummary[i].Kind != "future_combo" {
continue
}
var cp currency.Pair
cp, err = currency.NewPairFromString(marketSummary[i].InstrumentName)
if err != nil {
return nil, err
}
var ct futures.ContractType
switch marketSummary[i].SettlementPeriod {
case "day":
ct = futures.Daily
case "week":
ct = futures.Weekly
case "month":
ct = futures.Monthly
case "perpetual":
ct = futures.Perpetual
}
var contractSettlementType futures.ContractSettlementType
if marketSummary[i].InstrumentType == "reversed" {
contractSettlementType = futures.Inverse
} else {
contractSettlementType = futures.Linear
}
resp = append(resp, futures.Contract{
Exchange: d.Name,
Name: cp,
Underlying: currency.NewPair(currency.NewCode(marketSummary[i].BaseCurrency), currency.NewCode(marketSummary[i].QuoteCurrency)),
Asset: item,
SettlementCurrencies: []currency.Code{currency.NewCode(marketSummary[i].SettlementCurrency)},
StartDate: marketSummary[i].CreationTimestamp.Time(),
EndDate: marketSummary[i].ExpirationTimestamp.Time(),
Type: ct,
SettlementType: contractSettlementType,
IsActive: marketSummary[i].IsActive,
MaxLeverage: marketSummary[i].MaxLeverage,
Multiplier: marketSummary[i].ContractSize,
})
}
}
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) {
return fmt.Errorf("%s: %w - %v", d.Name, asset.ErrNotSupported, a)
}
for _, x := range baseCurrencies {
var instrumentsData []InstrumentData
var err error
if d.Websocket.IsConnected() {
instrumentsData, err = d.WSRetrieveInstrumentsData(currency.NewCode(x), d.GetAssetKind(a), false)
} else {
instrumentsData, err = d.GetInstruments(ctx, currency.NewCode(x), d.GetAssetKind(a), false)
}
if err != nil {
return err
} else if len(instrumentsData) == 0 {
continue
}
limits := make([]order.MinMaxLevel, len(instrumentsData))
for x := range instrumentsData {
var pair currency.Pair
pair, err = currency.NewPairFromString(instrumentsData[x].InstrumentName)
if err != nil {
return err
}
limits[x] = order.MinMaxLevel{
Pair: pair,
Asset: a,
PriceStepIncrementSize: instrumentsData[x].TickSize,
MinimumBaseAmount: instrumentsData[x].MinimumTradeAmount,
}
}
err = d.LoadLimits(limits)
if err != nil {
return err
}
}
return nil
}
// GetFuturesPositionSummary returns position summary details for an active position
func (d *Deribit) GetFuturesPositionSummary(ctx context.Context, r *futures.PositionSummaryRequest) (*futures.PositionSummary, error) {
if r == nil {
return nil, fmt.Errorf("%w HistoricalRatesRequest", common.ErrNilPointer)
}
if r.Asset != asset.Futures {
return nil, fmt.Errorf("%w %v", futures.ErrNotPerpetualFuture, r.Asset)
}
if r.Pair.IsEmpty() {
return nil, currency.ErrCurrencyPairEmpty
}
fPair, err := d.FormatExchangeCurrency(r.Pair, r.Asset)
if err != nil {
return nil, err
}
var pos []PositionData
if d.Websocket.IsConnected() && d.Websocket.CanUseAuthenticatedWebsocketForWrapper() {
pos, err = d.WSRetrievePositions(fPair.Base, d.GetAssetKind(r.Asset))
} else {
pos, err = d.GetPositions(ctx, fPair.Base, d.GetAssetKind(r.Asset))
}
if err != nil {
return nil, err
}
index := -1
for a := range pos {
if pos[a].InstrumentName == fPair.String() {
index = a
break
}
}
if index == -1 {
return nil, errors.New("position information for the instrument not found")
}
contracts, err := d.GetFuturesContractDetails(ctx, r.Asset)
if err != nil {
return nil, err
}
var multiplier, contractSize float64
var settlementType futures.ContractSettlementType
for i := range contracts {
if !contracts[i].Name.Equal(fPair) {
continue
}
multiplier = contracts[i].Multiplier
settlementType = contracts[i].SettlementType
break
}
var baseSize float64
if r.Asset == asset.Futures {
baseSize = pos[index].SizeCurrency
} else if r.Asset == asset.Options {
baseSize = pos[index].Size
}
contractSize = multiplier * baseSize
return &futures.PositionSummary{
Pair: r.Pair,
Asset: r.Asset,
Currency: fPair.Base,
NotionalSize: decimal.NewFromFloat(pos[index].MarkPrice),
Leverage: decimal.NewFromFloat(pos[index].Leverage),
InitialMarginRequirement: decimal.NewFromFloat(pos[index].InitialMargin),
EstimatedLiquidationPrice: decimal.NewFromFloat(pos[index].EstimatedLiquidationPrice),
MarkPrice: decimal.NewFromFloat(pos[index].MarkPrice),
CurrentSize: decimal.NewFromFloat(baseSize),
ContractSize: decimal.NewFromFloat(contractSize),
ContractMultiplier: decimal.NewFromFloat(multiplier),
ContractSettlementType: settlementType,
AverageOpenPrice: decimal.NewFromFloat(pos[index].AveragePrice),
UnrealisedPNL: decimal.NewFromFloat(pos[index].TotalProfitLoss - pos[index].RealizedProfitLoss),
RealisedPNL: decimal.NewFromFloat(pos[index].RealizedProfitLoss),
MaintenanceMarginFraction: decimal.NewFromFloat(pos[index].MaintenanceMargin),
}, nil
}
// GetOpenInterest returns the open interest rate for a given asset pair
func (d *Deribit) GetOpenInterest(ctx context.Context, k ...key.PairAsset) ([]futures.OpenInterest, error) {
if len(k) == 0 {
return nil, fmt.Errorf("%w requires pair", common.ErrFunctionNotSupported)
}
for i := range k {
if k[i].Asset == asset.Spot ||
!d.SupportsAsset(k[i].Asset) {
return nil, fmt.Errorf("%w %v %v", asset.ErrNotSupported, k[i].Asset, k[i].Pair())
}
}
result := make([]futures.OpenInterest, 0, len(k))
for i := range k {
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.WSRetrieveBookSummaryByInstrument(p)
} else {
oi, err = d.GetBookSummaryByInstrument(ctx, p)
}
if err != nil {
return nil, err
}
for a := range oi {
result = append(result, futures.OpenInterest{
Key: key.ExchangePairAsset{
Exchange: d.Name,
Base: k[i].Base,
Quote: k[i].Quote,
Asset: k[i].Asset,
},
OpenInterest: oi[a].OpenInterest,
})
break
}
}
if len(result) == 0 {
return nil, fmt.Errorf("%w, no data found for %v", currency.ErrCurrencyNotFound, k)
}
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 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
}
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 nil, err
}
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 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
func (d *Deribit) GetHistoricalFundingRates(ctx context.Context, r *fundingrate.HistoricalRatesRequest) (*fundingrate.HistoricalRates, error) {
if r == nil {
return nil, fmt.Errorf("%w LatestRateRequest", common.ErrNilPointer)
}
if r.Asset != asset.Futures {
return nil, fmt.Errorf("%w %v", asset.ErrNotSupported, r.Asset)
}
if r.Pair.IsEmpty() {
return nil, currency.ErrCurrencyPairEmpty
}
if !r.StartDate.IsZero() && !r.EndDate.IsZero() {
err := common.StartEndTimeCheck(r.StartDate, r.EndDate)
if err != nil {
return nil, err
}
}
if r.IncludePayments {
return nil, fmt.Errorf("include payments %w", common.ErrNotYetImplemented)
}
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
mfr := make(map[int64]struct{})
for {
if ed.Equal(r.StartDate) || ed.Before(r.StartDate) {
break
}
var records []FundingRateHistory
if d.Websocket.IsConnected() {
records, err = d.WSRetrieveFundingRateHistory(p, r.StartDate, ed)
} else {
records, err = d.GetFundingRateHistory(ctx, p, r.StartDate, ed)
}
if err != nil {
return nil, err
}
if len(records) == 0 || ed.Equal(records[0].Timestamp.Time()) {
break
}
for i := range records {
rt := records[i].Timestamp.Time()
if rt.Before(r.StartDate) || rt.After(r.EndDate) {
continue
}
if _, ok := mfr[rt.UnixMilli()]; ok {
continue
}
fundingRates = append(fundingRates, fundingrate.Rate{
Rate: decimal.NewFromFloat(records[i].Interest1H),
Time: rt,
})
mfr[rt.UnixMilli()] = struct{}{}
}
ed = records[0].Timestamp.Time()
}
if len(fundingRates) == 0 {
return nil, fundingrate.ErrNoFundingRatesFound
}
sort.Slice(fundingRates, func(i, j int) bool {
return fundingRates[i].Time.Before(fundingRates[j].Time)
})
return &fundingrate.HistoricalRates{
Exchange: d.Name,
Asset: r.Asset,
Pair: r.Pair,
FundingRates: fundingRates,
StartDate: fundingRates[0].Time,
EndDate: r.EndDate,
LatestRate: fundingRates[len(fundingRates)-1],
PaymentCurrency: r.PaymentCurrency,
}, nil
}
func (d *Deribit) formatPairString(assetType asset.Item, pair currency.Pair) string {
switch assetType {
case asset.Futures:
return d.formatFuturesTradablePair(pair)
case asset.Options:
return d.optionPairToString(pair)
}
return pair.String()
}