Files
gocryptotrader/exchanges/deribit/deribit_wrapper.go
Samuael A aeb4a87913 exchanges: Add Deribit exchange support (#1082)
* deribit implementation

* add ws impll

* cleanup

* Update deribit_wrapper.go

* Add missing endpoints

* Fix config file

* asset type update

* Update code structure

* Update authenticated private endpoints unit tests

* Updating websocket

* Updating websocket connection and subscription handling

* Finishing up adding subscription push data

* Adding websocket public endpoint

* Adding WS endpoints

* Adding websocket unit tests

* Minor clean-up

* Integrating websocket endpoints into the wrapper funcs

* Updating exchange documentations

* Fixing test issues

* Code cleaning-up

* fix test issues

* Updating validations and logic errors

* Updating wrapper issues

* fix test issues

* Slight test update

* Unit test and code structure update

* Update websocket tempos

* Slight update on code structure

* Minor update on unit tests

* Update depending on review comments

* Minor code fix and doc re-generating

* Update on Candlestick wrapper functions

* Minor updates

* minor unit test updates

* Minor updates on weboscket and unit tests

* minor linter fix

* codespell and rate limiter issues

* single linter issue fix

* adding rate limiter

* Add ratelimiter to websocket conn and overall code update

* fix websocket push data issue

* Implementing missing wrapper function

* Websocket fix

* Minor update on missing endpoint and other

* fixing websocket issues and cleaningup

* Minor tempo fix

* Minor linter issues

* unit test update

* Indexing error fix

* Websocket connection fix

* string formatting fix

* Small fix on unit tests

* fix minor json conversion issue

* websocket and documentation update

* websocket, wrapper and unit test updates

* Documentation and unit tests update

* Fix unit tests

* wrapper fix for new change

* Unit test fix

* timestamp conversion and unit tests update

* Minor instrument ID conversion fix

* instrument formats and unit test update

* formatting and unit test fix

* config update

* Updating websocket and adding the Spot support

* Add small unit test fix

* unit test and websocket handlers update

* Linter issues fix

* minor documentation and code update

* Minor fix

* added a wrapper func GetLatestFundingRates

* Types, wrapper update, and unit tests

* Minor config update

* fix wrapper unit tests

* Resolve all panic and wrapper test issues

* minor unit test fix

* fix issues and adding newly added endpoints

* updates and added remaining endpoints with unit tests

* Update unit tests using assert

* Added missing endpoints and unit tests

* Minor updates and clean-ups

* Resolve tradable pair fetching  panic

* Mutex fix

* Added Options assets test and minor fixes

* subscription mothod updated

* Remove misadded code lines

* resolve websocket

* Updating tests, types, endpoint methods and others

* Added GetFuturesContractDetails and minor fix

* fix linter issue

* revert change on candlestic time

* Added filters to candles

* minor unit test and wrapper fix

* Minor unit tests update

* cahnge param key for GetOrderMarginByID

* updating unit tests and resolve issues

* Update websocket unit tests

* Minor fix based on review

* Revert unit test change

* fix pair config issue

* Added missing wrapper functions

* Fix missing review changes

* Fix options request pair formatting

* fix AllExchangeWrappers test issue

* Changes with unit test and wrapper based on the review

* Fix to options reg-exp

* wrapper functions fix

* Update MaximumFundingRateHistory filter and minor fixes

* Fix besed on review comment

* Fix issues on review comment

* linter fix

* fix minor unit test issue

* Fix unit test issues

* Update trade order cancellation responses

* fix config files issue

* lint update config files

* Update unit tests

* Update return values and response handling

* added missing endpoint and fixes based on review comment

* toggle useTestNet back

* Update cancel by label and other fix

* fix forgotten cancel all response type

* update CancelResp type

* Fix unmarshaling error

* updated websocket orderbook load issue

* fix websocket lock and groups

* Change Items to Tranche and fix linter issues

* Fix orderbook issue

* Update unit tests offline error handling, and endpoints argument and  error handling

* Contributors documentation update and change error return type

* Updated unit tests based on review comment

* Update unit tests and removed password change endpoint

* Fix race condition

* Update on tests, test pairs, and wrapper config

* Update test tradable pairs loading

* Update unit tests, fix linter issues, and update wrapper functions

* remove credentials

* Update test and fix authentication method and few authenticated endpoints

* fix codespell issue

* group the repeated currency code check to a func

* added unit test for repeated pair check func

* Added a base coin and related updates

---------

Co-authored-by: E Sequeira <earncef@earncef.com>
2024-05-29 10:28:03 +10:00

1602 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"
)
// GetDefaultConfig returns a default exchange config
func (d *Deribit) GetDefaultConfig(ctx context.Context) (*config.Exchange, error) {
d.SetDefaults()
exchCfg, err := d.GetStandardConfig()
if err != nil {
return nil, err
}
err = d.SetupDefaults(exchCfg)
if err != nil {
return nil, err
}
if d.Features.Supports.RESTCapabilities.AutoPairUpdates {
err := d.UpdateTradablePairs(ctx, true)
if err != nil {
return nil, err
}
}
return exchCfg, nil
}
// 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
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}})
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 {
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,
},
},
}
d.Requester, err = request.New(d.Name,
common.NewHTTPClientWithTimeout(exchange.DefaultHTTPTimeout),
request.WithLimiter(SetRateLimit()),
)
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.GenerateDefaultSubscriptions,
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
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
}
}
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)
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
}
}
return nil
}
// 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()
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(interval)
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(interval)
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
}
// 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) {
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))
var err error
var pair currency.Pair
for i := range k {
pair, err = d.FormatExchangeCurrency(k[i].Pair(), k[i].Asset)
if err != nil {
return nil, err
}
var oi []BookSummaryData
if d.Websocket.IsConnected() {
oi, err = d.WSRetrieveBookBySummary(pair.Base, d.GetAssetKind(k[i].Asset))
} else {
oi, err = d.GetBookSummaryByCurrency(ctx, pair.Base, d.GetAssetKind(k[i].Asset))
}
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,
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
}
// 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
}
pair, err := d.FormatExchangeCurrency(pair, assetType)
if err != nil {
return false, err
}
var instrumentInfo *InstrumentData
if d.Websocket.IsConnected() {
instrumentInfo, err = d.WSRetrieveInstrumentData(pair.String())
} else {
instrumentInfo, err = d.GetInstrument(context.Background(), pair.String())
}
if err != nil {
return false, err
}
return strings.EqualFold(instrumentInfo.SettlementPeriod, "perpetual"), 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)
}
fPair, err := d.FormatExchangeCurrency(r.Pair, r.Asset)
if err != nil {
return nil, err
}
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(fPair.String(), r.StartDate, ed)
} else {
records, err = d.GetFundingRateHistory(ctx, fPair.String(), 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()
}