Files
gocryptotrader/exchanges/gateio/gateio_wrapper.go
Gareth Kirwan f05f24da8b Types: Switch convert.StringToFloat64 to types.Number (#1415)
* Types: Add Number type

* Types: Switch StringToFloat64 for Number

This change mostly just renames the type.
convert package and StringToFloat64 represent actions, not types,
and make it misleading to use outside of the API context,
especially when using it for a Float64ToString operation.

* Common: Remove StringToFloat64

Replaced by types.Number

* fixup! Types: Switch StringToFloat64 for Number

Second pass at Okx

* Spellcheck: Fix whitespace handling for okx line
2023-12-20 13:01:27 +11:00

2346 lines
72 KiB
Go

package gateio
import (
"context"
"errors"
"fmt"
"math"
"sort"
"strconv"
"strings"
"sync"
"time"
"github.com/shopspring/decimal"
"github.com/thrasher-corp/gocryptotrader/common"
"github.com/thrasher-corp/gocryptotrader/config"
"github.com/thrasher-corp/gocryptotrader/currency"
exchange "github.com/thrasher-corp/gocryptotrader/exchanges"
"github.com/thrasher-corp/gocryptotrader/exchanges/account"
"github.com/thrasher-corp/gocryptotrader/exchanges/asset"
"github.com/thrasher-corp/gocryptotrader/exchanges/deposit"
"github.com/thrasher-corp/gocryptotrader/exchanges/fundingrate"
"github.com/thrasher-corp/gocryptotrader/exchanges/futures"
"github.com/thrasher-corp/gocryptotrader/exchanges/kline"
"github.com/thrasher-corp/gocryptotrader/exchanges/order"
"github.com/thrasher-corp/gocryptotrader/exchanges/orderbook"
"github.com/thrasher-corp/gocryptotrader/exchanges/protocol"
"github.com/thrasher-corp/gocryptotrader/exchanges/request"
"github.com/thrasher-corp/gocryptotrader/exchanges/stream"
"github.com/thrasher-corp/gocryptotrader/exchanges/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"
)
// GetDefaultConfig returns a default exchange config
func (g *Gateio) GetDefaultConfig(ctx context.Context) (*config.Exchange, error) {
g.SetDefaults()
exchCfg, err := g.GetStandardConfig()
if err != nil {
return nil, err
}
err = g.SetupDefaults(exchCfg)
if err != nil {
return nil, err
}
if g.Features.Supports.RESTCapabilities.AutoPairUpdates {
err = g.UpdateTradablePairs(ctx, true)
if err != nil {
return nil, err
}
}
return exchCfg, nil
}
// SetDefaults sets default values for the exchange
func (g *Gateio) SetDefaults() {
g.Name = "GateIO"
g.Enabled = true
g.Verbose = true
g.API.CredentialsValidator.RequiresKey = true
g.API.CredentialsValidator.RequiresSecret = true
requestFmt := &currency.PairFormat{Delimiter: currency.UnderscoreDelimiter, Uppercase: true}
configFmt := &currency.PairFormat{Delimiter: currency.UnderscoreDelimiter, Uppercase: true}
err := g.SetGlobalPairsManager(requestFmt, configFmt, asset.Spot, asset.Futures, asset.Margin, asset.CrossMargin, asset.DeliveryFutures, asset.Options)
if err != nil {
log.Errorln(log.ExchangeSys, err)
}
g.Features = exchange.Features{
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,
FullPayloadSubscribe: true,
AuthenticatedEndpoints: true,
MessageCorrelation: true,
GetOrder: true,
AccountBalance: true,
Subscribe: 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.Futures: 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,
},
},
}
g.Requester, err = request.New(g.Name,
common.NewHTTPClientWithTimeout(exchange.DefaultHTTPTimeout),
request.WithLimiter(SetRateLimit()),
)
if err != nil {
log.Errorln(log.ExchangeSys, err)
}
err = g.DisableAssetWebsocketSupport(asset.Margin)
if err != nil {
log.Errorln(log.ExchangeSys, err)
}
err = g.DisableAssetWebsocketSupport(asset.CrossMargin)
if err != nil {
log.Errorln(log.ExchangeSys, err)
}
err = g.DisableAssetWebsocketSupport(asset.Futures)
if err != nil {
log.Errorln(log.ExchangeSys, err)
}
err = g.DisableAssetWebsocketSupport(asset.DeliveryFutures)
if err != nil {
log.Errorln(log.ExchangeSys, err)
}
err = g.DisableAssetWebsocketSupport(asset.Options)
if err != nil {
log.Errorln(log.ExchangeSys, err)
}
g.API.Endpoints = g.NewEndpoints()
err = g.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)
}
g.Websocket = stream.New()
g.WebsocketResponseMaxLimit = exchange.DefaultWebsocketResponseMaxLimit
g.WebsocketResponseCheckTimeout = exchange.DefaultWebsocketResponseCheckTimeout
g.WebsocketOrderbookBufferLimit = exchange.DefaultWebsocketOrderbookBufferLimit
}
// Setup sets user configuration
func (g *Gateio) Setup(exch *config.Exchange) error {
err := exch.Validate()
if err != nil {
return err
}
if !exch.Enabled {
g.SetEnabled(false)
return nil
}
err = g.SetupDefaults(exch)
if err != nil {
return err
}
wsRunningURL, err := g.API.Endpoints.GetURL(exchange.WebsocketSpot)
if err != nil {
return err
}
err = g.Websocket.Setup(&stream.WebsocketSetup{
ExchangeConfig: exch,
DefaultURL: gateioWebsocketEndpoint,
RunningURL: wsRunningURL,
Connector: g.WsConnect,
Subscriber: g.Subscribe,
Unsubscriber: g.Unsubscribe,
GenerateSubscriptions: g.GenerateDefaultSubscriptions,
Features: &g.Features.Supports.WebsocketCapabilities,
FillsFeed: g.Features.Enabled.FillsFeed,
TradeFeed: g.Features.Enabled.TradeFeed,
})
if err != nil {
return err
}
return g.Websocket.SetupNewConnection(stream.ConnectionSetup{
URL: gateioWebsocketEndpoint,
RateLimit: gateioWebsocketRateLimit,
ResponseCheckTimeout: exch.WebsocketResponseCheckTimeout,
ResponseMaxLimit: exch.WebsocketResponseMaxLimit,
})
}
// Start starts the GateIO go routine
func (g *Gateio) Start(ctx context.Context, wg *sync.WaitGroup) error {
if wg == nil {
return fmt.Errorf("%T %w", wg, common.ErrNilPointer)
}
wg.Add(1)
go func() {
g.Run(ctx)
wg.Done()
}()
return nil
}
// Run implements the GateIO wrapper
func (g *Gateio) Run(ctx context.Context) {
if g.Verbose {
g.PrintEnabledPairs()
}
if !g.GetEnabledFeatures().AutoPairUpdates {
return
}
err := g.UpdateTradablePairs(ctx, false)
if err != nil {
log.Errorf(log.ExchangeSys, "%s failed to update tradable pairs. Err: %s", g.Name, err)
}
}
// UpdateTicker updates and returns the ticker for a currency pair
func (g *Gateio) UpdateTicker(ctx context.Context, p currency.Pair, a asset.Item) (*ticker.Price, error) {
if !g.SupportsAsset(a) {
return nil, fmt.Errorf("%w asset type: %v", asset.ErrNotSupported, a)
}
fPair, err := g.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:
var available bool
available, err = g.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)
}
var tickerNew *Ticker
tickerNew, err = g.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: g.Name,
AssetType: a,
}
case asset.Futures:
var settle string
settle, err = g.getSettlementFromCurrency(fPair, true)
if err != nil {
return nil, err
}
var tickers []FuturesTicker
tickers, err = g.GetFuturesTickers(ctx, settle, fPair)
if err != nil {
return nil, err
}
var tick *FuturesTicker
for x := range tickers {
if tickers[x].Contract == fPair.String() {
tick = &tickers[x]
break
}
}
if tick == nil {
return nil, errNoTickerData
}
tickerData = &ticker.Price{
Pair: fPair,
Low: tick.Low24H.Float64(),
High: tick.High24H.Float64(),
Last: tick.Last.Float64(),
Volume: tick.Volume24HBase.Float64(),
QuoteVolume: tick.Volume24HQuote.Float64(),
ExchangeName: g.Name,
AssetType: a,
}
case asset.Options:
var underlying currency.Pair
var tickers []OptionsTicker
underlying, err = g.GetUnderlyingFromCurrencyPair(fPair)
if err != nil {
return nil, err
}
tickers, err = g.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)
if err != nil {
return nil, err
}
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: g.Name,
AssetType: a,
}
err = ticker.ProcessTicker(tickerData)
if err != nil {
return nil, err
}
}
return ticker.GetTicker(g.Name, fPair, a)
case asset.DeliveryFutures:
var settle string
settle, err = g.getSettlementFromCurrency(fPair, false)
if err != nil {
return nil, err
}
var tickers []FuturesTicker
tickers, err = g.GetDeliveryFutureTickers(ctx, settle, fPair)
if err != nil {
return nil, err
}
for x := range tickers {
if tickers[x].Contract == fPair.Upper().String() {
tickerData = &ticker.Price{
Pair: fPair,
Last: tickers[x].Last.Float64(),
High: tickers[x].High24H.Float64(),
Low: tickers[x].Low24H.Float64(),
Volume: tickers[x].Volume24H.Float64(),
QuoteVolume: tickers[x].Volume24HQuote.Float64(),
ExchangeName: g.Name,
AssetType: a,
}
break
}
}
}
err = ticker.ProcessTicker(tickerData)
if err != nil {
return nil, err
}
return ticker.GetTicker(g.Name, fPair, a)
}
// FetchTicker retrieves a list of tickers.
func (g *Gateio) FetchTicker(ctx context.Context, p currency.Pair, assetType asset.Item) (*ticker.Price, error) {
fPair, err := g.FormatExchangeCurrency(p, assetType)
if err != nil {
return nil, err
}
tickerNew, err := ticker.GetTicker(g.Name, fPair, assetType)
if err != nil {
return g.UpdateTicker(ctx, fPair, assetType)
}
return tickerNew, nil
}
// FetchTradablePairs returns a list of the exchanges tradable pairs
func (g *Gateio) FetchTradablePairs(ctx context.Context, a asset.Item) (currency.Pairs, error) {
if !g.SupportsAsset(a) {
return nil, fmt.Errorf("%w asset type: %v", asset.ErrNotSupported, a)
}
switch a {
case asset.Spot:
tradables, err := g.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
}
p := strings.ToUpper(tradables[x].ID)
if !g.IsValidPairString(p) {
continue
}
cp, err := currency.NewPairFromString(p)
if err != nil {
return nil, err
}
pairs = append(pairs, cp)
}
return pairs, nil
case asset.Margin, asset.CrossMargin:
tradables, err := g.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)
if !g.IsValidPairString(p) {
continue
}
cp, err := currency.NewPairFromString(p)
if err != nil {
return nil, err
}
pairs = append(pairs, cp)
}
return pairs, nil
case asset.Futures:
btcContracts, err := g.GetAllFutureContracts(ctx, settleBTC)
if err != nil {
return nil, err
}
usdtContracts, err := g.GetAllFutureContracts(ctx, settleUSDT)
if err != nil {
return nil, err
}
btcContracts = append(btcContracts, usdtContracts...)
pairs := make([]currency.Pair, 0, len(btcContracts))
for x := range btcContracts {
if btcContracts[x].InDelisting {
continue
}
p := strings.ToUpper(btcContracts[x].Name)
if !g.IsValidPairString(p) {
continue
}
cp, err := currency.NewPairFromString(p)
if err != nil {
return nil, err
}
pairs = append(pairs, cp)
}
return pairs, nil
case asset.DeliveryFutures:
btcContracts, err := g.GetAllDeliveryContracts(ctx, settleBTC)
if err != nil {
return nil, err
}
usdtContracts, err := g.GetAllDeliveryContracts(ctx, settleUSDT)
if err != nil {
return nil, err
}
btcContracts = append(btcContracts, usdtContracts...)
pairs := make([]currency.Pair, 0, len(btcContracts))
for x := range btcContracts {
if btcContracts[x].InDelisting {
continue
}
p := strings.ToUpper(btcContracts[x].Name)
if !g.IsValidPairString(p) {
continue
}
cp, err := currency.NewPairFromString(p)
if err != nil {
return nil, err
}
pairs = append(pairs, cp)
}
return pairs, nil
case asset.Options:
underlyings, err := g.GetAllOptionsUnderlyings(ctx)
if err != nil {
return nil, err
}
var pairs []currency.Pair
for x := range underlyings {
contracts, err := g.GetAllContractOfUnderlyingWithinExpiryDate(ctx, underlyings[x].Name, time.Time{})
if err != nil {
return nil, err
}
for c := range contracts {
if !g.IsValidPairString(contracts[c].Name) {
continue
}
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))
if err != nil {
return nil, err
}
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 (g *Gateio) UpdateTradablePairs(ctx context.Context, forceUpdate bool) error {
assets := g.GetAssetTypes(false)
for x := range assets {
pairs, err := g.FetchTradablePairs(ctx, assets[x])
if err != nil {
return err
}
if len(pairs) == 0 {
return errors.New("no tradable pairs found")
}
err = g.UpdatePairs(pairs, assets[x], false, forceUpdate)
if err != nil {
return err
}
}
return g.EnsureOnePairEnabled()
}
// UpdateTickers updates the ticker for all currency pairs of a given asset type
func (g *Gateio) UpdateTickers(ctx context.Context, a asset.Item) error {
if !g.SupportsAsset(a) {
return fmt.Errorf("%w asset type: %v", asset.ErrNotSupported, a)
}
var err error
switch a {
case asset.Spot, asset.Margin, asset.CrossMargin:
var tickers []Ticker
tickers, err = g.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: g.Name,
Pair: currencyPair,
AssetType: a,
})
if err != nil {
return err
}
}
case asset.Futures, asset.DeliveryFutures:
var tickers []FuturesTicker
var ticks []FuturesTicker
for _, settle := range []string{settleBTC, settleUSDT, settleUSD} {
if a == asset.Futures {
ticks, err = g.GetFuturesTickers(ctx, settle, currency.EMPTYPAIR)
} else {
if settle == settleUSD {
continue
}
ticks, err = g.GetDeliveryFutureTickers(ctx, settle, currency.EMPTYPAIR)
}
if err != nil {
return err
}
tickers = append(tickers, ticks...)
}
for x := range tickers {
currencyPair, err := currency.NewPairFromString(tickers[x].Contract)
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(),
Volume: tickers[x].Volume24H.Float64(),
QuoteVolume: tickers[x].Volume24HQuote.Float64(),
ExchangeName: g.Name,
Pair: currencyPair,
AssetType: a,
})
if err != nil {
return err
}
}
case asset.Options:
pairs, err := g.GetEnabledPairs(a)
if err != nil {
return err
}
for i := range pairs {
underlying, err := g.GetUnderlyingFromCurrencyPair(pairs[i])
if err != nil {
return err
}
tickers, err := g.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: g.Name,
AssetType: a,
})
if err != nil {
return err
}
}
}
default:
return fmt.Errorf("%w asset type: %v", asset.ErrNotSupported, a)
}
return nil
}
// FetchOrderbook returns orderbook base on the currency pair
func (g *Gateio) FetchOrderbook(ctx context.Context, p currency.Pair, assetType asset.Item) (*orderbook.Base, error) {
ob, err := orderbook.Get(g.Name, p, assetType)
if err != nil {
return g.UpdateOrderbook(ctx, p, assetType)
}
return ob, nil
}
// UpdateOrderbook updates and returns the orderbook for a currency pair
func (g *Gateio) UpdateOrderbook(ctx context.Context, p currency.Pair, a asset.Item) (*orderbook.Base, error) {
p, err := g.FormatExchangeCurrency(p, a)
if err != nil {
return nil, err
}
var orderbookNew *Orderbook
switch a {
case asset.Spot, asset.Margin, asset.CrossMargin:
var available bool
available, err = g.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)
}
orderbookNew, err = g.GetOrderbook(ctx, p.String(), "", 0, true)
case asset.Futures:
var settle string
settle, err = g.getSettlementFromCurrency(p, true)
if err != nil {
return nil, err
}
orderbookNew, err = g.GetFuturesOrderbook(ctx, settle, p.String(), "", 0, true)
case asset.DeliveryFutures:
var settle string
settle, err = g.getSettlementFromCurrency(p.Upper(), false)
if err != nil {
return nil, err
}
orderbookNew, err = g.GetDeliveryOrderbook(ctx, settle, "", p, 0, true)
case asset.Options:
orderbookNew, err = g.GetOptionsOrderbook(ctx, p, "", 0, true)
default:
return nil, fmt.Errorf("%w %v", asset.ErrNotSupported, a)
}
if err != nil {
return nil, err
}
book := &orderbook.Base{
Exchange: g.Name,
Asset: a,
VerifyOrderbook: g.CanVerifyOrderbook,
Pair: p.Upper(),
LastUpdateID: orderbookNew.ID,
LastUpdated: orderbookNew.Update.Time(),
}
book.Bids = make(orderbook.Items, len(orderbookNew.Bids))
for x := range orderbookNew.Bids {
book.Bids[x] = orderbook.Item{
Amount: orderbookNew.Bids[x].Amount,
Price: orderbookNew.Bids[x].Price.Float64(),
}
}
book.Asks = make(orderbook.Items, len(orderbookNew.Asks))
for x := range orderbookNew.Asks {
book.Asks[x] = orderbook.Item{
Amount: orderbookNew.Asks[x].Amount,
Price: orderbookNew.Asks[x].Price.Float64(),
}
}
err = book.Process()
if err != nil {
return book, err
}
return orderbook.Get(g.Name, book.Pair, a)
}
// UpdateAccountInfo retrieves balances for all enabled currencies for the
func (g *Gateio) UpdateAccountInfo(ctx context.Context, a asset.Item) (account.Holdings, error) {
var info account.Holdings
info.Exchange = g.Name
var err error
switch a {
case asset.Spot:
var balances []SpotAccount
balances, err = g.GetSpotAccounts(ctx, currency.EMPTYCODE)
currencies := make([]account.Balance, len(balances))
if err != nil {
return info, err
}
for x := range balances {
currencies[x] = account.Balance{
Currency: currency.NewCode(balances[x].Currency),
Total: balances[x].Available.Float64() - balances[x].Locked.Float64(),
Hold: balances[x].Locked.Float64(),
Free: balances[x].Available.Float64(),
}
}
info.Accounts = append(info.Accounts, account.SubAccount{
AssetType: a,
Currencies: currencies,
})
case asset.Margin, asset.CrossMargin:
var balances []MarginAccountItem
balances, err = g.GetMarginAccountList(ctx, currency.EMPTYPAIR)
if err != nil {
return info, err
}
var currencies []account.Balance
for x := range balances {
currencies = append(currencies, account.Balance{
Currency: currency.NewCode(balances[x].Base.Currency),
Total: balances[x].Base.Available.Float64() + balances[x].Base.LockedAmount.Float64(),
Hold: balances[x].Base.LockedAmount.Float64(),
Free: balances[x].Base.Available.Float64(),
}, account.Balance{
Currency: currency.NewCode(balances[x].Quote.Currency),
Total: balances[x].Quote.Available.Float64() + balances[x].Quote.LockedAmount.Float64(),
Hold: balances[x].Quote.LockedAmount.Float64(),
Free: balances[x].Quote.Available.Float64(),
})
}
info.Accounts = append(info.Accounts, account.SubAccount{
AssetType: a,
Currencies: currencies,
})
case asset.Futures, asset.DeliveryFutures:
currencies := make([]account.Balance, 3)
settles := []currency.Code{currency.BTC, currency.USDT, currency.USD}
for x := range settles {
var balance *FuturesAccount
if a == asset.Futures {
if settles[x].Equal(currency.USD) {
continue
}
balance, err = g.QueryFuturesAccount(ctx, settles[x].String())
} else {
balance, err = g.GetDeliveryFuturesAccounts(ctx, settles[x].String())
}
if err != nil {
return info, err
}
currencies[x] = account.Balance{
Currency: currency.NewCode(balance.Currency),
Total: balance.Total.Float64(),
Hold: balance.Total.Float64() - balance.Available.Float64(),
Free: balance.Available.Float64(),
}
}
info.Accounts = append(info.Accounts, account.SubAccount{
AssetType: a,
Currencies: currencies,
})
case asset.Options:
var balance *OptionAccount
balance, err = g.GetOptionAccounts(ctx)
if err != nil {
return info, err
}
info.Accounts = append(info.Accounts, account.SubAccount{
AssetType: a,
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 := g.GetCredentials(ctx)
if err != nil {
return info, err
}
err = account.Process(&info, creds)
if err != nil {
return info, err
}
return info, nil
}
// FetchAccountInfo retrieves balances for all enabled currencies
func (g *Gateio) FetchAccountInfo(ctx context.Context, assetType asset.Item) (account.Holdings, error) {
creds, err := g.GetCredentials(ctx)
if err != nil {
return account.Holdings{}, err
}
acc, err := account.GetHoldings(g.Name, creds, assetType)
if err != nil {
return g.UpdateAccountInfo(ctx, assetType)
}
return acc, nil
}
// GetAccountFundingHistory returns funding history, deposits and
// withdrawals
func (g *Gateio) GetAccountFundingHistory(_ context.Context) ([]exchange.FundingHistory, error) {
return nil, common.ErrFunctionNotSupported
}
// GetWithdrawalsHistory returns previous withdrawals data
func (g *Gateio) GetWithdrawalsHistory(ctx context.Context, c currency.Code, _ asset.Item) ([]exchange.WithdrawalHistory, error) {
records, err := g.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 (g *Gateio) GetRecentTrades(ctx context.Context, p currency.Pair, a asset.Item) ([]trade.Data, error) {
p, err := g.FormatExchangeCurrency(p, a)
if err != nil {
return nil, err
}
var resp []trade.Data
switch a {
case asset.Spot, asset.Margin, asset.CrossMargin:
var tradeData []Trade
if p.IsEmpty() {
return nil, currency.ErrCurrencyPairEmpty
}
tradeData, err = g.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 {
var side order.Side
side, err = order.StringToOrderSide(tradeData[i].Side)
if err != nil {
return nil, err
}
resp[i] = trade.Data{
Exchange: g.Name,
TID: tradeData[i].OrderID,
CurrencyPair: p,
AssetType: a,
Side: side,
Price: tradeData[i].Price.Float64(),
Amount: tradeData[i].Amount.Float64(),
Timestamp: tradeData[i].CreateTimeMs.Time(),
}
}
case asset.Futures:
var settle string
settle, err = g.getSettlementFromCurrency(p, true)
if err != nil {
return nil, err
}
var futuresTrades []TradingHistoryItem
futuresTrades, err = g.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: g.Name,
CurrencyPair: p,
AssetType: a,
Price: futuresTrades[i].Price.Float64(),
Amount: futuresTrades[i].Size,
Timestamp: futuresTrades[i].CreateTime.Time(),
}
}
case asset.DeliveryFutures:
var settle string
settle, err = g.getSettlementFromCurrency(p, false)
if err != nil {
return nil, err
}
var deliveryTrades []DeliveryTradingHistory
deliveryTrades, err = g.GetDeliveryTradingHistory(ctx, settle, "", p.Upper(), 0, time.Time{}, time.Time{})
if err != nil {
return nil, err
}
resp = make([]trade.Data, len(deliveryTrades))
for i := range deliveryTrades {
resp[i] = trade.Data{
TID: strconv.FormatInt(deliveryTrades[i].ID, 10),
Exchange: g.Name,
CurrencyPair: p,
AssetType: a,
Price: deliveryTrades[i].Price.Float64(),
Amount: deliveryTrades[i].Size,
Timestamp: deliveryTrades[i].CreateTime.Time(),
}
}
case asset.Options:
var trades []TradingHistoryItem
trades, err = g.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: g.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 = g.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 (g *Gateio) 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 (g *Gateio) SubmitOrder(ctx context.Context, s *order.Submit) (*order.SubmitResponse, error) {
err := s.Validate()
if err != nil {
return nil, err
}
var orderTypeFormat string
switch {
case s.Side.IsLong():
orderTypeFormat = order.Buy.Lower()
case s.Side.IsShort():
orderTypeFormat = order.Sell.Lower()
default:
return nil, errInvalidOrderSide
}
s.Pair, err = g.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:
if s.Type != order.Limit {
return nil, errOnlyLimitOrderType
}
sOrder, err := g.PlaceSpotOrder(ctx, &CreateOrderRequestData{
Side: orderTypeFormat,
Type: s.Type.Lower(),
Account: g.assetTypeToString(s.AssetType),
Amount: types.Number(s.Amount),
Price: types.Number(s.Price),
CurrencyPair: s.Pair,
Text: s.ClientOrderID,
})
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.CreateTimeMs.Time()
response.LastUpdated = sOrder.UpdateTimeMs.Time()
return response, nil
case asset.Futures:
settle, err := g.getSettlementFromCurrency(s.Pair, true)
if err != nil {
return nil, err
}
if orderTypeFormat == "bid" && s.Price < 0 {
s.Price = -s.Price
} else if orderTypeFormat == "ask" && s.Price > 0 {
s.Price = -s.Price
}
fOrder, err := g.PlaceFuturesOrder(ctx, &OrderCreateParams{
Contract: s.Pair,
Size: s.Amount,
Price: types.Number(s.Price),
Settle: settle,
ReduceOnly: s.ReduceOnly,
TimeInForce: "gtc",
Text: s.ClientOrderID,
})
if err != nil {
return nil, err
}
response, err := s.DeriveSubmitResponse(strconv.FormatInt(fOrder.ID, 10))
if err != nil {
return nil, err
}
status, err := order.StringToOrderStatus(fOrder.Status)
if err != nil {
return nil, err
}
response.Status = status
response.Pair = s.Pair
response.Date = fOrder.CreateTime.Time()
response.ClientOrderID = fOrder.Text
response.ReduceOnly = fOrder.IsReduceOnly
response.Amount = fOrder.RemainingAmount
return response, nil
case asset.DeliveryFutures:
settle, err := g.getSettlementFromCurrency(s.Pair, false)
if err != nil {
return nil, err
}
if orderTypeFormat == "bid" && s.Price < 0 {
s.Price = -s.Price
} else if orderTypeFormat == "ask" && s.Price > 0 {
s.Price = -s.Price
}
newOrder, err := g.PlaceDeliveryOrder(ctx, &OrderCreateParams{
Contract: s.Pair,
Size: s.Amount,
Price: types.Number(s.Price),
Settle: settle,
ReduceOnly: s.ReduceOnly,
TimeInForce: "gtc",
Text: s.ClientOrderID,
})
if err != nil {
return nil, err
}
response, err := s.DeriveSubmitResponse(strconv.FormatInt(newOrder.ID, 10))
if err != nil {
return nil, err
}
status, err := order.StringToOrderStatus(newOrder.Status)
if err != nil {
return nil, err
}
response.Status = status
response.Pair = s.Pair
response.Date = newOrder.CreateTime.Time()
response.ClientOrderID = newOrder.Text
response.Amount = newOrder.Size
response.Price = newOrder.OrderPrice.Float64()
return response, nil
case asset.Options:
optionOrder, err := g.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 will allow of changing orderbook placement and limit to market conversion
func (g *Gateio) ModifyOrder(_ context.Context, _ *order.Modify) (*order.ModifyResponse, error) {
return nil, common.ErrFunctionNotSupported
}
// CancelOrder cancels an order by its corresponding ID number
func (g *Gateio) CancelOrder(ctx context.Context, o *order.Cancel) error {
if err := o.Validate(o.StandardCancel()); err != nil {
return err
}
fPair, err := g.FormatExchangeCurrency(o.Pair, o.AssetType)
if err != nil {
return err
}
switch o.AssetType {
case asset.Spot, asset.Margin, asset.CrossMargin:
_, err = g.CancelSingleSpotOrder(ctx, o.OrderID, fPair.String(), o.AssetType == asset.CrossMargin)
case asset.Futures, asset.DeliveryFutures:
var settle string
settle, err = g.getSettlementFromCurrency(fPair, true)
if err != nil {
return err
}
if o.AssetType == asset.Futures {
_, err = g.CancelSingleFuturesOrder(ctx, settle, o.OrderID)
} else {
_, err = g.CancelSingleDeliveryOrder(ctx, settle, o.OrderID)
}
if err != nil {
return err
}
case asset.Options:
_, err = g.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 (g *Gateio) CancelBatchOrders(ctx context.Context, o []order.Cancel) (*order.CancelBatchResponse, error) {
var response order.CancelBatchResponse
response.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 = g.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 := 0; count < loop; count++ {
var input []CancelOrderByIDParam
if (count + 1) == loop {
input = cancelSpotOrdersParam[count*10:]
} else {
input = cancelSpotOrdersParam[count*10 : (count*10)+10]
}
var cancel []CancelOrderByIDResponse
cancel, err = g.CancelBatchOrdersWithIDList(ctx, input)
if err != nil {
return nil, err
}
for x := range cancel {
response.Status[cancel[x].OrderID] = func() string {
if cancel[x].Succeeded {
return order.Cancelled.String()
}
return ""
}()
}
}
case asset.Futures:
for a := range o {
cancel, err := g.CancelMultipleFuturesOpenOrders(ctx, o[a].Pair, o[a].Side.Lower(), o[a].Pair.Quote.String())
if err != nil {
return nil, err
}
for x := range cancel {
response.Status[strconv.FormatInt(cancel[x].ID, 10)] = cancel[x].Status
}
}
case asset.DeliveryFutures:
for a := range o {
settle, err := g.getSettlementFromCurrency(o[a].Pair, false)
if err != nil {
return nil, err
}
cancel, err := g.CancelMultipleDeliveryOrders(ctx, o[a].Pair, o[a].Side.Lower(), settle)
if err != nil {
return nil, err
}
for x := range cancel {
response.Status[strconv.FormatInt(cancel[x].ID, 10)] = cancel[x].Status
}
}
case asset.Options:
for a := range o {
cancel, err := g.CancelMultipleOptionOpenOrders(ctx, o[a].Pair, o[a].Pair.String(), o[a].Side.Lower())
if err != nil {
return nil, err
}
for x := range cancel {
response.Status[strconv.FormatInt(cancel[x].OptionOrderID, 10)] = cancel[x].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 (g *Gateio) CancelAllOrders(ctx context.Context, o *order.Cancel) (order.CancelAllResponse, error) {
err := o.Validate()
if err != nil {
return order.CancelAllResponse{}, err
}
var cancelAllOrdersResponse order.CancelAllResponse
cancelAllOrdersResponse.Status = map[string]string{}
switch o.AssetType {
case asset.Spot, asset.Margin, asset.CrossMargin:
if o.Pair.IsEmpty() {
return order.CancelAllResponse{}, currency.ErrCurrencyPairEmpty
}
var cancel []SpotPriceTriggeredOrder
cancel, err = g.CancelMultipleSpotOpenOrders(ctx, o.Pair, o.AssetType)
if err != nil {
return cancelAllOrdersResponse, err
}
for x := range cancel {
cancelAllOrdersResponse.Status[strconv.FormatInt(cancel[x].AutoOrderID, 10)] = cancel[x].Status
}
case asset.Futures:
if o.Pair.IsEmpty() {
return cancelAllOrdersResponse, currency.ErrCurrencyPairEmpty
}
var settle string
settle, err = g.getSettlementFromCurrency(o.Pair, true)
if err != nil {
return cancelAllOrdersResponse, err
}
var cancel []Order
cancel, err = g.CancelMultipleFuturesOpenOrders(ctx, o.Pair, o.Side.Lower(), settle)
if err != nil {
return cancelAllOrdersResponse, err
}
for f := range cancel {
cancelAllOrdersResponse.Status[strconv.FormatInt(cancel[f].ID, 10)] = cancel[f].Status
}
case asset.DeliveryFutures:
if o.Pair.IsEmpty() {
return cancelAllOrdersResponse, currency.ErrCurrencyPairEmpty
}
var settle string
settle, err = g.getSettlementFromCurrency(o.Pair, false)
if err != nil {
return cancelAllOrdersResponse, err
}
var cancel []Order
cancel, err = g.CancelMultipleDeliveryOrders(ctx, o.Pair, o.Side.Lower(), settle)
if err != nil {
return cancelAllOrdersResponse, err
}
for f := range cancel {
cancelAllOrdersResponse.Status[strconv.FormatInt(cancel[f].ID, 10)] = cancel[f].Status
}
case asset.Options:
var underlying currency.Pair
if !o.Pair.IsEmpty() {
underlying, err = g.GetUnderlyingFromCurrencyPair(o.Pair)
if err != nil {
return cancelAllOrdersResponse, err
}
}
cancel, err := g.CancelMultipleOptionOpenOrders(ctx, o.Pair, underlying.String(), o.Side.Lower())
if err != nil {
return cancelAllOrdersResponse, err
}
for x := range cancel {
cancelAllOrdersResponse.Status[strconv.FormatInt(cancel[x].OptionOrderID, 10)] = cancel[x].Status
}
default:
return cancelAllOrdersResponse, fmt.Errorf("%w asset type: %v", asset.ErrNotSupported, o.AssetType)
}
return cancelAllOrdersResponse, nil
}
// GetOrderInfo returns order information based on order ID
func (g *Gateio) GetOrderInfo(ctx context.Context, orderID string, pair currency.Pair, a asset.Item) (*order.Detail, error) {
if err := g.CurrencyPairs.IsAssetEnabled(a); err != nil {
return nil, err
}
pair, err := g.FormatExchangeCurrency(pair, a)
if err != nil {
return nil, err
}
switch a {
case asset.Spot, asset.Margin, asset.CrossMargin:
var spotOrder *SpotOrder
spotOrder, err = g.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: g.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.CreateTimeMs.Time(),
LastUpdated: spotOrder.UpdateTimeMs.Time(),
}, nil
case asset.Futures, asset.DeliveryFutures:
var settle string
if a == asset.Futures {
settle, err = g.getSettlementFromCurrency(pair, true)
} else {
settle, err = g.getSettlementFromCurrency(pair, false)
}
if err != nil {
return nil, err
}
var fOrder *Order
var err error
if asset.Futures == a {
fOrder, err = g.GetSingleFuturesOrder(ctx, settle, orderID)
} else {
fOrder, err = g.GetSingleDeliveryOrder(ctx, settle, orderID)
}
if err != nil {
return nil, err
}
orderStatus, err := order.StringToOrderStatus(fOrder.Status)
if err != nil {
return nil, err
}
pair, err = currency.NewPairFromString(fOrder.Contract)
if err != nil {
return nil, err
}
return &order.Detail{
Amount: fOrder.Size,
ExecutedAmount: fOrder.Size - fOrder.RemainingAmount,
Exchange: g.Name,
OrderID: orderID,
Status: orderStatus,
Price: fOrder.OrderPrice.Float64(),
Date: fOrder.CreateTime.Time(),
LastUpdated: fOrder.FinishTime.Time(),
Pair: pair,
AssetType: a,
}, nil
case asset.Options:
optionOrder, err := g.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: g.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 (g *Gateio) GetDepositAddress(ctx context.Context, cryptocurrency currency.Code, _, chain string) (*deposit.Address, error) {
addr, err := g.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 (g *Gateio) WithdrawCryptocurrencyFunds(ctx context.Context, withdrawRequest *withdraw.Request) (*withdraw.ExchangeResponse, error) {
if err := withdrawRequest.Validate(); err != nil {
return nil, err
}
response, err := g.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 (g *Gateio) WithdrawFiatFunds(_ context.Context, _ *withdraw.Request) (*withdraw.ExchangeResponse, error) {
return nil, common.ErrFunctionNotSupported
}
// WithdrawFiatFundsToInternationalBank returns a withdrawal ID when a
// withdrawal is submitted
func (g *Gateio) WithdrawFiatFundsToInternationalBank(_ context.Context, _ *withdraw.Request) (*withdraw.ExchangeResponse, error) {
return nil, common.ErrFunctionNotSupported
}
// GetFeeByType returns an estimate of fee based on type of transaction
func (g *Gateio) GetFeeByType(ctx context.Context, feeBuilder *exchange.FeeBuilder) (float64, error) {
if feeBuilder == nil {
return 0, fmt.Errorf("%T %w", feeBuilder, common.ErrNilPointer)
}
if !g.AreCredentialsValid(ctx) && // Todo check connection status
feeBuilder.FeeType == exchange.CryptocurrencyTradeFee {
feeBuilder.FeeType = exchange.OfflineTradeFee
}
return g.GetFee(ctx, feeBuilder)
}
// GetActiveOrders retrieves any orders that are active/open
func (g *Gateio) 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 := g.GetPairFormat(req.AssetType, false)
if err != nil {
return nil, err
}
switch req.AssetType {
case asset.Spot, asset.Margin, asset.CrossMargin:
var spotOrders []SpotOrdersDetail
spotOrders, err = g.GateioSpotOpenOrders(ctx, 0, 0, req.AssetType == asset.CrossMargin)
if err != nil {
return nil, err
}
for x := range spotOrders {
var symbol currency.Pair
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 != "open" {
continue
}
var side order.Side
side, err = order.StringToOrderSide(spotOrders[x].Orders[x].Side)
if err != nil {
log.Errorf(log.ExchangeSys, "%s %v", g.Name, err)
}
var oType order.Type
oType, err = order.StringToOrderType(spotOrders[x].Orders[y].Type)
if err != nil {
return nil, err
}
var status order.Status
status, err = order.StringToOrderStatus(spotOrders[x].Orders[y].Status)
if err != nil {
log.Errorf(log.ExchangeSys, "%s %v", g.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].CreateTimeMs.Time(),
LastUpdated: spotOrders[x].Orders[y].UpdateTimeMs.Time(),
Exchange: g.Name,
AssetType: req.AssetType,
ClientOrderID: spotOrders[x].Orders[y].Text,
FeeAsset: currency.NewCode(spotOrders[x].Orders[y].FeeCurrency),
})
}
}
case asset.Futures, asset.DeliveryFutures:
if len(req.Pairs) == 0 {
return nil, currency.ErrCurrencyPairsEmpty
}
for z := range req.Pairs {
var settle string
if req.AssetType == asset.Futures {
settle, err = g.getSettlementFromCurrency(req.Pairs[z], true)
} else {
settle, err = g.getSettlementFromCurrency(req.Pairs[z], false)
}
if err != nil {
return nil, err
}
var futuresOrders []Order
if req.AssetType == asset.Futures {
futuresOrders, err = g.GetFuturesOrders(ctx, req.Pairs[z], "open", "", settle, 0, 0, 0)
} else {
futuresOrders, err = g.GetDeliveryOrders(ctx, req.Pairs[z], "open", settle, "", 0, 0, 0)
}
if err != nil {
return nil, err
}
for x := range futuresOrders {
if futuresOrders[x].Status != "open" {
continue
}
var status order.Status
status, err = order.StringToOrderStatus(futuresOrders[x].Status)
if err != nil {
log.Errorf(log.ExchangeSys, "%s %v", g.Name, err)
}
orders = append(orders, order.Detail{
Status: status,
Amount: futuresOrders[x].Size,
Pair: req.Pairs[x],
OrderID: strconv.FormatInt(futuresOrders[x].ID, 10),
Price: futuresOrders[x].OrderPrice.Float64(),
ExecutedAmount: futuresOrders[x].Size - futuresOrders[x].RemainingAmount,
RemainingAmount: futuresOrders[x].RemainingAmount,
LastUpdated: futuresOrders[x].FinishTime.Time(),
Date: futuresOrders[x].CreateTime.Time(),
ClientOrderID: futuresOrders[x].Text,
Exchange: g.Name,
AssetType: req.AssetType,
})
}
}
case asset.Options:
var optionsOrders []OptionOrderResponse
optionsOrders, err = g.GetOptionFuturesOrders(ctx, currency.EMPTYPAIR, "", "open", 0, 0, req.StartTime, req.EndTime)
if err != nil {
return nil, err
}
for x := range optionsOrders {
var currencyPair currency.Pair
var status order.Status
currencyPair, err = currency.NewPairFromString(optionsOrders[x].Contract)
if err != nil {
return nil, err
}
status, err = order.StringToOrderStatus(optionsOrders[x].Status)
if err != nil {
return nil, err
}
orders = append(orders, order.Detail{
Status: status,
Amount: optionsOrders[x].Size,
Pair: currencyPair,
OrderID: strconv.FormatInt(optionsOrders[x].OptionOrderID, 10),
Price: optionsOrders[x].Price.Float64(),
ExecutedAmount: optionsOrders[x].Size - optionsOrders[x].Left,
RemainingAmount: optionsOrders[x].Left,
LastUpdated: optionsOrders[x].FinishTime.Time(),
Date: optionsOrders[x].CreateTime.Time(),
Exchange: g.Name,
AssetType: req.AssetType,
ClientOrderID: optionsOrders[x].Text,
})
}
default:
return nil, fmt.Errorf("%w asset type: %v", asset.ErrNotSupported, req.AssetType)
}
return req.Filter(g.Name, orders), nil
}
// GetOrderHistory retrieves account order information
// Can Limit response to specific order status
func (g *Gateio) GetOrderHistory(ctx context.Context, req *order.MultiOrderRequest) (order.FilteredOrders, error) {
err := req.Validate()
if err != nil {
return nil, err
}
var orders []order.Detail
format, err := g.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)
var spotOrders []SpotPersonalTradeHistory
spotOrders, err = g.GateIOGetPersonalTradingHistory(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: g.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.Futures, asset.DeliveryFutures:
for x := range req.Pairs {
fPair := req.Pairs[x].Format(format)
var settle string
if req.AssetType == asset.Futures {
settle, err = g.getSettlementFromCurrency(fPair, true)
} else {
settle, err = g.getSettlementFromCurrency(fPair, false)
}
if err != nil {
return nil, err
}
if req.AssetType == asset.Futures && settle == settleUSD {
settle = settleBTC
}
var futuresOrder []TradingHistoryItem
if req.AssetType == asset.Futures {
futuresOrder, err = g.GetMyPersonalTradingHistory(ctx, settle, "", req.FromOrderID, fPair, 0, 0, 0)
} else {
futuresOrder, err = g.GetDeliveryPersonalTradingHistory(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: g.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)
var optionOrders []OptionTradingHistory
optionOrders, err = g.GetOptionsPersonalTradingHistory(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: g.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(g.Name, orders), nil
}
// GetHistoricCandles returns candles between a time period for a set time interval
func (g *Gateio) GetHistoricCandles(ctx context.Context, pair currency.Pair, a asset.Item, interval kline.Interval, start, end time.Time) (*kline.Item, error) {
req, err := g.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:
var candles []Candlestick
candles, err = g.GetCandlesticks(ctx, req.RequestFormatted, 0, start, end, interval)
if err != nil {
return nil, err
}
listCandlesticks = make([]kline.Candle, len(candles))
for x := range candles {
listCandlesticks[x] = kline.Candle{
Time: candles[x].Timestamp,
Open: candles[x].OpenPrice,
High: candles[x].HighestPrice,
Low: candles[x].LowestPrice,
Close: candles[x].ClosePrice,
Volume: candles[x].QuoteCcyVolume,
}
}
case asset.Futures, asset.DeliveryFutures:
var settlement string
if req.Asset == asset.Futures {
settlement, err = g.getSettlementFromCurrency(req.RequestFormatted, true)
} else {
settlement, err = g.getSettlementFromCurrency(req.RequestFormatted, false)
}
if err != nil {
return nil, err
}
if req.Asset == asset.Futures && settlement == settleUSD {
settlement = settleBTC
}
var candles []FuturesCandlestick
if a == asset.Futures {
candles, err = g.GetFuturesCandlesticks(ctx, settlement, req.RequestFormatted.String(), start, end, 0, interval)
} else {
candles, err = g.GetDeliveryFuturesCandlesticks(ctx, settlement, req.RequestFormatted.Upper(), start, end, 0, interval)
}
if err != nil {
return nil, err
}
listCandlesticks = make([]kline.Candle, len(candles))
for x := range candles {
listCandlesticks[x] = kline.Candle{
Time: candles[x].Timestamp.Time(),
Open: candles[x].OpenPrice.Float64(),
High: candles[x].HighestPrice.Float64(),
Low: candles[x].LowestPrice.Float64(),
Close: candles[x].ClosePrice.Float64(),
Volume: candles[x].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 (g *Gateio) GetHistoricCandlesExtended(ctx context.Context, pair currency.Pair, a asset.Item, interval kline.Interval, start, end time.Time) (*kline.Item, error) {
req, err := g.GetKlineExtendedRequest(pair, a, interval, start, end)
if err != nil {
return nil, err
}
candlestickItems := make([]kline.Candle, 0, req.Size())
for b := range req.RangeHolder.Ranges {
switch a {
case asset.Spot, asset.Margin, asset.CrossMargin:
var candles []Candlestick
candles, err = g.GetCandlesticks(ctx, req.RequestFormatted, 0, req.RangeHolder.Ranges[b].Start.Time, req.RangeHolder.Ranges[b].End.Time, interval)
if err != nil {
return nil, err
}
for x := range candles {
candlestickItems = append(candlestickItems, kline.Candle{
Time: candles[x].Timestamp,
Open: candles[x].OpenPrice,
High: candles[x].HighestPrice,
Low: candles[x].LowestPrice,
Close: candles[x].ClosePrice,
Volume: candles[x].QuoteCcyVolume,
})
}
case asset.Futures, asset.DeliveryFutures:
var settle string
if req.Asset == asset.Futures {
settle, err = g.getSettlementFromCurrency(req.RequestFormatted, true)
} else {
settle, err = g.getSettlementFromCurrency(req.RequestFormatted, false)
}
if err != nil {
return nil, err
}
if req.Asset == asset.Futures && settle == settleUSD {
settle = settleBTC
}
var candles []FuturesCandlestick
if a == asset.Futures {
candles, err = g.GetFuturesCandlesticks(ctx, settle, req.RequestFormatted.String(), req.RangeHolder.Ranges[b].Start.Time, req.RangeHolder.Ranges[b].End.Time, 0, interval)
} else {
candles, err = g.GetDeliveryFuturesCandlesticks(ctx, settle, req.RequestFormatted.Upper(), req.RangeHolder.Ranges[b].Start.Time, req.RangeHolder.Ranges[b].End.Time, 0, interval)
}
if err != nil {
return nil, err
}
for x := range candles {
candlestickItems = append(candlestickItems, kline.Candle{
Time: candles[x].Timestamp.Time(),
Open: candles[x].OpenPrice.Float64(),
High: candles[x].HighestPrice.Float64(),
Low: candles[x].LowestPrice.Float64(),
Close: candles[x].ClosePrice.Float64(),
Volume: candles[x].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 (g *Gateio) GetAvailableTransferChains(ctx context.Context, cryptocurrency currency.Code) ([]string, error) {
chains, err := g.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 (g *Gateio) ValidateAPICredentials(ctx context.Context, assetType asset.Item) error {
_, err := g.UpdateAccountInfo(ctx, assetType)
return g.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 (g *Gateio) checkInstrumentAvailabilityInSpot(instrument currency.Pair) (bool, error) {
availables, err := g.CurrencyPairs.GetPairs(asset.Spot, false)
if err != nil {
return false, err
}
return availables.Contains(instrument, true), nil
}
// GetFuturesContractDetails returns details about futures contracts
func (g *Gateio) GetFuturesContractDetails(ctx context.Context, item asset.Item) ([]futures.Contract, error) {
if !item.IsFutures() {
return nil, futures.ErrNotFuturesAsset
}
if !g.SupportsAsset(item) {
return nil, fmt.Errorf("%w %v", asset.ErrNotSupported, item)
}
switch item {
case asset.Futures:
settlePairs := []string{"btc", "usdt", "usd"}
var resp []futures.Contract
for k := range settlePairs {
contracts, err := g.GetAllFutureContracts(ctx, settlePairs[k])
if err != nil {
return nil, err
}
contractsToAdd := make([]futures.Contract, len(contracts))
for j := range contracts {
var name currency.Pair
name, err = currency.NewPairFromString(contracts[j].Name)
if err != nil {
return nil, err
}
settlePair := currency.NewCode(settlePairs[k])
contractSettlementType := futures.Linear
switch {
case name.Base.Equal(currency.BTC) && settlePair.Equal(currency.BTC):
contractSettlementType = futures.Inverse
case !name.Base.Equal(settlePair) && !settlePair.Equal(currency.USDT):
contractSettlementType = futures.Quanto
}
c := futures.Contract{
Exchange: g.Name,
Name: name,
Underlying: name,
Asset: item,
IsActive: !contracts[j].InDelisting,
Type: futures.Perpetual,
SettlementType: contractSettlementType,
SettlementCurrencies: currency.Currencies{settlePair},
Multiplier: contracts[j].QuantoMultiplier.Float64(),
MaxLeverage: contracts[j].LeverageMax.Float64(),
}
if contracts[j].FundingRate > 0 {
c.LatestRate = fundingrate.Rate{
Time: contracts[j].FundingNextApply.Time().Add(-time.Duration(contracts[j].FundingInterval) * time.Second),
Rate: contracts[j].FundingRate.Decimal(),
}
}
contractsToAdd[j] = c
}
resp = append(resp, contractsToAdd...)
}
return resp, nil
case asset.DeliveryFutures:
settlePairs := []string{"btc", "usdt"}
var resp []futures.Contract
for k := range settlePairs {
contracts, err := g.GetAllDeliveryContracts(ctx, settlePairs[k])
if err != nil {
return nil, err
}
contractsToAdd := make([]futures.Contract, len(contracts))
for j := range contracts {
var name, underlying currency.Pair
name, err = currency.NewPairFromString(contracts[j].Name)
if err != nil {
return nil, err
}
underlying, err = currency.NewPairFromString(contracts[j].Underlying)
if err != nil {
return nil, err
}
var ct futures.ContractType
// 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 s, e time.Time
e = contracts[j].ExpireTime.Time()
switch contracts[j].Cycle {
case "WEEKLY":
ct = futures.Weekly
s = e.Add(-kline.OneWeek.Duration())
case "BI-WEEKLY":
ct = futures.Fortnightly
s = e.Add(-kline.TwoWeek.Duration())
case "QUARTERLY":
ct = futures.Quarterly
s = e.Add(-kline.ThreeMonth.Duration())
case "BI-QUARTERLY":
ct = futures.HalfYearly
s = e.Add(-kline.SixMonth.Duration())
default:
ct = futures.LongDated
}
contractsToAdd[j] = futures.Contract{
Exchange: g.Name,
Name: name,
Underlying: underlying,
Asset: item,
StartDate: s,
EndDate: e,
SettlementType: futures.Linear,
IsActive: !contracts[j].InDelisting,
Type: ct,
SettlementCurrencies: currency.Currencies{currency.NewCode(settlePairs[k])},
MarginCurrency: currency.Code{},
Multiplier: contracts[j].QuantoMultiplier.Float64(),
MaxLeverage: contracts[j].LeverageMax.Float64(),
}
}
resp = append(resp, contractsToAdd...)
}
return resp, nil
}
return nil, fmt.Errorf("%w %v", asset.ErrNotSupported, item)
}
// UpdateOrderExecutionLimits sets exchange executions for a required asset type
func (g *Gateio) UpdateOrderExecutionLimits(ctx context.Context, a asset.Item) error {
if !g.SupportsAsset(a) {
return fmt.Errorf("%s %w", a, asset.ErrNotSupported)
}
var limits []order.MinMaxLevel
switch a {
case asset.Spot:
var pairsData []CurrencyPairDetail
pairsData, err := g.ListSpotCurrencyPairs(ctx)
if err != nil {
return err
}
limits = make([]order.MinMaxLevel, 0, len(pairsData))
for x := range pairsData {
if pairsData[x].TradeStatus == "untradable" {
continue
}
var pair currency.Pair
pair, err = g.MatchSymbolWithAvailablePairs(pairsData[x].ID, a, true)
if err != nil {
return err
}
// Minimum base amounts are not always provided this will default to
// precision for base deployment. This can't be done for quote.
minBaseAmount := pairsData[x].MinBaseAmount.Float64()
if minBaseAmount == 0 {
minBaseAmount = math.Pow10(-int(pairsData[x].AmountPrecision))
}
limits = append(limits, order.MinMaxLevel{
Asset: a,
Pair: pair,
QuoteStepIncrementSize: math.Pow10(-int(pairsData[x].Precision)),
AmountStepIncrementSize: math.Pow10(-int(pairsData[x].AmountPrecision)),
MinimumBaseAmount: minBaseAmount,
MinimumQuoteAmount: pairsData[x].MinQuoteAmount.Float64(),
})
}
default:
// TODO: Add in other assets
return fmt.Errorf("%s %w", a, common.ErrNotYetImplemented)
}
return g.LoadLimits(limits)
}
// GetHistoricalFundingRates returns historical funding rates for a futures contract
func (g *Gateio) 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
}
}
// 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 := g.FormatExchangeCurrency(r.Pair, r.Asset)
if err != nil {
return nil, err
}
records, err := g.GetFutureFundingRates(ctx, r.PaymentCurrency.String(), 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: g.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 (g *Gateio) 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.Futures {
return nil, fmt.Errorf("%w %v", asset.ErrNotSupported, r.Asset)
}
if !r.Pair.IsEmpty() {
resp := make([]fundingrate.LatestRateResponse, 1)
fPair, err := g.FormatExchangeCurrency(r.Pair, r.Asset)
if err != nil {
return nil, err
}
var settle string
settle, err = g.getSettlementFromCurrency(fPair, true)
if err != nil {
return nil, err
}
contract, err := g.GetSingleContract(ctx, settle, fPair.String())
if err != nil {
return nil, err
}
resp[0] = contractToFundingRate(g.Name, r.Asset, fPair, contract, r.IncludePredictedRate)
return resp, nil
}
var resp []fundingrate.LatestRateResponse
settleCurrencies := []string{"btc", "usdt", "usd"}
pairs, err := g.GetEnabledPairs(asset.Futures)
if err != nil {
return nil, err
}
for i := range settleCurrencies {
contracts, err := g.GetAllFutureContracts(ctx, settleCurrencies[i])
if err != nil {
return nil, err
}
for j := range contracts {
p := strings.ToUpper(contracts[j].Name)
if !g.IsValidPairString(p) {
continue
}
cp, err := currency.NewPairFromString(p)
if err != nil {
return nil, err
}
if !pairs.Contains(cp, false) {
continue
}
var isPerp bool
isPerp, err = g.IsPerpetualFutureCurrency(r.Asset, cp)
if err != nil {
return nil, err
}
if !isPerp {
continue
}
resp = append(resp, contractToFundingRate(g.Name, r.Asset, cp, &contracts[j], r.IncludePredictedRate))
}
}
return 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 (g *Gateio) IsPerpetualFutureCurrency(a asset.Item, _ currency.Pair) (bool, error) {
return a == asset.Futures, nil
}