mirror of
https://github.com/d0zingcat/gocryptotrader.git
synced 2026-05-13 15:09:42 +00:00
* Added TimeInForce type and updated related files * Linter issue fix and minor coinbasepro type update * Bitrex consts update * added unit test and minor changes in bittrex * Unit tests update * Fix minor linter issues * Update TestStringToTimeInForce unit test * Exchange test template change * A different approach * fix conflict with gateio timeInForce * minor exchange template update * Minor fix to test_files template * Update order tests * Complete updating the order unit tests * Updating exchange wrapper and test template files * update kucoin and deribit wrapper to match the time in force change * minor comment update * fix time-in-force related test errors * linter issue fix * ADD_NEW_EXCHANGE documentation update * time in force constants, functions and unit tests update * shift tif policies to TimeInForce * Update time-in-force, related functions, and unit tests * fix linter issue and time-in-force processing * added a good till crossing tif value * order type fix and fix related tim-in-force entries * update time-in-force unmarshaling and unit test * consistency guideline added * fix time-in-force error in gateio * linter issue fix * update based on review comments * add unit test and fix missing issues * minor fix and added benchmark unit test * change GTT to GTC for limit * fix linter issue * added time-in-force value to place order param * fix minor issues based on review comment and move tif code to separate files * update on exchanges linked to time-in-force * resolve missing review comments * minor linter issues fix * added time-in-force handler and update timeInForce parametered endpoint * minor fixes based on review * nits fix * update based on review * linter fix * rm getTimeInForce func and minor change to time-in-force * minor change * update based on review comments * wrappers and time-in-force calling approach * minor change * update gateio string to timeInForce conversion and unit test * update exchange template * update wrapper template file * policy comments, and template files update * rename all exchange types name to Exchange * update on template files and template generation * templates and generation code and other updates * linter issue fix * added subscriptions and websocket templates * update ADD_NEW_EXCHANGE.md with recent binance functions and implementations * rename template files and update unit tests * minor template and unit test fix * rename templates and fix on unit tests * update on template files and documentation * removed unnecessary tag fix and update templates * fix Add_NEW_EXCHANGE.md doc file * formatting, comments, and error checks update on template files * rename exchange receivers to e and ex for consistency * rename unit test exchange receiver and minor updates * linter issues fix * fix deribit issue and minor style update * fix test issues caused by receiver change * raname local variables exchange declaration variables * update templates comments * update templates and related comments * renamed ex to e * update template comments * toggle WS to false to improve coverage * template comments update * added test coverage to Ws enabled and minor changes --------- Co-authored-by: Samuel Reid <43227667+cranktakular@users.noreply.github.com>
1583 lines
53 KiB
Go
1583 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"
|
|
"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/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/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 := ¤cy.PairFormat{Uppercase: true, Delimiter: currency.DashDelimiter}
|
|
underscoreFormat := ¤cy.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, forceUpdate bool) error {
|
|
assets := e.GetAssetTypes(false)
|
|
errs := common.CollectErrors(len(assets))
|
|
for x := range assets {
|
|
go func(x int) {
|
|
defer errs.Wg.Done()
|
|
pairs, err := e.FetchTradablePairs(ctx, assets[x])
|
|
if err != nil {
|
|
errs.C <- err
|
|
return
|
|
}
|
|
errs.C <- e.UpdatePairs(pairs, assets[x], false, forceUpdate)
|
|
}(x)
|
|
}
|
|
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 := e.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 := e.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)
|
|
}
|
|
|
|
// UpdateAccountInfo retrieves balances for all enabled currencies
|
|
func (e *Exchange) UpdateAccountInfo(ctx context.Context, _ asset.Item) (account.Holdings, error) {
|
|
var resp account.Holdings
|
|
resp.Exchange = e.Name
|
|
currencies, err := e.GetCurrencies(ctx)
|
|
if err != nil {
|
|
return resp, err
|
|
}
|
|
resp.Accounts = make([]account.SubAccount, len(currencies))
|
|
for x := range currencies {
|
|
var data *AccountSummaryData
|
|
if e.Websocket.IsConnected() && e.Websocket.CanUseAuthenticatedWebsocketForWrapper() {
|
|
data, err = e.WSRetrieveAccountSummary(ctx, currency.NewCode(currencies[x].Currency), false)
|
|
} else {
|
|
data, err = e.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
|
|
}
|
|
|
|
// 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, currency.NewCode(currencies[x].Currency), 100, 0)
|
|
} else {
|
|
deposits, err = e.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: e.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 e.Websocket.IsConnected() && e.Websocket.CanUseAuthenticatedWebsocketForWrapper() {
|
|
withdrawalData, err = e.WSRetrieveWithdrawals(ctx, currency.NewCode(currencies[x].Currency), 100, 0)
|
|
} else {
|
|
withdrawalData, err = e.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: e.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 (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 !strings.EqualFold(currencies[x].Currency, c.String()) {
|
|
continue
|
|
}
|
|
var withdrawalData *WithdrawalsData
|
|
if e.Websocket.IsConnected() && e.Websocket.CanUseAuthenticatedWebsocketForWrapper() {
|
|
withdrawalData, err = e.WSRetrieveWithdrawals(ctx, currency.NewCode(currencies[x].Currency), 100, 0)
|
|
} else {
|
|
withdrawalData, err = e.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 (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 := e.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 = e.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 will allow of changing orderbook placement and limit to
|
|
// market conversion
|
|
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, orderCancellation *order.Cancel) (order.CancelAllResponse, error) {
|
|
if err := orderCancellation.Validate(); err != nil {
|
|
return order.CancelAllResponse{}, err
|
|
}
|
|
var cancelData *MultipleCancelResponse
|
|
pairFmt, err := e.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", e.Name, orderCancellation.Type)
|
|
}
|
|
if e.Websocket.IsConnected() && e.Websocket.CanUseAuthenticatedWebsocketForWrapper() {
|
|
cancelData, err = e.WSSubmitCancelAllByInstrument(ctx, pairFmt.Format(orderCancellation.Pair), orderTypeStr, true, true)
|
|
} else {
|
|
cancelData, err = e.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 (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.UpdateAccountInfo(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, e.formatFuturesTradablePair(req.RequestFormatted), intervalString, start, end)
|
|
} else {
|
|
tradingViewData, err = e.GetTradingViewChart(ctx, e.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 (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, e.formatFuturesTradablePair(req.RequestFormatted), intervalString, req.RangeHolder.Ranges[x].Start.Time, req.RangeHolder.Ranges[x].End.Time)
|
|
} else {
|
|
tradingViewData, err = e.GetTradingViewChart(ctx, e.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, e.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 (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(currency.NewCode(inst.BaseCurrency), currency.NewCode(inst.QuoteCurrency)),
|
|
Asset: item,
|
|
SettlementCurrencies: []currency.Code{currency.NewCode(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 _, x := range baseCurrencies {
|
|
var instrumentsData []*InstrumentData
|
|
var err error
|
|
if e.Websocket.IsConnected() {
|
|
instrumentsData, err = e.WSRetrieveInstrumentsData(ctx, currency.NewCode(x), e.GetAssetKind(a), false)
|
|
} else {
|
|
instrumentsData, err = e.GetInstruments(ctx, currency.NewCode(x), e.GetAssetKind(a), false)
|
|
}
|
|
if err != nil {
|
|
return err
|
|
} else if len(instrumentsData) == 0 {
|
|
continue
|
|
}
|
|
|
|
limits := make([]order.MinMaxLevel, len(instrumentsData))
|
|
for x, inst := range instrumentsData {
|
|
var pair currency.Pair
|
|
pair, err = currency.NewPairFromString(inst.InstrumentName)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
limits[x] = order.MinMaxLevel{
|
|
Pair: pair,
|
|
Asset: a,
|
|
PriceStepIncrementSize: inst.TickSize,
|
|
MinimumBaseAmount: inst.MinimumTradeAmount,
|
|
}
|
|
}
|
|
err = e.LoadLimits(limits)
|
|
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 := e.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.ExchangePairAsset{
|
|
Exchange: e.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 (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 %v", 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 := e.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 := e.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 (e *Exchange) formatPairString(assetType asset.Item, pair currency.Pair) string {
|
|
switch assetType {
|
|
case asset.Futures:
|
|
return e.formatFuturesTradablePair(pair)
|
|
case asset.Options:
|
|
return e.optionPairToString(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
|
|
}
|