Files
gocryptotrader/exchanges/deribit/deribit_wrapper.go
Ryan O'Hara-Reid 497e13dc62 common: Update ErrorCollector to use mutex and simplify error collection in concurrent operations (#2090)
* refactor: Update ErrorCollector to use mutex and simplify error collection in concurrent operations

* glorious: nits

* linter: fix

* another find

* Apply suggestion from @gbjk

Co-authored-by: Gareth Kirwan <gbjkirwan@gmail.com>

* Apply suggestion from @gbjk

Co-authored-by: Gareth Kirwan <gbjkirwan@gmail.com>

* one liner defer

* Update common/common.go

Co-authored-by: Gareth Kirwan <gbjkirwan@gmail.com>

* gk: nits

* Update common/common_test.go

Co-authored-by: Scott <gloriousCode@users.noreply.github.com>

* thrasher-: nits

---------

Co-authored-by: shazbert <ryan.oharareid@thrasher.io>
Co-authored-by: Gareth Kirwan <gbjkirwan@gmail.com>
Co-authored-by: shazbert <shazbert@DESKTOP-3QKKR6J.localdomain>
Co-authored-by: Scott <gloriousCode@users.noreply.github.com>
2025-11-11 15:08:48 +11:00

1556 lines
51 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"
"github.com/thrasher-corp/gocryptotrader/exchange/accounts"
"github.com/thrasher-corp/gocryptotrader/exchange/order/limits"
"github.com/thrasher-corp/gocryptotrader/exchange/websocket"
"github.com/thrasher-corp/gocryptotrader/exchange/websocket/buffer"
exchange "github.com/thrasher-corp/gocryptotrader/exchanges"
"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/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 (e *Exchange) SetDefaults() {
e.Name = "Deribit"
e.Enabled = true
e.Verbose = true
e.API.CredentialsValidator.RequiresKey = true
e.API.CredentialsValidator.RequiresSecret = true
dashFormat := &currency.PairFormat{Uppercase: true, Delimiter: currency.DashDelimiter}
underscoreFormat := &currency.PairFormat{Uppercase: true, Delimiter: currency.UnderscoreDelimiter}
if err := e.SetAssetPairStore(asset.Spot, currency.PairStore{AssetEnabled: true, RequestFormat: underscoreFormat, ConfigFormat: underscoreFormat}); err != nil {
log.Errorf(log.ExchangeSys, "%s error storing %q default asset formats: %s", e.Name, asset.Spot, err)
}
for _, a := range []asset.Item{asset.Futures, asset.Options, asset.OptionCombo, asset.FutureCombo} {
if err := e.SetAssetPairStore(a, currency.PairStore{AssetEnabled: true, RequestFormat: dashFormat, ConfigFormat: dashFormat}); err != nil {
log.Errorf(log.ExchangeSys, "%s error storing %q default asset formats: %s", e.Name, a, err)
}
}
// Fill out the capabilities/features that the exchange supports
e.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(),
}
var err error
e.Requester, err = request.New(e.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 = e.DisableAssetWebsocketSupport(assetType); err != nil {
log.Errorln(log.ExchangeSys, err)
}
}
e.API.Endpoints = e.NewEndpoints()
err = e.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)
}
e.Websocket = websocket.NewManager()
e.WebsocketResponseMaxLimit = exchange.DefaultWebsocketResponseMaxLimit
e.WebsocketResponseCheckTimeout = exchange.DefaultWebsocketResponseCheckTimeout
e.WebsocketOrderbookBufferLimit = exchange.DefaultWebsocketOrderbookBufferLimit
}
// Setup takes in the supplied exchange configuration details and sets params
func (e *Exchange) Setup(exch *config.Exchange) error {
err := exch.Validate()
if err != nil {
return err
}
if !exch.Enabled {
e.SetEnabled(false)
return nil
}
err = e.SetupDefaults(exch)
if err != nil {
return err
}
err = e.Websocket.Setup(&websocket.ManagerSetup{
ExchangeConfig: exch,
DefaultURL: deribitWebsocketAddress,
RunningURL: deribitWebsocketAddress,
Connector: e.WsConnect,
Subscriber: e.Subscribe,
Unsubscriber: e.Unsubscribe,
GenerateSubscriptions: e.generateSubscriptions,
Features: &e.Features.Supports.WebsocketCapabilities,
OrderbookBufferConfig: buffer.Config{
SortBuffer: true,
SortBufferByUpdateIDs: true,
},
})
if err != nil {
return err
}
return e.Websocket.SetupNewConnection(&websocket.ConnectionSetup{
URL: e.Websocket.GetWebsocketURL(),
ResponseCheckTimeout: exch.WebsocketResponseCheckTimeout,
ResponseMaxLimit: exch.WebsocketResponseMaxLimit,
})
}
// FetchTradablePairs returns a list of the exchanges tradable pairs
func (e *Exchange) FetchTradablePairs(ctx context.Context, assetType asset.Item) (currency.Pairs, error) {
if !e.SupportsAsset(assetType) {
return nil, fmt.Errorf("%s: %w - %v", e.Name, asset.ErrNotSupported, assetType)
}
instruments, err := e.GetInstruments(ctx, currency.EMPTYCODE, e.GetAssetKind(assetType), false)
if err != nil {
return nil, err
}
resp := make(currency.Pairs, 0, len(instruments))
for _, inst := range instruments {
if !inst.IsActive {
continue
}
cp, err := currency.NewPairFromString(inst.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 (e *Exchange) UpdateTradablePairs(ctx context.Context) error {
var errs common.ErrorCollector
for _, a := range e.GetAssetTypes(false) {
errs.Go(func() error {
pairs, err := e.FetchTradablePairs(ctx, a)
if err != nil {
return err
}
return e.UpdatePairs(pairs, a, false)
})
}
return errs.Collect()
}
// UpdateTickers updates the ticker for all currency pairs of a given asset type
func (e *Exchange) UpdateTickers(_ context.Context, _ asset.Item) error {
return common.ErrFunctionNotSupported
}
// UpdateTicker updates and returns the ticker for a currency pair
func (e *Exchange) UpdateTicker(ctx context.Context, p currency.Pair, assetType asset.Item) (*ticker.Price, error) {
if !e.SupportsAsset(assetType) {
return nil, fmt.Errorf("%s: %w - %s", e.Name, asset.ErrNotSupported, assetType)
}
p, err := e.FormatExchangeCurrency(p, assetType)
if err != nil {
return nil, err
}
instrumentID := formatPairString(assetType, p)
var tickerData *TickerData
if e.Websocket.IsConnected() {
tickerData, err = e.WSRetrievePublicTicker(ctx, instrumentID)
} else {
tickerData, err = e.GetPublicTicker(ctx, instrumentID)
}
if err != nil {
return nil, err
}
resp := ticker.Price{
ExchangeName: e.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(e.Name, p, assetType)
}
// UpdateOrderbook updates and returns the orderbook for a currency pair
func (e *Exchange) UpdateOrderbook(ctx context.Context, p currency.Pair, assetType asset.Item) (*orderbook.Book, error) {
p, err := e.FormatExchangeCurrency(p, assetType)
if err != nil {
return nil, err
}
instrumentID := formatPairString(assetType, p)
var obData *Orderbook
if e.Websocket.IsConnected() {
obData, err = e.WSRetrieveOrderbookData(ctx, instrumentID, 50)
} else {
obData, err = e.GetOrderbook(ctx, instrumentID, 50)
}
if err != nil {
return nil, err
}
book := &orderbook.Book{
Exchange: e.Name,
Pair: p,
Asset: assetType,
ValidateOrderbook: e.ValidateOrderbook,
}
book.Asks = make(orderbook.Levels, 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.Level{
Price: obData.Asks[x][0],
Amount: obData.Asks[x][1],
})
}
book.Bids = make(orderbook.Levels, 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.Level{
Price: obData.Bids[x][0],
Amount: obData.Bids[x][1],
})
}
err = book.Process()
if err != nil {
return book, err
}
return orderbook.Get(e.Name, p, assetType)
}
// UpdateAccountBalances retrieves currency balances
func (e *Exchange) UpdateAccountBalances(ctx context.Context, _ asset.Item) (accounts.SubAccounts, error) {
currencies, err := e.GetCurrencies(ctx)
if err != nil {
return nil, err
}
subAccts := accounts.SubAccounts{accounts.NewSubAccount(asset.All, "")}
for i := range currencies {
var resp *AccountSummaryData
if e.Websocket.IsConnected() && e.Websocket.CanUseAuthenticatedWebsocketForWrapper() {
resp, err = e.WSRetrieveAccountSummary(ctx, currencies[i].Currency, false)
} else {
resp, err = e.GetAccountSummary(ctx, currencies[i].Currency, false)
}
if err != nil {
return nil, err
}
subAccts[0].Balances.Set(currencies[i].Currency, accounts.Balance{
Total: resp.Balance,
Hold: resp.Balance - resp.AvailableFunds,
})
}
return subAccts, e.Accounts.Save(ctx, subAccts, true)
}
// GetAccountFundingHistory returns funding history, deposits and withdrawals
func (e *Exchange) GetAccountFundingHistory(ctx context.Context) ([]exchange.FundingHistory, error) {
var currencies []CurrencyData
var err error
if e.Websocket.IsConnected() {
currencies, err = e.WSRetrieveCurrencies(ctx)
} else {
currencies, err = e.GetCurrencies(ctx)
}
if err != nil {
return nil, err
}
var resp []exchange.FundingHistory
for x := range currencies {
var deposits *DepositsData
if e.Websocket.IsConnected() && e.Websocket.CanUseAuthenticatedWebsocketForWrapper() {
deposits, err = e.WSRetrieveDeposits(ctx, currencies[x].Currency, 100, 0)
} else {
deposits, err = e.GetDeposits(ctx, currencies[x].Currency, 100, 0)
}
if err != nil {
return nil, err
}
for y := range deposits.Data {
resp = append(resp, exchange.FundingHistory{
ExchangeName: e.Name,
Status: deposits.Data[y].State,
TransferID: deposits.Data[y].TransactionID,
Timestamp: deposits.Data[y].UpdatedTimestamp.Time(),
Currency: currencies[x].Currency.String(),
Amount: deposits.Data[y].Amount,
CryptoToAddress: deposits.Data[y].Address,
TransferType: "deposit",
})
}
var withdrawalData *WithdrawalsData
if e.Websocket.IsConnected() && e.Websocket.CanUseAuthenticatedWebsocketForWrapper() {
withdrawalData, err = e.WSRetrieveWithdrawals(ctx, currencies[x].Currency, 100, 0)
} else {
withdrawalData, err = e.GetWithdrawals(ctx, currencies[x].Currency, 100, 0)
}
if err != nil {
return nil, err
}
for z := range withdrawalData.Data {
resp = append(resp, exchange.FundingHistory{
ExchangeName: e.Name,
Status: withdrawalData.Data[z].State,
TransferID: withdrawalData.Data[z].TransactionID,
Timestamp: withdrawalData.Data[z].UpdatedTimestamp.Time(),
Currency: currencies[x].Currency.String(),
Amount: withdrawalData.Data[z].Amount,
CryptoToAddress: withdrawalData.Data[z].Address,
TransferType: "withdrawal",
})
}
}
return resp, nil
}
// GetWithdrawalsHistory returns previous withdrawals data
func (e *Exchange) GetWithdrawalsHistory(ctx context.Context, c currency.Code, _ asset.Item) ([]exchange.WithdrawalHistory, error) {
var currencies []CurrencyData
var err error
if e.Websocket.IsConnected() {
currencies, err = e.WSRetrieveCurrencies(ctx)
} else {
currencies, err = e.GetCurrencies(ctx)
}
if err != nil {
return nil, err
}
resp := []exchange.WithdrawalHistory{}
for x := range currencies {
if !currencies[x].Currency.Equal(c) {
continue
}
var withdrawalData *WithdrawalsData
if e.Websocket.IsConnected() && e.Websocket.CanUseAuthenticatedWebsocketForWrapper() {
withdrawalData, err = e.WSRetrieveWithdrawals(ctx, currencies[x].Currency, 100, 0)
} else {
withdrawalData, err = e.GetWithdrawals(ctx, 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.String(),
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 (e *Exchange) GetRecentTrades(ctx context.Context, p currency.Pair, assetType asset.Item) ([]trade.Data, error) {
if !e.SupportsAsset(assetType) {
return nil, fmt.Errorf("%s: %w - %s", e.Name, asset.ErrNotSupported, assetType)
}
p, err := e.FormatExchangeCurrency(p, assetType)
if err != nil {
return nil, err
}
instrumentID := formatPairString(assetType, p)
resp := []trade.Data{}
var trades *PublicTradesData
if e.Websocket.IsConnected() {
trades, err = e.WSRetrieveLastTradesByInstrument(ctx, instrumentID, "", "", "", 0, false)
} else {
trades, err = e.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: e.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 (e *Exchange) 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 := e.FormatExchangeCurrency(p, assetType)
if err != nil {
return nil, err
}
var instrumentID string
switch assetType {
case asset.Futures, asset.Options, asset.Spot:
instrumentID = formatPairString(assetType, p)
default:
return nil, fmt.Errorf("%w asset type %v", asset.ErrNotSupported, assetType)
}
var resp []trade.Data
var tradesData *PublicTradesData
hasMore := true
for hasMore {
if e.Websocket.IsConnected() {
tradesData, err = e.WSRetrieveLastTradesByInstrumentAndTime(ctx, instrumentID, "asc", 100, true, timestampStart, timestampEnd)
} else {
tradesData, err = e.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: e.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 (e *Exchange) SubmitOrder(ctx context.Context, s *order.Submit) (*order.SubmitResponse, error) {
err := s.Validate(e.GetTradingRequirements())
if err != nil {
return nil, err
}
if !e.SupportsAsset(s.AssetType) {
return nil, fmt.Errorf("%s: orderType %v is not valid", e.Name, s.AssetType)
}
var orderID string
var fmtPair currency.Pair
status := order.New
fmtPair, err = e.FormatExchangeCurrency(s.Pair, s.AssetType)
if err != nil {
return nil, err
}
timeInForce := ""
if s.TimeInForce.Is(order.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.TimeInForce.Is(order.PostOnly),
ReduceOnly: s.ReduceOnly,
}
switch {
case s.Side.IsLong():
if e.Websocket.IsConnected() && e.Websocket.CanUseAuthenticatedWebsocketForWrapper() {
data, err = e.WSSubmitBuy(ctx, reqParams)
} else {
data, err = e.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 e.Websocket.IsConnected() && e.Websocket.CanUseAuthenticatedWebsocketForWrapper() {
data, err = e.WSSubmitSell(ctx, reqParams)
} else {
data, err = e.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 modifies an existing order
func (e *Exchange) ModifyOrder(ctx context.Context, action *order.Modify) (*order.ModifyResponse, error) {
if err := action.Validate(); err != nil {
return nil, err
}
if !e.SupportsAsset(action.AssetType) {
return nil, fmt.Errorf("%s: %w - %v", e.Name, asset.ErrNotSupported, action.AssetType)
}
var modify *PrivateTradeData
var err error
reqParam := &OrderBuyAndSellParams{
TriggerPrice: action.TriggerPrice,
PostOnly: action.TimeInForce.Is(order.PostOnly),
Amount: action.Amount,
OrderID: action.OrderID,
Price: action.Price,
}
if e.Websocket.IsConnected() && e.Websocket.CanUseAuthenticatedWebsocketForWrapper() {
modify, err = e.WSSubmitEdit(ctx, reqParam)
} else {
modify, err = e.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 (e *Exchange) CancelOrder(ctx context.Context, ord *order.Cancel) error {
if !e.SupportsAsset(ord.AssetType) {
return fmt.Errorf("%s: %w - %s", e.Name, asset.ErrNotSupported, ord.AssetType)
}
err := ord.Validate(ord.StandardCancel())
if err != nil {
return err
}
if e.Websocket.IsConnected() && e.Websocket.CanUseAuthenticatedWebsocketForWrapper() {
_, err = e.WSSubmitCancel(ctx, ord.OrderID)
} else {
_, err = e.SubmitCancel(ctx, ord.OrderID)
}
if err != nil {
return err
}
return nil
}
// CancelBatchOrders cancels orders by their corresponding ID numbers
func (e *Exchange) CancelBatchOrders(_ context.Context, _ []order.Cancel) (*order.CancelBatchResponse, error) {
return nil, common.ErrFunctionNotSupported
}
// CancelAllOrders cancels all orders associated with a currency pair
func (e *Exchange) CancelAllOrders(ctx context.Context, cancel *order.Cancel) (order.CancelAllResponse, error) {
var resp order.CancelAllResponse
if err := cancel.Validate(); err != nil {
return resp, err
}
fPair, err := e.FormatExchangeCurrency(cancel.Pair, cancel.AssetType)
if err != nil {
return resp, err
}
var orderTypeStr string
switch cancel.Type {
case order.Limit:
orderTypeStr = order.Limit.String()
case order.Market:
orderTypeStr = order.Market.String()
case order.AnyType, order.UnknownType:
orderTypeStr = "all"
default:
return resp, fmt.Errorf("%s %w: %v", e.Name, order.ErrTypeIsInvalid, cancel.Type)
}
var cancelData *MultipleCancelResponse
if e.Websocket.IsConnected() && e.Websocket.CanUseAuthenticatedWebsocketForWrapper() {
cancelData, err = e.WSSubmitCancelAllByInstrument(ctx, fPair.String(), orderTypeStr, true, true)
} else {
cancelData, err = e.SubmitCancelAllByInstrument(ctx, fPair.String(), orderTypeStr, true, true)
}
if err != nil {
return resp, err
}
for a := range cancelData.CancelDetails {
for b := range cancelData.CancelDetails[a].Result {
resp.Add(cancelData.CancelDetails[a].Result[b].OrderID, cancelData.CancelDetails[a].Result[b].OrderState)
}
}
return resp, nil
}
// GetOrderInfo returns order information based on order ID
func (e *Exchange) GetOrderInfo(ctx context.Context, orderID string, _ currency.Pair, assetType asset.Item) (*order.Detail, error) {
if !e.SupportsAsset(assetType) {
return nil, fmt.Errorf("%w assetType %v", asset.ErrNotSupported, assetType)
}
var orderInfo *OrderData
var err error
if e.Websocket.IsConnected() && e.Websocket.CanUseAuthenticatedWebsocketForWrapper() {
orderInfo, err = e.WSRetrievesOrderState(ctx, orderID)
} else {
orderInfo, err = e.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", e.Name, orderInfo.OrderState)
}
}
var tif order.TimeInForce
tif, err = timeInForceFromString(orderInfo.TimeInForce, orderInfo.PostOnly)
if err != nil {
return nil, err
}
return &order.Detail{
AssetType: assetType,
Exchange: e.Name,
TimeInForce: tif,
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 (e *Exchange) GetDepositAddress(ctx context.Context, cryptocurrency currency.Code, _, _ string) (*deposit.Address, error) {
var addressData *DepositAddressData
var err error
if e.Websocket.IsConnected() && e.Websocket.CanUseAuthenticatedWebsocketForWrapper() {
addressData, err = e.WSRetrieveCurrentDepositAddress(ctx, cryptocurrency)
} else {
addressData, err = e.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 (e *Exchange) WithdrawCryptocurrencyFunds(ctx context.Context, withdrawRequest *withdraw.Request) (*withdraw.ExchangeResponse, error) {
err := withdrawRequest.Validate()
if err != nil {
return nil, err
}
var withdrawData *WithdrawData
if e.Websocket.IsConnected() && e.Websocket.CanUseAuthenticatedWebsocketForWrapper() {
withdrawData, err = e.WSSubmitWithdraw(ctx, withdrawRequest.Currency, withdrawRequest.Crypto.Address, "", withdrawRequest.Amount)
} else {
withdrawData, err = e.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 (e *Exchange) WithdrawFiatFunds(_ context.Context, _ *withdraw.Request) (*withdraw.ExchangeResponse, error) {
return nil, common.ErrFunctionNotSupported
}
// WithdrawFiatFundsToInternationalBank returns a withdrawal ID when a withdrawal is submitted
func (e *Exchange) WithdrawFiatFundsToInternationalBank(_ context.Context, _ *withdraw.Request) (*withdraw.ExchangeResponse, error) {
return nil, common.ErrFunctionNotSupported
}
// GetActiveOrders retrieves any orders that are active/open
func (e *Exchange) GetActiveOrders(ctx context.Context, getOrdersRequest *order.MultiOrderRequest) (order.FilteredOrders, error) {
if err := getOrdersRequest.Validate(); err != nil {
return nil, err
}
if !e.SupportsAsset(getOrdersRequest.AssetType) {
return nil, fmt.Errorf("%s: %w - %v", e.Name, asset.ErrNotSupported, getOrdersRequest.AssetType)
}
if len(getOrdersRequest.Pairs) == 0 {
return nil, currency.ErrCurrencyPairsEmpty
}
resp := []order.Detail{}
for x := range getOrdersRequest.Pairs {
fmtPair, err := e.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 e.Websocket.IsConnected() && e.Websocket.CanUseAuthenticatedWebsocketForWrapper() {
ordersData, err = e.WSRetrieveOpenOrdersByInstrument(ctx, fmtPair.String(), oTypeString)
} else {
ordersData, err = e.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
}
var tif order.TimeInForce
tif, err = timeInForceFromString(ordersData[y].TimeInForce, ordersData[y].PostOnly)
if err != nil {
return nil, err
}
resp = append(resp, order.Detail{
AssetType: getOrdersRequest.AssetType,
Exchange: e.Name,
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,
TimeInForce: tif,
})
}
}
return resp, nil
}
// GetOrderHistory retrieves account order information
// Can Limit response to specific order status
func (e *Exchange) 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 := e.FormatExchangeCurrency(getOrdersRequest.Pairs[x], getOrdersRequest.AssetType)
if err != nil {
return nil, err
}
var ordersData []OrderData
if e.Websocket.IsConnected() && e.Websocket.CanUseAuthenticatedWebsocketForWrapper() {
ordersData, err = e.WSRetrieveOrderHistoryByInstrument(ctx, fmtPair.String(), 100, 0, true, true)
} else {
ordersData, err = e.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", e.Name, ordersData[y].OrderState)
}
}
var tif order.TimeInForce
tif, err = timeInForceFromString(ordersData[y].TimeInForce, ordersData[y].PostOnly)
if err != nil {
return nil, err
}
resp = append(resp, order.Detail{
AssetType: getOrdersRequest.AssetType,
Exchange: e.Name,
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,
TimeInForce: tif,
})
}
}
return resp, nil
}
// GetFeeByType returns an estimate of fee based on the type of transaction
func (e *Exchange) GetFeeByType(ctx context.Context, feeBuilder *exchange.FeeBuilder) (float64, error) {
if feeBuilder == nil {
return 0, fmt.Errorf("%T %w", feeBuilder, common.ErrNilPointer)
}
if !e.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 (e *Exchange) ValidateAPICredentials(ctx context.Context, assetType asset.Item) error {
_, err := e.UpdateAccountBalances(ctx, assetType)
return e.CheckTransientError(err)
}
// GetHistoricCandles returns candles between a time period for a set time interval
func (e *Exchange) GetHistoricCandles(ctx context.Context, pair currency.Pair, a asset.Item, interval kline.Interval, start, end time.Time) (*kline.Item, error) {
req, err := e.GetKlineRequest(pair, a, interval, start, end, false)
if err != nil {
return nil, err
}
intervalString, err := e.GetResolutionFromInterval(req.ExchangeInterval)
if err != nil {
return nil, err
}
switch a {
case asset.Futures, asset.Spot:
var tradingViewData *TVChartData
if e.Websocket.IsConnected() {
tradingViewData, err = e.WSRetrievesTradingViewChartData(ctx, formatFuturesTradablePair(req.RequestFormatted), intervalString, start, end)
} else {
tradingViewData, err = e.GetTradingViewChart(ctx, formatFuturesTradablePair(req.RequestFormatted), intervalString, start, end)
}
if err != nil {
return nil, err
}
listCandles, err := appendCandles(tradingViewData, start)
if err != nil {
return nil, err
}
return req.ProcessResponse(listCandles)
}
return nil, fmt.Errorf("%w candlestick data for asset type %v", asset.ErrNotSupported, a)
}
func appendCandles(tradingViewData *TVChartData, start time.Time) ([]kline.Candle, error) {
if tradingViewData == nil || 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("%w: ohlcv len must be equal", kline.ErrInsufficientCandleData)
}
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 listCandles, nil
}
// GetHistoricCandlesExtended returns candles between a time period for a set time interval
func (e *Exchange) GetHistoricCandlesExtended(ctx context.Context, pair currency.Pair, a asset.Item, interval kline.Interval, start, end time.Time) (*kline.Item, error) {
req, err := e.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 := e.GetResolutionFromInterval(req.ExchangeInterval)
if err != nil {
return nil, err
}
if e.Websocket.IsConnected() {
tradingViewData, err = e.WSRetrievesTradingViewChartData(ctx, formatFuturesTradablePair(req.RequestFormatted), intervalString, req.RangeHolder.Ranges[x].Start.Time, req.RangeHolder.Ranges[x].End.Time)
} else {
tradingViewData, err = e.GetTradingViewChart(ctx, formatFuturesTradablePair(req.RequestFormatted), intervalString, req.RangeHolder.Ranges[x].Start.Time, req.RangeHolder.Ranges[x].End.Time)
}
if err != nil {
return nil, err
}
timeSeries, err = appendCandles(tradingViewData, start)
if err != nil {
return nil, err
}
}
return req.ProcessResponse(timeSeries)
}
return nil, fmt.Errorf("%w candlestick data for asset type %v", asset.ErrNotSupported, a)
}
// GetServerTime returns the current exchange server time.
func (e *Exchange) GetServerTime(ctx context.Context, _ asset.Item) (time.Time, error) {
return e.GetTime(ctx)
}
// AuthenticateWebsocket sends an authentication message to the websocket
func (e *Exchange) AuthenticateWebsocket(ctx context.Context) error {
return e.wsLogin(ctx)
}
// GetFuturesContractDetails returns all contracts from the exchange by asset type
func (e *Exchange) 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 e.Websocket.IsConnected() {
marketSummary, err = e.WSRetrieveInstrumentsData(ctx, currency.NewCode(ccy), e.GetAssetKind(item), false)
} else {
marketSummary, err = e.GetInstruments(ctx, currency.NewCode(ccy), e.GetAssetKind(item), false)
}
if err != nil {
return nil, err
}
for _, inst := range marketSummary {
if inst.Kind != "future" && inst.Kind != "future_combo" {
continue
}
cp, err := currency.NewPairFromString(inst.InstrumentName)
if err != nil {
return nil, err
}
var ct futures.ContractType
switch inst.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 inst.InstrumentType == "reversed" {
contractSettlementType = futures.Inverse
} else {
contractSettlementType = futures.Linear
}
resp = append(resp, futures.Contract{
Exchange: e.Name,
Name: cp,
Underlying: currency.NewPair(inst.BaseCurrency, inst.QuoteCurrency),
Asset: item,
SettlementCurrency: inst.SettlementCurrency,
StartDate: inst.CreationTimestamp.Time(),
EndDate: inst.ExpirationTimestamp.Time(),
Type: ct,
SettlementType: contractSettlementType,
IsActive: inst.IsActive,
MaxLeverage: inst.MaxLeverage,
Multiplier: inst.ContractSize,
})
}
}
return resp, nil
}
// UpdateOrderExecutionLimits sets exchange execution order limits for an asset type
func (e *Exchange) UpdateOrderExecutionLimits(ctx context.Context, a asset.Item) error {
if !e.SupportsAsset(a) {
return fmt.Errorf("%s: %w - %v", e.Name, asset.ErrNotSupported, a)
}
for _, bc := range baseCurrencies {
var instrumentsData []*InstrumentData
var err error
if e.Websocket.IsConnected() {
instrumentsData, err = e.WSRetrieveInstrumentsData(ctx, currency.NewCode(bc), e.GetAssetKind(a), false)
} else {
instrumentsData, err = e.GetInstruments(ctx, currency.NewCode(bc), e.GetAssetKind(a), false)
}
if err != nil {
return err
} else if len(instrumentsData) == 0 {
continue
}
l := make([]limits.MinMaxLevel, len(instrumentsData))
for i, inst := range instrumentsData {
var pair currency.Pair
pair, err = currency.NewPairFromString(inst.InstrumentName)
if err != nil {
return err
}
l[i] = limits.MinMaxLevel{
Key: key.NewExchangeAssetPair(e.Name, a, pair),
PriceStepIncrementSize: inst.TickSize,
MinimumBaseAmount: inst.MinimumTradeAmount,
}
}
err = limits.Load(l)
if err != nil {
return err
}
}
return nil
}
// GetFuturesPositionSummary returns position summary details for an active position
func (e *Exchange) 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 := e.FormatExchangeCurrency(r.Pair, r.Asset)
if err != nil {
return nil, err
}
var pos []PositionData
if e.Websocket.IsConnected() && e.Websocket.CanUseAuthenticatedWebsocketForWrapper() {
pos, err = e.WSRetrievePositions(ctx, fPair.Base, e.GetAssetKind(r.Asset))
} else {
pos, err = e.GetPositions(ctx, fPair.Base, e.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 := e.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
switch r.Asset {
case asset.Futures:
baseSize = pos[index].SizeCurrency
case 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 (e *Exchange) 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 ||
!e.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 := e.CurrencyPairs.GetFormat(k[i].Asset, true)
if err != nil {
return nil, err
}
cp := k[i].Pair().Format(pFmt)
p := formatPairString(k[i].Asset, cp)
var oi []BookSummaryData
if e.Websocket.IsConnected() {
oi, err = e.WSRetrieveBookSummaryByInstrument(ctx, p)
} else {
oi, err = e.GetBookSummaryByInstrument(ctx, p)
}
if err != nil {
return nil, err
}
for a := range oi {
result = append(result, futures.OpenInterest{
Key: key.NewExchangeAssetPair(e.Name, k[i].Asset, k[i].Pair()),
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 (e *Exchange) GetCurrencyTradeURL(_ context.Context, a asset.Item, cp currency.Pair) (string, error) {
if cp.IsEmpty() {
return "", currency.ErrCurrencyPairEmpty
}
switch a {
case asset.Futures:
isPerp, err := e.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 %q", asset.ErrNotSupported, a)
}
}
// IsPerpetualFutureCurrency ensures a given asset and currency is a perpetual future
// differs by exchange
func (e *Exchange) 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 (e *Exchange) GetLatestFundingRates(ctx context.Context, r *fundingrate.LatestRateRequest) ([]fundingrate.LatestRateResponse, error) {
if r == nil {
return nil, fmt.Errorf("%w LatestRateRequest", common.ErrNilPointer)
}
if !e.SupportsAsset(r.Asset) {
return nil, fmt.Errorf("%s %w", r.Asset, asset.ErrNotSupported)
}
isPerpetual, err := e.IsPerpetualFutureCurrency(r.Asset, r.Pair)
if err != nil {
return nil, err
}
if !isPerpetual {
return nil, fmt.Errorf("%w %q", futures.ErrNotPerpetualFuture, r.Pair)
}
pFmt, err := e.CurrencyPairs.GetFormat(r.Asset, true)
if err != nil {
return nil, err
}
cp := r.Pair.Format(pFmt)
p := formatPairString(r.Asset, cp)
var fri []FundingRateHistory
fri, err = e.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: e.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 (e *Exchange) 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 := e.CurrencyPairs.GetFormat(r.Asset, true)
if err != nil {
return nil, err
}
cp := r.Pair.Format(pFmt)
p := formatPairString(r.Asset, cp)
ed := r.EndDate
var fundingRates []fundingrate.Rate
mfr := make(map[int64]struct{})
for ed.After(r.StartDate) {
var records []FundingRateHistory
if e.Websocket.IsConnected() {
records, err = e.WSRetrieveFundingRateHistory(ctx, p, r.StartDate, ed)
} else {
records, err = e.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: e.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 formatPairString(assetType asset.Item, pair currency.Pair) string {
switch assetType {
case asset.Futures:
return formatFuturesTradablePair(pair)
case asset.Options:
return optionPairToString(pair)
case asset.OptionCombo:
return optionComboPairToString(pair)
}
return pair.String()
}
func timeInForceFromString(timeInForceString string, postOnly bool) (order.TimeInForce, error) {
tif, err := order.StringToTimeInForce(timeInForceString)
if err != nil {
return order.UnknownTIF, err
}
if postOnly {
tif |= order.PostOnly
}
return tif, nil
}