mirror of
https://github.com/d0zingcat/gocryptotrader.git
synced 2026-05-13 15:09:42 +00:00
* 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 )
1599 lines
53 KiB
Go
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 := ¤cy.PairFormat{Uppercase: true, Delimiter: currency.DashDelimiter}
|
|
underscoreFormat := ¤cy.PairFormat{Uppercase: true, Delimiter: currency.UnderscoreDelimiter}
|
|
err := d.StoreAssetPairFormat(asset.Spot, currency.PairStore{RequestFormat: underscoreFormat, ConfigFormat: underscoreFormat})
|
|
if err != nil {
|
|
log.Errorln(log.ExchangeSys, err)
|
|
}
|
|
for _, assetType := range []asset.Item{asset.Futures, asset.Options, asset.OptionCombo, asset.FutureCombo} {
|
|
if err = d.StoreAssetPairFormat(assetType, currency.PairStore{RequestFormat: 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()
|
|
}
|