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

* Okx: Replace conn.RequestIDGenerator with MesssageID

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

* Exchanges: Add MessageSequence

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

* GateIO: Split usage of MessageID and MessageSequence

* Binance: Switch to UUID message IDs

* Kraken: Switch to e.MessageSequence

* Kucoin: Switch to MessageID

* HitBTC: Switch to UUIDv7 for ws message ID

* Bybit: Switch to UUIDv7 for ws message ID

* Bitfinex: Switch to UUIDv7 and MessageSequence

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

* Websocket: Remove now unused MessageID function

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

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

3045 lines
99 KiB
Go

package okx
import (
"context"
"errors"
"fmt"
"math"
"sort"
"strconv"
"strings"
"time"
"github.com/gofrs/uuid"
"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/collateral"
"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/margin"
"github.com/thrasher-corp/gocryptotrader/exchanges/order"
"github.com/thrasher-corp/gocryptotrader/exchanges/orderbook"
"github.com/thrasher-corp/gocryptotrader/exchanges/protocol"
"github.com/thrasher-corp/gocryptotrader/exchanges/request"
"github.com/thrasher-corp/gocryptotrader/exchanges/ticker"
"github.com/thrasher-corp/gocryptotrader/exchanges/trade"
"github.com/thrasher-corp/gocryptotrader/log"
"github.com/thrasher-corp/gocryptotrader/portfolio/withdraw"
)
const (
websocketResponseMaxLimit = time.Second * 3
)
// SetDefaults sets the basic defaults for Okx
func (e *Exchange) SetDefaults() {
e.Name = "Okx"
e.Enabled = true
e.Verbose = true
e.API.CredentialsValidator.RequiresKey = true
e.API.CredentialsValidator.RequiresSecret = true
e.API.CredentialsValidator.RequiresClientID = true
e.instrumentsInfoMap = make(map[string][]Instrument)
cpf := &currency.PairFormat{
Delimiter: currency.DashDelimiter,
Uppercase: true,
}
// In this exchange, we represent deliverable futures contracts as 'FUTURES'/asset.Futures and perpetual futures as 'SWAP'/asset.PerpetualSwap
err := e.SetGlobalPairsManager(cpf, cpf, asset.Spot, asset.Futures, asset.PerpetualSwap, asset.Options, asset.Margin, asset.Spread)
if err != nil {
log.Errorln(log.ExchangeSys, err)
}
// TODO: Disabled until spread/business websocket is implemented
if err := e.DisableAssetWebsocketSupport(asset.Spread); err != nil {
log.Errorf(log.ExchangeSys, "%s error disabling %q asset websocket support: %s", e.Name, asset.Spread.String(), err)
}
// Fill out the capabilities/features that the exchange supports
e.Features = exchange.Features{
CurrencyTranslations: currency.NewTranslations(map[currency.Code]currency.Code{
currency.NewCode("USDT-SWAP"): currency.USDT,
currency.NewCode("USD-SWAP"): currency.USD,
currency.NewCode("USDC-SWAP"): currency.USDC,
}),
Supports: exchange.FeaturesSupported{
REST: true,
Websocket: true,
MaximumOrderHistory: kline.OneDay.Duration() * 90,
RESTCapabilities: protocol.Features{
TickerFetching: true,
OrderbookFetching: true,
AutoPairUpdates: true,
AccountInfo: true,
CryptoDeposit: true,
CryptoWithdrawalFee: true,
CryptoWithdrawal: true,
TradeFee: true,
SubmitOrder: true,
GetOrder: true,
GetOrders: true,
CancelOrder: true,
CancelOrders: true,
TradeFetching: true,
UserTradeHistory: true,
MultiChainDeposits: true,
MultiChainWithdrawals: true,
KlineFetching: true,
DepositHistory: true,
WithdrawalHistory: true,
ModifyOrder: true,
FundingRateFetching: true,
PredictedFundingRate: true,
},
WebsocketCapabilities: protocol.Features{
TickerFetching: true,
OrderbookFetching: true,
Subscribe: true,
Unsubscribe: true,
AuthenticatedEndpoints: true,
AccountInfo: true,
GetOrders: true,
TradeFetching: true,
KlineFetching: true,
GetOrder: true,
SubmitOrder: true,
CancelOrder: true,
CancelOrders: true,
ModifyOrder: true,
},
WithdrawPermissions: exchange.AutoWithdrawCrypto,
FuturesCapabilities: exchange.FuturesCapabilities{
Positions: true,
Leverage: true,
CollateralMode: true,
OpenInterest: exchange.OpenInterestSupport{
Supported: true,
SupportsRestBatch: true,
},
FundingRates: true,
MaximumFundingRateHistory: kline.ThreeMonth.Duration(),
SupportedFundingRateFrequencies: map[kline.Interval]bool{
kline.EightHour: true,
},
},
},
Enabled: exchange.FeaturesEnabled{
AutoPairUpdates: true,
Kline: kline.ExchangeCapabilitiesEnabled{
Intervals: kline.DeployExchangeIntervals(
kline.IntervalCapacity{Interval: kline.OneMin},
kline.IntervalCapacity{Interval: kline.ThreeMin},
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.SixHour},
kline.IntervalCapacity{Interval: kline.TwelveHour},
kline.IntervalCapacity{Interval: kline.OneDay},
kline.IntervalCapacity{Interval: kline.TwoDay},
kline.IntervalCapacity{Interval: kline.ThreeDay},
kline.IntervalCapacity{Interval: kline.FiveDay},
kline.IntervalCapacity{Interval: kline.OneWeek},
kline.IntervalCapacity{Interval: kline.OneMonth},
kline.IntervalCapacity{Interval: kline.ThreeMonth},
kline.IntervalCapacity{Interval: kline.SixMonth},
kline.IntervalCapacity{Interval: kline.OneYear},
),
GlobalResultLimit: 100, // Reference: https://www.okx.com/docs-v5/en/#rest-api-market-data-get-candlesticks-history
},
},
Subscriptions: defaultSubscriptions.Clone(),
}
e.Requester, err = request.New(e.Name,
common.NewHTTPClientWithTimeout(exchange.DefaultHTTPTimeout),
request.WithLimiter(rateLimits))
if err != nil {
log.Errorln(log.ExchangeSys, err)
}
e.API.Endpoints = e.NewEndpoints()
err = e.API.Endpoints.SetDefaultEndpoints(map[exchange.URL]string{
exchange.RestSpot: apiURL,
exchange.WebsocketSpot: apiWebsocketPublicURL,
})
if err != nil {
log.Errorln(log.ExchangeSys, err)
}
e.Websocket = websocket.NewManager()
e.WebsocketResponseMaxLimit = websocketResponseMaxLimit
e.WebsocketResponseCheckTimeout = websocketResponseMaxLimit
e.WebsocketOrderbookBufferLimit = exchange.DefaultWebsocketOrderbookBufferLimit
}
// Setup takes in the supplied exchange configuration details and sets params
func (e *Exchange) Setup(exch *config.Exchange) error {
if err := exch.Validate(); err != nil {
return err
}
if !exch.Enabled {
e.SetEnabled(false)
return nil
}
if err := e.SetupDefaults(exch); err != nil {
return err
}
wsRunningEndpoint, err := e.API.Endpoints.GetURL(exchange.WebsocketSpot)
if err != nil {
return err
}
if err := e.Websocket.Setup(&websocket.ManagerSetup{
ExchangeConfig: exch,
DefaultURL: apiWebsocketPublicURL,
RunningURL: wsRunningEndpoint,
Connector: e.WsConnect,
Subscriber: e.Subscribe,
Unsubscriber: e.Unsubscribe,
GenerateSubscriptions: e.generateSubscriptions,
Features: &e.Features.Supports.WebsocketCapabilities,
MaxWebsocketSubscriptionsPerConnection: 240,
RateLimitDefinitions: rateLimits,
}); err != nil {
return err
}
if err := e.Websocket.SetupNewConnection(&websocket.ConnectionSetup{
URL: apiWebsocketPublicURL,
ResponseCheckTimeout: exch.WebsocketResponseCheckTimeout,
ResponseMaxLimit: websocketResponseMaxLimit,
RateLimit: request.NewRateLimitWithWeight(time.Second, 2, 1),
}); err != nil {
return err
}
return e.Websocket.SetupNewConnection(&websocket.ConnectionSetup{
URL: apiWebsocketPrivateURL,
ResponseCheckTimeout: exch.WebsocketResponseCheckTimeout,
ResponseMaxLimit: websocketResponseMaxLimit,
Authenticated: true,
RateLimit: request.NewRateLimitWithWeight(time.Second, 2, 1),
})
}
// GetServerTime returns the current exchange server time.
func (e *Exchange) GetServerTime(ctx context.Context, _ asset.Item) (time.Time, error) {
t, err := e.GetSystemTime(ctx)
return t.Time(), err
}
// FetchTradablePairs returns a list of the exchanges tradable pairs
func (e *Exchange) FetchTradablePairs(ctx context.Context, a asset.Item) (currency.Pairs, error) {
switch a {
case asset.Options, asset.Futures, asset.Spot, asset.PerpetualSwap, asset.Margin:
format, err := e.GetPairFormat(a, true)
if err != nil {
return nil, err
}
insts, err := e.getInstrumentsForAsset(ctx, a)
if err != nil {
return nil, err
}
pairs := make([]currency.Pair, 0, len(insts))
for x := range insts {
if insts[x].State != "live" {
continue
}
pairs = append(pairs, insts[x].InstrumentID.Format(format))
}
return pairs, nil
case asset.Spread:
format, err := e.GetPairFormat(a, true)
if err != nil {
return nil, err
}
spreadInstruments, err := e.GetPublicSpreads(ctx, "", "", "", "live")
if err != nil {
return nil, fmt.Errorf("%w asset type: %v", err, a)
}
pairs := make(currency.Pairs, len(spreadInstruments))
for x := range spreadInstruments {
pairs[x] = spreadInstruments[x].SpreadID.Format(format)
}
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 {
assetTypes := e.GetAssetTypes(true)
for i := range assetTypes {
pairs, err := e.FetchTradablePairs(ctx, assetTypes[i])
if err != nil {
return fmt.Errorf("%w for asset %v", err, assetTypes[i])
}
if err := e.UpdatePairs(pairs, assetTypes[i], false); err != nil {
return fmt.Errorf("%w for asset %v", err, assetTypes[i])
}
}
return e.EnsureOnePairEnabled()
}
// UpdateOrderExecutionLimits sets exchange execution order limits for an asset type
func (e *Exchange) UpdateOrderExecutionLimits(ctx context.Context, a asset.Item) error {
switch a {
case asset.Spot, asset.Margin, asset.Options,
asset.PerpetualSwap, asset.Futures:
insts, err := e.getInstrumentsForAsset(ctx, a)
if err != nil {
return err
}
if len(insts) == 0 {
return common.ErrNoResponse
}
l := make([]limits.MinMaxLevel, len(insts))
for i := range insts {
l[i] = limits.MinMaxLevel{
Key: key.NewExchangeAssetPair(e.Name, a, insts[i].InstrumentID),
PriceStepIncrementSize: insts[i].TickSize.Float64(),
MinimumBaseAmount: insts[i].MinimumOrderSize.Float64(),
}
}
return limits.Load(l)
case asset.Spread:
insts, err := e.GetPublicSpreads(ctx, "", "", "", "live")
if err != nil {
return err
}
if len(insts) == 0 {
return common.ErrNoResponse
}
l := make([]limits.MinMaxLevel, len(insts))
for i := range insts {
l[i] = limits.MinMaxLevel{
Key: key.NewExchangeAssetPair(e.Name, a, insts[i].SpreadID),
PriceStepIncrementSize: insts[i].MinSize.Float64(),
MinimumBaseAmount: insts[i].MinSize.Float64(),
QuoteStepIncrementSize: insts[i].TickSize.Float64(),
}
}
return limits.Load(l)
default:
return fmt.Errorf("%w %q", asset.ErrNotSupported, a)
}
}
// 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: %v", asset.ErrNotSupported, a)
}
p, err := e.FormatExchangeCurrency(p, a)
if err != nil {
return nil, err
}
if a == asset.Spread {
spreadTicker, err := e.GetPublicSpreadTickers(ctx, p.String())
if err != nil {
return nil, err
}
if len(spreadTicker) == 0 {
return nil, fmt.Errorf("no ticker data for %s", p.String())
}
if err := ticker.ProcessTicker(&ticker.Price{
Last: spreadTicker[0].Last.Float64(),
High: spreadTicker[0].High24Hour.Float64(),
Low: spreadTicker[0].Low24Hour.Float64(),
Bid: spreadTicker[0].BidPrice.Float64(),
BidSize: spreadTicker[0].BidSize.Float64(),
Ask: spreadTicker[0].AskPrice.Float64(),
AskSize: spreadTicker[0].AskSize.Float64(),
Volume: spreadTicker[0].Volume24Hour.Float64(),
Open: spreadTicker[0].Open24Hour.Float64(),
LastUpdated: spreadTicker[0].Timestamp.Time(),
Pair: p,
AssetType: a,
ExchangeName: e.Name,
}); err != nil {
return nil, err
}
} else {
mdata, err := e.GetTicker(ctx, p.String())
if err != nil {
return nil, err
}
var baseVolume, quoteVolume float64
switch a {
case asset.Spot, asset.Margin:
baseVolume = mdata.Vol24H.Float64()
quoteVolume = mdata.VolCcy24H.Float64()
case asset.PerpetualSwap, asset.Futures, asset.Options:
baseVolume = mdata.VolCcy24H.Float64()
quoteVolume = mdata.Vol24H.Float64()
}
if err := ticker.ProcessTicker(&ticker.Price{
Last: mdata.LastTradePrice.Float64(),
High: mdata.High24H.Float64(),
Low: mdata.Low24H.Float64(),
Bid: mdata.BestBidPrice.Float64(),
BidSize: mdata.BestBidSize.Float64(),
Ask: mdata.BestAskPrice.Float64(),
AskSize: mdata.BestAskSize.Float64(),
Volume: baseVolume,
QuoteVolume: quoteVolume,
Open: mdata.Open24H.Float64(),
Pair: p,
ExchangeName: e.Name,
AssetType: a,
}); err != nil {
return nil, err
}
}
return ticker.GetTicker(e.Name, p, a)
}
// UpdateTickers updates all currency pairs of a given asset type
func (e *Exchange) UpdateTickers(ctx context.Context, assetType asset.Item) error {
switch assetType {
case asset.Spread:
format, err := e.GetPairFormat(asset.Spread, true)
if err != nil {
return err
}
pairs, err := e.GetEnabledPairs(assetType)
if err != nil {
return err
}
for y := range pairs {
var spreadTickers []SpreadTicker
spreadTickers, err = e.GetPublicSpreadTickers(ctx, format.Format(pairs[y]))
if err != nil {
return err
}
for x := range spreadTickers {
pair, err := currency.NewPairDelimiter(spreadTickers[x].SpreadID, format.Delimiter)
if err != nil {
return err
}
err = ticker.ProcessTicker(&ticker.Price{
Last: spreadTickers[x].Last.Float64(),
Bid: spreadTickers[x].BidPrice.Float64(),
BidSize: spreadTickers[x].BidSize.Float64(),
Ask: spreadTickers[x].AskPrice.Float64(),
AskSize: spreadTickers[x].AskSize.Float64(),
Pair: pair,
ExchangeName: e.Name,
AssetType: assetType,
})
if err != nil {
return err
}
}
}
case asset.Spot, asset.PerpetualSwap, asset.Futures, asset.Options, asset.Margin:
pairs, err := e.GetEnabledPairs(assetType)
if err != nil {
return err
}
instrumentType := GetInstrumentTypeFromAssetItem(assetType)
if assetType == asset.Margin {
instrumentType = instTypeSpot
}
ticks, err := e.GetTickers(ctx, instrumentType, "", "")
if err != nil {
return err
}
for y := range ticks {
pair, err := e.GetPairFromInstrumentID(ticks[y].InstrumentID.String())
if err != nil {
return err
}
for i := range pairs {
pairFmt, err := e.FormatExchangeCurrency(pairs[i], assetType)
if err != nil {
return err
}
if !pair.Equal(pairFmt) {
continue
}
err = ticker.ProcessTicker(&ticker.Price{
Last: ticks[y].LastTradePrice.Float64(),
High: ticks[y].High24H.Float64(),
Low: ticks[y].Low24H.Float64(),
Bid: ticks[y].BestBidPrice.Float64(),
BidSize: ticks[y].BestBidSize.Float64(),
Ask: ticks[y].BestAskPrice.Float64(),
AskSize: ticks[y].BestAskSize.Float64(),
Volume: ticks[y].Vol24H.Float64(),
QuoteVolume: ticks[y].VolCcy24H.Float64(),
Open: ticks[y].Open24H.Float64(),
Pair: pairFmt,
ExchangeName: e.Name,
AssetType: assetType,
})
if err != nil {
return err
}
}
}
default:
return fmt.Errorf("%w %v", asset.ErrNotSupported, assetType)
}
return nil
}
// UpdateOrderbook updates and returns the orderbook for a currency pair
func (e *Exchange) UpdateOrderbook(ctx context.Context, pair currency.Pair, assetType asset.Item) (*orderbook.Book, error) {
if pair.IsEmpty() {
return nil, currency.ErrCurrencyPairEmpty
}
var err error
switch assetType {
case asset.Spread:
var (
pairFormat currency.PairFormat
spreadOrderbook []SpreadOrderbook
)
pairFormat, err = e.GetPairFormat(assetType, true)
if err != nil {
return nil, err
}
spreadOrderbook, err = e.GetPublicSpreadOrderBooks(ctx, pairFormat.Format(pair), 50)
if err != nil {
return nil, err
}
for y := range spreadOrderbook {
book := &orderbook.Book{
Exchange: e.Name,
Pair: pair,
Asset: assetType,
ValidateOrderbook: e.ValidateOrderbook,
}
book.Bids = make(orderbook.Levels, 0, len(spreadOrderbook[y].Bids))
for b := range spreadOrderbook[y].Bids {
// Skip order book bid depths where the price value is zero.
if spreadOrderbook[y].Bids[b][0].Float64() == 0 {
continue
}
book.Bids = append(book.Bids, orderbook.Level{
Price: spreadOrderbook[y].Bids[b][0].Float64(),
Amount: spreadOrderbook[y].Bids[b][1].Float64(),
OrderCount: spreadOrderbook[y].Bids[b][2].Int64(),
})
}
book.Asks = make(orderbook.Levels, 0, len(spreadOrderbook[y].Asks))
for a := range spreadOrderbook[y].Asks {
// Skip order book ask depths where the price value is zero.
if spreadOrderbook[y].Asks[a][0].Float64() == 0 {
continue
}
book.Asks = append(book.Asks, orderbook.Level{
Price: spreadOrderbook[y].Asks[a][0].Float64(),
Amount: spreadOrderbook[y].Asks[a][1].Float64(),
OrderCount: spreadOrderbook[y].Asks[a][2].Int64(),
})
}
err = book.Process()
if err != nil {
return book, err
}
}
case asset.Spot, asset.Options, asset.Margin, asset.PerpetualSwap, asset.Futures:
err = e.CurrencyPairs.IsAssetEnabled(assetType)
if err != nil {
return nil, err
}
var instrumentID string
pairFormat, err := e.GetPairFormat(assetType, true)
if err != nil {
return nil, err
}
if !pair.IsPopulated() {
return nil, currency.ErrCurrencyPairsEmpty
}
instrumentID = pairFormat.Format(pair)
book := &orderbook.Book{
Exchange: e.Name,
Pair: pair,
Asset: assetType,
ValidateOrderbook: e.ValidateOrderbook,
}
var orderBookD *OrderBookResponseDetail
orderBookD, err = e.GetOrderBookDepth(ctx, instrumentID, 400)
if err != nil {
return book, err
}
book.Bids = make(orderbook.Levels, len(orderBookD.Bids))
for x := range orderBookD.Bids {
book.Bids[x] = orderbook.Level{
Amount: orderBookD.Bids[x].Amount.Float64(),
Price: orderBookD.Bids[x].DepthPrice.Float64(),
}
}
book.Asks = make(orderbook.Levels, len(orderBookD.Asks))
for x := range orderBookD.Asks {
book.Asks[x] = orderbook.Level{
Amount: orderBookD.Asks[x].Amount.Float64(),
Price: orderBookD.Asks[x].DepthPrice.Float64(),
}
}
err = book.Process()
if err != nil {
return book, err
}
default:
return nil, fmt.Errorf("%w %v", asset.ErrNotSupported, assetType)
}
return orderbook.Get(e.Name, pair, assetType)
}
// UpdateAccountInfo retrieves balances for all enabled currencies.
func (e *Exchange) UpdateAccountInfo(ctx context.Context, assetType asset.Item) (account.Holdings, error) {
if err := e.CurrencyPairs.IsAssetEnabled(assetType); err != nil {
return account.Holdings{}, err
}
var info account.Holdings
var acc account.SubAccount
info.Exchange = e.Name
if !e.SupportsAsset(assetType) {
return info, fmt.Errorf("%w: %v", asset.ErrNotSupported, assetType)
}
accountBalances, err := e.AccountBalance(ctx, currency.EMPTYCODE)
if err != nil {
return info, err
}
currencyBalances := []account.Balance{}
for i := range accountBalances {
for j := range accountBalances[i].Details {
currencyBalances = append(currencyBalances, account.Balance{
Currency: accountBalances[i].Details[j].Currency,
Total: accountBalances[i].Details[j].EquityOfCurrency.Float64(),
Hold: accountBalances[i].Details[j].FrozenBalance.Float64(),
Free: accountBalances[i].Details[j].AvailableBalance.Float64(),
})
}
}
acc.Currencies = currencyBalances
acc.AssetType = assetType
info.Accounts = append(info.Accounts, acc)
creds, err := e.GetCredentials(ctx)
if err != nil {
return info, err
}
if err := account.Process(&info, creds); err != nil {
return account.Holdings{}, err
}
return info, nil
}
// GetAccountFundingHistory returns funding history, deposits and withdrawals
func (e *Exchange) GetAccountFundingHistory(ctx context.Context) ([]exchange.FundingHistory, error) {
depositHistories, err := e.GetCurrencyDepositHistory(ctx, currency.EMPTYCODE, "", "", "", "", time.Time{}, time.Time{}, -1, 0)
if err != nil {
return nil, err
}
withdrawalHistories, err := e.GetWithdrawalHistory(ctx, currency.EMPTYCODE, "", "", "", "", time.Time{}, time.Time{}, -5)
if err != nil {
return nil, err
}
resp := make([]exchange.FundingHistory, 0, len(depositHistories)+len(withdrawalHistories))
for x := range depositHistories {
resp = append(resp, exchange.FundingHistory{
ExchangeName: e.Name,
Status: strconv.FormatInt(depositHistories[x].State.Int64(), 10),
Timestamp: depositHistories[x].Timestamp.Time(),
Currency: depositHistories[x].Currency,
Amount: depositHistories[x].Amount.Float64(),
TransferType: "deposit",
CryptoToAddress: depositHistories[x].ToDepositAddress,
CryptoTxID: depositHistories[x].TransactionID,
})
}
for x := range withdrawalHistories {
resp = append(resp, exchange.FundingHistory{
ExchangeName: e.Name,
Status: withdrawalHistories[x].StateOfWithdrawal,
Timestamp: withdrawalHistories[x].Timestamp.Time(),
Currency: withdrawalHistories[x].Currency,
Amount: withdrawalHistories[x].Amount.Float64(),
TransferType: "withdrawal",
CryptoToAddress: withdrawalHistories[x].ToReceivingAddress,
CryptoTxID: withdrawalHistories[x].TransactionID,
TransferID: withdrawalHistories[x].WithdrawalID,
Fee: withdrawalHistories[x].WithdrawalFee.Float64(),
CryptoChain: withdrawalHistories[x].ChainName,
})
}
return resp, nil
}
// GetWithdrawalsHistory returns previous withdrawals data
func (e *Exchange) GetWithdrawalsHistory(ctx context.Context, c currency.Code, _ asset.Item) ([]exchange.WithdrawalHistory, error) {
withdrawals, err := e.GetWithdrawalHistory(ctx, c, "", "", "", "", time.Time{}, time.Time{}, -5)
if err != nil {
return nil, err
}
resp := make([]exchange.WithdrawalHistory, 0, len(withdrawals))
for x := range withdrawals {
resp = append(resp, exchange.WithdrawalHistory{
Status: withdrawals[x].StateOfWithdrawal,
Timestamp: withdrawals[x].Timestamp.Time(),
Currency: withdrawals[x].Currency,
Amount: withdrawals[x].Amount.Float64(),
TransferType: "withdrawal",
CryptoToAddress: withdrawals[x].ToReceivingAddress,
CryptoTxID: withdrawals[x].TransactionID,
CryptoChain: withdrawals[x].ChainName,
TransferID: withdrawals[x].WithdrawalID,
Fee: withdrawals[x].WithdrawalFee.Float64(),
})
}
return resp, nil
}
// GetRecentTrades returns the most recent trades for a currency and asset
func (e *Exchange) GetRecentTrades(ctx context.Context, p currency.Pair, assetType asset.Item) ([]trade.Data, error) {
format, err := e.GetPairFormat(assetType, true)
if err != nil {
return nil, err
}
var resp []trade.Data
switch assetType {
case asset.Spread:
var spreadTrades []SpreadPublicTradeItem
spreadTrades, err = e.GetPublicSpreadTrades(ctx, "")
if err != nil {
return nil, err
}
resp = make([]trade.Data, len(spreadTrades))
var oSide order.Side
for x := range spreadTrades {
oSide, err = order.StringToOrderSide(spreadTrades[x].Side)
if err != nil {
return nil, err
}
resp[x] = trade.Data{
TID: spreadTrades[x].TradeID,
Exchange: e.Name,
CurrencyPair: p,
AssetType: assetType,
Side: oSide,
Price: spreadTrades[x].Price.Float64(),
Amount: spreadTrades[x].Size.Float64(),
Timestamp: spreadTrades[x].Timestamp.Time(),
}
}
case asset.Spot, asset.Futures, asset.PerpetualSwap, asset.Options:
if p.IsEmpty() {
return nil, currency.ErrCurrencyPairEmpty
}
instrumentID := format.Format(p)
var tradeData []TradeResponse
tradeData, err = e.GetTrades(ctx, instrumentID, 1000)
if err != nil {
return nil, err
}
resp = make([]trade.Data, len(tradeData))
for x := range tradeData {
resp[x] = trade.Data{
TID: tradeData[x].TradeID,
Exchange: e.Name,
CurrencyPair: p,
AssetType: assetType,
Side: tradeData[x].Side,
Price: tradeData[x].Price.Float64(),
Amount: tradeData[x].Quantity.Float64(),
Timestamp: tradeData[x].Timestamp.Time(),
}
}
default:
return nil, fmt.Errorf("%w %v", asset.ErrNotSupported, assetType)
}
if e.IsSaveTradeDataEnabled() {
err = trade.AddTradesToBuffer(resp...)
if err != nil {
return nil, err
}
}
sort.Sort(trade.ByDate(resp))
return resp, nil
}
// GetHistoricTrades retrieves historic trade data within the timeframe provided
func (e *Exchange) GetHistoricTrades(ctx context.Context, p currency.Pair, assetType asset.Item, timestampStart, timestampEnd time.Time) ([]trade.Data, error) {
if !e.SupportsAsset(assetType) || assetType == asset.Spread {
return nil, fmt.Errorf("%w: %v", asset.ErrNotSupported, assetType)
}
if timestampStart.Before(time.Now().Add(-kline.ThreeMonth.Duration())) {
return nil, errOnlyThreeMonthsSupported
}
const limit = 100
pairFormat, err := e.GetPairFormat(assetType, true)
if err != nil {
return nil, err
}
if p.IsEmpty() {
return nil, currency.ErrCurrencyPairEmpty
}
var resp []trade.Data
instrumentID := pairFormat.Format(p)
tradeIDEnd := ""
allTrades:
for {
var trades []TradeResponse
trades, err = e.GetTradesHistory(ctx, instrumentID, "", tradeIDEnd, limit)
if err != nil {
return nil, err
}
if len(trades) == 0 {
break
}
for i := range trades {
if timestampStart.Equal(trades[i].Timestamp.Time()) ||
trades[i].Timestamp.Time().Before(timestampStart) ||
tradeIDEnd == trades[len(trades)-1].TradeID {
// reached end of trades to crawl
break allTrades
}
resp = append(resp, trade.Data{
TID: trades[i].TradeID,
Exchange: e.Name,
CurrencyPair: p,
AssetType: assetType,
Price: trades[i].Price.Float64(),
Amount: trades[i].Quantity.Float64(),
Timestamp: trades[i].Timestamp.Time(),
Side: trades[i].Side,
})
}
tradeIDEnd = trades[len(trades)-1].TradeID
}
if e.IsSaveTradeDataEnabled() {
err = trade.AddTradesToBuffer(resp...)
if err != nil {
return nil, err
}
}
sort.Sort(trade.ByDate(resp))
return trade.FilterTradesByTime(resp, timestampStart, timestampEnd), nil
}
// SubmitOrder submits a new order
func (e *Exchange) SubmitOrder(ctx context.Context, s *order.Submit) (*order.SubmitResponse, error) {
if !e.SupportsAsset(s.AssetType) {
return nil, fmt.Errorf("%w: %v", asset.ErrNotSupported, s.AssetType)
}
if s.Amount <= 0 {
return nil, limits.ErrAmountBelowMin
}
pairFormat, err := e.GetPairFormat(s.AssetType, true)
if err != nil {
return nil, err
}
if s.Pair.IsEmpty() {
return nil, currency.ErrCurrencyPairEmpty
}
pairString := pairFormat.Format(s.Pair)
tradeMode := e.marginTypeToString(s.MarginType)
if s.AssetType.IsFutures() && s.Leverage != 0 && s.Leverage != 1 {
return nil, fmt.Errorf("%w received '%v'", order.ErrSubmitLeverageNotSupported, s.Leverage)
}
var sideType, positionSide string
switch s.AssetType {
case asset.Spot, asset.Margin, asset.Spread:
sideType = s.Side.String()
case asset.Futures, asset.PerpetualSwap, asset.Options:
positionSide = s.Side.Lower()
}
amount := s.Amount
var targetCurrency string
if s.AssetType == asset.Spot && s.Type == order.Market {
targetCurrency = "base_ccy" // Default to base currency
if s.QuoteAmount > 0 {
amount = s.QuoteAmount
targetCurrency = "quote_ccy"
}
}
// If asset type is spread
if s.AssetType == asset.Spread {
spreadParam := &SpreadOrderParam{
SpreadID: pairString,
ClientOrderID: s.ClientOrderID,
Side: sideType,
OrderType: s.Type.Lower(),
Size: s.Amount,
Price: s.Price,
}
var placeSpreadOrderResponse *SpreadOrderResponse
if e.Websocket.CanUseAuthenticatedWebsocketForWrapper() {
placeSpreadOrderResponse, err = e.WSPlaceSpreadOrder(ctx, spreadParam)
if err != nil {
return nil, err
}
} else {
placeSpreadOrderResponse, err = e.PlaceSpreadOrder(ctx, spreadParam)
if err != nil {
return nil, err
}
}
return s.DeriveSubmitResponse(placeSpreadOrderResponse.OrderID)
}
orderTypeString, err := orderTypeString(s.Type, s.TimeInForce)
if err != nil {
return nil, err
}
var placeOrderResponse *OrderData
var result *AlgoOrder
switch orderTypeString {
case orderLimit, orderMarket, orderPostOnly, orderFOK, orderIOC, orderOptimalLimitIOC, "mmp", "mmp_and_post_only":
orderRequest := &PlaceOrderRequestParam{
InstrumentID: pairString,
TradeMode: tradeMode,
Side: sideType,
PositionSide: positionSide,
OrderType: orderTypeString,
Amount: amount,
ClientOrderID: s.ClientOrderID,
Price: s.Price,
TargetCurrency: targetCurrency,
AssetType: s.AssetType,
}
switch s.Type.Lower() {
case orderLimit, orderPostOnly, orderFOK, orderIOC:
orderRequest.Price = s.Price
}
if s.AssetType == asset.PerpetualSwap || s.AssetType == asset.Futures {
if s.Type.Lower() == "" {
orderRequest.OrderType = orderOptimalLimitIOC
}
// TODO: handle positionSideLong while side is Short and positionSideShort while side is Long
if s.Side.IsLong() {
orderRequest.PositionSide = positionSideLong
} else {
orderRequest.PositionSide = positionSideShort
}
}
if e.Websocket.CanUseAuthenticatedWebsocketForWrapper() {
placeOrderResponse, err = e.WSPlaceOrder(ctx, orderRequest)
} else {
placeOrderResponse, err = e.PlaceOrder(ctx, orderRequest)
}
if err != nil {
return nil, err
}
return s.DeriveSubmitResponse(placeOrderResponse.OrderID)
case orderTrigger:
result, err = e.PlaceTriggerAlgoOrder(ctx, &AlgoOrderParams{
InstrumentID: pairString,
TradeMode: tradeMode,
Side: s.Side.Lower(),
PositionSide: positionSide,
OrderType: orderTypeString,
Size: s.Amount,
ReduceOnly: s.ReduceOnly,
TriggerPrice: s.TriggerPrice,
TriggerPriceType: priceTypeString(s.TriggerPriceType),
})
case orderConditional:
// Trigger Price and type are used as a stop losss trigger price and type.
result, err = e.PlaceTakeProfitStopLossOrder(ctx, &AlgoOrderParams{
InstrumentID: pairString,
TradeMode: tradeMode,
Side: s.Side.Lower(),
PositionSide: positionSide,
OrderType: orderTypeString,
Size: s.Amount,
ReduceOnly: s.ReduceOnly,
StopLossTriggerPrice: s.TriggerPrice,
StopLossOrderPrice: s.Price,
StopLossTriggerPriceType: priceTypeString(s.TriggerPriceType),
})
case orderChase:
if s.TrackingMode == order.UnknownTrackingMode {
return nil, fmt.Errorf("%w, tracking mode unset", order.ErrUnknownTrackingMode)
}
if s.TrackingValue == 0 {
return nil, fmt.Errorf("%w, tracking value required", limits.ErrAmountBelowMin)
}
result, err = e.PlaceChaseAlgoOrder(ctx, &AlgoOrderParams{
InstrumentID: pairString,
TradeMode: tradeMode,
Side: s.Side.Lower(),
PositionSide: positionSide,
OrderType: orderTypeString,
Size: s.Amount,
ReduceOnly: s.ReduceOnly,
MaxChaseType: s.TrackingMode.String(),
MaxChaseValue: s.TrackingValue,
})
case orderMoveOrderStop:
if s.TrackingMode == order.UnknownTrackingMode {
return nil, fmt.Errorf("%w, tracking mode unset", order.ErrUnknownTrackingMode)
}
var callbackSpread, callbackRatio float64
switch s.TrackingMode {
case order.Distance:
callbackSpread = s.TrackingValue
case order.Percentage:
callbackRatio = s.TrackingValue
}
result, err = e.PlaceTrailingStopOrder(ctx, &AlgoOrderParams{
InstrumentID: pairString,
TradeMode: tradeMode,
Side: sideType,
PositionSide: positionSide,
OrderType: orderTypeString,
Size: s.Amount,
ReduceOnly: s.ReduceOnly,
CallbackRatio: callbackRatio,
CallbackSpreadVariance: callbackSpread,
ActivePrice: s.TriggerPrice,
})
case orderTWAP:
if s.TrackingMode == order.UnknownTrackingMode {
return nil, fmt.Errorf("%w, tracking mode unset", order.ErrUnknownTrackingMode)
}
var priceVar, priceSpread float64
switch s.TrackingMode {
case order.Distance:
priceSpread = s.TrackingValue
case order.Percentage:
priceVar = s.TrackingValue
}
result, err = e.PlaceTWAPOrder(ctx, &AlgoOrderParams{
InstrumentID: pairString,
TradeMode: tradeMode,
Side: sideType,
PositionSide: positionSide,
OrderType: orderTypeString,
Size: s.Amount,
ReduceOnly: s.ReduceOnly,
PriceVariance: priceVar,
PriceSpread: priceSpread,
SizeLimit: s.Amount,
LimitPrice: s.Price,
TimeInterval: kline.FifteenMin,
})
case orderOCO:
switch {
case s.RiskManagementModes.TakeProfit.Price <= 0:
return nil, fmt.Errorf("%w, take profit price is required", limits.ErrPriceBelowMin)
case s.RiskManagementModes.StopLoss.Price <= 0:
return nil, fmt.Errorf("%w, stop loss price is required", limits.ErrPriceBelowMin)
}
result, err = e.PlaceAlgoOrder(ctx, &AlgoOrderParams{
InstrumentID: pairString,
TradeMode: tradeMode,
Side: sideType,
PositionSide: positionSide,
OrderType: orderTypeString,
Size: s.Amount,
ReduceOnly: s.ReduceOnly,
TakeProfitTriggerPrice: s.RiskManagementModes.TakeProfit.Price,
TakeProfitOrderPrice: s.RiskManagementModes.TakeProfit.LimitPrice,
TakeProfitTriggerPriceType: priceTypeString(s.TriggerPriceType),
StopLossTriggerPrice: s.RiskManagementModes.TakeProfit.Price,
StopLossOrderPrice: s.RiskManagementModes.StopLoss.LimitPrice,
StopLossTriggerPriceType: priceTypeString(s.TriggerPriceType),
})
default:
return nil, fmt.Errorf("%w, order type %s", order.ErrTypeIsInvalid, orderTypeString)
}
if err != nil {
return nil, err
}
return s.DeriveSubmitResponse(result.AlgoID)
}
func priceTypeString(pt order.PriceType) string {
switch pt {
case order.LastPrice:
return "last"
case order.IndexPrice:
return "index"
case order.MarkPrice:
return "mark"
default:
return ""
}
}
var allowedMarginTypes = margin.Isolated | margin.NoMargin | margin.SpotIsolated
func (e *Exchange) marginTypeToString(m margin.Type) string {
if allowedMarginTypes&m == m {
return m.String()
} else if margin.Multi == m {
return TradeModeCross
}
return ""
}
// ModifyOrder modifies an existing order
func (e *Exchange) ModifyOrder(ctx context.Context, action *order.Modify) (*order.ModifyResponse, error) {
if err := action.Validate(); err != nil {
return nil, err
}
var err error
if math.Trunc(action.Amount) != action.Amount {
return nil, errors.New("contract amount can not be decimal")
}
// When asset type is asset.Spread
if action.AssetType == asset.Spread {
amendSpreadOrder := &AmendSpreadOrderParam{
OrderID: action.OrderID,
ClientOrderID: action.ClientOrderID,
NewSize: action.Amount,
NewPrice: action.Price,
}
if e.Websocket.CanUseAuthenticatedWebsocketForWrapper() {
_, err = e.WSAmendSpreadOrder(ctx, amendSpreadOrder)
} else {
_, err = e.AmendSpreadOrder(ctx, amendSpreadOrder)
}
if err != nil {
return nil, err
}
return action.DeriveModifyResponse()
}
// For other asset type instances.
pairFormat, err := e.GetPairFormat(action.AssetType, true)
if err != nil {
return nil, err
}
if action.Pair.IsEmpty() {
return nil, currency.ErrCurrencyPairEmpty
}
switch action.Type {
case order.UnknownType, order.Market, order.Limit, order.OptimalLimit, order.MarketMakerProtection:
amendRequest := AmendOrderRequestParams{
InstrumentID: pairFormat.Format(action.Pair),
NewQuantity: action.Amount,
OrderID: action.OrderID,
ClientOrderID: action.ClientOrderID,
}
if e.Websocket.CanUseAuthenticatedWebsocketForWrapper() {
_, err = e.WSAmendOrder(ctx, &amendRequest)
} else {
_, err = e.AmendOrder(ctx, &amendRequest)
}
if err != nil {
return nil, err
}
case order.Trigger:
if action.TriggerPrice == 0 {
return nil, fmt.Errorf("%w, trigger price required", limits.ErrPriceBelowMin)
}
var postTriggerTPSLOrders []SubTPSLParams
if action.RiskManagementModes.StopLoss.Price > 0 && action.RiskManagementModes.TakeProfit.Price > 0 {
postTriggerTPSLOrders = []SubTPSLParams{
{
NewTakeProfitTriggerPrice: action.RiskManagementModes.TakeProfit.Price,
NewTakeProfitOrderPrice: action.RiskManagementModes.TakeProfit.LimitPrice,
NewStopLossTriggerPrice: action.RiskManagementModes.StopLoss.Price,
NewStopLossOrderPrice: action.RiskManagementModes.StopLoss.Price,
NewTakeProfitTriggerPriceType: priceTypeString(action.RiskManagementModes.TakeProfit.TriggerPriceType),
NewStopLossTriggerPriceType: priceTypeString(action.RiskManagementModes.StopLoss.TriggerPriceType),
},
}
}
_, err = e.AmendAlgoOrder(ctx, &AmendAlgoOrderParam{
InstrumentID: pairFormat.Format(action.Pair),
AlgoID: action.OrderID,
ClientSuppliedAlgoOrderID: action.ClientOrderID,
NewSize: action.Amount,
NewTriggerPrice: action.TriggerPrice,
NewOrderPrice: action.Price,
NewTriggerPriceType: priceTypeString(action.TriggerPriceType),
// An one-cancel-other order to be placed after executing the trigger order
AttachAlgoOrders: postTriggerTPSLOrders,
})
if err != nil {
return nil, err
}
case order.OCO:
switch {
case action.RiskManagementModes.TakeProfit.Price <= 0 &&
action.RiskManagementModes.TakeProfit.LimitPrice <= 0:
return nil, fmt.Errorf("%w, either take profit trigger price or order price is required", limits.ErrPriceBelowMin)
case action.RiskManagementModes.StopLoss.Price <= 0 &&
action.RiskManagementModes.StopLoss.LimitPrice <= 0:
return nil, fmt.Errorf("%w, either stop loss trigger price or order price is required", limits.ErrPriceBelowMin)
}
_, err = e.AmendAlgoOrder(ctx, &AmendAlgoOrderParam{
InstrumentID: pairFormat.Format(action.Pair),
AlgoID: action.OrderID,
ClientSuppliedAlgoOrderID: action.ClientOrderID,
NewSize: action.Amount,
NewTakeProfitTriggerPrice: action.RiskManagementModes.TakeProfit.Price,
NewTakeProfitOrderPrice: action.RiskManagementModes.TakeProfit.LimitPrice,
NewStopLossTriggerPrice: action.RiskManagementModes.StopLoss.Price,
NewStopLossOrderPrice: action.RiskManagementModes.StopEntry.LimitPrice,
NewTakeProfitTriggerPriceType: priceTypeString(action.RiskManagementModes.TakeProfit.TriggerPriceType),
NewStopLossTriggerPriceType: priceTypeString(action.RiskManagementModes.StopLoss.TriggerPriceType),
})
if err != nil {
return nil, err
}
default:
return nil, fmt.Errorf("%w, could not amend order of type %v", order.ErrUnsupportedOrderType, action.Type)
}
return action.DeriveModifyResponse()
}
// CancelOrder cancels an order by its corresponding ID number
func (e *Exchange) CancelOrder(ctx context.Context, ord *order.Cancel) error {
if !e.SupportsAsset(ord.AssetType) {
return fmt.Errorf("%w: %v", asset.ErrNotSupported, ord.AssetType)
}
var err error
if ord.AssetType == asset.Spread {
if e.Websocket.CanUseAuthenticatedWebsocketForWrapper() {
_, err = e.WSCancelSpreadOrder(ctx, ord.OrderID, ord.ClientOrderID)
} else {
_, err = e.CancelSpreadOrder(ctx, ord.OrderID, ord.ClientOrderID)
}
return err
}
pairFormat, err := e.GetPairFormat(ord.AssetType, true)
if err != nil {
return err
}
if ord.Pair.IsEmpty() {
return currency.ErrCurrencyPairEmpty
}
instrumentID := pairFormat.Format(ord.Pair)
switch ord.Type {
case order.UnknownType, order.Market, order.Limit, order.OptimalLimit, order.MarketMakerProtection:
req := CancelOrderRequestParam{
InstrumentID: instrumentID,
OrderID: ord.OrderID,
ClientOrderID: ord.ClientOrderID,
}
if e.Websocket.CanUseAuthenticatedWebsocketForWrapper() {
_, err = e.WSCancelOrder(ctx, &req)
} else {
_, err = e.CancelSingleOrder(ctx, &req)
}
case order.Trigger, order.OCO, order.ConditionalStop, order.TWAP, order.TrailingStop, order.Chase:
var response *AlgoOrder
response, err = e.CancelAdvanceAlgoOrder(ctx, []AlgoOrderCancelParams{
{
AlgoOrderID: ord.OrderID,
InstrumentID: instrumentID,
},
})
if err != nil {
return err
}
return getStatusError(response.StatusCode, response.StatusMessage)
default:
return fmt.Errorf("%w, order type %v", order.ErrUnsupportedOrderType, ord.Type)
}
return err
}
// CancelBatchOrders cancels orders by their corresponding ID numbers
func (e *Exchange) CancelBatchOrders(ctx context.Context, o []order.Cancel) (*order.CancelBatchResponse, error) {
if len(o) > 20 {
return nil, fmt.Errorf("%w, cannot cancel more than 20 orders", errExceedLimit)
} else if len(o) == 0 {
return nil, fmt.Errorf("%w, must have at least 1 cancel order", order.ErrCancelOrderIsNil)
}
cancelOrderParams := make([]CancelOrderRequestParam, 0, len(o))
cancelAlgoOrderParams := make([]AlgoOrderCancelParams, 0, len(o))
var err error
for x := range o {
ord := o[x]
if !e.SupportsAsset(ord.AssetType) {
return nil, fmt.Errorf("%w: %v", asset.ErrNotSupported, ord.AssetType)
}
var pairFormat currency.PairFormat
pairFormat, err = e.GetPairFormat(ord.AssetType, true)
if err != nil {
return nil, err
}
if !ord.Pair.IsPopulated() {
return nil, currency.ErrCurrencyPairsEmpty
}
switch ord.Type {
case order.UnknownType, order.Market, order.Limit, order.OptimalLimit, order.MarketMakerProtection:
if o[x].ClientID == "" && o[x].OrderID == "" {
return nil, fmt.Errorf("%w, order ID required for order of type %v", order.ErrOrderIDNotSet, o[x].Type)
}
cancelOrderParams = append(cancelOrderParams, CancelOrderRequestParam{
InstrumentID: pairFormat.Format(ord.Pair),
OrderID: ord.OrderID,
ClientOrderID: ord.ClientOrderID,
})
case order.Trigger, order.OCO, order.ConditionalStop,
order.TWAP, order.TrailingStop, order.Chase:
if o[x].OrderID == "" {
return nil, fmt.Errorf("%w, order ID required for order of type %v", order.ErrOrderIDNotSet, o[x].Type)
}
cancelAlgoOrderParams = append(cancelAlgoOrderParams, AlgoOrderCancelParams{
AlgoOrderID: o[x].OrderID,
InstrumentID: pairFormat.Format(ord.Pair),
})
default:
return nil, fmt.Errorf("%w order of type %v not supported", order.ErrUnsupportedOrderType, o[x].Type)
}
}
resp := &order.CancelBatchResponse{Status: make(map[string]string)}
if len(cancelOrderParams) > 0 {
var canceledOrders []*OrderData
if e.Websocket.CanUseAuthenticatedWebsocketForWrapper() {
canceledOrders, err = e.WSCancelMultipleOrders(ctx, cancelOrderParams)
} else {
canceledOrders, err = e.CancelMultipleOrders(ctx, cancelOrderParams)
}
if err != nil {
return nil, err
}
for x := range canceledOrders {
resp.Status[canceledOrders[x].OrderID] = func() string {
if canceledOrders[x].StatusCode != 0 {
return ""
}
return order.Cancelled.String()
}()
}
}
if len(cancelAlgoOrderParams) > 0 {
cancelationResponse, err := e.CancelAdvanceAlgoOrder(ctx, cancelAlgoOrderParams)
if err != nil {
if len(resp.Status) > 0 {
return resp, nil
}
return nil, err
} else if cancelationResponse.StatusCode != 0 {
if len(resp.Status) > 0 {
return resp, nil
}
return resp, getStatusError(cancelationResponse.StatusCode, cancelationResponse.StatusMessage)
}
for x := range cancelAlgoOrderParams {
resp.Status[cancelAlgoOrderParams[x].AlgoOrderID] = order.Cancelled.String()
}
}
return resp, nil
}
// CancelAllOrders cancels all orders associated with a currency pair
func (e *Exchange) CancelAllOrders(ctx context.Context, orderCancellation *order.Cancel) (order.CancelAllResponse, error) {
err := orderCancellation.Validate()
if err != nil {
return order.CancelAllResponse{}, err
}
cancelAllResponse := order.CancelAllResponse{
Status: map[string]string{},
}
// For asset.Spread asset orders cancellation
if orderCancellation.AssetType == asset.Spread {
var success bool
success, err = e.CancelAllSpreadOrders(ctx, orderCancellation.OrderID)
if err != nil {
return cancelAllResponse, err
}
cancelAllResponse.Status[orderCancellation.OrderID] = strconv.FormatBool(success)
return cancelAllResponse, nil
}
var instrumentType string
if orderCancellation.AssetType.IsValid() {
err = e.CurrencyPairs.IsAssetEnabled(orderCancellation.AssetType)
if err != nil {
return order.CancelAllResponse{}, err
}
instrumentType = GetInstrumentTypeFromAssetItem(orderCancellation.AssetType)
}
var oType string
if orderCancellation.Type != order.UnknownType && orderCancellation.Type != order.AnyType {
oType, err = orderTypeString(orderCancellation.Type, orderCancellation.TimeInForce)
if err != nil {
return order.CancelAllResponse{}, err
}
}
var curr string
if orderCancellation.Pair.IsPopulated() {
curr = orderCancellation.Pair.Upper().String()
}
myOrders, err := e.GetOrderList(ctx, &OrderListRequestParams{
InstrumentType: instrumentType,
OrderType: oType,
InstrumentID: curr,
})
if err != nil {
return cancelAllResponse, err
}
cancelAllOrdersRequestParams := make([]CancelOrderRequestParam, len(myOrders))
ordersLoop:
for x := range myOrders {
switch {
case orderCancellation.OrderID != "" || orderCancellation.ClientOrderID != "":
if myOrders[x].OrderID == orderCancellation.OrderID ||
myOrders[x].ClientOrderID == orderCancellation.ClientOrderID {
cancelAllOrdersRequestParams[x] = CancelOrderRequestParam{
OrderID: myOrders[x].OrderID,
ClientOrderID: myOrders[x].ClientOrderID,
}
break ordersLoop
}
case orderCancellation.Side == order.Buy || orderCancellation.Side == order.Sell:
if myOrders[x].Side == order.Buy || myOrders[x].Side == order.Sell {
cancelAllOrdersRequestParams[x] = CancelOrderRequestParam{
OrderID: myOrders[x].OrderID,
ClientOrderID: myOrders[x].ClientOrderID,
}
continue
}
default:
cancelAllOrdersRequestParams[x] = CancelOrderRequestParam{
OrderID: myOrders[x].OrderID,
ClientOrderID: myOrders[x].ClientOrderID,
}
}
}
remaining := cancelAllOrdersRequestParams
loop := int(math.Ceil(float64(len(remaining)) / 20.0))
for range loop {
var response []*OrderData
if len(remaining) > 20 {
if e.Websocket.CanUseAuthenticatedWebsocketForWrapper() {
response, err = e.WSCancelMultipleOrders(ctx, remaining[:20])
} else {
response, err = e.CancelMultipleOrders(ctx, remaining[:20])
}
remaining = remaining[20:]
} else {
if e.Websocket.CanUseAuthenticatedWebsocketForWrapper() {
response, err = e.WSCancelMultipleOrders(ctx, remaining)
} else {
response, err = e.CancelMultipleOrders(ctx, remaining)
}
}
if err != nil {
if len(cancelAllResponse.Status) == 0 {
return cancelAllResponse, err
}
}
for y := range response {
if response[y].StatusCode == 0 {
cancelAllResponse.Status[response[y].OrderID] = order.Cancelled.String()
} else {
cancelAllResponse.Status[response[y].OrderID] = response[y].StatusMessage
}
}
}
return cancelAllResponse, nil
}
// GetOrderInfo returns order information based on order ID
func (e *Exchange) GetOrderInfo(ctx context.Context, orderID string, pair currency.Pair, assetType asset.Item) (*order.Detail, error) {
if !e.SupportsAsset(assetType) {
return nil, fmt.Errorf("%w %v", asset.ErrNotSupported, assetType)
}
if assetType == asset.Spread {
var resp *SpreadOrder
resp, err := e.GetSpreadOrderDetails(ctx, orderID, "")
if err != nil {
return nil, err
}
oSide, err := order.StringToOrderSide(resp.Side)
if err != nil {
return nil, err
}
oType, err := order.StringToOrderType(resp.OrderType)
if err != nil {
return nil, err
}
oStatus, err := order.StringToOrderStatus(resp.State)
if err != nil {
return nil, err
}
cp, err := currency.NewPairFromString(resp.InstrumentID)
if err != nil {
return nil, err
}
if !pair.IsEmpty() && !cp.Equal(pair) {
return nil, fmt.Errorf("%w, unexpected instrument ID %v for order ID %s", order.ErrOrderNotFound, pair, orderID)
}
return &order.Detail{
Amount: resp.Size.Float64(),
Exchange: e.Name,
OrderID: resp.OrderID,
ClientOrderID: resp.ClientOrderID,
Side: oSide,
Type: oType,
Pair: cp,
Cost: resp.Price.Float64(),
AssetType: assetType,
Status: oStatus,
Price: resp.Price.Float64(),
ExecutedAmount: resp.FillSize.Float64(),
Date: resp.CreationTime.Time(),
LastUpdated: resp.UpdateTime.Time(),
AverageExecutedPrice: resp.AveragePrice.Float64(),
RemainingAmount: resp.Size.Float64() - resp.FillSize.Float64(),
}, nil
}
if pair.IsEmpty() {
return nil, currency.ErrCurrencyPairEmpty
}
if err := e.CurrencyPairs.IsAssetEnabled(assetType); err != nil {
return nil, err
}
pairFormat, err := e.GetPairFormat(assetType, false)
if err != nil {
return nil, err
}
if !pair.IsPopulated() {
return nil, currency.ErrCurrencyPairsEmpty
}
instrumentID := pairFormat.Format(pair)
orderDetail, err := e.GetOrderDetail(ctx, &OrderDetailRequestParam{
InstrumentID: instrumentID,
OrderID: orderID,
})
if err != nil {
return nil, err
}
status, err := order.StringToOrderStatus(orderDetail.State)
if err != nil {
return nil, err
}
orderType, tif, err := orderTypeFromString(orderDetail.OrderType)
if err != nil {
return nil, err
}
return &order.Detail{
Amount: orderDetail.Size.Float64(),
Exchange: e.Name,
OrderID: orderDetail.OrderID,
ClientOrderID: orderDetail.ClientOrderID,
Side: orderDetail.Side,
Type: orderType,
Pair: pair,
Cost: orderDetail.Price.Float64(),
AssetType: assetType,
Status: status,
Price: orderDetail.Price.Float64(),
ExecutedAmount: orderDetail.RebateAmount.Float64(),
Date: orderDetail.CreationTime.Time(),
LastUpdated: orderDetail.UpdateTime.Time(),
TimeInForce: tif,
}, nil
}
// GetDepositAddress returns a deposit address for a specified currency
func (e *Exchange) GetDepositAddress(ctx context.Context, c currency.Code, _, chain string) (*deposit.Address, error) {
response, err := e.GetCurrencyDepositAddress(ctx, c)
if err != nil {
return nil, err
}
// Check if a specific chain was requested
if chain != "" {
for x := range response {
if !strings.EqualFold(response[x].Chain, chain) {
continue
}
return &deposit.Address{
Address: response[x].Address,
Tag: response[x].Tag,
Chain: response[x].Chain,
}, nil
}
return nil, fmt.Errorf("specified chain %s not found", chain)
}
// If no specific chain was requested, return the first selected address (mainnet addresses are returned first by default)
for x := range response {
if !response[x].Selected {
continue
}
return &deposit.Address{
Address: response[x].Address,
Tag: response[x].Tag,
Chain: response[x].Chain,
}, nil
}
return nil, deposit.ErrAddressNotFound
}
// 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
}
input := WithdrawalInput{
ChainName: withdrawRequest.Crypto.Chain,
Amount: withdrawRequest.Amount,
Currency: withdrawRequest.Currency,
ToAddress: withdrawRequest.Crypto.Address,
TransactionFee: withdrawRequest.Crypto.FeeAmount,
WithdrawalDestination: "3",
}
resp, err := e.Withdrawal(ctx, &input)
if err != nil {
return nil, err
}
return &withdraw.ExchangeResponse{
ID: resp.WithdrawalID,
}, 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
}
// GetActiveOrders retrieves any orders that are active/open
func (e *Exchange) GetActiveOrders(ctx context.Context, req *order.MultiOrderRequest) (order.FilteredOrders, error) {
err := req.Validate()
if err != nil {
return nil, err
}
if !req.StartTime.IsZero() && req.StartTime.Before(time.Now().Add(-kline.ThreeMonth.Duration())) {
return nil, errOnlyThreeMonthsSupported
}
if !e.SupportsAsset(req.AssetType) {
return nil, fmt.Errorf("%w: %v", asset.ErrNotSupported, req.AssetType)
}
var resp []order.Detail
var format currency.PairFormat
if req.AssetType == asset.Spread {
var spreads []SpreadOrder
spreads, err = e.GetActiveSpreadOrders(ctx, "", req.Type.String(), "", req.FromOrderID, "", 0)
if err != nil {
return nil, err
}
for x := range spreads {
format, err = e.GetPairFormat(asset.Spread, true)
if err != nil {
return nil, err
}
var (
pair currency.Pair
oType order.Type
oSide order.Side
oStatus order.Status
)
pair, err = currency.NewPairDelimiter(spreads[x].SpreadID, format.Delimiter)
if err != nil {
return nil, err
}
oType, err = order.StringToOrderType(spreads[x].OrderType)
if err != nil {
return nil, err
}
oSide, err = order.StringToOrderSide(spreads[x].Side)
if err != nil {
return nil, err
}
oStatus, err = order.StringToOrderStatus(spreads[x].State)
if err != nil {
return nil, err
}
resp = append(resp, order.Detail{
Amount: spreads[x].Size.Float64(),
Pair: pair,
Price: spreads[x].Price.Float64(),
ExecutedAmount: spreads[x].FillSize.Float64(),
RemainingAmount: spreads[x].Size.Float64() - spreads[x].FillSize.Float64(),
Exchange: e.Name,
OrderID: spreads[x].OrderID,
ClientOrderID: spreads[x].ClientOrderID,
Type: oType,
Side: oSide,
Status: oStatus,
AssetType: req.AssetType,
Date: spreads[x].CreationTime.Time(),
LastUpdated: spreads[x].UpdateTime.Time(),
})
}
return req.Filter(e.Name, resp), nil
}
instrumentType := GetInstrumentTypeFromAssetItem(req.AssetType)
var orderType string
if req.Type != order.UnknownType && req.Type != order.AnyType {
orderType, err = orderTypeString(req.Type, req.TimeInForce)
if err != nil {
return nil, err
}
}
endTime := req.EndTime
allOrders:
for {
requestParam := &OrderListRequestParams{
OrderType: orderType,
End: endTime,
InstrumentType: instrumentType,
}
var orderList []OrderDetail
orderList, err = e.GetOrderList(ctx, requestParam)
if err != nil {
return nil, err
}
if len(orderList) == 0 {
break
}
for i := range orderList {
if req.StartTime.Equal(orderList[i].CreationTime.Time()) ||
orderList[i].CreationTime.Time().Before(req.StartTime) ||
endTime.Equal(orderList[i].CreationTime.Time()) {
// reached end of orders to crawl
break allOrders
}
orderSide := orderList[i].Side
pair, err := currency.NewPairFromString(orderList[i].InstrumentID)
if err != nil {
return nil, err
}
if len(req.Pairs) > 0 {
x := 0
for x = range req.Pairs {
if req.Pairs[x].Equal(pair) {
break
}
}
if !req.Pairs[x].Equal(pair) {
continue
}
}
orderStatus, err := order.StringToOrderStatus(strings.ToUpper(orderList[i].State))
if err != nil {
return nil, err
}
oType, tif, err := orderTypeFromString(orderList[i].OrderType)
if err != nil {
return nil, err
}
resp = append(resp, order.Detail{
Amount: orderList[i].Size.Float64(),
Pair: pair,
Price: orderList[i].Price.Float64(),
ExecutedAmount: orderList[i].FillSize.Float64(),
RemainingAmount: orderList[i].Size.Float64() - orderList[i].FillSize.Float64(),
Fee: orderList[i].TransactionFee.Float64(),
FeeAsset: currency.NewCode(orderList[i].FeeCurrency),
Exchange: e.Name,
OrderID: orderList[i].OrderID,
ClientOrderID: orderList[i].ClientOrderID,
Type: oType,
Side: orderSide,
Status: orderStatus,
AssetType: req.AssetType,
Date: orderList[i].CreationTime.Time(),
LastUpdated: orderList[i].UpdateTime.Time(),
TimeInForce: tif,
})
}
if len(orderList) < 100 {
// Since the we passed a limit of 0 to the method GetOrderList,
// we expect 100 orders to be retrieved if the number of orders are more that 100.
// If not, break out of the loop to not send another request.
break
}
endTime = orderList[len(orderList)-1].CreationTime.Time()
}
return req.Filter(e.Name, resp), 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
}
if !req.StartTime.IsZero() && req.StartTime.Before(time.Now().Add(-kline.ThreeMonth.Duration())) {
return nil, errOnlyThreeMonthsSupported
}
if !e.SupportsAsset(req.AssetType) {
return nil, fmt.Errorf("%w: %v", asset.ErrNotSupported, req.AssetType)
}
var resp []order.Detail
// For Spread orders.
if req.AssetType == asset.Spread {
oType, err := orderTypeString(req.Type, req.TimeInForce)
if err != nil {
return nil, err
}
spreadOrders, err := e.GetCompletedSpreadOrdersLast7Days(ctx, "", oType, "", req.FromOrderID, "", req.StartTime, req.EndTime, 0)
if err != nil {
return nil, err
}
for x := range spreadOrders {
var format currency.PairFormat
format, err = e.GetPairFormat(asset.Spread, true)
if err != nil {
return nil, err
}
var pair currency.Pair
pair, err = currency.NewPairDelimiter(spreadOrders[x].SpreadID, format.Delimiter)
if err != nil {
return nil, err
}
oType, err := order.StringToOrderType(spreadOrders[x].OrderType)
if err != nil {
return nil, err
}
oSide, err := order.StringToOrderSide(spreadOrders[x].Side)
if err != nil {
return nil, err
}
oStatus, err := order.StringToOrderStatus(spreadOrders[x].State)
if err != nil {
return nil, err
}
resp = append(resp, order.Detail{
Price: spreadOrders[x].Price.Float64(),
AverageExecutedPrice: spreadOrders[x].AveragePrice.Float64(),
Amount: spreadOrders[x].Size.Float64(),
ExecutedAmount: spreadOrders[x].FillSize.Float64(),
RemainingAmount: spreadOrders[x].PendingFillSize.Float64(),
Exchange: e.Name,
OrderID: spreadOrders[x].OrderID,
ClientOrderID: spreadOrders[x].ClientOrderID,
Type: oType,
Side: oSide,
Status: oStatus,
AssetType: req.AssetType,
Date: spreadOrders[x].CreationTime.Time(),
LastUpdated: spreadOrders[x].UpdateTime.Time(),
Pair: pair,
})
}
return req.Filter(e.Name, resp), nil
}
if len(req.Pairs) == 0 {
return nil, currency.ErrCurrencyPairsEmpty
}
instrumentType := GetInstrumentTypeFromAssetItem(req.AssetType)
endTime := req.EndTime
allOrders:
for {
orderList, err := e.Get3MonthOrderHistory(ctx, &OrderHistoryRequestParams{
OrderListRequestParams: OrderListRequestParams{
InstrumentType: instrumentType,
End: endTime,
},
})
if err != nil {
return nil, err
}
if len(orderList) == 0 {
break
}
for i := range orderList {
if req.StartTime.Equal(orderList[i].CreationTime.Time()) ||
orderList[i].CreationTime.Time().Before(req.StartTime) ||
endTime.Equal(orderList[i].CreationTime.Time()) {
// reached end of orders to crawl
break allOrders
}
pair, err := currency.NewPairFromString(orderList[i].InstrumentID)
if err != nil {
return nil, err
}
for j := range req.Pairs {
if !req.Pairs[j].Equal(pair) {
continue
}
orderStatus, err := order.StringToOrderStatus(strings.ToUpper(orderList[i].State))
if err != nil {
log.Errorf(log.ExchangeSys, "%s %v", e.Name, err)
}
if orderStatus == order.Active {
continue
}
oType, tif, err := orderTypeFromString(orderList[i].OrderType)
if err != nil {
return nil, err
}
orderAmount := orderList[i].Size
if orderList[i].QuantityType == "quote_ccy" {
// Size is quote amount.
orderAmount /= orderList[i].AveragePrice
}
remainingAmount := float64(0)
if orderStatus != order.Filled {
remainingAmount = orderAmount.Float64() - orderList[i].AccumulatedFillSize.Float64()
}
resp = append(resp, order.Detail{
Price: orderList[i].Price.Float64(),
AverageExecutedPrice: orderList[i].AveragePrice.Float64(),
Amount: orderAmount.Float64(),
ExecutedAmount: orderList[i].AccumulatedFillSize.Float64(),
RemainingAmount: remainingAmount,
Fee: orderList[i].TransactionFee.Float64(),
FeeAsset: currency.NewCode(orderList[i].FeeCurrency),
Exchange: e.Name,
OrderID: orderList[i].OrderID,
ClientOrderID: orderList[i].ClientOrderID,
Type: oType,
Side: orderList[i].Side,
Status: orderStatus,
AssetType: req.AssetType,
Date: orderList[i].CreationTime.Time(),
LastUpdated: orderList[i].UpdateTime.Time(),
Pair: pair,
Cost: orderList[i].AveragePrice.Float64() * orderList[i].AccumulatedFillSize.Float64(),
CostAsset: currency.NewCode(orderList[i].RebateCurrency),
TimeInForce: tif,
})
}
}
if len(orderList) < 100 {
break
}
endTime = orderList[len(orderList)-1].CreationTime.Time()
}
return req.Filter(e.Name, resp), nil
}
// GetFeeByType returns an estimate of fee based on the type of transaction
func (e *Exchange) GetFeeByType(ctx context.Context, feeBuilder *exchange.FeeBuilder) (float64, error) {
if feeBuilder == nil {
return 0, fmt.Errorf("%T %w", feeBuilder, common.ErrNilPointer)
}
if !e.AreCredentialsValid(ctx) && feeBuilder.FeeType == exchange.CryptocurrencyTradeFee {
feeBuilder.FeeType = exchange.OfflineTradeFee
}
return e.GetFee(ctx, feeBuilder)
}
// ValidateAPICredentials validates current credentials used for wrapper
func (e *Exchange) ValidateAPICredentials(ctx context.Context, assetType asset.Item) error {
_, err := e.UpdateAccountInfo(ctx, assetType)
return e.CheckTransientError(err)
}
// GetHistoricCandles returns candles between a time period for a set time interval
func (e *Exchange) GetHistoricCandles(ctx context.Context, pair currency.Pair, a asset.Item, interval kline.Interval, start, end time.Time) (*kline.Item, error) {
if !e.SupportsAsset(a) {
return nil, fmt.Errorf("%w: %v", asset.ErrNotSupported, a)
}
req, err := e.GetKlineRequest(pair, a, interval, start, end, false)
if err != nil {
return nil, err
}
var timeSeries []kline.Candle
switch a {
case asset.Spread:
candles, err := e.GetSpreadCandlesticksHistory(ctx, req.RequestFormatted.String(), req.ExchangeInterval, start.Add(-time.Nanosecond), end, 100)
if err != nil {
return nil, err
}
timeSeries = make([]kline.Candle, len(candles))
for x := range candles {
timeSeries[x] = kline.Candle{
Time: candles[x].Timestamp.Time(),
Open: candles[x].Open.Float64(),
High: candles[x].High.Float64(),
Low: candles[x].Low.Float64(),
Close: candles[x].Close.Float64(),
Volume: candles[x].Volume.Float64(),
}
}
default:
candles, err := e.GetCandlesticksHistory(ctx,
req.RequestFormatted.String(),
req.ExchangeInterval,
start.Add(-time.Nanosecond), // Start time not inclusive of candle.
end,
100)
if err != nil {
return nil, err
}
timeSeries = make([]kline.Candle, len(candles))
for x := range candles {
timeSeries[x] = kline.Candle{
Time: candles[x].OpenTime.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.Float64(),
}
}
}
return req.ProcessResponse(timeSeries)
}
// 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) {
if !e.SupportsAsset(a) {
return nil, fmt.Errorf("%w: %v", asset.ErrNotSupported, a)
}
req, err := e.GetKlineExtendedRequest(pair, a, interval, start, end)
if err != nil {
return nil, err
}
count := kline.TotalCandlesPerInterval(req.Start, req.End, req.ExchangeInterval)
if count > 1440 {
return nil,
fmt.Errorf("candles count: %d max lookback: %d, %w",
count, 1440, kline.ErrRequestExceedsMaxLookback)
}
timeSeries := make([]kline.Candle, 0, req.Size())
for y := range req.RangeHolder.Ranges {
switch a {
case asset.Spread:
candles, err := e.GetSpreadCandlesticksHistory(ctx,
req.RequestFormatted.String(),
req.ExchangeInterval,
req.RangeHolder.Ranges[y].Start.Time.Add(-time.Nanosecond), // Start time not inclusive of candle.
req.RangeHolder.Ranges[y].End.Time,
100)
if err != nil {
return nil, err
}
for x := range candles {
timeSeries = append(timeSeries, kline.Candle{
Time: candles[x].Timestamp.Time(),
Open: candles[x].Open.Float64(),
High: candles[x].High.Float64(),
Low: candles[x].Low.Float64(),
Close: candles[x].Close.Float64(),
Volume: candles[x].Volume.Float64(),
})
}
default:
candles, err := e.GetCandlesticksHistory(ctx,
req.RequestFormatted.String(),
req.ExchangeInterval,
req.RangeHolder.Ranges[y].Start.Time.Add(-time.Nanosecond), // Start time not inclusive of candle.
req.RangeHolder.Ranges[y].End.Time,
100)
if err != nil {
return nil, err
}
for x := range candles {
timeSeries = append(timeSeries, kline.Candle{
Time: candles[x].OpenTime.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.Float64(),
})
}
}
}
return req.ProcessResponse(timeSeries)
}
// GetAvailableTransferChains returns the available transfer blockchains for the specific cryptocurrency
func (e *Exchange) GetAvailableTransferChains(ctx context.Context, cryptocurrency currency.Code) ([]string, error) {
currencyChains, err := e.GetFundingCurrencies(ctx, cryptocurrency)
if err != nil {
return nil, err
}
chains := make([]string, 0, len(currencyChains))
for x := range currencyChains {
if (!cryptocurrency.IsEmpty() && !strings.EqualFold(cryptocurrency.String(), currencyChains[x].Currency)) ||
(!currencyChains[x].CanDeposit && !currencyChains[x].CanWithdraw) ||
// Lightning network is currently not supported by transfer chains
// as it is an invoice string which is generated per request and is
// not a static address. TODO: Add a hook to generate a new invoice
// string per request.
(currencyChains[x].Chain != "" && currencyChains[x].Chain == "BTC-Lightning") {
continue
}
chains = append(chains, currencyChains[x].Chain)
}
return chains, nil
}
// getInstrumentsForOptions returns the instruments for options asset type
func (e *Exchange) getInstrumentsForOptions(ctx context.Context) ([]Instrument, error) {
underlyings, err := e.GetPublicUnderlyings(ctx, instTypeOption)
if err != nil {
return nil, err
}
var insts []Instrument
for x := range underlyings {
var instruments []Instrument
instruments, err = e.GetInstruments(ctx, &InstrumentsFetchParams{
InstrumentType: instTypeOption,
Underlying: underlyings[x],
})
if err != nil {
return nil, err
}
insts = append(insts, instruments...)
}
return insts, nil
}
// getInstrumentsForAsset returns the instruments for an asset type
func (e *Exchange) getInstrumentsForAsset(ctx context.Context, a asset.Item) ([]Instrument, error) {
if !e.SupportsAsset(a) {
return nil, fmt.Errorf("%w: %v", asset.ErrNotSupported, a)
}
var instruments []Instrument
var instType string
var err error
switch a {
case asset.Options:
instruments, err = e.getInstrumentsForOptions(ctx)
if err != nil {
return nil, err
}
e.instrumentsInfoMapLock.Lock()
e.instrumentsInfoMap[instTypeOption] = instruments
e.instrumentsInfoMapLock.Unlock()
return instruments, nil
case asset.Spot:
instType = instTypeSpot
case asset.Futures:
instType = instTypeFutures
case asset.PerpetualSwap:
instType = instTypeSwap
case asset.Margin:
instType = instTypeMargin
}
instruments, err = e.GetInstruments(ctx, &InstrumentsFetchParams{
InstrumentType: instType,
})
if err != nil {
return nil, err
}
e.instrumentsInfoMapLock.Lock()
e.instrumentsInfoMap[instType] = instruments
e.instrumentsInfoMapLock.Unlock()
return instruments, 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.PerpetualSwap {
return nil, fmt.Errorf("%w %v", futures.ErrNotPerpetualFuture, r.Asset)
}
if r.Pair.IsEmpty() {
return nil, fmt.Errorf("%w, pair required", currency.ErrCurrencyPairEmpty)
}
format, err := e.GetPairFormat(r.Asset, true)
if err != nil {
return nil, err
}
fPair := r.Pair.Format(format)
pairRate := fundingrate.LatestRateResponse{
TimeChecked: time.Now(),
Exchange: e.Name,
Asset: r.Asset,
Pair: fPair,
}
fr, err := e.GetSingleFundingRate(ctx, fPair.String())
if err != nil {
return nil, err
}
var fri time.Duration
if len(e.Features.Supports.FuturesCapabilities.SupportedFundingRateFrequencies) == 1 {
// can infer funding rate interval from the only funding rate frequency defined
for k := range e.Features.Supports.FuturesCapabilities.SupportedFundingRateFrequencies {
fri = k.Duration()
}
}
pairRate.LatestRate = fundingrate.Rate{
// okx funding rate is settlement time, not when it started
Time: fr.FundingTime.Time().Add(-fri),
Rate: fr.FundingRate.Decimal(),
}
if r.IncludePredictedRate {
pairRate.TimeOfNextRate = fr.NextFundingTime.Time()
pairRate.PredictedUpcomingRate = fundingrate.Rate{
Time: fr.NextFundingTime.Time().Add(-fri),
Rate: fr.NextFundingRate.Decimal(),
}
}
return []fundingrate.LatestRateResponse{pairRate}, nil
}
// GetHistoricalFundingRates returns funding rates for a given asset and currency for a time period
func (e *Exchange) GetHistoricalFundingRates(ctx context.Context, r *fundingrate.HistoricalRatesRequest) (*fundingrate.HistoricalRates, error) {
if r == nil {
return nil, fmt.Errorf("%w HistoricalRatesRequest", common.ErrNilPointer)
}
requestLimit := 100
sd := r.StartDate
maxLookback := time.Now().Add(-e.Features.Supports.FuturesCapabilities.MaximumFundingRateHistory)
if r.StartDate.Before(maxLookback) {
if r.RespectHistoryLimits {
r.StartDate = maxLookback
} else {
return nil, fmt.Errorf("%w earliest date is %v", fundingrate.ErrFundingRateOutsideLimits, maxLookback)
}
if r.EndDate.Before(maxLookback) {
return nil, futures.ErrGetFundingDataRequired
}
r.StartDate = maxLookback
}
format, err := e.GetPairFormat(r.Asset, true)
if err != nil {
return nil, err
}
fPair := r.Pair.Format(format)
pairRate := fundingrate.HistoricalRates{
Exchange: e.Name,
Asset: r.Asset,
Pair: fPair,
StartDate: r.StartDate,
EndDate: r.EndDate,
}
// map of time indexes, allowing for easy lookup of slice index from unix time data
mti := make(map[int64]int)
for sd.Before(r.EndDate) {
var frh []FundingRateResponse
frh, err = e.GetFundingRateHistory(ctx, fPair.String(), sd, r.EndDate, int64(requestLimit))
if err != nil {
return nil, err
}
if len(frh) == 0 {
break
}
for i := range frh {
if r.IncludePayments {
mti[frh[i].FundingTime.Time().Unix()] = i
}
pairRate.FundingRates = append(pairRate.FundingRates, fundingrate.Rate{
Time: frh[i].FundingTime.Time(),
Rate: frh[i].FundingRate.Decimal(),
})
}
if len(frh) < requestLimit {
break
}
sd = frh[len(frh)-1].FundingTime.Time()
}
var fr *FundingRateResponse
fr, err = e.GetSingleFundingRate(ctx, fPair.String())
if err != nil {
return nil, err
}
if fr == nil {
return nil, fmt.Errorf("%w GetSingleFundingRate", common.ErrNilPointer)
}
pairRate.LatestRate = fundingrate.Rate{
Time: fr.FundingTime.Time(),
Rate: fr.FundingRate.Decimal(),
}
pairRate.TimeOfNextRate = fr.NextFundingTime.Time()
if r.IncludePredictedRate {
pairRate.PredictedUpcomingRate = fundingrate.Rate{
Time: fr.NextFundingTime.Time(),
Rate: fr.NextFundingRate.Decimal(),
}
}
if r.IncludePayments {
pairRate.PaymentCurrency = r.Pair.Base
if !r.PaymentCurrency.IsEmpty() {
pairRate.PaymentCurrency = r.PaymentCurrency
}
sd = r.StartDate
billDetailsFunc := e.GetBillsDetail3Months
if time.Since(r.StartDate) < kline.OneWeek.Duration() {
billDetailsFunc = e.GetBillsDetailLast7Days
}
for sd.Before(r.EndDate) {
var fri time.Duration
if len(e.Features.Supports.FuturesCapabilities.SupportedFundingRateFrequencies) == 1 {
// can infer funding rate interval from the only funding rate frequency defined
for k := range e.Features.Supports.FuturesCapabilities.SupportedFundingRateFrequencies {
fri = k.Duration()
}
}
var billDetails []BillsDetailResponse
billDetails, err = billDetailsFunc(ctx, &BillsDetailQueryParameter{
InstrumentType: GetInstrumentTypeFromAssetItem(r.Asset),
Currency: pairRate.PaymentCurrency,
BillType: 137,
BeginTime: sd,
EndTime: r.EndDate,
Limit: int64(requestLimit),
})
if err != nil {
return nil, err
}
for i := range billDetails {
if index, okay := mti[billDetails[i].Timestamp.Time().Truncate(fri).Unix()]; okay {
pairRate.FundingRates[index].Payment = billDetails[i].ProfitAndLoss.Decimal()
continue
}
}
if len(billDetails) < requestLimit {
break
}
sd = billDetails[len(billDetails)-1].Timestamp.Time()
}
for i := range pairRate.FundingRates {
pairRate.PaymentSum = pairRate.PaymentSum.Add(pairRate.FundingRates[i].Payment)
}
}
return &pairRate, nil
}
// 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.PerpetualSwap, nil
}
// SetMarginType sets the default margin type for when opening a new position
// okx allows this to be set with an order, however this sets a default
func (e *Exchange) SetMarginType(_ context.Context, _ asset.Item, _ currency.Pair, _ margin.Type) error {
return fmt.Errorf("%w margin type is set per order", common.ErrFunctionNotSupported)
}
// SetCollateralMode sets the collateral type for your account
func (e *Exchange) SetCollateralMode(_ context.Context, _ asset.Item, _ collateral.Mode) error {
return fmt.Errorf("%w must be set via website", common.ErrFunctionNotSupported)
}
// GetCollateralMode returns the collateral type for your account
func (e *Exchange) GetCollateralMode(ctx context.Context, item asset.Item) (collateral.Mode, error) {
if !e.SupportsAsset(item) {
return 0, fmt.Errorf("%w: %v", asset.ErrNotSupported, item)
}
cfg, err := e.GetAccountConfiguration(ctx)
if err != nil {
return 0, err
}
switch cfg.AccountLevel {
case 1:
if item != asset.Spot {
return 0, fmt.Errorf("%w %v", asset.ErrNotSupported, item)
}
fallthrough
case 2:
return collateral.SpotFuturesMode, nil
case 3:
return collateral.MultiMode, nil
case 4:
return collateral.PortfolioMode, nil
default:
return collateral.UnknownMode, fmt.Errorf("%w %v", order.ErrCollateralInvalid, cfg.AccountLevel)
}
}
// ChangePositionMargin will modify a position/currencies margin parameters
func (e *Exchange) ChangePositionMargin(ctx context.Context, req *margin.PositionChangeRequest) (*margin.PositionChangeResponse, error) {
if req == nil {
return nil, fmt.Errorf("%w PositionChangeRequest", common.ErrNilPointer)
}
if !e.SupportsAsset(req.Asset) {
return nil, fmt.Errorf("%w: %v", asset.ErrNotSupported, req.Asset)
}
if req.NewAllocatedMargin == 0 {
return nil, fmt.Errorf("%w %v %v", margin.ErrNewAllocatedMarginRequired, req.Asset, req.Pair)
}
if req.OriginalAllocatedMargin == 0 {
return nil, margin.ErrOriginalPositionMarginRequired
}
if req.MarginType != margin.Isolated {
return nil, fmt.Errorf("%w %v", margin.ErrMarginTypeUnsupported, req.MarginType)
}
pairFormat, err := e.GetPairFormat(req.Asset, true)
if err != nil {
return nil, err
}
fPair := req.Pair.Format(pairFormat)
marginType := "add"
amt := req.NewAllocatedMargin - req.OriginalAllocatedMargin
if req.NewAllocatedMargin < req.OriginalAllocatedMargin {
marginType = "reduce"
amt = req.OriginalAllocatedMargin - req.NewAllocatedMargin
}
if req.MarginSide == "" {
req.MarginSide = "net"
}
r := &IncreaseDecreaseMarginInput{
InstrumentID: fPair.String(),
PositionSide: req.MarginSide,
MarginBalanceType: marginType,
Amount: amt,
}
if req.Asset == asset.Margin {
r.Currency = req.Pair.Base.Item.Symbol
}
resp, err := e.IncreaseDecreaseMargin(ctx, r)
if err != nil {
return nil, err
}
return &margin.PositionChangeResponse{
Exchange: e.Name,
Pair: req.Pair,
Asset: req.Asset,
AllocatedMargin: resp.Amount.Float64(),
MarginType: req.MarginType,
}, nil
}
// GetFuturesPositionSummary returns position summary details for an active position
func (e *Exchange) GetFuturesPositionSummary(ctx context.Context, req *futures.PositionSummaryRequest) (*futures.PositionSummary, error) {
if req == nil {
return nil, fmt.Errorf("%w PositionSummaryRequest", common.ErrNilPointer)
}
if req.CalculateOffline {
return nil, common.ErrCannotCalculateOffline
}
if !e.SupportsAsset(req.Asset) || !req.Asset.IsFutures() {
return nil, fmt.Errorf("%w %v", asset.ErrNotSupported, req.Asset)
}
fPair, err := e.FormatExchangeCurrency(req.Pair, req.Asset)
if err != nil {
return nil, err
}
instrumentType := GetInstrumentTypeFromAssetItem(req.Asset)
var contracts []futures.Contract
contracts, err = e.GetFuturesContractDetails(ctx, req.Asset)
if err != nil {
return nil, err
}
multiplier := 1.0
var contractSettlementType futures.ContractSettlementType
for i := range contracts {
if !contracts[i].Name.Equal(fPair) {
continue
}
multiplier = contracts[i].Multiplier
contractSettlementType = contracts[i].SettlementType
break
}
positionSummaries, err := e.GetPositions(ctx, instrumentType, fPair.String(), "")
if err != nil {
return nil, err
}
var positionSummary *AccountPosition
for i := range positionSummaries {
if positionSummaries[i].QuantityOfPosition.Float64() <= 0 {
continue
}
positionSummary = &positionSummaries[i]
break
}
if positionSummary == nil {
return nil, fmt.Errorf("%w, received '%v', no positions found", errOnlyOneResponseExpected, len(positionSummaries))
}
marginMode := margin.Isolated
if positionSummary.MarginMode == TradeModeCross {
marginMode = margin.Multi
}
acc, err := e.AccountBalance(ctx, currency.EMPTYCODE)
if err != nil {
return nil, err
}
if len(acc) != 1 {
return nil, fmt.Errorf("%w, received '%v'", errOnlyOneResponseExpected, len(acc))
}
var freeCollateral, totalCollateral, equityOfCurrency, frozenBalance,
availableEquity, cashBalance, discountEquity,
equityUSD, totalEquity, isolatedEquity, isolatedLiabilities,
isolatedUnrealisedProfit, notionalLeverage,
strategyEquity decimal.Decimal
for i := range acc[0].Details {
if !acc[0].Details[i].Currency.Equal(positionSummary.Currency) {
continue
}
freeCollateral = acc[0].Details[i].AvailableBalance.Decimal()
frozenBalance = acc[0].Details[i].FrozenBalance.Decimal()
totalCollateral = freeCollateral.Add(frozenBalance)
equityOfCurrency = acc[0].Details[i].EquityOfCurrency.Decimal()
availableEquity = acc[0].Details[i].AvailableEquity.Decimal()
cashBalance = acc[0].Details[i].CashBalance.Decimal()
discountEquity = acc[0].Details[i].DiscountEquity.Decimal()
equityUSD = acc[0].Details[i].EquityUsd.Decimal()
totalEquity = acc[0].Details[i].TotalEquity.Decimal()
isolatedEquity = acc[0].Details[i].IsoEquity.Decimal()
isolatedLiabilities = acc[0].Details[i].IsolatedLiabilities.Decimal()
isolatedUnrealisedProfit = acc[0].Details[i].IsoUpl.Decimal()
notionalLeverage = acc[0].Details[i].NotionalLever.Decimal()
strategyEquity = acc[0].Details[i].StrategyEquity.Decimal()
break
}
collateralMode, err := e.GetCollateralMode(ctx, req.Asset)
if err != nil {
return nil, err
}
return &futures.PositionSummary{
Pair: req.Pair,
Asset: req.Asset,
MarginType: marginMode,
CollateralMode: collateralMode,
Currency: positionSummary.Currency,
AvailableEquity: availableEquity,
CashBalance: cashBalance,
DiscountEquity: discountEquity,
EquityUSD: equityUSD,
IsolatedEquity: isolatedEquity,
IsolatedLiabilities: isolatedLiabilities,
IsolatedUPL: isolatedUnrealisedProfit,
NotionalLeverage: notionalLeverage,
TotalEquity: totalEquity,
StrategyEquity: strategyEquity,
IsolatedMargin: positionSummary.Margin.Decimal(),
NotionalSize: positionSummary.NotionalUsd.Decimal(),
Leverage: positionSummary.Leverage.Decimal(),
MaintenanceMarginRequirement: positionSummary.MaintenanceMarginRequirement.Decimal(),
InitialMarginRequirement: positionSummary.InitialMarginRequirement.Decimal(),
EstimatedLiquidationPrice: positionSummary.LiquidationPrice.Decimal(),
CollateralUsed: positionSummary.Margin.Decimal(),
MarkPrice: positionSummary.MarkPrice.Decimal(),
CurrentSize: positionSummary.QuantityOfPosition.Decimal().Mul(decimal.NewFromFloat(multiplier)),
ContractSize: positionSummary.QuantityOfPosition.Decimal(),
ContractMultiplier: decimal.NewFromFloat(multiplier),
ContractSettlementType: contractSettlementType,
AverageOpenPrice: positionSummary.AveragePrice.Decimal(),
UnrealisedPNL: positionSummary.UPNL.Decimal(),
MaintenanceMarginFraction: positionSummary.MarginRatio.Decimal(),
FreeCollateral: freeCollateral,
TotalCollateral: totalCollateral,
FrozenBalance: frozenBalance,
EquityOfCurrency: equityOfCurrency,
}, nil
}
// GetFuturesPositionOrders returns the orders for futures positions
func (e *Exchange) GetFuturesPositionOrders(ctx context.Context, req *futures.PositionsRequest) ([]futures.PositionResponse, error) {
if req == nil {
return nil, fmt.Errorf("%w PositionSummaryRequest", common.ErrNilPointer)
}
if !e.SupportsAsset(req.Asset) || !req.Asset.IsFutures() {
return nil, fmt.Errorf("%w %v", asset.ErrNotSupported, req.Asset)
}
if time.Since(req.StartDate) > e.Features.Supports.MaximumOrderHistory {
if req.RespectOrderHistoryLimits {
req.StartDate = time.Now().Add(-e.Features.Supports.MaximumOrderHistory)
} else {
return nil, fmt.Errorf("%w max lookup %v", futures.ErrOrderHistoryTooLarge, time.Now().Add(-e.Features.Supports.MaximumOrderHistory))
}
}
err := common.StartEndTimeCheck(req.StartDate, req.EndDate)
if err != nil {
return nil, err
}
resp := make([]futures.PositionResponse, len(req.Pairs))
var contracts []futures.Contract
contracts, err = e.GetFuturesContractDetails(ctx, req.Asset)
if err != nil {
return nil, err
}
contractsMap := make(map[currency.Pair]*futures.Contract)
for i := range contracts {
contractsMap[contracts[i].Name] = &contracts[i]
}
for i := range req.Pairs {
fPair, err := e.FormatExchangeCurrency(req.Pairs[i], req.Asset)
if err != nil {
return nil, err
}
instrumentType := GetInstrumentTypeFromAssetItem(req.Asset)
contract, exist := contractsMap[fPair]
if !exist {
return nil, fmt.Errorf("%w %v", futures.ErrContractNotSupported, fPair)
}
multiplier := contract.Multiplier
contractSettlementType := contract.SettlementType
resp[i] = futures.PositionResponse{
Pair: req.Pairs[i],
Asset: req.Asset,
ContractSettlementType: contractSettlementType,
}
var positions []OrderDetail
historyRequest := &OrderHistoryRequestParams{
OrderListRequestParams: OrderListRequestParams{
InstrumentType: instrumentType,
InstrumentID: fPair.String(),
Start: req.StartDate,
End: req.EndDate,
},
}
if time.Since(req.StartDate) <= time.Hour*24*7 {
positions, err = e.Get7DayOrderHistory(ctx, historyRequest)
} else {
positions, err = e.Get3MonthOrderHistory(ctx, historyRequest)
}
if err != nil {
return nil, err
}
for j := range positions {
if fPair.String() != positions[j].InstrumentID {
continue
}
orderStatus, err := order.StringToOrderStatus(strings.ToUpper(positions[j].State))
if err != nil {
log.Errorf(log.ExchangeSys, "%s %v", e.Name, err)
}
oType, tif, err := orderTypeFromString(positions[j].OrderType)
if err != nil {
return nil, err
}
orderAmount := positions[j].Size
if positions[j].QuantityType == "quote_ccy" {
// Size is quote amount.
orderAmount /= positions[j].AveragePrice
}
remainingAmount := float64(0)
if orderStatus != order.Filled {
remainingAmount = orderAmount.Float64() - positions[j].AccumulatedFillSize.Float64()
}
cost := positions[j].AveragePrice.Float64() * positions[j].AccumulatedFillSize.Float64()
if multiplier != 1 {
cost *= multiplier
}
resp[i].Orders = append(resp[i].Orders, order.Detail{
Price: positions[j].Price.Float64(),
AverageExecutedPrice: positions[j].AveragePrice.Float64(),
Amount: orderAmount.Float64() * multiplier,
ContractAmount: orderAmount.Float64(),
ExecutedAmount: positions[j].AccumulatedFillSize.Float64(),
RemainingAmount: remainingAmount,
Fee: positions[j].TransactionFee.Float64(),
FeeAsset: currency.NewCode(positions[j].FeeCurrency),
Exchange: e.Name,
OrderID: positions[j].OrderID,
ClientOrderID: positions[j].ClientOrderID,
Type: oType,
Side: positions[j].Side,
Status: orderStatus,
AssetType: req.Asset,
Date: positions[j].CreationTime.Time(),
LastUpdated: positions[j].UpdateTime.Time(),
Pair: req.Pairs[i],
Cost: cost,
CostAsset: currency.NewCode(positions[j].RebateCurrency),
TimeInForce: tif,
})
}
}
return resp, nil
}
// SetLeverage sets the account's initial leverage for the asset type and pair
func (e *Exchange) SetLeverage(ctx context.Context, item asset.Item, pair currency.Pair, marginType margin.Type, amount float64, orderSide order.Side) error {
posSide := "net"
switch item {
case asset.Futures, asset.PerpetualSwap:
if marginType == margin.Isolated {
switch {
case orderSide == order.UnknownSide:
return order.ErrSideIsInvalid
case orderSide.IsLong():
posSide = "long"
case orderSide.IsShort():
posSide = "short"
default:
return fmt.Errorf("%w %v requires long/short", order.ErrSideIsInvalid, orderSide)
}
}
fallthrough
case asset.Margin, asset.Options:
instrumentID, err := e.FormatSymbol(pair, item)
if err != nil {
return err
}
marginMode := e.marginTypeToString(marginType)
_, err = e.SetLeverageRate(ctx, &SetLeverageInput{
Leverage: amount,
MarginMode: marginMode,
InstrumentID: instrumentID,
PositionSide: posSide,
})
return err
default:
return fmt.Errorf("%w %v", asset.ErrNotSupported, item)
}
}
// GetLeverage gets the account's initial leverage for the asset type and pair
func (e *Exchange) GetLeverage(ctx context.Context, item asset.Item, pair currency.Pair, marginType margin.Type, orderSide order.Side) (float64, error) {
var inspectLeverage bool
switch item {
case asset.Futures, asset.PerpetualSwap:
if marginType == margin.Isolated {
switch {
case orderSide == order.UnknownSide:
return 0, order.ErrSideIsInvalid
case orderSide.IsLong(), orderSide.IsShort():
inspectLeverage = true
default:
return 0, fmt.Errorf("%w '%v', requires long/short", order.ErrSideIsInvalid, orderSide)
}
}
fallthrough
case asset.Margin, asset.Options:
instrumentID, err := e.FormatSymbol(pair, item)
if err != nil {
return -1, err
}
marginMode := e.marginTypeToString(marginType)
lev, err := e.GetLeverageRate(ctx, instrumentID, marginMode, currency.EMPTYCODE)
if err != nil {
return -1, err
}
if len(lev) == 0 {
return -1, fmt.Errorf("%w %v %v %s", futures.ErrPositionNotFound, item, pair, marginType)
}
if inspectLeverage {
for i := range lev {
if lev[i].PositionSide == orderSide.Lower() {
return lev[i].Leverage.Float64(), nil
}
}
}
// leverage is the same across positions
return lev[0].Leverage.Float64(), nil
default:
return -1, fmt.Errorf("%w %v", asset.ErrNotSupported, item)
}
}
// GetFuturesContractDetails returns details about futures contracts
func (e *Exchange) GetFuturesContractDetails(ctx context.Context, item asset.Item) ([]futures.Contract, error) {
if !item.IsFutures() {
return nil, futures.ErrNotFuturesAsset
}
switch item {
case asset.Futures, asset.PerpetualSwap:
instType := GetInstrumentTypeFromAssetItem(item)
result, err := e.GetInstruments(ctx, &InstrumentsFetchParams{
InstrumentType: instType,
})
if err != nil {
return nil, err
}
resp := make([]futures.Contract, len(result))
for i := range result {
var (
underlying currency.Pair
settleCurr currency.Code
contractSettlementType futures.ContractSettlementType
)
if result[i].State == "live" {
underlying, err = currency.NewPairFromString(result[i].Underlying)
if err != nil {
return nil, err
}
settleCurr = currency.NewCode(result[i].SettlementCurrency)
contractSettlementType = futures.Linear
if result[i].SettlementCurrency == result[i].BaseCurrency {
contractSettlementType = futures.Inverse
}
}
var ct futures.ContractType
if item == asset.PerpetualSwap {
ct = futures.Perpetual
} else {
switch result[i].Alias {
case "this_week", "next_week":
ct = futures.Weekly
case "quarter", "next_quarter":
ct = futures.Quarterly
}
}
resp[i] = futures.Contract{
Exchange: e.Name,
Name: result[i].InstrumentID,
Underlying: underlying,
Asset: item,
StartDate: result[i].ListTime.Time(),
EndDate: result[i].ExpTime.Time(),
IsActive: result[i].State == "live",
Status: result[i].State,
Type: ct,
SettlementType: contractSettlementType,
MarginCurrency: settleCurr,
Multiplier: result[i].ContractValue.Float64(),
MaxLeverage: result[i].MaxLeverage.Float64(),
}
if !settleCurr.IsEmpty() {
resp[i].SettlementCurrencies = currency.Currencies{settleCurr}
}
}
return resp, nil
case asset.Spread:
results, err := e.GetPublicSpreads(ctx, "", "", "", "")
if err != nil {
return nil, err
}
resp := make([]futures.Contract, len(results))
for s := range results {
contractSettlementType, err := futures.StringToContractSettlementType(results[s].SpreadType)
if err != nil {
return nil, err
}
resp[s] = futures.Contract{
Exchange: e.Name,
Name: results[s].SpreadID,
Asset: asset.Spread,
StartDate: results[s].ListTime.Time(),
EndDate: results[s].ExpTime.Time(),
IsActive: results[s].State == "live",
Status: results[s].State,
Type: futures.LongDated,
SettlementType: contractSettlementType,
MarginCurrency: currency.NewCode(results[s].QuoteCurrency),
}
}
return resp, nil
default:
return nil, fmt.Errorf("%w %v", asset.ErrNotSupported, item)
}
}
// GetOpenInterest returns the open interest rate for a given asset pair
func (e *Exchange) GetOpenInterest(ctx context.Context, k ...key.PairAsset) ([]futures.OpenInterest, error) {
for i := range k {
switch k[i].Asset {
case asset.Futures, asset.PerpetualSwap, asset.Options:
default:
// avoid API calls or returning errors after a successful retrieval
return nil, fmt.Errorf("%w %v %v", asset.ErrNotSupported, k[i].Asset, k[i].Pair())
}
}
if len(k) != 1 {
var resp []futures.OpenInterest
// TODO: Options support
instTypes := map[string]asset.Item{
instTypeSwap: asset.PerpetualSwap,
instTypeFutures: asset.Futures,
instTypeOption: asset.Options,
}
for instType, v := range instTypes {
var oid []OpenInterest
var err error
switch instType {
case instTypeOption:
var underlyings []string
underlyings, err = e.GetPublicUnderlyings(ctx, instTypeOption)
if err != nil {
return nil, err
}
for u := range underlyings {
var incOID []OpenInterest
incOID, err = e.GetOpenInterestData(ctx, instType, underlyings[u], "", "")
if err != nil {
return nil, err
}
oid = append(oid, incOID...)
}
case instTypeSwap,
instTypeFutures:
oid, err = e.GetOpenInterestData(ctx, instType, "", "", "")
if err != nil {
return nil, err
}
}
for j := range oid {
var isEnabled bool
var p currency.Pair
p, isEnabled, err = e.MatchSymbolCheckEnabled(oid[j].InstrumentID, v, true)
if err != nil && !errors.Is(err, currency.ErrPairNotFound) {
return nil, err
}
if !isEnabled {
continue
}
var appendData bool
for j := range k {
if k[j].Pair().Equal(p) {
appendData = true
break
}
}
if len(k) > 0 && !appendData {
continue
}
resp = append(resp, futures.OpenInterest{
Key: key.NewExchangeAssetPair(e.Name, v, p),
OpenInterest: oid[j].OpenInterest.Float64(),
})
}
}
return resp, nil
}
resp := make([]futures.OpenInterest, 1)
instTypes := map[asset.Item]string{
asset.PerpetualSwap: "SWAP",
asset.Futures: "FUTURES",
}
pFmt, err := e.FormatSymbol(k[0].Pair(), k[0].Asset)
if err != nil {
return nil, err
}
var oid []OpenInterest
switch instTypes[k[0].Asset] {
case instTypeOption:
var underlyings []string
underlyings, err = e.GetPublicUnderlyings(ctx, instTypeOption)
if err != nil {
return nil, err
}
for u := range underlyings {
var incOID []OpenInterest
incOID, err = e.GetOpenInterestData(ctx, instTypes[k[0].Asset], underlyings[u], "", "")
if err != nil {
return nil, err
}
oid = append(oid, incOID...)
}
case instTypeSwap, instTypeFutures:
oid, err = e.GetOpenInterestData(ctx, instTypes[k[0].Asset], "", "", pFmt)
if err != nil {
return nil, err
}
}
for i := range oid {
p, isEnabled, err := e.MatchSymbolCheckEnabled(oid[i].InstrumentID, k[0].Asset, true)
if err != nil && !errors.Is(err, currency.ErrPairNotFound) {
return nil, err
}
if !isEnabled {
continue
}
resp[0] = futures.OpenInterest{
Key: key.NewExchangeAssetPair(e.Name, k[0].Asset, p),
OpenInterest: oid[i].OpenInterest.Float64(),
}
}
return resp, nil
}
// GetCurrencyTradeURL returns the URL to the exchange's trade page for the given asset and currency pair
func (e *Exchange) GetCurrencyTradeURL(ctx context.Context, a asset.Item, cp currency.Pair) (string, error) {
_, err := e.CurrencyPairs.IsPairEnabled(cp, a)
if err != nil {
return "", err
}
cp.Delimiter = currency.DashDelimiter
switch a {
case asset.Spot:
return baseURL + "trade-spot/" + cp.Lower().String(), nil
case asset.Margin:
return baseURL + "trade-margin/" + cp.Lower().String(), nil
case asset.PerpetualSwap:
return baseURL + "trade-swap/" + cp.Lower().String(), nil
case asset.Options:
return baseURL + "trade-option/" + cp.Base.Lower().String() + "-usd", nil
case asset.Spread:
return baseURL, nil
case asset.Futures:
cp, err = e.FormatExchangeCurrency(cp, a)
if err != nil {
return "", err
}
insts, err := e.GetInstruments(ctx, &InstrumentsFetchParams{
InstrumentType: instTypeFutures,
InstrumentID: cp.String(),
})
if err != nil {
return "", err
}
if len(insts) != 1 {
return "", fmt.Errorf("%w response len: %v currency expected: %v", errOnlyOneResponseExpected, len(insts), cp)
}
var ct string
switch insts[0].Alias {
case "this_week":
ct = "-weekly"
case "next_week":
ct = "-biweekly"
case "this_month":
ct = "-monthly"
case "next_month":
ct = "-bimonthly"
case "quarter":
ct = "-quarterly"
case "next_quarter":
ct = "-biquarterly"
}
return baseURL + "trade-futures/" + strings.ToLower(insts[0].Underlying) + ct, nil
default:
return "", fmt.Errorf("%w %q", asset.ErrNotSupported, a)
}
}
// MessageID returns a universally unique ID using UUID V7, with hyphens removed to fit the maximum 32-character field for okx
func (e *Exchange) MessageID() string {
return strings.Replace(uuid.Must(uuid.NewV7()).String(), "-", "", 4)
}