Files
gocryptotrader/exchanges/gateio/gateio_wrapper.go
Gareth Kirwan bda9bbec66 websocket: Remove GenerateMessageID (#2008)
* Exchanges: Remove example BespokeGenerateMessageID

* Okx: Replace conn.RequestIDGenerator with MesssageID

Continued overall direction to remove the closed-loop of e => conn => e
roundtrip for message ids

* Exchanges: Add MessageSequence

This method removes the either/or nature of message id generation.
We don't tie the message ids to connections, or to anything.
Consumers just call whichever they want, or even combine them as they
want.
Anything more complicated will need a separate installation anyway

* GateIO: Split usage of MessageID and MessageSequence

* Binance: Switch to UUID message IDs

* Kraken: Switch to e.MessageSequence

* Kucoin: Switch to MessageID

* HitBTC: Switch to UUIDv7 for ws message ID

* Bybit: Switch to UUIDv7 for ws message ID

* Bitfinex: Switch to UUIDv7 and MessageSequence

Tested CID - It accepts 53 bits only for an int, so MessageSequence
makes sense. Can't use MessageID

* Websocket: Remove now unused MessageID function

Moved all MessageID usage into funcs and onto base methods, to remove
the closed loop of message IDs

* Docs: Update guidance for message signatures
2025-10-24 11:14:24 +11:00

2669 lines
86 KiB
Go

package gateio
import (
"context"
"errors"
"fmt"
"math"
"slices"
"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/order/limits"
"github.com/thrasher-corp/gocryptotrader/exchange/websocket"
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/subscription"
"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"
"github.com/thrasher-corp/gocryptotrader/types"
)
// SetDefaults sets default values for the exchange
func (e *Exchange) SetDefaults() {
e.Name = "GateIO"
e.Enabled = true
e.Verbose = true
e.API.CredentialsValidator.RequiresKey = true
e.API.CredentialsValidator.RequiresSecret = true
requestFmt := &currency.PairFormat{Delimiter: currency.UnderscoreDelimiter, Uppercase: true}
configFmt := &currency.PairFormat{Delimiter: currency.UnderscoreDelimiter, Uppercase: true}
err := e.SetGlobalPairsManager(requestFmt, configFmt, asset.Spot, asset.CoinMarginedFutures, asset.USDTMarginedFutures, asset.Margin, asset.CrossMargin, asset.DeliveryFutures, asset.Options)
if err != nil {
log.Errorln(log.ExchangeSys, err)
}
e.Features = exchange.Features{
CurrencyTranslations: currency.NewTranslations(map[currency.Code]currency.Code{
currency.NewCode("MBABYDOGE"): currency.BABYDOGE,
}),
TradingRequirements: protocol.TradingRequirements{
SpotMarketBuyQuotation: true,
SpotMarketSellBase: true,
},
Supports: exchange.FeaturesSupported{
REST: true,
Websocket: true,
RESTCapabilities: protocol.Features{
TickerBatching: true,
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,
PredictedFundingRate: true,
FundingRateFetching: true,
},
WebsocketCapabilities: protocol.Features{
TickerFetching: true,
OrderbookFetching: true,
TradeFetching: true,
KlineFetching: true,
AuthenticatedEndpoints: true,
MessageCorrelation: true,
GetOrder: true,
AccountBalance: true,
Subscribe: true,
Unsubscribe: true,
},
WithdrawPermissions: exchange.AutoWithdrawCrypto |
exchange.NoFiatWithdrawals,
Kline: kline.ExchangeCapabilitiesSupported{
Intervals: true,
},
FuturesCapabilities: exchange.FuturesCapabilities{
FundingRates: true,
SupportedFundingRateFrequencies: map[kline.Interval]bool{
kline.FourHour: true,
kline.EightHour: true,
},
FundingRateBatching: map[asset.Item]bool{
asset.USDTMarginedFutures: true,
asset.CoinMarginedFutures: true,
},
OpenInterest: exchange.OpenInterestSupport{
Supported: true,
SupportsRestBatch: true,
},
},
},
Enabled: exchange.FeaturesEnabled{
AutoPairUpdates: true,
Kline: kline.ExchangeCapabilitiesEnabled{
Intervals: kline.DeployExchangeIntervals(
kline.IntervalCapacity{Interval: kline.HundredMilliseconds},
kline.IntervalCapacity{Interval: kline.ThousandMilliseconds},
kline.IntervalCapacity{Interval: kline.TenSecond},
kline.IntervalCapacity{Interval: kline.ThirtySecond},
kline.IntervalCapacity{Interval: kline.OneMin},
kline.IntervalCapacity{Interval: kline.FiveMin},
kline.IntervalCapacity{Interval: kline.FifteenMin},
kline.IntervalCapacity{Interval: kline.ThirtyMin},
kline.IntervalCapacity{Interval: kline.OneHour},
kline.IntervalCapacity{Interval: kline.TwoHour},
kline.IntervalCapacity{Interval: kline.FourHour},
kline.IntervalCapacity{Interval: kline.EightHour},
kline.IntervalCapacity{Interval: kline.TwelveHour},
kline.IntervalCapacity{Interval: kline.OneDay},
kline.IntervalCapacity{Interval: kline.OneWeek},
kline.IntervalCapacity{Interval: kline.OneMonth},
kline.IntervalCapacity{Interval: kline.ThreeMonth},
kline.IntervalCapacity{Interval: kline.SixMonth},
),
GlobalResultLimit: 1000,
},
},
Subscriptions: defaultSubscriptions.Clone(),
}
e.Requester, err = request.New(e.Name,
common.NewHTTPClientWithTimeout(exchange.DefaultHTTPTimeout),
request.WithLimiter(packageRateLimits),
)
if err != nil {
log.Errorln(log.ExchangeSys, err)
}
// TODO: Majority of margin REST endpoints are labelled as deprecated on the API docs. These will need to be removed.
err = e.DisableAssetWebsocketSupport(asset.Margin)
if err != nil {
log.Errorln(log.ExchangeSys, err)
}
// TODO: Add websocket cross margin support.
err = e.DisableAssetWebsocketSupport(asset.CrossMargin)
if err != nil {
log.Errorln(log.ExchangeSys, err)
}
e.API.Endpoints = e.NewEndpoints()
err = e.API.Endpoints.SetDefaultEndpoints(map[exchange.URL]string{
exchange.RestSpot: gateioTradeURL,
exchange.RestFutures: gateioFuturesLiveTradingAlternative,
exchange.RestSpotSupplementary: gateioFuturesTestnetTrading,
exchange.WebsocketSpot: gateioWebsocketEndpoint,
})
if err != nil {
log.Errorln(log.ExchangeSys, err)
}
e.Websocket = websocket.NewManager()
e.WebsocketResponseMaxLimit = exchange.DefaultWebsocketResponseMaxLimit
e.WebsocketResponseCheckTimeout = exchange.DefaultWebsocketResponseCheckTimeout
e.WebsocketOrderbookBufferLimit = exchange.DefaultWebsocketOrderbookBufferLimit
e.wsOBUpdateMgr = newWsOBUpdateManager(defaultWSSnapshotSyncDelay)
}
// Setup sets user configuration
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,
Features: &e.Features.Supports.WebsocketCapabilities,
FillsFeed: e.Features.Enabled.FillsFeed,
TradeFeed: e.Features.Enabled.TradeFeed,
UseMultiConnectionManagement: true,
RateLimitDefinitions: packageRateLimits,
})
if err != nil {
return err
}
// Spot connection
err = e.Websocket.SetupNewConnection(&websocket.ConnectionSetup{
URL: gateioWebsocketEndpoint,
ResponseCheckTimeout: exch.WebsocketResponseCheckTimeout,
ResponseMaxLimit: exch.WebsocketResponseMaxLimit,
Handler: e.WsHandleSpotData,
Subscriber: e.Subscribe,
Unsubscriber: e.Unsubscribe,
GenerateSubscriptions: e.generateSubscriptionsSpot,
Connector: e.WsConnectSpot,
Authenticate: e.authenticateSpot,
MessageFilter: asset.Spot,
})
if err != nil {
return err
}
// Futures connection - USDT margined
err = e.Websocket.SetupNewConnection(&websocket.ConnectionSetup{
URL: usdtFuturesWebsocketURL,
ResponseCheckTimeout: exch.WebsocketResponseCheckTimeout,
ResponseMaxLimit: exch.WebsocketResponseMaxLimit,
Handler: func(ctx context.Context, conn websocket.Connection, incoming []byte) error {
return e.WsHandleFuturesData(ctx, conn, incoming, asset.USDTMarginedFutures)
},
Subscriber: e.FuturesSubscribe,
Unsubscriber: e.FuturesUnsubscribe,
GenerateSubscriptions: func() (subscription.List, error) {
return e.GenerateFuturesDefaultSubscriptions(asset.USDTMarginedFutures)
},
Connector: e.WsFuturesConnect,
Authenticate: e.authenticateFutures,
MessageFilter: asset.USDTMarginedFutures,
})
if err != nil {
return err
}
// Futures connection - BTC margined
err = e.Websocket.SetupNewConnection(&websocket.ConnectionSetup{
URL: btcFuturesWebsocketURL,
ResponseCheckTimeout: exch.WebsocketResponseCheckTimeout,
ResponseMaxLimit: exch.WebsocketResponseMaxLimit,
Handler: func(ctx context.Context, conn websocket.Connection, incoming []byte) error {
return e.WsHandleFuturesData(ctx, conn, incoming, asset.CoinMarginedFutures)
},
Subscriber: e.FuturesSubscribe,
Unsubscriber: e.FuturesUnsubscribe,
GenerateSubscriptions: func() (subscription.List, error) {
return e.GenerateFuturesDefaultSubscriptions(asset.CoinMarginedFutures)
},
Connector: e.WsFuturesConnect,
MessageFilter: asset.CoinMarginedFutures,
})
if err != nil {
return err
}
// TODO: Add BTC margined delivery futures.
// Futures connection - Delivery - USDT margined
err = e.Websocket.SetupNewConnection(&websocket.ConnectionSetup{
URL: deliveryRealUSDTTradingURL,
ResponseCheckTimeout: exch.WebsocketResponseCheckTimeout,
ResponseMaxLimit: exch.WebsocketResponseMaxLimit,
Handler: func(ctx context.Context, conn websocket.Connection, incoming []byte) error {
return e.WsHandleFuturesData(ctx, conn, incoming, asset.DeliveryFutures)
},
Subscriber: e.DeliveryFuturesSubscribe,
Unsubscriber: e.DeliveryFuturesUnsubscribe,
GenerateSubscriptions: e.GenerateDeliveryFuturesDefaultSubscriptions,
Connector: e.WsDeliveryFuturesConnect,
MessageFilter: asset.DeliveryFutures,
})
if err != nil {
return err
}
// Futures connection - Options
return e.Websocket.SetupNewConnection(&websocket.ConnectionSetup{
URL: optionsWebsocketURL,
ResponseCheckTimeout: exch.WebsocketResponseCheckTimeout,
ResponseMaxLimit: exch.WebsocketResponseMaxLimit,
Handler: e.WsHandleOptionsData,
Subscriber: e.OptionsSubscribe,
Unsubscriber: e.OptionsUnsubscribe,
GenerateSubscriptions: e.GenerateOptionsDefaultSubscriptions,
Connector: e.WsOptionsConnect,
MessageFilter: asset.Options,
})
}
// UpdateTicker updates and returns the ticker for a currency pair
func (e *Exchange) UpdateTicker(ctx context.Context, p currency.Pair, a asset.Item) (*ticker.Price, error) {
if !e.SupportsAsset(a) {
return nil, fmt.Errorf("%w asset type: %v", asset.ErrNotSupported, a)
}
fPair, err := e.FormatExchangeCurrency(p, a)
if err != nil {
return nil, err
}
if fPair.IsEmpty() || fPair.Quote.IsEmpty() {
return nil, currency.ErrCurrencyPairEmpty
}
fPair = fPair.Upper()
var tickerData *ticker.Price
switch a {
case asset.Margin, asset.Spot, asset.CrossMargin:
available, err := e.checkInstrumentAvailabilityInSpot(fPair)
if err != nil {
return nil, err
}
if a != asset.Spot && !available {
return nil, fmt.Errorf("%v instrument %v does not have ticker data", a, fPair)
}
tickerNew, err := e.GetTicker(ctx, fPair.String(), "")
if err != nil {
return nil, err
}
tickerData = &ticker.Price{
Pair: fPair,
Low: tickerNew.Low24H.Float64(),
High: tickerNew.High24H.Float64(),
Bid: tickerNew.HighestBid.Float64(),
Ask: tickerNew.LowestAsk.Float64(),
Last: tickerNew.Last.Float64(),
ExchangeName: e.Name,
AssetType: a,
}
case asset.USDTMarginedFutures, asset.CoinMarginedFutures, asset.DeliveryFutures:
settle, err := getSettlementCurrency(fPair, a)
if err != nil {
return nil, err
}
var tickers []FuturesTicker
if a == asset.DeliveryFutures {
tickers, err = e.GetDeliveryFutureTickers(ctx, settle, fPair)
} else {
tickers, err = e.GetFuturesTickers(ctx, settle, fPair)
}
if err != nil {
return nil, err
}
if len(tickers) != 1 {
return nil, errNoTickerData
}
tickerData = &ticker.Price{
Pair: fPair,
Low: tickers[0].Low24H.Float64(),
High: tickers[0].High24H.Float64(),
Last: tickers[0].Last.Float64(),
Volume: tickers[0].Volume24HBase.Float64(),
QuoteVolume: tickers[0].Volume24HQuote.Float64(),
ExchangeName: e.Name,
AssetType: a,
}
case asset.Options:
var underlying currency.Pair
var tickers []OptionsTicker
underlying, err = e.GetUnderlyingFromCurrencyPair(fPair)
if err != nil {
return nil, err
}
tickers, err = e.GetOptionsTickers(ctx, underlying.String())
if err != nil {
return nil, err
}
for x := range tickers {
if !tickers[x].Name.Equal(fPair) {
continue
}
cleanQuote := strings.ReplaceAll(tickers[x].Name.Quote.String(), currency.UnderscoreDelimiter, currency.DashDelimiter)
tickers[x].Name.Quote = currency.NewCode(cleanQuote)
tickerData = &ticker.Price{
Pair: tickers[x].Name,
Last: tickers[x].LastPrice.Float64(),
Bid: tickers[x].Bid1Price.Float64(),
Ask: tickers[x].Ask1Price.Float64(),
AskSize: tickers[x].Ask1Size,
BidSize: tickers[x].Bid1Size,
ExchangeName: e.Name,
AssetType: a,
}
err = ticker.ProcessTicker(tickerData)
if err != nil {
return nil, err
}
}
return ticker.GetTicker(e.Name, fPair, a)
}
if err := ticker.ProcessTicker(tickerData); err != nil {
return nil, err
}
return ticker.GetTicker(e.Name, fPair, a)
}
// FetchTradablePairs returns a list of the exchanges tradable pairs
func (e *Exchange) FetchTradablePairs(ctx context.Context, a asset.Item) (currency.Pairs, error) {
if !e.SupportsAsset(a) {
return nil, fmt.Errorf("%w asset type: %v", asset.ErrNotSupported, a)
}
switch a {
case asset.Spot:
tradables, err := e.ListSpotCurrencyPairs(ctx)
if err != nil {
return nil, err
}
pairs := make([]currency.Pair, 0, len(tradables))
for x := range tradables {
if tradables[x].TradeStatus == "untradable" {
continue
}
pairs = append(pairs, currency.NewPair(tradables[x].Base, tradables[x].Quote))
}
return pairs, nil
case asset.Margin, asset.CrossMargin:
tradables, err := e.GetMarginSupportedCurrencyPairs(ctx)
if err != nil {
return nil, err
}
pairs := make([]currency.Pair, 0, len(tradables))
for x := range tradables {
if tradables[x].Status == 0 {
continue
}
p := strings.ToUpper(tradables[x].Base + currency.UnderscoreDelimiter + tradables[x].Quote)
cp, err := currency.NewPairFromString(p)
if err != nil {
return nil, err
}
pairs = append(pairs, cp)
}
return pairs, nil
case asset.CoinMarginedFutures, asset.USDTMarginedFutures:
settle, err := getSettlementCurrency(currency.EMPTYPAIR, a)
if err != nil {
return nil, err
}
contracts, err := e.GetAllFutureContracts(ctx, settle)
if err != nil {
return nil, err
}
pairs := make([]currency.Pair, 0, len(contracts))
for i := range contracts {
if !contracts[i].DelistedTime.Time().IsZero() && contracts[i].DelistedTime.Time().Before(time.Now()) {
continue
}
pairs = append(pairs, contracts[i].Name)
}
return slices.Clip(pairs), nil
case asset.DeliveryFutures:
contracts, err := e.GetAllDeliveryContracts(ctx, currency.USDT)
if err != nil {
return nil, err
}
pairs := make([]currency.Pair, 0, len(contracts))
for i := range contracts {
if contracts[i].InDelisting {
continue
}
p := strings.ToUpper(contracts[i].Name)
cp, err := currency.NewPairFromString(p)
if err != nil {
return nil, err
}
pairs = append(pairs, cp)
}
return slices.Clip(pairs), nil
case asset.Options:
underlyings, err := e.GetAllOptionsUnderlyings(ctx)
if err != nil {
return nil, err
}
var pairs []currency.Pair
for x := range underlyings {
contracts, err := e.GetAllContractOfUnderlyingWithinExpiryDate(ctx, underlyings[x].Name, time.Time{})
if err != nil {
return nil, err
}
for c := range contracts {
cp, err := currency.NewPairFromString(strings.ReplaceAll(contracts[c].Name, currency.DashDelimiter, currency.UnderscoreDelimiter))
if err != nil {
return nil, err
}
cp.Quote = currency.NewCode(strings.ReplaceAll(cp.Quote.String(), currency.UnderscoreDelimiter, currency.DashDelimiter))
pairs = append(pairs, cp)
}
}
return pairs, nil
default:
return nil, fmt.Errorf("%w asset type: %v", asset.ErrNotSupported, a)
}
}
// UpdateTradablePairs updates the exchanges available pairs and stores
// them in the exchanges config
func (e *Exchange) UpdateTradablePairs(ctx context.Context) error {
assets := e.GetAssetTypes(false)
for x := range assets {
pairs, err := e.FetchTradablePairs(ctx, assets[x])
if err != nil {
return err
}
if err := e.UpdatePairs(pairs, assets[x], false); err != nil {
return err
}
}
return e.EnsureOnePairEnabled()
}
// UpdateTickers updates the ticker for all currency pairs of a given asset type
func (e *Exchange) UpdateTickers(ctx context.Context, a asset.Item) error {
if !e.SupportsAsset(a) {
return fmt.Errorf("%w asset type: %v", asset.ErrNotSupported, a)
}
switch a {
case asset.Spot, asset.Margin, asset.CrossMargin:
tickers, err := e.GetTickers(ctx, currency.EMPTYPAIR.String(), "")
if err != nil {
return err
}
for x := range tickers {
var currencyPair currency.Pair
currencyPair, err = currency.NewPairFromString(tickers[x].CurrencyPair)
if err != nil {
return err
}
err = ticker.ProcessTicker(&ticker.Price{
Last: tickers[x].Last.Float64(),
High: tickers[x].High24H.Float64(),
Low: tickers[x].Low24H.Float64(),
Bid: tickers[x].HighestBid.Float64(),
Ask: tickers[x].LowestAsk.Float64(),
QuoteVolume: tickers[x].QuoteVolume.Float64(),
Volume: tickers[x].BaseVolume.Float64(),
ExchangeName: e.Name,
Pair: currencyPair,
AssetType: a,
})
if err != nil {
return err
}
}
case asset.CoinMarginedFutures, asset.USDTMarginedFutures, asset.DeliveryFutures:
settle, errs := getSettlementCurrency(currency.EMPTYPAIR, a)
if errs != nil {
return errs
}
var tickers []FuturesTicker
if a == asset.DeliveryFutures {
tickers, errs = e.GetDeliveryFutureTickers(ctx, settle, currency.EMPTYPAIR)
} else {
tickers, errs = e.GetFuturesTickers(ctx, settle, currency.EMPTYPAIR)
}
for i := range tickers {
currencyPair, err := currency.NewPairFromString(tickers[i].Contract)
if err != nil {
errs = common.AppendError(errs, err)
continue
}
if err = ticker.ProcessTicker(&ticker.Price{
Last: tickers[i].Last.Float64(),
High: tickers[i].High24H.Float64(),
Low: tickers[i].Low24H.Float64(),
Volume: tickers[i].Volume24H.Float64(),
QuoteVolume: tickers[i].Volume24HQuote.Float64(),
ExchangeName: e.Name,
Pair: currencyPair,
AssetType: a,
}); err != nil {
errs = common.AppendError(errs, err)
}
}
return errs
case asset.Options:
pairs, err := e.GetEnabledPairs(a)
if err != nil {
return err
}
for i := range pairs {
underlying, err := e.GetUnderlyingFromCurrencyPair(pairs[i])
if err != nil {
return err
}
tickers, err := e.GetOptionsTickers(ctx, underlying.String())
if err != nil {
return err
}
for x := range tickers {
err = ticker.ProcessTicker(&ticker.Price{
Last: tickers[x].LastPrice.Float64(),
Ask: tickers[x].Ask1Price.Float64(),
AskSize: tickers[x].Ask1Size,
Bid: tickers[x].Bid1Price.Float64(),
BidSize: tickers[x].Bid1Size,
Pair: tickers[x].Name,
ExchangeName: e.Name,
AssetType: a,
})
if err != nil {
return err
}
}
}
default:
return fmt.Errorf("%w asset type: %v", asset.ErrNotSupported, a)
}
return nil
}
// UpdateOrderbook updates and returns the orderbook for a currency pair
func (e *Exchange) UpdateOrderbook(ctx context.Context, p currency.Pair, a asset.Item) (*orderbook.Book, error) {
return e.UpdateOrderbookWithLimit(ctx, p, a, 0)
}
// UpdateOrderbookWithLimit updates and returns the orderbook for a currency pair with a set orderbook size limit
func (e *Exchange) UpdateOrderbookWithLimit(ctx context.Context, p currency.Pair, a asset.Item, limit uint64) (*orderbook.Book, error) {
p, err := e.FormatExchangeCurrency(p, a)
if err != nil {
return nil, err
}
var o *Orderbook
switch a {
case asset.Spot, asset.Margin, asset.CrossMargin:
var available bool
available, err = e.checkInstrumentAvailabilityInSpot(p)
if err != nil {
return nil, err
}
if a != asset.Spot && !available {
return nil, fmt.Errorf("%v instrument %v does not have orderbook data", a, p)
}
o, err = e.GetOrderbook(ctx, p.String(), "", limit, true)
case asset.CoinMarginedFutures, asset.USDTMarginedFutures:
var settle currency.Code
settle, err = getSettlementCurrency(p, a)
if err != nil {
return nil, err
}
o, err = e.GetFuturesOrderbook(ctx, settle, p.String(), "", limit, true)
case asset.DeliveryFutures:
o, err = e.GetDeliveryOrderbook(ctx, currency.USDT, "", p, limit, true)
case asset.Options:
o, err = e.GetOptionsOrderbook(ctx, p, "", limit, true)
default:
return nil, fmt.Errorf("%w %q", asset.ErrNotSupported, a)
}
if err != nil {
return nil, err
}
ob := &orderbook.Book{
Exchange: e.Name,
Asset: a,
ValidateOrderbook: e.ValidateOrderbook,
Pair: p,
LastUpdateID: o.ID,
LastUpdated: o.Update.Time(),
LastPushed: o.Current.Time(),
Bids: o.Bids.Levels(),
Asks: o.Asks.Levels(),
}
if err := ob.Process(); err != nil {
return nil, err
}
return orderbook.Get(e.Name, p, a)
}
// UpdateAccountInfo retrieves balances for all enabled currencies for the
func (e *Exchange) UpdateAccountInfo(ctx context.Context, a asset.Item) (account.Holdings, error) {
info := account.Holdings{
Exchange: e.Name,
Accounts: []account.SubAccount{{
AssetType: a,
}},
}
switch a {
case asset.Spot:
balances, err := e.GetSpotAccounts(ctx, currency.EMPTYCODE)
if err != nil {
return info, err
}
currencies := make([]account.Balance, len(balances))
for i := range balances {
currencies[i] = account.Balance{
Currency: currency.NewCode(balances[i].Currency),
Total: balances[i].Available.Float64() + balances[i].Locked.Float64(),
Hold: balances[i].Locked.Float64(),
Free: balances[i].Available.Float64(),
}
}
info.Accounts[0].Currencies = currencies
case asset.Margin, asset.CrossMargin:
balances, err := e.GetMarginAccountList(ctx, currency.EMPTYPAIR)
if err != nil {
return info, err
}
currencies := make([]account.Balance, 0, 2*len(balances))
for i := range balances {
currencies = append(currencies,
account.Balance{
Currency: currency.NewCode(balances[i].Base.Currency),
Total: balances[i].Base.Available.Float64() + balances[i].Base.LockedAmount.Float64(),
Hold: balances[i].Base.LockedAmount.Float64(),
Free: balances[i].Base.Available.Float64(),
},
account.Balance{
Currency: currency.NewCode(balances[i].Quote.Currency),
Total: balances[i].Quote.Available.Float64() + balances[i].Quote.LockedAmount.Float64(),
Hold: balances[i].Quote.LockedAmount.Float64(),
Free: balances[i].Quote.Available.Float64(),
})
}
info.Accounts[0].Currencies = currencies
case asset.CoinMarginedFutures, asset.USDTMarginedFutures, asset.DeliveryFutures:
settle, err := getSettlementCurrency(currency.EMPTYPAIR, a)
if err != nil {
return info, err
}
var acc *FuturesAccount
if a == asset.DeliveryFutures {
acc, err = e.GetDeliveryFuturesAccounts(ctx, settle)
} else {
acc, err = e.QueryFuturesAccount(ctx, settle)
}
if err != nil {
return info, err
}
info.Accounts[0].Currencies = []account.Balance{{
Currency: currency.NewCode(acc.Currency),
Total: acc.Total.Float64(),
Hold: acc.Total.Float64() - acc.Available.Float64(),
Free: acc.Available.Float64(),
}}
case asset.Options:
balance, err := e.GetOptionAccounts(ctx)
if err != nil {
return info, err
}
info.Accounts[0].Currencies = []account.Balance{{
Currency: currency.NewCode(balance.Currency),
Total: balance.Total.Float64(),
Hold: balance.Total.Float64() - balance.Available.Float64(),
Free: balance.Available.Float64(),
}}
default:
return info, fmt.Errorf("%w asset type: %v", asset.ErrNotSupported, a)
}
creds, err := e.GetCredentials(ctx)
if err == nil {
err = account.Process(&info, creds)
}
return info, err
}
// GetAccountFundingHistory returns funding history, deposits and
// withdrawals
func (e *Exchange) GetAccountFundingHistory(_ context.Context) ([]exchange.FundingHistory, error) {
return nil, common.ErrFunctionNotSupported
}
// GetWithdrawalsHistory returns previous withdrawals data
func (e *Exchange) GetWithdrawalsHistory(ctx context.Context, c currency.Code, _ asset.Item) ([]exchange.WithdrawalHistory, error) {
records, err := e.GetWithdrawalRecords(ctx, c, time.Time{}, time.Time{}, 0, 0)
if err != nil {
return nil, err
}
withdrawalHistories := make([]exchange.WithdrawalHistory, len(records))
for x := range records {
withdrawalHistories[x] = exchange.WithdrawalHistory{
Status: records[x].Status,
TransferID: records[x].ID,
Currency: records[x].Currency,
Amount: records[x].Amount.Float64(),
CryptoTxID: records[x].TransactionID,
CryptoToAddress: records[x].WithdrawalAddress,
Timestamp: records[x].Timestamp.Time(),
}
}
return withdrawalHistories, nil
}
// GetRecentTrades returns the most recent trades for a currency and asset
func (e *Exchange) GetRecentTrades(ctx context.Context, p currency.Pair, a asset.Item) ([]trade.Data, error) {
p, err := e.FormatExchangeCurrency(p, a)
if err != nil {
return nil, err
}
var resp []trade.Data
switch a {
case asset.Spot, asset.Margin, asset.CrossMargin:
if p.IsEmpty() {
return nil, currency.ErrCurrencyPairEmpty
}
tradeData, err := e.GetMarketTrades(ctx, p, 0, "", false, time.Time{}, time.Time{}, 0)
if err != nil {
return nil, err
}
resp = make([]trade.Data, len(tradeData))
for i := range tradeData {
side, err := order.StringToOrderSide(tradeData[i].Side)
if err != nil {
return nil, err
}
resp[i] = trade.Data{
Exchange: e.Name,
TID: tradeData[i].OrderID,
CurrencyPair: p,
AssetType: a,
Side: side,
Price: tradeData[i].Price.Float64(),
Amount: tradeData[i].Amount.Float64(),
Timestamp: tradeData[i].CreateTime.Time(),
}
}
case asset.CoinMarginedFutures, asset.USDTMarginedFutures, asset.DeliveryFutures:
settle, err := getSettlementCurrency(p, a)
if err != nil {
return nil, err
}
var futuresTrades []TradingHistoryItem
if a == asset.DeliveryFutures {
futuresTrades, err = e.GetDeliveryTradingHistory(ctx, settle, "", p.Upper(), 0, time.Time{}, time.Time{})
} else {
futuresTrades, err = e.GetFuturesTradingHistory(ctx, settle, p, 0, 0, "", time.Time{}, time.Time{})
}
if err != nil {
return nil, err
}
resp = make([]trade.Data, len(futuresTrades))
for i := range futuresTrades {
resp[i] = trade.Data{
TID: strconv.FormatInt(futuresTrades[i].ID, 10),
Exchange: e.Name,
CurrencyPair: p,
AssetType: a,
Price: futuresTrades[i].Price.Float64(),
Amount: futuresTrades[i].Size,
Timestamp: futuresTrades[i].CreateTime.Time(),
}
}
case asset.Options:
trades, err := e.GetOptionsTradeHistory(ctx, p.Upper(), "", 0, 0, time.Time{}, time.Time{})
if err != nil {
return nil, err
}
resp = make([]trade.Data, len(trades))
for i := range trades {
resp[i] = trade.Data{
TID: strconv.FormatInt(trades[i].ID, 10),
Exchange: e.Name,
CurrencyPair: p,
AssetType: a,
Price: trades[i].Price.Float64(),
Amount: trades[i].Size,
Timestamp: trades[i].CreateTime.Time(),
}
}
default:
return nil, fmt.Errorf("%w asset type: %v", asset.ErrNotSupported, a)
}
err = e.AddTradesToBuffer(resp...)
if err != nil {
return nil, err
}
sort.Sort(trade.ByDate(resp))
return resp, nil
}
// GetHistoricTrades returns historic trade data within the timeframe provided
func (e *Exchange) GetHistoricTrades(_ context.Context, _ currency.Pair, _ asset.Item, _, _ time.Time) ([]trade.Data, error) {
return nil, common.ErrFunctionNotSupported
}
// SubmitOrder submits a new order
// TODO: support multiple order types (IOC)
func (e *Exchange) SubmitOrder(ctx context.Context, s *order.Submit) (*order.SubmitResponse, error) {
err := s.Validate(e.GetTradingRequirements())
if err != nil {
return nil, err
}
s.ClientOrderID = formatClientOrderID(s.ClientOrderID)
s.Pair, err = e.FormatExchangeCurrency(s.Pair, s.AssetType)
if err != nil {
return nil, err
}
s.Pair = s.Pair.Upper()
switch s.AssetType {
case asset.Spot, asset.Margin, asset.CrossMargin:
req, err := e.getSpotOrderRequest(s)
if err != nil {
return nil, err
}
sOrder, err := e.PlaceSpotOrder(ctx, req)
if err != nil {
return nil, err
}
response, err := s.DeriveSubmitResponse(sOrder.OrderID)
if err != nil {
return nil, err
}
side, err := order.StringToOrderSide(sOrder.Side)
if err != nil {
return nil, err
}
response.Side = side
status, err := order.StringToOrderStatus(sOrder.Status)
if err != nil {
return nil, err
}
response.Status = status
response.Fee = sOrder.FeeDeducted.Float64()
response.FeeAsset = currency.NewCode(sOrder.FeeCurrency)
response.Pair = s.Pair
response.Date = sOrder.CreateTime.Time()
response.ClientOrderID = sOrder.Text
response.Date = sOrder.CreateTime.Time()
response.LastUpdated = sOrder.UpdateTime.Time()
return response, nil
case asset.CoinMarginedFutures, asset.USDTMarginedFutures, asset.DeliveryFutures:
// TODO: See https://www.gate.io/docs/developers/apiv4/en/#create-a-futures-order
// * iceberg orders
// * auto_size (close_long, close_short)
// * stp_act (self trade prevention)
fOrder, err := getFuturesOrderRequest(s)
if err != nil {
return nil, err
}
fOrder.Settle, err = getSettlementCurrency(s.Pair, s.AssetType)
if err != nil {
return nil, err
}
op := e.PlaceFuturesOrder
if s.AssetType == asset.DeliveryFutures {
op = e.PlaceDeliveryOrder
}
o, err := op(ctx, fOrder)
if err != nil {
return nil, err
}
resp, err := s.DeriveSubmitResponse(strconv.FormatInt(o.ID, 10))
if err != nil {
return nil, err
}
resp.Status = order.Open
if o.Status != statusOpen {
if resp.Status, err = order.StringToOrderStatus(o.FinishAs); err != nil {
return nil, err
}
}
resp.Date = o.CreateTime.Time()
resp.ClientOrderID = getClientOrderIDFromText(o.Text)
resp.Amount = math.Abs(o.Size)
resp.Price = o.OrderPrice.Float64()
resp.AverageExecutedPrice = o.FillPrice.Float64()
return resp, nil
case asset.Options:
optionOrder, err := e.PlaceOptionOrder(ctx, &OptionOrderParam{
Contract: s.Pair.String(),
OrderSize: s.Amount,
Price: types.Number(s.Price),
ReduceOnly: s.ReduceOnly,
Text: s.ClientOrderID,
})
if err != nil {
return nil, err
}
response, err := s.DeriveSubmitResponse(strconv.FormatInt(optionOrder.OptionOrderID, 10))
if err != nil {
return nil, err
}
status, err := order.StringToOrderStatus(optionOrder.Status)
if err != nil {
return nil, err
}
response.Status = status
response.Pair = s.Pair
response.Date = optionOrder.CreateTime.Time()
response.ClientOrderID = optionOrder.Text
return response, nil
default:
return nil, fmt.Errorf("%w asset type: %v", asset.ErrNotSupported, s.AssetType)
}
}
// ModifyOrder modifies an existing order
func (e *Exchange) ModifyOrder(context.Context, *order.Modify) (*order.ModifyResponse, error) {
return nil, common.ErrFunctionNotSupported
}
// CancelOrder cancels an order by its corresponding ID number
func (e *Exchange) CancelOrder(ctx context.Context, o *order.Cancel) error {
if err := o.Validate(o.StandardCancel()); err != nil {
return err
}
fPair, err := e.FormatExchangeCurrency(o.Pair, o.AssetType)
if err != nil {
return err
}
switch o.AssetType {
case asset.Spot, asset.Margin, asset.CrossMargin:
_, err = e.CancelSingleSpotOrder(ctx, o.OrderID, fPair.String(), o.AssetType == asset.CrossMargin)
case asset.CoinMarginedFutures, asset.USDTMarginedFutures, asset.DeliveryFutures:
var settle currency.Code
if settle, err = getSettlementCurrency(o.Pair, o.AssetType); err == nil {
if o.AssetType == asset.DeliveryFutures {
_, err = e.CancelSingleDeliveryOrder(ctx, settle, o.OrderID)
} else {
_, err = e.CancelSingleFuturesOrder(ctx, settle, o.OrderID)
}
}
case asset.Options:
_, err = e.CancelOptionSingleOrder(ctx, o.OrderID)
default:
return fmt.Errorf("%w asset type: %v", asset.ErrNotSupported, o.AssetType)
}
return err
}
// CancelBatchOrders cancels an orders by their corresponding ID numbers
func (e *Exchange) CancelBatchOrders(ctx context.Context, o []order.Cancel) (*order.CancelBatchResponse, error) {
response := order.CancelBatchResponse{
Status: map[string]string{},
}
if len(o) == 0 {
return nil, errors.New("no cancel order passed")
}
var err error
var cancelSpotOrdersParam []CancelOrderByIDParam
a := o[0].AssetType
for x := range o {
o[x].Pair, err = e.FormatExchangeCurrency(o[x].Pair, a)
if err != nil {
return nil, err
}
o[x].Pair = o[x].Pair.Upper()
if a != o[x].AssetType {
return nil, errors.New("cannot cancel orders of different asset types")
}
if a == asset.Spot || a == asset.Margin || a == asset.CrossMargin {
cancelSpotOrdersParam = append(cancelSpotOrdersParam, CancelOrderByIDParam{
ID: o[x].OrderID,
CurrencyPair: o[x].Pair,
})
continue
}
err = o[x].Validate(o[x].StandardCancel())
if err != nil {
return nil, err
}
}
switch a {
case asset.Spot, asset.Margin, asset.CrossMargin:
loop := int(math.Ceil(float64(len(cancelSpotOrdersParam)) / 10))
for count := range loop {
var input []CancelOrderByIDParam
if (count + 1) == loop {
input = cancelSpotOrdersParam[count*10:]
} else {
input = cancelSpotOrdersParam[count*10 : (count*10)+10]
}
var cancel []CancelOrderByIDResponse
cancel, err = e.CancelBatchOrdersWithIDList(ctx, input)
if err != nil {
return nil, err
}
for j := range cancel {
if cancel[j].Succeeded {
response.Status[cancel[j].OrderID] = order.Cancelled.String()
}
}
}
case asset.CoinMarginedFutures, asset.USDTMarginedFutures, asset.DeliveryFutures:
for i := range o {
settle, err := getSettlementCurrency(o[i].Pair, a)
if err != nil {
return nil, err
}
var resp []Order
if a == asset.DeliveryFutures {
resp, err = e.CancelMultipleDeliveryOrders(ctx, o[i].Pair, o[i].Side.Lower(), settle)
} else {
resp, err = e.CancelMultipleFuturesOpenOrders(ctx, o[i].Pair, o[i].Side.Lower(), settle)
}
if err != nil {
return nil, err
}
for j := range resp {
response.Status[strconv.FormatInt(resp[j].ID, 10)] = resp[j].Status
}
}
case asset.Options:
for i := range o {
cancel, err := e.CancelMultipleOptionOpenOrders(ctx, o[i].Pair, o[i].Pair.String(), o[i].Side.Lower())
if err != nil {
return nil, err
}
for j := range cancel {
response.Status[strconv.FormatInt(cancel[j].OptionOrderID, 10)] = cancel[j].Status
}
}
default:
return nil, fmt.Errorf("%w asset type: %v", asset.ErrNotSupported, a)
}
return &response, nil
}
// CancelAllOrders cancels all orders associated with a currency pair
func (e *Exchange) CancelAllOrders(ctx context.Context, o *order.Cancel) (order.CancelAllResponse, error) {
var resp order.CancelAllResponse
if err := o.Validate(); err != nil {
return resp, err
}
fmtPair, err := e.FormatExchangeCurrency(o.Pair, o.AssetType)
if err != nil {
return resp, err
}
var side string
switch {
case o.Side.IsLong():
side = order.Bid.Lower()
case o.Side.IsShort():
side = order.Ask.Lower()
case o.Side == order.UnknownSide, o.Side == order.AnySide:
default:
return resp, fmt.Errorf("%w: %q", order.ErrSideIsInvalid, o.Side)
}
switch o.AssetType {
case asset.Spot, asset.Margin, asset.CrossMargin:
cancel, err := e.CancelMultipleSpotOpenOrders(ctx, fmtPair, o.AssetType)
if err != nil {
return resp, err
}
for x := range cancel {
resp.Add(strconv.FormatInt(cancel[x].AutoOrderID, 10), cancel[x].Status)
}
case asset.CoinMarginedFutures, asset.USDTMarginedFutures, asset.DeliveryFutures:
settle, err := getSettlementCurrency(fmtPair, o.AssetType)
if err != nil {
return resp, err
}
var cancel []Order
if o.AssetType == asset.DeliveryFutures {
cancel, err = e.CancelMultipleDeliveryOrders(ctx, fmtPair, side, settle)
} else {
cancel, err = e.CancelMultipleFuturesOpenOrders(ctx, fmtPair, side, settle)
}
if err != nil {
return resp, err
}
for f := range cancel {
resp.Add(strconv.FormatInt(cancel[f].ID, 10), cancel[f].FinishAs)
}
case asset.Options:
var underlying currency.Pair
if !o.Pair.IsEmpty() {
underlying, err = e.GetUnderlyingFromCurrencyPair(o.Pair)
if err != nil {
return resp, err
}
}
cancel, err := e.CancelMultipleOptionOpenOrders(ctx, fmtPair, underlying.String(), side)
if err != nil {
return resp, err
}
for x := range cancel {
resp.Add(strconv.FormatInt(cancel[x].OptionOrderID, 10), cancel[x].FinishAs)
}
default:
return resp, fmt.Errorf("%w asset type: %v", asset.ErrNotSupported, o.AssetType)
}
return resp, nil
}
// GetOrderInfo returns order information based on order ID
func (e *Exchange) GetOrderInfo(ctx context.Context, orderID string, pair currency.Pair, a asset.Item) (*order.Detail, error) {
if err := e.CurrencyPairs.IsAssetEnabled(a); err != nil {
return nil, err
}
pair, err := e.FormatExchangeCurrency(pair, a)
if err != nil {
return nil, err
}
switch a {
case asset.Spot, asset.Margin, asset.CrossMargin:
var spotOrder *SpotOrder
spotOrder, err = e.GetSpotOrder(ctx, orderID, pair, a)
if err != nil {
return nil, err
}
var side order.Side
side, err = order.StringToOrderSide(spotOrder.Side)
if err != nil {
return nil, err
}
var orderType order.Type
orderType, err = order.StringToOrderType(spotOrder.Type)
if err != nil {
return nil, err
}
var orderStatus order.Status
orderStatus, err = order.StringToOrderStatus(spotOrder.Status)
if err != nil {
return nil, err
}
return &order.Detail{
Amount: spotOrder.Amount.Float64(),
Exchange: e.Name,
OrderID: spotOrder.OrderID,
Side: side,
Type: orderType,
Pair: pair,
Cost: spotOrder.FeeDeducted.Float64(),
AssetType: a,
Status: orderStatus,
Price: spotOrder.Price.Float64(),
ExecutedAmount: spotOrder.Amount.Float64() - spotOrder.Left.Float64(),
Date: spotOrder.CreateTime.Time(),
LastUpdated: spotOrder.UpdateTime.Time(),
}, nil
case asset.USDTMarginedFutures, asset.CoinMarginedFutures, asset.DeliveryFutures:
settle, err := getSettlementCurrency(pair, a)
if err != nil {
return nil, err
}
var fOrder *Order
if a == asset.DeliveryFutures {
fOrder, err = e.GetSingleDeliveryOrder(ctx, settle, orderID)
} else {
fOrder, err = e.GetSingleFuturesOrder(ctx, settle, orderID)
}
if err != nil {
return nil, err
}
orderStatus := order.Open
if fOrder.Status != statusOpen {
orderStatus, err = order.StringToOrderStatus(fOrder.FinishAs)
if err != nil {
return nil, err
}
}
pair, err = currency.NewPairFromString(fOrder.Contract)
if err != nil {
return nil, err
}
side, amount, remaining := getSideAndAmountFromSize(fOrder.Size, fOrder.RemainingAmount)
tif, err := timeInForceFromString(fOrder.TimeInForce)
if err != nil {
return nil, err
}
return &order.Detail{
Amount: amount,
ExecutedAmount: amount - remaining,
RemainingAmount: remaining,
Exchange: e.Name,
OrderID: orderID,
ClientOrderID: getClientOrderIDFromText(fOrder.Text),
Status: orderStatus,
Price: fOrder.OrderPrice.Float64(),
AverageExecutedPrice: fOrder.FillPrice.Float64(),
Date: fOrder.CreateTime.Time(),
LastUpdated: fOrder.FinishTime.Time(),
Pair: pair,
AssetType: a,
Type: getTypeFromTimeInForce(fOrder.TimeInForce, fOrder.OrderPrice.Float64()),
TimeInForce: tif,
Side: side,
}, nil
case asset.Options:
optionOrder, err := e.GetSingleOptionOrder(ctx, orderID)
if err != nil {
return nil, err
}
orderStatus, err := order.StringToOrderStatus(optionOrder.Status)
if err != nil {
return nil, err
}
pair, err = currency.NewPairFromString(optionOrder.Contract)
if err != nil {
return nil, err
}
return &order.Detail{
Amount: optionOrder.Size,
ExecutedAmount: optionOrder.Size - optionOrder.Left,
Exchange: e.Name,
OrderID: orderID,
Status: orderStatus,
Price: optionOrder.Price.Float64(),
Date: optionOrder.CreateTime.Time(),
LastUpdated: optionOrder.FinishTime.Time(),
Pair: pair,
AssetType: a,
}, nil
default:
return nil, fmt.Errorf("%w asset type: %v", asset.ErrNotSupported, a)
}
}
// GetDepositAddress returns a deposit address for a specified currency
func (e *Exchange) GetDepositAddress(ctx context.Context, cryptocurrency currency.Code, _, chain string) (*deposit.Address, error) {
addr, err := e.GenerateCurrencyDepositAddress(ctx, cryptocurrency)
if err != nil {
return nil, err
}
if chain != "" {
for x := range addr.MultichainAddresses {
if addr.MultichainAddresses[x].ObtainFailed == 1 {
continue
}
if addr.MultichainAddresses[x].Chain == chain {
return &deposit.Address{
Chain: addr.MultichainAddresses[x].Chain,
Address: addr.MultichainAddresses[x].Address,
Tag: addr.MultichainAddresses[x].PaymentName,
}, nil
}
}
return nil, fmt.Errorf("network %s not found", chain)
}
return &deposit.Address{
Address: addr.Address,
Chain: chain,
}, nil
}
// WithdrawCryptocurrencyFunds returns a withdrawal ID when a withdrawal is
// submitted
func (e *Exchange) WithdrawCryptocurrencyFunds(ctx context.Context, withdrawRequest *withdraw.Request) (*withdraw.ExchangeResponse, error) {
if err := withdrawRequest.Validate(); err != nil {
return nil, err
}
response, err := e.WithdrawCurrency(ctx,
WithdrawalRequestParam{
Amount: types.Number(withdrawRequest.Amount),
Currency: withdrawRequest.Currency,
Address: withdrawRequest.Crypto.Address,
Chain: withdrawRequest.Crypto.Chain,
})
if err != nil {
return nil, err
}
return &withdraw.ExchangeResponse{
Name: response.Chain,
ID: response.TransactionID,
Status: response.Status,
}, nil
}
// 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
}
// GetFeeByType returns an estimate of fee based on 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
}
return e.GetFee(ctx, feeBuilder)
}
// GetActiveOrders retrieves any orders that are active/open
func (e *Exchange) GetActiveOrders(ctx context.Context, req *order.MultiOrderRequest) (order.FilteredOrders, error) {
if err := req.Validate(); err != nil {
return nil, err
}
var orders []order.Detail
format, err := e.GetPairFormat(req.AssetType, false)
if err != nil {
return nil, err
}
switch req.AssetType {
case asset.Spot, asset.Margin, asset.CrossMargin:
spotOrders, err := e.GetSpotOpenOrders(ctx, 0, 0, req.AssetType == asset.CrossMargin)
if err != nil {
return nil, err
}
for x := range spotOrders {
symbol, err := currency.NewPairDelimiter(spotOrders[x].CurrencyPair, format.Delimiter)
if err != nil {
return nil, err
}
for y := range spotOrders[x].Orders {
if spotOrders[x].Orders[y].Status != statusOpen {
continue
}
side, err := order.StringToOrderSide(spotOrders[x].Orders[y].Side)
if err != nil {
log.Errorf(log.ExchangeSys, "%s %v", e.Name, err)
}
oType, err := order.StringToOrderType(spotOrders[x].Orders[y].Type)
if err != nil {
return nil, err
}
status, err := order.StringToOrderStatus(spotOrders[x].Orders[y].Status)
if err != nil {
log.Errorf(log.ExchangeSys, "%s %v", e.Name, err)
}
orders = append(orders, order.Detail{
Side: side,
Type: oType,
Status: status,
Pair: symbol,
OrderID: spotOrders[x].Orders[y].OrderID,
Amount: spotOrders[x].Orders[y].Amount.Float64(),
ExecutedAmount: spotOrders[x].Orders[y].Amount.Float64() - spotOrders[x].Orders[y].Left.Float64(),
RemainingAmount: spotOrders[x].Orders[y].Left.Float64(),
Price: spotOrders[x].Orders[y].Price.Float64(),
AverageExecutedPrice: spotOrders[x].Orders[y].AverageFillPrice.Float64(),
Date: spotOrders[x].Orders[y].CreateTime.Time(),
LastUpdated: spotOrders[x].Orders[y].UpdateTime.Time(),
Exchange: e.Name,
AssetType: req.AssetType,
ClientOrderID: spotOrders[x].Orders[y].Text,
FeeAsset: currency.NewCode(spotOrders[x].Orders[y].FeeCurrency),
})
}
}
case asset.CoinMarginedFutures, asset.USDTMarginedFutures, asset.DeliveryFutures:
settle, err := getSettlementCurrency(currency.EMPTYPAIR, req.AssetType)
if err != nil {
return nil, err
}
var futuresOrders []Order
if req.AssetType == asset.DeliveryFutures {
futuresOrders, err = e.GetDeliveryOrders(ctx, currency.EMPTYPAIR, statusOpen, settle, "", 0, 0, false)
} else {
futuresOrders, err = e.GetFuturesOrders(ctx, currency.EMPTYPAIR, statusOpen, "", settle, 0, 0, false)
}
if err != nil {
return nil, err
}
for i := range futuresOrders {
pair, err := currency.NewPairFromString(futuresOrders[i].Contract)
if err != nil {
return nil, err
}
if futuresOrders[i].Status != statusOpen || (len(req.Pairs) > 0 && !req.Pairs.Contains(pair, true)) {
continue
}
side, amount, remaining := getSideAndAmountFromSize(futuresOrders[i].Size, futuresOrders[i].RemainingAmount)
tif, err := timeInForceFromString(futuresOrders[i].TimeInForce)
if err != nil {
return nil, err
}
orders = append(orders, order.Detail{
Status: order.Open,
Amount: amount,
ContractAmount: amount,
Pair: pair,
OrderID: strconv.FormatInt(futuresOrders[i].ID, 10),
ClientOrderID: getClientOrderIDFromText(futuresOrders[i].Text),
Price: futuresOrders[i].OrderPrice.Float64(),
ExecutedAmount: amount - remaining,
RemainingAmount: remaining,
LastUpdated: futuresOrders[i].FinishTime.Time(),
Date: futuresOrders[i].CreateTime.Time(),
Exchange: e.Name,
AssetType: req.AssetType,
Side: side,
Type: order.Limit,
SettlementCurrency: settle,
ReduceOnly: futuresOrders[i].IsReduceOnly,
TimeInForce: tif,
AverageExecutedPrice: futuresOrders[i].FillPrice.Float64(),
})
}
case asset.Options:
var optionsOrders []OptionOrderResponse
optionsOrders, err = e.GetOptionFuturesOrders(ctx, currency.EMPTYPAIR, "", statusOpen, 0, 0, req.StartTime, req.EndTime)
if err != nil {
return nil, err
}
for i := range optionsOrders {
var currencyPair currency.Pair
var status order.Status
currencyPair, err = currency.NewPairFromString(optionsOrders[i].Contract)
if err != nil {
return nil, err
}
status, err = order.StringToOrderStatus(optionsOrders[i].Status)
if err != nil {
return nil, err
}
orders = append(orders, order.Detail{
Status: status,
Amount: optionsOrders[i].Size,
Pair: currencyPair,
OrderID: strconv.FormatInt(optionsOrders[i].OptionOrderID, 10),
Price: optionsOrders[i].Price.Float64(),
ExecutedAmount: optionsOrders[i].Size - optionsOrders[i].Left,
RemainingAmount: optionsOrders[i].Left,
LastUpdated: optionsOrders[i].FinishTime.Time(),
Date: optionsOrders[i].CreateTime.Time(),
Exchange: e.Name,
AssetType: req.AssetType,
ClientOrderID: optionsOrders[i].Text,
})
}
default:
return nil, fmt.Errorf("%w asset type: %v", asset.ErrNotSupported, req.AssetType)
}
return req.Filter(e.Name, orders), nil
}
// GetOrderHistory retrieves account order information
// Can Limit response to specific order status
func (e *Exchange) GetOrderHistory(ctx context.Context, req *order.MultiOrderRequest) (order.FilteredOrders, error) {
if err := req.Validate(); err != nil {
return nil, err
}
var orders []order.Detail
format, err := e.GetPairFormat(req.AssetType, true)
if err != nil {
return nil, err
}
switch req.AssetType {
case asset.Spot, asset.Margin, asset.CrossMargin:
for x := range req.Pairs {
fPair := req.Pairs[x].Format(format)
spotOrders, err := e.GetMySpotTradingHistory(ctx, fPair, req.FromOrderID, 0, 0, req.AssetType == asset.CrossMargin, req.StartTime, req.EndTime)
if err != nil {
return nil, err
}
for o := range spotOrders {
var side order.Side
side, err = order.StringToOrderSide(spotOrders[o].Side)
if err != nil {
return nil, err
}
detail := order.Detail{
OrderID: spotOrders[o].OrderID,
Amount: spotOrders[o].Amount.Float64(),
ExecutedAmount: spotOrders[o].Amount.Float64(),
Price: spotOrders[o].Price.Float64(),
Date: spotOrders[o].CreateTime.Time(),
Side: side,
Exchange: e.Name,
Pair: fPair,
AssetType: req.AssetType,
Fee: spotOrders[o].Fee.Float64(),
FeeAsset: currency.NewCode(spotOrders[o].FeeCurrency),
}
detail.InferCostsAndTimes()
orders = append(orders, detail)
}
}
case asset.CoinMarginedFutures, asset.USDTMarginedFutures, asset.DeliveryFutures:
for x := range req.Pairs {
fPair := req.Pairs[x].Format(format)
settle, err := getSettlementCurrency(fPair, req.AssetType)
if err != nil {
return nil, err
}
var futuresOrder []TradingHistoryItem
if req.AssetType == asset.DeliveryFutures {
futuresOrder, err = e.GetMyDeliveryTradingHistory(ctx, settle, req.FromOrderID, fPair, 0, 0, 0, "")
} else {
futuresOrder, err = e.GetMyFuturesTradingHistory(ctx, settle, "", req.FromOrderID, fPair, 0, 0, 0)
}
if err != nil {
return nil, err
}
for o := range futuresOrder {
detail := order.Detail{
OrderID: strconv.FormatInt(futuresOrder[o].ID, 10),
Amount: futuresOrder[o].Size,
Price: futuresOrder[o].Price.Float64(),
Date: futuresOrder[o].CreateTime.Time(),
Exchange: e.Name,
Pair: fPair,
AssetType: req.AssetType,
}
detail.InferCostsAndTimes()
orders = append(orders, detail)
}
}
case asset.Options:
for x := range req.Pairs {
fPair := req.Pairs[x].Format(format)
optionOrders, err := e.GetMyOptionsTradingHistory(ctx, fPair.String(), fPair.Upper(), 0, 0, req.StartTime, req.EndTime)
if err != nil {
return nil, err
}
for o := range optionOrders {
detail := order.Detail{
OrderID: strconv.FormatInt(optionOrders[o].OrderID, 10),
Amount: optionOrders[o].Size,
Price: optionOrders[o].Price.Float64(),
Date: optionOrders[o].CreateTime.Time(),
Exchange: e.Name,
Pair: fPair,
AssetType: req.AssetType,
}
detail.InferCostsAndTimes()
orders = append(orders, detail)
}
}
default:
return nil, fmt.Errorf("%w asset type: %v", asset.ErrNotSupported, req.AssetType)
}
return req.Filter(e.Name, orders), nil
}
// 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
}
var listCandlesticks []kline.Candle
switch a {
case asset.Spot, asset.Margin, asset.CrossMargin:
candles, err := e.GetCandlesticks(ctx, req.RequestFormatted, 0, start, end, interval)
if err != nil {
return nil, err
}
listCandlesticks = make([]kline.Candle, len(candles))
for i := range candles {
listCandlesticks[i] = kline.Candle{
Time: candles[i].Timestamp.Time(),
Open: candles[i].OpenPrice.Float64(),
High: candles[i].HighestPrice.Float64(),
Low: candles[i].LowestPrice.Float64(),
Close: candles[i].ClosePrice.Float64(),
Volume: candles[i].BaseCcyAmount.Float64(),
}
}
case asset.CoinMarginedFutures, asset.USDTMarginedFutures, asset.DeliveryFutures:
settle, err := getSettlementCurrency(pair, a)
if err != nil {
return nil, err
}
var candles []FuturesCandlestick
if a == asset.DeliveryFutures {
candles, err = e.GetDeliveryFuturesCandlesticks(ctx, settle, req.RequestFormatted.Upper(), start, end, 0, interval)
} else {
candles, err = e.GetFuturesCandlesticks(ctx, settle, req.RequestFormatted.String(), start, end, 0, interval)
}
if err != nil {
return nil, err
}
listCandlesticks = make([]kline.Candle, len(candles))
for i := range candles {
listCandlesticks[i] = kline.Candle{
Time: candles[i].Timestamp.Time(),
Open: candles[i].OpenPrice.Float64(),
High: candles[i].HighestPrice.Float64(),
Low: candles[i].LowestPrice.Float64(),
Close: candles[i].ClosePrice.Float64(),
Volume: candles[i].Volume,
}
}
default:
return nil, fmt.Errorf("%w asset type: %v", asset.ErrNotSupported, a)
}
return req.ProcessResponse(listCandlesticks)
}
// 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
}
candlestickItems := make([]kline.Candle, 0, req.Size())
for _, r := range req.RangeHolder.Ranges {
switch a {
case asset.Spot, asset.Margin, asset.CrossMargin:
candles, err := e.GetCandlesticks(ctx, req.RequestFormatted, 0, r.Start.Time, r.End.Time, interval)
if err != nil {
return nil, err
}
for j := range candles {
candlestickItems = append(candlestickItems, kline.Candle{
Time: candles[j].Timestamp.Time(),
Open: candles[j].OpenPrice.Float64(),
High: candles[j].HighestPrice.Float64(),
Low: candles[j].LowestPrice.Float64(),
Close: candles[j].ClosePrice.Float64(),
Volume: candles[j].QuoteCcyVolume.Float64(),
})
}
case asset.CoinMarginedFutures, asset.USDTMarginedFutures, asset.DeliveryFutures:
settle, err := getSettlementCurrency(pair, a)
if err != nil {
return nil, err
}
var candles []FuturesCandlestick
if a == asset.DeliveryFutures {
candles, err = e.GetDeliveryFuturesCandlesticks(ctx, settle, req.RequestFormatted.Upper(), r.Start.Time, r.End.Time, 0, interval)
} else {
candles, err = e.GetFuturesCandlesticks(ctx, settle, req.RequestFormatted.String(), r.Start.Time, r.End.Time, 0, interval)
}
if err != nil {
return nil, err
}
for i := range candles {
candlestickItems = append(candlestickItems, kline.Candle{
Time: candles[i].Timestamp.Time(),
Open: candles[i].OpenPrice.Float64(),
High: candles[i].HighestPrice.Float64(),
Low: candles[i].LowestPrice.Float64(),
Close: candles[i].ClosePrice.Float64(),
Volume: candles[i].Volume,
})
}
default:
return nil, fmt.Errorf("%w asset type: %v", asset.ErrNotSupported, a)
}
}
return req.ProcessResponse(candlestickItems)
}
// GetAvailableTransferChains returns the available transfer blockchains for the specific cryptocurrency
func (e *Exchange) GetAvailableTransferChains(ctx context.Context, cryptocurrency currency.Code) ([]string, error) {
chains, err := e.ListCurrencyChain(ctx, cryptocurrency.Upper())
if err != nil {
return nil, err
}
availableChains := make([]string, 0, len(chains))
for x := range chains {
if chains[x].IsDisabled == 0 {
availableChains = append(availableChains, chains[x].Chain)
}
}
return availableChains, 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)
}
// checkInstrumentAvailabilityInSpot checks whether the instrument is available in the spot exchange
// if so we can use the instrument to retrieve orderbook and ticker information using the spot endpoints.
func (e *Exchange) checkInstrumentAvailabilityInSpot(instrument currency.Pair) (bool, error) {
availables, err := e.CurrencyPairs.GetPairs(asset.Spot, false)
if err != nil {
return false, err
}
return availables.Contains(instrument, true), nil
}
// GetFuturesContractDetails returns details about futures contracts
func (e *Exchange) GetFuturesContractDetails(ctx context.Context, a asset.Item) ([]futures.Contract, error) {
if !a.IsFutures() {
return nil, futures.ErrNotFuturesAsset
}
if !e.SupportsAsset(a) {
return nil, fmt.Errorf("%w %q", asset.ErrNotSupported, a)
}
settle, err := getSettlementCurrency(currency.EMPTYPAIR, a)
if err != nil {
return nil, err
}
switch a {
case asset.CoinMarginedFutures, asset.USDTMarginedFutures:
contracts, err := e.GetAllFutureContracts(ctx, settle)
if err != nil {
return nil, err
}
resp := make([]futures.Contract, len(contracts))
for i := range contracts {
contractSettlementType := futures.Linear
switch {
case contracts[i].Name.Base.Equal(currency.BTC) && settle.Equal(currency.BTC):
contractSettlementType = futures.Inverse
case !contracts[i].Name.Base.Equal(settle) && !settle.Equal(currency.USDT):
contractSettlementType = futures.Quanto
}
c := futures.Contract{
Exchange: e.Name,
Name: contracts[i].Name,
Underlying: contracts[i].Name,
Asset: a,
IsActive: contracts[i].DelistedTime.Time().IsZero() || contracts[i].DelistedTime.Time().After(time.Now()),
Type: futures.Perpetual,
SettlementType: contractSettlementType,
SettlementCurrencies: currency.Currencies{settle},
Multiplier: contracts[i].QuantoMultiplier.Float64(),
MaxLeverage: contracts[i].LeverageMax.Float64(),
}
c.LatestRate = fundingrate.Rate{
Time: contracts[i].FundingNextApply.Time().Add(-time.Duration(contracts[i].FundingInterval) * time.Second),
Rate: contracts[i].FundingRate.Decimal(),
}
resp[i] = c
}
return resp, nil
case asset.DeliveryFutures:
contracts, err := e.GetAllDeliveryContracts(ctx, settle)
if err != nil {
return nil, err
}
resp := make([]futures.Contract, len(contracts))
for i := range contracts {
name, err := currency.NewPairFromString(contracts[i].Name)
if err != nil {
return nil, err
}
underlying, err := currency.NewPairFromString(contracts[i].Underlying)
if err != nil {
return nil, err
}
// no start information, inferring it based on contract type
// gateio also reuses contracts for kline data, cannot use a lookup to see the first trade
var startTime time.Time
endTime := contracts[i].ExpireTime.Time()
ct := futures.LongDated
switch contracts[i].Cycle {
case "WEEKLY":
ct = futures.Weekly
startTime = endTime.Add(-kline.OneWeek.Duration())
case "BI-WEEKLY":
ct = futures.Fortnightly
startTime = endTime.Add(-kline.TwoWeek.Duration())
case "QUARTERLY":
ct = futures.Quarterly
startTime = endTime.Add(-kline.ThreeMonth.Duration())
case "BI-QUARTERLY":
ct = futures.HalfYearly
startTime = endTime.Add(-kline.SixMonth.Duration())
}
resp[i] = futures.Contract{
Exchange: e.Name,
Name: name,
Underlying: underlying,
Asset: a,
StartDate: startTime,
EndDate: endTime,
SettlementType: futures.Linear,
IsActive: !contracts[i].InDelisting,
Type: ct,
SettlementCurrencies: currency.Currencies{settle},
MarginCurrency: currency.Code{},
Multiplier: contracts[i].QuantoMultiplier.Float64(),
MaxLeverage: contracts[i].LeverageMax.Float64(),
}
}
return resp, nil
}
return nil, fmt.Errorf("%w %q", asset.ErrNotSupported, a)
}
// UpdateOrderExecutionLimits sets exchange executions for a required asset type
func (e *Exchange) UpdateOrderExecutionLimits(ctx context.Context, a asset.Item) error {
if !e.SupportsAsset(a) {
return fmt.Errorf("%s %w", a, asset.ErrNotSupported)
}
var l []limits.MinMaxLevel
switch a {
case asset.Spot:
pairsData, err := e.ListSpotCurrencyPairs(ctx)
if err != nil {
return err
}
l = make([]limits.MinMaxLevel, 0, len(pairsData))
for i := range pairsData {
if pairsData[i].TradeStatus == "untradable" {
continue
}
// Minimum base amounts are not always provided this will default to
// precision for base deployment. This can't be done for quote.
minBaseAmount := pairsData[i].MinBaseAmount.Float64()
if minBaseAmount == 0 {
minBaseAmount = math.Pow10(-int(pairsData[i].AmountPrecision))
}
l = append(l, limits.MinMaxLevel{
Key: key.NewExchangeAssetPair(e.Name, a, currency.NewPair(pairsData[i].Base, pairsData[i].Quote)),
QuoteStepIncrementSize: math.Pow10(-int(pairsData[i].PricePrecision)),
AmountStepIncrementSize: math.Pow10(-int(pairsData[i].AmountPrecision)),
MinimumBaseAmount: minBaseAmount,
MinimumQuoteAmount: pairsData[i].MinQuoteAmount.Float64(),
Delisted: pairsData[i].DelistingTime.Time(),
})
}
case asset.USDTMarginedFutures, asset.CoinMarginedFutures:
settlement := currency.USDT
if a == asset.CoinMarginedFutures {
settlement = currency.BTC
}
contractInfo, err := e.GetAllFutureContracts(ctx, settlement)
if err != nil {
return err
}
// MBABYDOGE price is 1e6 x spot price
divCurrency := currency.NewCode("MBABYDOGE")
l = make([]limits.MinMaxLevel, 0, len(contractInfo))
for i := range contractInfo {
priceDiv := 1.0
if contractInfo[i].Name.Base.Equal(divCurrency) {
priceDiv = 1e6
}
l = append(l, limits.MinMaxLevel{
Key: key.NewExchangeAssetPair(e.Name, a, contractInfo[i].Name),
MinimumBaseAmount: contractInfo[i].OrderSizeMin.Float64(),
MaximumBaseAmount: contractInfo[i].OrderSizeMax.Float64(),
PriceStepIncrementSize: contractInfo[i].OrderPriceRound.Float64(),
AmountStepIncrementSize: 1, // 1 Contract
MultiplierDecimal: contractInfo[i].QuantoMultiplier.Float64(),
PriceDivisor: priceDiv,
Delisting: contractInfo[i].DelistingTime.Time(),
Delisted: contractInfo[i].DelistedTime.Time(),
Listed: contractInfo[i].LaunchTime.Time(),
})
}
case asset.DeliveryFutures:
for _, settlement := range []currency.Code{currency.BTC, currency.USDT} {
contractInfo, err := e.GetAllDeliveryContracts(ctx, settlement)
if err != nil {
return err
}
l = slices.Grow(l, len(contractInfo))
for x := range contractInfo {
p := strings.ToUpper(contractInfo[x].Name)
cp, err := currency.NewPairFromString(p)
if err != nil {
return err
}
l = append(l, limits.MinMaxLevel{
Key: key.NewExchangeAssetPair(e.Name, a, cp),
MinimumBaseAmount: float64(contractInfo[x].OrderSizeMin),
MaximumBaseAmount: float64(contractInfo[x].OrderSizeMax),
PriceStepIncrementSize: contractInfo[x].OrderPriceRound.Float64(),
AmountStepIncrementSize: 1,
Expiry: contractInfo[x].ExpireTime.Time(),
})
}
}
case asset.Options:
underlyings, err := e.GetAllOptionsUnderlyings(ctx)
if err != nil {
return err
}
for x := range underlyings {
contracts, err := e.GetAllContractOfUnderlyingWithinExpiryDate(ctx, underlyings[x].Name, time.Time{})
if err != nil {
return err
}
l = make([]limits.MinMaxLevel, 0, len(contracts))
for c := range contracts {
cp, err := currency.NewPairFromString(strings.ReplaceAll(contracts[c].Name, currency.DashDelimiter, currency.UnderscoreDelimiter))
if err != nil {
return err
}
cp.Quote = currency.NewCode(strings.ReplaceAll(cp.Quote.String(), currency.UnderscoreDelimiter, currency.DashDelimiter))
l = append(l, limits.MinMaxLevel{
Key: key.NewExchangeAssetPair(e.Name, a, cp),
MinimumBaseAmount: float64(contracts[c].OrderSizeMin),
MaximumBaseAmount: float64(contracts[c].OrderSizeMax),
PriceStepIncrementSize: contracts[c].OrderPriceRound.Float64(),
AmountStepIncrementSize: 1,
})
}
}
default:
return fmt.Errorf("%w %q", asset.ErrNotSupported, a)
}
return limits.Load(l)
}
// GetHistoricalFundingRates returns historical funding rates for a futures contract
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.CoinMarginedFutures && r.Asset != asset.USDTMarginedFutures {
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() {
if err := common.StartEndTimeCheck(r.StartDate, r.EndDate); err != nil {
return nil, err
}
}
// NOTE: Opted to fail here as a misconfigured request will result in
// {"label":"CONTRACT_NOT_FOUND"} and rather not mutate request using
// quote currency as the settlement currency.
if r.PaymentCurrency.IsEmpty() {
return nil, fundingrate.ErrPaymentCurrencyCannotBeEmpty
}
if r.IncludePayments {
return nil, fmt.Errorf("include payments %w", common.ErrNotYetImplemented)
}
if r.IncludePredictedRate {
return nil, fmt.Errorf("include predicted rate %w", common.ErrNotYetImplemented)
}
fPair, err := e.FormatExchangeCurrency(r.Pair, r.Asset)
if err != nil {
return nil, err
}
records, err := e.GetFutureFundingRates(ctx, r.PaymentCurrency, fPair, 1000)
if err != nil {
return nil, err
}
if len(records) == 0 {
return nil, fundingrate.ErrNoFundingRatesFound
}
if !r.StartDate.IsZero() && !r.RespectHistoryLimits && r.StartDate.Before(records[len(records)-1].Timestamp.Time()) {
return nil, fmt.Errorf("%w start date requested: %v last returned record: %v", fundingrate.ErrFundingRateOutsideLimits, r.StartDate, records[len(records)-1].Timestamp.Time())
}
fundingRates := make([]fundingrate.Rate, 0, len(records))
for i := range records {
if (!r.EndDate.IsZero() && r.EndDate.Before(records[i].Timestamp.Time())) ||
(!r.StartDate.IsZero() && r.StartDate.After(records[i].Timestamp.Time())) {
continue
}
fundingRates = append(fundingRates, fundingrate.Rate{
Rate: decimal.NewFromFloat(records[i].Rate.Float64()),
Time: records[i].Timestamp.Time(),
})
}
if len(fundingRates) == 0 {
return nil, fundingrate.ErrNoFundingRatesFound
}
return &fundingrate.HistoricalRates{
Exchange: e.Name,
Asset: r.Asset,
Pair: r.Pair,
FundingRates: fundingRates,
StartDate: fundingRates[len(fundingRates)-1].Time,
EndDate: fundingRates[0].Time,
LatestRate: fundingRates[0],
PaymentCurrency: r.PaymentCurrency,
}, 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 r.Asset != asset.CoinMarginedFutures && r.Asset != asset.USDTMarginedFutures {
return nil, fmt.Errorf("%w %v", asset.ErrNotSupported, r.Asset)
}
settle, err := getSettlementCurrency(r.Pair, r.Asset)
if err != nil {
return nil, err
}
if !r.Pair.IsEmpty() {
fPair, err := e.FormatExchangeCurrency(r.Pair, r.Asset)
if err != nil {
return nil, err
}
contract, err := e.GetFuturesContract(ctx, settle, fPair.String())
if err != nil {
return nil, err
}
return []fundingrate.LatestRateResponse{
contractToFundingRate(e.Name, r.Asset, fPair, contract, r.IncludePredictedRate),
}, nil
}
pairs, err := e.GetEnabledPairs(r.Asset)
if err != nil {
return nil, err
}
contracts, err := e.GetAllFutureContracts(ctx, settle)
if err != nil {
return nil, err
}
resp := make([]fundingrate.LatestRateResponse, 0, len(contracts))
for i := range contracts {
if !pairs.Contains(contracts[i].Name, true) {
continue
}
resp = append(resp, contractToFundingRate(e.Name, r.Asset, contracts[i].Name, &contracts[i], r.IncludePredictedRate))
}
return slices.Clip(resp), nil
}
func contractToFundingRate(name string, item asset.Item, fPair currency.Pair, contract *FuturesContract, includeUpcomingRate bool) fundingrate.LatestRateResponse {
resp := fundingrate.LatestRateResponse{
Exchange: name,
Asset: item,
Pair: fPair,
LatestRate: fundingrate.Rate{
Time: contract.FundingNextApply.Time().Add(-time.Duration(contract.FundingInterval) * time.Second),
Rate: contract.FundingRate.Decimal(),
},
TimeOfNextRate: contract.FundingNextApply.Time(),
TimeChecked: time.Now(),
}
if includeUpcomingRate {
resp.PredictedUpcomingRate = fundingrate.Rate{
Time: contract.FundingNextApply.Time(),
Rate: contract.FundingRateIndicative.Decimal(),
}
}
return resp
}
// IsPerpetualFutureCurrency ensures a given asset and currency is a perpetual future
func (e *Exchange) IsPerpetualFutureCurrency(a asset.Item, _ currency.Pair) (bool, error) {
return a == asset.CoinMarginedFutures || a == asset.USDTMarginedFutures, nil
}
// GetOpenInterest returns the open interest rate for a given asset pair
// If no pairs are provided, all enabled assets and pairs will be used
// If keys are provided, those asset pairs only need to be available, not enabled
func (e *Exchange) GetOpenInterest(ctx context.Context, keys ...key.PairAsset) ([]futures.OpenInterest, error) {
var errs error
resp := make([]futures.OpenInterest, 0, len(keys))
assets := asset.Items{}
if len(keys) == 0 {
assets = asset.Items{asset.DeliveryFutures, asset.CoinMarginedFutures, asset.USDTMarginedFutures}
} else {
for _, k := range keys {
if !slices.Contains(assets, k.Asset) {
assets = append(assets, k.Asset)
}
}
}
for _, a := range assets {
var p currency.Pair
if len(keys) == 1 && a == keys[0].Asset {
if p, errs = e.MatchSymbolWithAvailablePairs(keys[0].Pair().String(), a, false); errs != nil {
return nil, errs
}
}
contracts, err := e.getOpenInterestContracts(ctx, a, p)
if err != nil {
errs = common.AppendError(errs, fmt.Errorf("%w fetching %s", err, a))
continue
}
for _, c := range contracts {
if p.IsEmpty() { // If not exactly one key provided
p, err = e.MatchSymbolWithAvailablePairs(c.contractName(), a, true)
if err != nil {
if err := common.ExcludeError(err, currency.ErrPairNotFound); err != nil {
errs = common.AppendError(errs, fmt.Errorf("%w from %s contract %s", err, a, c.contractName()))
}
continue
}
if len(keys) == 0 { // No keys: All enabled pairs
if enabled, err := e.IsPairEnabled(p, a); err != nil {
errs = common.AppendError(errs, fmt.Errorf("%w: %s %s", err, a, p))
continue
} else if !enabled {
continue
}
} else { // More than one key; Any available pair
if !slices.ContainsFunc(keys, func(k key.PairAsset) bool { return a == k.Asset && k.Pair().Equal(p) }) {
continue
}
}
}
resp = append(resp, futures.OpenInterest{
Key: key.ExchangeAssetPair{
Exchange: e.Name,
Base: p.Base.Item,
Quote: p.Quote.Item,
Asset: a,
},
OpenInterest: c.openInterest(),
})
}
}
return slices.Clip(resp), errs
}
type openInterestContract interface {
openInterest() float64
contractName() string
}
func (c *FuturesContract) openInterest() float64 {
i := float64(c.PositionSize) * c.IndexPrice.Float64()
if q := c.QuantoMultiplier.Float64(); q != 0 {
i *= q
}
return i
}
func (c *FuturesContract) contractName() string {
return c.Name.String()
}
func (c *DeliveryContract) openInterest() float64 {
return c.QuantoMultiplier.Float64() * float64(c.PositionSize) * c.IndexPrice.Float64()
}
func (c *DeliveryContract) contractName() string {
return c.Name
}
func (e *Exchange) getOpenInterestContracts(ctx context.Context, a asset.Item, p currency.Pair) ([]openInterestContract, error) {
settle, err := getSettlementCurrency(p, a)
if err != nil {
return nil, err
}
if a == asset.DeliveryFutures {
if p != currency.EMPTYPAIR {
d, err := e.GetDeliveryContract(ctx, settle, p)
return []openInterestContract{d}, err
}
d, err := e.GetAllDeliveryContracts(ctx, settle)
contracts := make([]openInterestContract, len(d))
for i := range d {
contracts[i] = &d[i]
}
return contracts, err
}
if p != currency.EMPTYPAIR {
contract, err := e.GetFuturesContract(ctx, settle, p.String())
return []openInterestContract{contract}, err
}
fc, err := e.GetAllFutureContracts(ctx, settle)
contracts := make([]openInterestContract, len(fc))
for i := range fc {
contracts[i] = &fc[i]
}
return contracts, err
}
// getClientOrderIDFromText returns the client order ID from the text response
func getClientOrderIDFromText(text string) string {
if strings.HasPrefix(text, "t-") {
return text
}
return ""
}
func formatClientOrderID(clientOrderID string) string {
if clientOrderID == "" || strings.HasPrefix(clientOrderID, "t-") {
return clientOrderID
}
return "t-" + clientOrderID
}
// getTypeFromTimeInForce returns the order type and if the order is post only
func getTypeFromTimeInForce(tif string, price float64) (orderType order.Type) {
switch tif {
case iocTIF, fokTIF:
return order.Market
case pocTIF, gtcTIF:
return order.Limit
default:
if price == 0 {
return order.Market
}
return order.Limit
}
}
// getSideAndAmountFromSize returns the order side, amount and remaining amounts
func getSideAndAmountFromSize(size, left float64) (side order.Side, amount, remaining float64) {
if size < 0 {
return order.Short, math.Abs(size), math.Abs(left)
}
return order.Long, size, left
}
// getFutureOrderSize sets the amount to a negative value if shorting.
func getFutureOrderSize(s *order.Submit) (float64, error) {
switch {
case s.Side.IsLong():
return s.Amount, nil
case s.Side.IsShort():
return -s.Amount, nil
default:
return 0, order.ErrSideIsInvalid
}
}
// 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) {
_, err := e.CurrencyPairs.IsPairEnabled(cp, a)
if err != nil {
return "", err
}
cp.Delimiter = currency.UnderscoreDelimiter
switch a {
case asset.Spot, asset.CrossMargin, asset.Margin:
return tradeBaseURL + "trade/" + cp.Upper().String(), nil
case asset.CoinMarginedFutures, asset.USDTMarginedFutures, asset.DeliveryFutures:
settle, err := getSettlementCurrency(cp, a)
if err != nil {
return "", err
}
if a == asset.DeliveryFutures {
return tradeBaseURL + "futures-delivery/" + settle.String() + "/" + cp.Upper().String(), nil
}
return tradeBaseURL + futuresPath + settle.String() + "/" + cp.Upper().String(), nil
default:
return "", fmt.Errorf("%w %q", asset.ErrNotSupported, a)
}
}
// WebsocketSubmitOrder submits an order to the exchange
// NOTE: Regarding spot orders, fee is applied to purchased currency.
func (e *Exchange) WebsocketSubmitOrder(ctx context.Context, s *order.Submit) (*order.SubmitResponse, error) {
err := s.Validate(e.GetTradingRequirements())
if err != nil {
return nil, err
}
s.ClientOrderID = formatClientOrderID(s.ClientOrderID)
s.Pair, err = e.FormatExchangeCurrency(s.Pair, s.AssetType)
if err != nil {
return nil, err
}
s.Pair = s.Pair.Upper()
switch s.AssetType {
case asset.Spot:
req, err := e.getSpotOrderRequest(s)
if err != nil {
return nil, err
}
resp, err := e.WebsocketSpotSubmitOrder(ctx, req)
if err != nil {
return nil, err
}
return e.deriveSpotWebsocketOrderResponse(resp)
case asset.CoinMarginedFutures, asset.USDTMarginedFutures:
req, err := getFuturesOrderRequest(s)
if err != nil {
return nil, err
}
resp, err := e.WebsocketFuturesSubmitOrder(ctx, s.AssetType, req)
if err != nil {
return nil, err
}
return e.deriveFuturesWebsocketOrderResponse(resp)
default:
return nil, common.ErrNotYetImplemented
}
}
func getFuturesOrderRequest(s *order.Submit) (*ContractOrderCreateParams, error) {
amountWithDirection, err := getFutureOrderSize(s)
if err != nil {
return nil, err
}
tif, err := toExchangeTIF(s.TimeInForce, s.Price)
if err != nil {
return nil, err
}
return &ContractOrderCreateParams{
Contract: s.Pair,
Size: amountWithDirection,
Price: number(s.Price),
ReduceOnly: s.ReduceOnly,
TimeInForce: tif,
Text: s.ClientOrderID,
}, nil
}
// toExchangeTIF converts a TimeInForce to its corresponding exchange-compatible string.
func toExchangeTIF(tif order.TimeInForce, price float64) (string, error) {
switch {
case tif == order.UnknownTIF:
if price == 0 {
return iocTIF, nil // Market orders default to IOC
}
return gtcTIF, nil // Default to GTC for limit orders
case tif.Is(order.PostOnly):
return pocTIF, nil
case tif.Is(order.ImmediateOrCancel):
return iocTIF, nil
case tif.Is(order.FillOrKill):
return fokTIF, nil
case tif.Is(order.GoodTillCancel):
return gtcTIF, nil
default:
return "", fmt.Errorf("%w: %q", order.ErrUnsupportedTimeInForce, tif)
}
}
func (e *Exchange) deriveSpotWebsocketOrderResponse(responses *WebsocketOrderResponse) (*order.SubmitResponse, error) {
resp, err := e.deriveSpotWebsocketOrderResponses([]*WebsocketOrderResponse{responses})
if err != nil {
return nil, err
}
return resp[0], nil
}
// deriveSpotWebsocketOrderResponses returns the order submission responses for spot
func (e *Exchange) deriveSpotWebsocketOrderResponses(responses []*WebsocketOrderResponse) ([]*order.SubmitResponse, error) {
if len(responses) == 0 {
return nil, common.ErrNoResponse
}
out := make([]*order.SubmitResponse, len(responses))
for i, resp := range responses {
if resp.Label != "" { // batch only, denotes error type in string format
out[i] = &order.SubmitResponse{
Exchange: e.Name,
ClientOrderID: resp.Text,
SubmissionError: fmt.Errorf("%w reason label:%q message:%q", order.ErrUnableToPlaceOrder, resp.Label, resp.Message),
}
continue
}
side, err := order.StringToOrderSide(resp.Side)
if err != nil {
return nil, err
}
status := order.Open
if resp.FinishAs != "" && resp.FinishAs != statusOpen {
status, err = order.StringToOrderStatus(resp.FinishAs)
if err != nil {
return nil, err
}
}
oType, err := order.StringToOrderType(resp.Type)
if err != nil {
return nil, err
}
var cost float64
var purchased float64
if resp.AverageDealPrice != 0 {
if side.IsLong() {
cost = resp.FilledTotal.Float64()
purchased = resp.FilledTotal.Decimal().Div(resp.AverageDealPrice.Decimal()).InexactFloat64()
} else {
cost = resp.Amount.Float64()
purchased = resp.FilledTotal.Float64()
}
}
tif, err := order.StringToTimeInForce(resp.TimeInForce)
if err != nil {
return nil, err
}
out[i] = &order.SubmitResponse{
Exchange: e.Name,
OrderID: resp.ID,
AssetType: resp.Account,
Pair: resp.CurrencyPair,
ClientOrderID: resp.Text,
Date: resp.CreateTimeMs.Time(),
LastUpdated: resp.UpdateTimeMs.Time(),
RemainingAmount: resp.Left.Float64(),
Amount: resp.Amount.Float64(),
Price: resp.Price.Float64(),
Type: oType,
Side: side,
Fee: resp.Fee.Float64(),
FeeAsset: resp.FeeCurrency,
TimeInForce: tif,
Cost: cost,
Purchased: purchased,
Status: status,
AverageExecutedPrice: resp.AverageDealPrice.Float64(),
}
}
return out, nil
}
func (e *Exchange) deriveFuturesWebsocketOrderResponse(responses *WebsocketFuturesOrderResponse) (*order.SubmitResponse, error) {
resp, err := e.deriveFuturesWebsocketOrderResponses([]*WebsocketFuturesOrderResponse{responses})
if err != nil {
return nil, err
}
return resp[0], nil
}
// deriveFuturesWebsocketOrderResponses returns the order submission responses for futures
func (e *Exchange) deriveFuturesWebsocketOrderResponses(responses []*WebsocketFuturesOrderResponse) ([]*order.SubmitResponse, error) {
if len(responses) == 0 {
return nil, common.ErrNoResponse
}
out := make([]*order.SubmitResponse, 0, len(responses))
for _, resp := range responses {
status := order.Open
if resp.FinishAs != "" && resp.FinishAs != statusOpen {
var err error
status, err = order.StringToOrderStatus(resp.FinishAs)
if err != nil {
return nil, err
}
}
oType := order.Market
if resp.Price != 0 {
oType = order.Limit
}
side := order.Long
if resp.Size < 0 {
side = order.Short
}
var clientOrderID string
if resp.Text != "" && strings.HasPrefix(resp.Text, "t-") {
clientOrderID = resp.Text
}
tif, err := order.StringToTimeInForce(resp.TimeInForce)
if err != nil {
return nil, err
}
out = append(out, &order.SubmitResponse{
Exchange: e.Name,
OrderID: strconv.FormatInt(resp.ID, 10),
AssetType: asset.Futures,
Pair: resp.Contract,
ClientOrderID: clientOrderID,
Date: resp.CreateTime.Time(),
LastUpdated: resp.UpdateTime.Time(),
RemainingAmount: math.Abs(resp.Left),
Amount: math.Abs(resp.Size),
Price: resp.Price.Float64(),
AverageExecutedPrice: resp.FillPrice.Float64(),
Type: oType,
Side: side,
Status: status,
TimeInForce: tif,
ReduceOnly: resp.IsReduceOnly,
})
}
return out, nil
}
func (e *Exchange) getSpotOrderRequest(s *order.Submit) (*CreateOrderRequest, error) {
var side string
switch {
case s.Side.IsLong():
side = order.Buy.Lower()
case s.Side.IsShort():
side = order.Sell.Lower()
default:
return nil, fmt.Errorf("%w: %q", order.ErrSideIsInvalid, s.Side)
}
tif, err := toExchangeTIF(s.TimeInForce, s.Price)
if err != nil {
return nil, err
}
return &CreateOrderRequest{
Side: side,
Type: s.Type.Lower(),
Account: e.assetTypeToString(s.AssetType),
Amount: types.Number(s.GetTradeAmount(e.GetTradingRequirements())),
Price: types.Number(s.Price),
CurrencyPair: s.Pair,
Text: s.ClientOrderID,
TimeInForce: tif,
}, nil
}
func getSettlementCurrency(p currency.Pair, a asset.Item) (currency.Code, error) {
switch a {
case asset.DeliveryFutures:
return currency.USDT, nil
case asset.USDTMarginedFutures:
if p.IsEmpty() || p.Quote.Equal(currency.USDT) {
return currency.USDT, nil
}
return currency.EMPTYCODE, fmt.Errorf("%w %s %s", errInvalidSettlementQuote, a, p)
case asset.CoinMarginedFutures:
if !p.IsEmpty() {
if !p.Base.Equal(currency.BTC) { // Only BTC endpoint currently available
return currency.EMPTYCODE, fmt.Errorf("%w %s %s", errInvalidSettlementBase, a, p)
}
if !p.Quote.Equal(currency.USD) { // We expect all Coin-M to be quoted in USD
return currency.EMPTYCODE, fmt.Errorf("%w %s %s", errInvalidSettlementQuote, a, p)
}
}
return currency.BTC, nil
}
return currency.EMPTYCODE, fmt.Errorf("%w: %s", asset.ErrNotSupported, a)
}
// WebsocketSubmitOrders submits orders to the exchange through the websocket
func (e *Exchange) WebsocketSubmitOrders(ctx context.Context, orders []*order.Submit) ([]*order.SubmitResponse, error) {
var a asset.Item
for x := range orders {
if err := orders[x].Validate(e.GetTradingRequirements()); err != nil {
return nil, err
}
if a == asset.Empty {
a = orders[x].AssetType
continue
}
if a != orders[x].AssetType {
return nil, fmt.Errorf("%w; Passed %q and %q", errSingleAssetRequired, a, orders[x].AssetType)
}
}
if !e.CurrencyPairs.IsAssetSupported(a) {
return nil, fmt.Errorf("%w: %q", asset.ErrNotSupported, a)
}
switch a {
case asset.Spot:
reqs := make([]*CreateOrderRequest, len(orders))
for x := range orders {
var err error
if reqs[x], err = e.getSpotOrderRequest(orders[x]); err != nil {
return nil, err
}
}
resp, err := e.WebsocketSpotSubmitOrders(ctx, reqs...)
if err != nil {
return nil, err
}
return e.deriveSpotWebsocketOrderResponses(resp)
default:
return nil, fmt.Errorf("%w for %s", common.ErrNotYetImplemented, a)
}
}