mirror of
https://github.com/d0zingcat/gocryptotrader.git
synced 2026-05-13 23:16:45 +00:00
* refactor(gateio): enhance order execution limits and currency pair details * Update exchanges/gateio/gateio_wrapper.go Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * REEEEEHHHHHH * linter: fix * fix GetOpenInterest when a contract is delisted * add handling for delisting end time correctly * Update exchange/order/limits/limits_types.go Co-authored-by: Gareth Kirwan <gbjkirwan@gmail.com> * Update exchange/order/limits/limits_types.go Co-authored-by: Gareth Kirwan <gbjkirwan@gmail.com> * Update exchanges/gateio/gateio_types.go Co-authored-by: Gareth Kirwan <gbjkirwan@gmail.com> * Update exchanges/gateio/gateio_types.go Co-authored-by: Gareth Kirwan <gbjkirwan@gmail.com> * gk: nits * gci: fix * linter: fix * gateio: Add launch and update tests (cherry-pick) * gk: nits + removed spot setting delisting as delisted because it is not a start time value * glorious: apply diff --------- Co-authored-by: Ryan O'Hara-Reid <ryan.oharareid@thrasher.io> Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> Co-authored-by: Gareth Kirwan <gbjkirwan@gmail.com>
2665 lines
86 KiB
Go
2665 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 := ¤cy.PairFormat{Delimiter: currency.UnderscoreDelimiter, Uppercase: true}
|
|
configFmt := ¤cy.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,
|
|
RequestIDGenerator: e.messageIDSeq.IncrementAndGet,
|
|
})
|
|
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,
|
|
RequestIDGenerator: e.messageIDSeq.IncrementAndGet,
|
|
})
|
|
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,
|
|
RequestIDGenerator: e.messageIDSeq.IncrementAndGet,
|
|
})
|
|
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,
|
|
RequestIDGenerator: e.messageIDSeq.IncrementAndGet,
|
|
})
|
|
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,
|
|
RequestIDGenerator: e.messageIDSeq.IncrementAndGet,
|
|
})
|
|
}
|
|
|
|
// 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) {
|
|
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 = e.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.CoinMarginedFutures, asset.USDTMarginedFutures, asset.DeliveryFutures:
|
|
if o.Pair.IsEmpty() {
|
|
return cancelAllOrdersResponse, currency.ErrCurrencyPairEmpty
|
|
}
|
|
settle, err := getSettlementCurrency(o.Pair, o.AssetType)
|
|
if err != nil {
|
|
return cancelAllOrdersResponse, err
|
|
}
|
|
var cancel []Order
|
|
if o.AssetType == asset.DeliveryFutures {
|
|
cancel, err = e.CancelMultipleDeliveryOrders(ctx, o.Pair, o.Side.Lower(), settle)
|
|
} else {
|
|
cancel, err = e.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.Options:
|
|
var underlying currency.Pair
|
|
if !o.Pair.IsEmpty() {
|
|
underlying, err = e.GetUnderlyingFromCurrencyPair(o.Pair)
|
|
if err != nil {
|
|
return cancelAllOrdersResponse, err
|
|
}
|
|
}
|
|
cancel, err := e.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 (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) {
|
|
switch {
|
|
case s.Side.IsLong():
|
|
s.Side = order.Buy
|
|
case s.Side.IsShort():
|
|
s.Side = order.Sell
|
|
default:
|
|
return nil, order.ErrSideIsInvalid
|
|
}
|
|
|
|
tif, err := toExchangeTIF(s.TimeInForce, s.Price)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return &CreateOrderRequest{
|
|
Side: s.Side.Lower(),
|
|
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)
|
|
}
|
|
}
|