Files
gocryptotrader/exchanges/okx/okx_wrapper.go
2025-03-06 12:50:03 +11:00

3056 lines
99 KiB
Go

package okx
import (
"context"
"errors"
"fmt"
"math"
"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"
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/stream"
"github.com/thrasher-corp/gocryptotrader/exchanges/stream/buffer"
"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 (ok *Okx) SetDefaults() {
ok.Name = "Okx"
ok.Enabled = true
ok.Verbose = true
ok.WsRequestSemaphore = make(chan int, 20)
ok.API.CredentialsValidator.RequiresKey = true
ok.API.CredentialsValidator.RequiresSecret = true
ok.API.CredentialsValidator.RequiresClientID = true
ok.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 := ok.SetGlobalPairsManager(cpf, cpf, asset.Spot, asset.Futures, asset.PerpetualSwap, asset.Options, asset.Margin, asset.Spread)
if err != nil {
log.Errorln(log.ExchangeSys, err)
}
// Fill out the capabilities/features that the exchange supports
ok.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(),
}
ok.Requester, err = request.New(ok.Name,
common.NewHTTPClientWithTimeout(exchange.DefaultHTTPTimeout),
request.WithLimiter(GetRateLimit()))
if err != nil {
log.Errorln(log.ExchangeSys, err)
}
ok.API.Endpoints = ok.NewEndpoints()
err = ok.API.Endpoints.SetDefaultEndpoints(map[exchange.URL]string{
exchange.RestSpot: apiURL,
exchange.WebsocketSpot: apiWebsocketPublicURL,
})
if err != nil {
log.Errorln(log.ExchangeSys, err)
}
ok.Websocket = stream.NewWebsocket()
ok.WebsocketResponseMaxLimit = websocketResponseMaxLimit
ok.WebsocketResponseCheckTimeout = websocketResponseMaxLimit
ok.WebsocketOrderbookBufferLimit = exchange.DefaultWebsocketOrderbookBufferLimit
}
// Setup takes in the supplied exchange configuration details and sets params
func (ok *Okx) Setup(exch *config.Exchange) error {
if err := exch.Validate(); err != nil {
return err
}
if !exch.Enabled {
ok.SetEnabled(false)
return nil
}
if err := ok.SetupDefaults(exch); err != nil {
return err
}
ok.WsResponseMultiplexer = wsRequestDataChannelsMultiplexer{
WsResponseChannelsMap: make(map[string]*wsRequestInfo),
Register: make(chan *wsRequestInfo),
Unregister: make(chan string),
Message: make(chan *wsIncomingData),
shutdown: make(chan bool),
}
wsRunningEndpoint, err := ok.API.Endpoints.GetURL(exchange.WebsocketSpot)
if err != nil {
return err
}
if err := ok.Websocket.Setup(&stream.WebsocketSetup{
ExchangeConfig: exch,
DefaultURL: apiWebsocketPublicURL,
RunningURL: wsRunningEndpoint,
Connector: ok.WsConnect,
Subscriber: ok.Subscribe,
Unsubscriber: ok.Unsubscribe,
GenerateSubscriptions: ok.generateSubscriptions,
Features: &ok.Features.Supports.WebsocketCapabilities,
MaxWebsocketSubscriptionsPerConnection: 240,
OrderbookBufferConfig: buffer.Config{
Checksum: ok.CalculateUpdateOrderbookChecksum,
},
RateLimitDefinitions: ok.Requester.GetRateLimiterDefinitions(),
}); err != nil {
return err
}
go ok.WsResponseMultiplexer.Run()
if err := ok.Websocket.SetupNewConnection(&stream.ConnectionSetup{
URL: apiWebsocketPublicURL,
ResponseCheckTimeout: exch.WebsocketResponseCheckTimeout,
ResponseMaxLimit: websocketResponseMaxLimit,
RateLimit: request.NewRateLimitWithWeight(time.Second, 2, 1),
}); err != nil {
return err
}
return ok.Websocket.SetupNewConnection(&stream.ConnectionSetup{
URL: apiWebsocketPrivateURL,
ResponseCheckTimeout: exch.WebsocketResponseCheckTimeout,
ResponseMaxLimit: websocketResponseMaxLimit,
Authenticated: true,
RateLimit: request.NewRateLimitWithWeight(time.Second, 2, 1),
})
}
// Shutdown calls Base.Shutdown and then shuts down the response multiplexer
func (ok *Okx) Shutdown() error {
if err := ok.Base.Shutdown(); err != nil {
return err
}
// Must happen after the Websocket shutdown in Base.Shutdown, so there are no new blocking writes to the multiplexer
ok.WsResponseMultiplexer.Shutdown()
return nil
}
// GetServerTime returns the current exchange server time.
func (ok *Okx) GetServerTime(ctx context.Context, _ asset.Item) (time.Time, error) {
t, err := ok.GetSystemTime(ctx)
return t.Time(), err
}
// FetchTradablePairs returns a list of the exchanges tradable pairs
func (ok *Okx) 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 := ok.GetPairFormat(a, true)
if err != nil {
return nil, err
}
insts, err := ok.getInstrumentsForAsset(ctx, a)
if err != nil {
return nil, err
}
var pair currency.Pair
pairs := make([]currency.Pair, 0, len(insts))
for x := range insts {
if insts[x].State != "live" {
continue
}
pair, err = currency.NewPairDelimiter(insts[x].InstrumentID, format.Delimiter)
if err != nil {
return nil, err
}
pairs = append(pairs, pair)
}
return pairs, nil
case asset.Spread:
format, err := ok.GetPairFormat(a, true)
if err != nil {
return nil, err
}
spreadInstruments, err := ok.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], err = currency.NewPairDelimiter(spreadInstruments[x].SpreadID, format.Delimiter)
if err != nil {
return nil, err
}
}
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 (ok *Okx) UpdateTradablePairs(ctx context.Context, forceUpdate bool) error {
assetTypes := ok.GetAssetTypes(true)
for i := range assetTypes {
pairs, err := ok.FetchTradablePairs(ctx, assetTypes[i])
if err != nil {
return fmt.Errorf("%w for asset %v", err, assetTypes[i])
}
err = ok.UpdatePairs(pairs, assetTypes[i], false, forceUpdate)
if err != nil {
return fmt.Errorf("%w for asset %v", err, assetTypes[i])
}
}
return ok.EnsureOnePairEnabled()
}
// UpdateOrderExecutionLimits sets exchange execution order limits for an asset type
func (ok *Okx) UpdateOrderExecutionLimits(ctx context.Context, a asset.Item) error {
switch a {
case asset.Spot, asset.Margin, asset.Options,
asset.PerpetualSwap, asset.Futures:
insts, err := ok.getInstrumentsForAsset(ctx, a)
if err != nil {
return err
}
if len(insts) == 0 {
return common.ErrNoResponse
}
limits := make([]order.MinMaxLevel, len(insts))
for x := range insts {
pair, err := currency.NewPairFromString(insts[x].InstrumentID)
if err != nil {
return err
}
limits[x] = order.MinMaxLevel{
Pair: pair,
Asset: a,
PriceStepIncrementSize: insts[x].TickSize.Float64(),
MinimumBaseAmount: insts[x].MinimumOrderSize.Float64(),
}
}
return ok.LoadLimits(limits)
case asset.Spread:
insts, err := ok.GetPublicSpreads(ctx, "", "", "", "live")
if err != nil {
return err
}
if len(insts) == 0 {
return common.ErrNoResponse
}
limits := make([]order.MinMaxLevel, len(insts))
for x := range insts {
pair, err := currency.NewPairFromString(insts[x].SpreadID)
if err != nil {
return err
}
limits[x] = order.MinMaxLevel{
Pair: pair,
Asset: a,
PriceStepIncrementSize: insts[x].MinSize.Float64(),
MinimumBaseAmount: insts[x].MinSize.Float64(),
QuoteStepIncrementSize: insts[x].TickSize.Float64(),
}
}
return ok.LoadLimits(limits)
default:
return fmt.Errorf("%w %v", asset.ErrNotSupported, a)
}
}
// UpdateTicker updates and returns the ticker for a currency pair
func (ok *Okx) UpdateTicker(ctx context.Context, p currency.Pair, a asset.Item) (*ticker.Price, error) {
var err error
p, err = ok.FormatExchangeCurrency(p, a)
if err != nil {
return nil, err
}
if !ok.SupportsAsset(a) {
return nil, fmt.Errorf("%w: %v", asset.ErrNotSupported, a)
}
mdata, err := ok.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()
default:
return nil, fmt.Errorf("%w %v", asset.ErrNotSupported, a)
}
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: ok.Name,
AssetType: a,
})
if err != nil {
return nil, err
}
return ticker.GetTicker(ok.Name, p, a)
}
// UpdateTickers updates all currency pairs of a given asset type
func (ok *Okx) UpdateTickers(ctx context.Context, assetType asset.Item) error {
switch assetType {
case asset.Spread:
format, err := ok.GetPairFormat(asset.Spread, true)
if err != nil {
return err
}
pairs, err := ok.GetEnabledPairs(assetType)
if err != nil {
return err
}
for y := range pairs {
var spreadTickers []SpreadTicker
spreadTickers, err = ok.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: ok.Name,
AssetType: assetType,
})
if err != nil {
return err
}
}
}
case asset.Spot, asset.PerpetualSwap, asset.Futures, asset.Options, asset.Margin:
pairs, err := ok.GetEnabledPairs(assetType)
if err != nil {
return err
}
instrumentType := GetInstrumentTypeFromAssetItem(assetType)
if assetType == asset.Margin {
instrumentType = instTypeSpot
}
ticks, err := ok.GetTickers(ctx, instrumentType, "", "")
if err != nil {
return err
}
for y := range ticks {
pair, err := ok.GetPairFromInstrumentID(ticks[y].InstrumentID)
if err != nil {
return err
}
for i := range pairs {
pairFmt, err := ok.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: ok.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 (ok *Okx) UpdateOrderbook(ctx context.Context, pair currency.Pair, assetType asset.Item) (*orderbook.Base, error) {
if pair.IsEmpty() {
return nil, currency.ErrCurrencyPairEmpty
}
var err error
switch assetType {
case asset.Spread:
var (
pairFormat currency.PairFormat
spreadOrderbook []SpreadOrderbook
)
pairFormat, err = ok.GetPairFormat(assetType, true)
if err != nil {
return nil, err
}
spreadOrderbook, err = ok.GetPublicSpreadOrderBooks(ctx, pairFormat.Format(pair), 50)
if err != nil {
return nil, err
}
for y := range spreadOrderbook {
book := &orderbook.Base{
Exchange: ok.Name,
Pair: pair,
Asset: assetType,
VerifyOrderbook: ok.CanVerifyOrderbook,
}
book.Bids = make(orderbook.Tranches, 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.Tranche{
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.Tranches, 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.Tranche{
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 = ok.CurrencyPairs.IsAssetEnabled(assetType)
if err != nil {
return nil, err
}
var instrumentID string
pairFormat, err := ok.GetPairFormat(assetType, true)
if err != nil {
return nil, err
}
if !pair.IsPopulated() {
return nil, currency.ErrCurrencyPairsEmpty
}
instrumentID = pairFormat.Format(pair)
book := &orderbook.Base{
Exchange: ok.Name,
Pair: pair,
Asset: assetType,
VerifyOrderbook: ok.CanVerifyOrderbook,
}
var orderBookD *OrderBookResponseDetail
orderBookD, err = ok.GetOrderBookDepth(ctx, instrumentID, 400)
if err != nil {
return book, err
}
book.Bids = make(orderbook.Tranches, len(orderBookD.Bids))
for x := range orderBookD.Bids {
book.Bids[x] = orderbook.Tranche{
Amount: orderBookD.Bids[x].Amount.Float64(),
Price: orderBookD.Bids[x].DepthPrice.Float64(),
}
}
book.Asks = make(orderbook.Tranches, len(orderBookD.Asks))
for x := range orderBookD.Asks {
book.Asks[x] = orderbook.Tranche{
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(ok.Name, pair, assetType)
}
// UpdateAccountInfo retrieves balances for all enabled currencies.
func (ok *Okx) UpdateAccountInfo(ctx context.Context, assetType asset.Item) (account.Holdings, error) {
if err := ok.CurrencyPairs.IsAssetEnabled(assetType); err != nil {
return account.Holdings{}, err
}
var info account.Holdings
var acc account.SubAccount
info.Exchange = ok.Name
if !ok.SupportsAsset(assetType) {
return info, fmt.Errorf("%w: %v", asset.ErrNotSupported, assetType)
}
accountBalances, err := ok.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 := ok.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 (ok *Okx) GetAccountFundingHistory(ctx context.Context) ([]exchange.FundingHistory, error) {
depositHistories, err := ok.GetCurrencyDepositHistory(ctx, currency.EMPTYCODE, "", "", "", "", time.Time{}, time.Time{}, -1, 0)
if err != nil {
return nil, err
}
withdrawalHistories, err := ok.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: ok.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: ok.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 (ok *Okx) GetWithdrawalsHistory(ctx context.Context, c currency.Code, _ asset.Item) ([]exchange.WithdrawalHistory, error) {
withdrawals, err := ok.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 (ok *Okx) GetRecentTrades(ctx context.Context, p currency.Pair, assetType asset.Item) ([]trade.Data, error) {
format, err := ok.GetPairFormat(assetType, true)
if err != nil {
return nil, err
}
var resp []trade.Data
switch assetType {
case asset.Spread:
var spreadTrades []SpreadPublicTradeItem
spreadTrades, err = ok.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: ok.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 = ok.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: ok.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 ok.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 (ok *Okx) GetHistoricTrades(ctx context.Context, p currency.Pair, assetType asset.Item, timestampStart, timestampEnd time.Time) ([]trade.Data, error) {
if timestampStart.Before(time.Now().Add(-kline.ThreeMonth.Duration())) {
return nil, errOnlyThreeMonthsSupported
}
const limit = 100
pairFormat, err := ok.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 = ok.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: ok.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 ok.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 (ok *Okx) SubmitOrder(ctx context.Context, s *order.Submit) (*order.SubmitResponse, error) {
if !ok.SupportsAsset(s.AssetType) {
return nil, fmt.Errorf("%w: %v", asset.ErrNotSupported, s.AssetType)
}
if s.Amount <= 0 {
return nil, order.ErrAmountBelowMin
}
pairFormat, err := ok.GetPairFormat(s.AssetType, true)
if err != nil {
return nil, err
}
if s.Pair.IsEmpty() {
return nil, currency.ErrCurrencyPairEmpty
}
pairString := pairFormat.Format(s.Pair)
tradeMode := ok.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 ok.Websocket.CanUseAuthenticatedWebsocketForWrapper() {
placeSpreadOrderResponse, err = ok.WsPlaceSpreadOrder(ctx, spreadParam)
if err != nil {
return nil, err
}
} else {
placeSpreadOrderResponse, err = ok.PlaceSpreadOrder(ctx, spreadParam)
if err != nil {
return nil, err
}
}
return s.DeriveSubmitResponse(placeSpreadOrderResponse.OrderID)
}
orderTypeString, err := orderTypeString(s.Type)
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,
QuantityType: 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 ok.Websocket.CanUseAuthenticatedWebsocketForWrapper() {
placeOrderResponse, err = ok.WsPlaceOrder(ctx, orderRequest)
if err != nil {
return nil, err
}
} else {
placeOrderResponse, err = ok.PlaceOrder(ctx, orderRequest)
if err != nil {
return nil, err
}
}
return s.DeriveSubmitResponse(placeOrderResponse.OrderID)
case "trigger":
result, err = ok.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 "conditional":
// Trigger Price and type are used as a stop losss trigger price and type.
result, err = ok.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 "chase":
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", order.ErrAmountBelowMin)
}
result, err = ok.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 "move_order_stop":
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 = ok.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 "twap":
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 = ok.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 "oco":
switch {
case s.RiskManagementModes.TakeProfit.Price <= 0:
return nil, fmt.Errorf("%w, take profit price is required", order.ErrPriceBelowMin)
case s.RiskManagementModes.StopLoss.Price <= 0:
return nil, fmt.Errorf("%w, stop loss price is required", order.ErrPriceBelowMin)
}
result, err = ok.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 (ok *Okx) marginTypeToString(m margin.Type) string {
if allowedMarginTypes&m == m {
return m.String()
} else if margin.Multi == m {
return TradeModeCross
}
return ""
}
// ModifyOrder will allow of changing orderbook placement and limit to market conversion
func (ok *Okx) 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 ok.Websocket.CanUseAuthenticatedWebsocketForWrapper() {
_, err = ok.WsAmandSpreadOrder(ctx, amendSpreadOrder)
} else {
_, err = ok.AmendSpreadOrder(ctx, amendSpreadOrder)
}
if err != nil {
return nil, err
}
return action.DeriveModifyResponse()
}
// For other asset type instances.
pairFormat, err := ok.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.PostOnly, order.FillOrKill, order.ImmediateOrCancel,
order.OptimalLimitIOC, order.MarketMakerProtection, order.MarketMakerProtectionAndPostOnly:
amendRequest := AmendOrderRequestParams{
InstrumentID: pairFormat.Format(action.Pair),
NewQuantity: action.Amount,
OrderID: action.OrderID,
ClientOrderID: action.ClientOrderID,
}
if ok.Websocket.CanUseAuthenticatedWebsocketForWrapper() {
_, err = ok.WsAmendOrder(ctx, &amendRequest)
} else {
_, err = ok.AmendOrder(ctx, &amendRequest)
}
if err != nil {
return nil, err
}
case order.Trigger:
if action.TriggerPrice == 0 {
return nil, fmt.Errorf("%w, trigger price required", order.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 = ok.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", order.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", order.ErrPriceBelowMin)
}
_, err = ok.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 (ok *Okx) CancelOrder(ctx context.Context, ord *order.Cancel) error {
if !ok.SupportsAsset(ord.AssetType) {
return fmt.Errorf("%w: %v", asset.ErrNotSupported, ord.AssetType)
}
var err error
if ord.AssetType == asset.Spread {
if ok.Websocket.CanUseAuthenticatedWebsocketForWrapper() {
_, err = ok.WsCancelSpreadOrder(ctx, ord.OrderID, ord.ClientOrderID)
} else {
_, err = ok.CancelSpreadOrder(ctx, ord.OrderID, ord.ClientOrderID)
}
return err
}
pairFormat, err := ok.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.PostOnly, order.FillOrKill, order.ImmediateOrCancel,
order.OptimalLimitIOC, order.MarketMakerProtection, order.MarketMakerProtectionAndPostOnly:
req := CancelOrderRequestParam{
InstrumentID: instrumentID,
OrderID: ord.OrderID,
ClientOrderID: ord.ClientOrderID,
}
if ok.Websocket.CanUseAuthenticatedWebsocketForWrapper() {
_, err = ok.WsCancelOrder(ctx, &req)
} else {
_, err = ok.CancelSingleOrder(ctx, &req)
}
case order.Trigger, order.OCO, order.ConditionalStop,
order.TWAP, order.TrailingStop, order.Chase:
var response *AlgoOrder
response, err = ok.CancelAdvanceAlgoOrder(ctx, []AlgoOrderCancelParams{
{
AlgoOrderID: ord.OrderID,
InstrumentID: instrumentID,
},
})
if err != nil {
return err
}
if response.StatusCode != "0" {
return fmt.Errorf("sCode: %s sMessage: %s", response.StatusCode, response.StatusMessage)
}
return nil
default:
return fmt.Errorf("%w, order type %v", order.ErrUnsupportedOrderType, ord.Type)
}
return err
}
// CancelBatchOrders cancels orders by their corresponding ID numbers
func (ok *Okx) 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 !ok.SupportsAsset(ord.AssetType) {
return nil, fmt.Errorf("%w: %v", asset.ErrNotSupported, ord.AssetType)
}
var pairFormat currency.PairFormat
pairFormat, err = ok.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.PostOnly, order.FillOrKill, order.ImmediateOrCancel,
order.OptimalLimitIOC, order.MarketMakerProtection, order.MarketMakerProtectionAndPostOnly:
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 ok.Websocket.CanUseAuthenticatedWebsocketForWrapper() {
canceledOrders, err = ok.WsCancelMultipleOrder(ctx, cancelOrderParams)
} else {
canceledOrders, err = ok.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" && canceledOrders[x].StatusCode != "2" {
return ""
}
return order.Cancelled.String()
}()
}
}
if len(cancelAlgoOrderParams) > 0 {
cancelationResponse, err := ok.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, fmt.Errorf("sCode: %s sMessage: %s", 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 (ok *Okx) 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 = ok.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 = ok.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)
if err != nil {
return order.CancelAllResponse{}, err
}
}
var curr string
if orderCancellation.Pair.IsPopulated() {
curr = orderCancellation.Pair.Upper().String()
}
myOrders, err := ok.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 ok.Websocket.CanUseAuthenticatedWebsocketForWrapper() {
response, err = ok.WsCancelMultipleOrder(ctx, remaining[:20])
} else {
response, err = ok.CancelMultipleOrders(ctx, remaining[:20])
}
remaining = remaining[20:]
} else {
if ok.Websocket.CanUseAuthenticatedWebsocketForWrapper() {
response, err = ok.WsCancelMultipleOrder(ctx, remaining)
} else {
response, err = ok.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 (ok *Okx) GetOrderInfo(ctx context.Context, orderID string, pair currency.Pair, assetType asset.Item) (*order.Detail, error) {
if !ok.SupportsAsset(assetType) {
return nil, fmt.Errorf("%w %v", asset.ErrNotSupported, assetType)
}
if assetType == asset.Spread {
var resp *SpreadOrder
resp, err := ok.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: ok.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 := ok.CurrencyPairs.IsAssetEnabled(assetType); err != nil {
return nil, err
}
pairFormat, err := ok.GetPairFormat(assetType, false)
if err != nil {
return nil, err
}
if !pair.IsPopulated() {
return nil, currency.ErrCurrencyPairsEmpty
}
instrumentID := pairFormat.Format(pair)
if !ok.SupportsAsset(assetType) {
return nil, fmt.Errorf("%w: %v", asset.ErrNotSupported, assetType)
}
orderDetail, err := ok.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, err := orderTypeFromString(orderDetail.OrderType)
if err != nil {
return nil, err
}
return &order.Detail{
Amount: orderDetail.Size.Float64(),
Exchange: ok.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(),
}, nil
}
// GetDepositAddress returns a deposit address for a specified currency
func (ok *Okx) GetDepositAddress(ctx context.Context, c currency.Code, _, chain string) (*deposit.Address, error) {
response, err := ok.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 (ok *Okx) 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 := ok.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 (ok *Okx) WithdrawFiatFunds(_ context.Context, _ *withdraw.Request) (*withdraw.ExchangeResponse, error) {
return nil, common.ErrFunctionNotSupported
}
// WithdrawFiatFundsToInternationalBank returns a withdrawal ID when a withdrawal is submitted
func (ok *Okx) WithdrawFiatFundsToInternationalBank(_ context.Context, _ *withdraw.Request) (*withdraw.ExchangeResponse, error) {
return nil, common.ErrFunctionNotSupported
}
// GetActiveOrders retrieves any orders that are active/open
func (ok *Okx) 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 !ok.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 = ok.GetActiveSpreadOrders(ctx, "", req.Type.String(), "", req.FromOrderID, "", 0)
if err != nil {
return nil, err
}
for x := range spreads {
format, err = ok.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: ok.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(ok.Name, resp), nil
}
instrumentType := GetInstrumentTypeFromAssetItem(req.AssetType)
var orderType string
if req.Type != order.UnknownType && req.Type != order.AnyType {
orderType, err = orderTypeString(req.Type)
if err != nil {
return nil, err
}
}
endTime := req.EndTime
allOrders:
for {
requestParam := &OrderListRequestParams{
OrderType: orderType,
End: endTime,
InstrumentType: instrumentType,
}
var orderList []OrderDetail
orderList, err = ok.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 == 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
}
}
var orderStatus order.Status
orderStatus, err = order.StringToOrderStatus(strings.ToUpper(orderList[i].State))
if err != nil {
return nil, err
}
var oType order.Type
oType, 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: ok.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(),
})
}
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(ok.Name, resp), nil
}
// GetOrderHistory retrieves account order information Can Limit response to specific order status
func (ok *Okx) 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 !ok.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)
if err != nil {
return nil, err
}
spreadOrders, err := ok.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 = ok.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: ok.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(ok.Name, resp), nil
}
if len(req.Pairs) == 0 {
return nil, currency.ErrCurrencyPairsEmpty
}
instrumentType := GetInstrumentTypeFromAssetItem(req.AssetType)
endTime := req.EndTime
allOrders:
for {
orderList, err := ok.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 == 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
}
var orderStatus order.Status
orderStatus, err = order.StringToOrderStatus(strings.ToUpper(orderList[i].State))
if err != nil {
log.Errorf(log.ExchangeSys, "%s %v", ok.Name, err)
}
if orderStatus == order.Active {
continue
}
orderSide := orderList[i].Side
var oType order.Type
oType, 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: ok.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(),
Pair: pair,
Cost: orderList[i].AveragePrice.Float64() * orderList[i].AccumulatedFillSize.Float64(),
CostAsset: currency.NewCode(orderList[i].RebateCurrency),
})
}
}
if len(orderList) < 100 {
break
}
endTime = orderList[len(orderList)-1].CreationTime.Time()
}
return req.Filter(ok.Name, resp), nil
}
// GetFeeByType returns an estimate of fee based on the type of transaction
func (ok *Okx) GetFeeByType(ctx context.Context, feeBuilder *exchange.FeeBuilder) (float64, error) {
if feeBuilder == nil {
return 0, fmt.Errorf("%T %w", feeBuilder, common.ErrNilPointer)
}
if !ok.AreCredentialsValid(ctx) && feeBuilder.FeeType == exchange.CryptocurrencyTradeFee {
feeBuilder.FeeType = exchange.OfflineTradeFee
}
return ok.GetFee(ctx, feeBuilder)
}
// ValidateAPICredentials validates current credentials used for wrapper
func (ok *Okx) ValidateAPICredentials(ctx context.Context, assetType asset.Item) error {
_, err := ok.UpdateAccountInfo(ctx, assetType)
return ok.CheckTransientError(err)
}
// GetHistoricCandles returns candles between a time period for a set time interval
func (ok *Okx) GetHistoricCandles(ctx context.Context, pair currency.Pair, a asset.Item, interval kline.Interval, start, end time.Time) (*kline.Item, error) {
req, err := ok.GetKlineRequest(pair, a, interval, start, end, false)
if err != nil {
return nil, err
}
candles, err := ok.GetCandlesticksHistory(ctx,
req.RequestFormatted.Base.String()+
currency.DashDelimiter+
req.RequestFormatted.Quote.String(),
req.ExchangeInterval,
start.Add(-time.Nanosecond), // Start time not inclusive of candle.
end,
300)
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 (ok *Okx) GetHistoricCandlesExtended(ctx context.Context, pair currency.Pair, a asset.Item, interval kline.Interval, start, end time.Time) (*kline.Item, error) {
req, err := ok.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 {
var candles []CandleStick
candles, err = ok.GetCandlesticksHistory(ctx,
req.RequestFormatted.Base.String()+
currency.DashDelimiter+
req.RequestFormatted.Quote.String(),
req.ExchangeInterval,
req.RangeHolder.Ranges[y].Start.Time.Add(-time.Nanosecond), // Start time not inclusive of candle.
req.RangeHolder.Ranges[y].End.Time,
300)
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 (ok *Okx) GetAvailableTransferChains(ctx context.Context, cryptocurrency currency.Code) ([]string, error) {
currencyChains, err := ok.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 (ok *Okx) getInstrumentsForOptions(ctx context.Context) ([]Instrument, error) {
underlyings, err := ok.GetPublicUnderlyings(context.Background(), instTypeOption)
if err != nil {
return nil, err
}
var insts []Instrument
for x := range underlyings {
var instruments []Instrument
instruments, err = ok.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 (ok *Okx) getInstrumentsForAsset(ctx context.Context, a asset.Item) ([]Instrument, error) {
if !ok.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 = ok.getInstrumentsForOptions(ctx)
if err != nil {
return nil, err
}
ok.instrumentsInfoMapLock.Lock()
ok.instrumentsInfoMap[instTypeOption] = instruments
ok.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 = ok.GetInstruments(ctx, &InstrumentsFetchParams{
InstrumentType: instType,
})
if err != nil {
return nil, err
}
ok.instrumentsInfoMapLock.Lock()
ok.instrumentsInfoMap[instType] = instruments
ok.instrumentsInfoMapLock.Unlock()
return instruments, nil
}
// GetLatestFundingRates returns the latest funding rates data
func (ok *Okx) 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 := ok.GetPairFormat(r.Asset, true)
if err != nil {
return nil, err
}
fPair := r.Pair.Format(format)
pairRate := fundingrate.LatestRateResponse{
TimeChecked: time.Now(),
Exchange: ok.Name,
Asset: r.Asset,
Pair: fPair,
}
fr, err := ok.GetSingleFundingRate(ctx, fPair.String())
if err != nil {
return nil, err
}
var fri time.Duration
if len(ok.Features.Supports.FuturesCapabilities.SupportedFundingRateFrequencies) == 1 {
// can infer funding rate interval from the only funding rate frequency defined
for k := range ok.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 (ok *Okx) 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(-ok.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 := ok.GetPairFormat(r.Asset, true)
if err != nil {
return nil, err
}
fPair := r.Pair.Format(format)
pairRate := fundingrate.HistoricalRates{
Exchange: ok.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 {
if sd.Equal(r.EndDate) || sd.After(r.EndDate) {
break
}
var frh []FundingRateResponse
frh, err = ok.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 = ok.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 := ok.GetBillsDetail3Months
if time.Since(r.StartDate) < kline.OneWeek.Duration() {
billDetailsFunc = ok.GetBillsDetailLast7Days
}
for {
if sd.Equal(r.EndDate) || sd.After(r.EndDate) {
break
}
var fri time.Duration
if len(ok.Features.Supports.FuturesCapabilities.SupportedFundingRateFrequencies) == 1 {
// can infer funding rate interval from the only funding rate frequency defined
for k := range ok.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 (ok *Okx) 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 (ok *Okx) 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 (ok *Okx) 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 (ok *Okx) GetCollateralMode(ctx context.Context, item asset.Item) (collateral.Mode, error) {
if !ok.SupportsAsset(item) {
return 0, fmt.Errorf("%w: %v", asset.ErrNotSupported, item)
}
cfg, err := ok.GetAccountConfiguration(ctx)
if err != nil {
return 0, err
}
switch cfg[0].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[0].AccountLevel)
}
}
// ChangePositionMargin will modify a position/currencies margin parameters
func (ok *Okx) ChangePositionMargin(ctx context.Context, req *margin.PositionChangeRequest) (*margin.PositionChangeResponse, error) {
if req == nil {
return nil, fmt.Errorf("%w PositionChangeRequest", common.ErrNilPointer)
}
if !ok.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 := ok.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 := ok.IncreaseDecreaseMargin(ctx, r)
if err != nil {
return nil, err
}
return &margin.PositionChangeResponse{
Exchange: ok.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 (ok *Okx) 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 !ok.SupportsAsset(req.Asset) || !req.Asset.IsFutures() {
return nil, fmt.Errorf("%w %v", asset.ErrNotSupported, req.Asset)
}
fPair, err := ok.FormatExchangeCurrency(req.Pair, req.Asset)
if err != nil {
return nil, err
}
instrumentType := GetInstrumentTypeFromAssetItem(req.Asset)
var contracts []futures.Contract
contracts, err = ok.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 := ok.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 := ok.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 := ok.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 (ok *Okx) GetFuturesPositionOrders(ctx context.Context, req *futures.PositionsRequest) ([]futures.PositionResponse, error) {
if req == nil {
return nil, fmt.Errorf("%w PositionSummaryRequest", common.ErrNilPointer)
}
if !ok.SupportsAsset(req.Asset) || !req.Asset.IsFutures() {
return nil, fmt.Errorf("%w %v", asset.ErrNotSupported, req.Asset)
}
if time.Since(req.StartDate) > ok.Features.Supports.MaximumOrderHistory {
if req.RespectOrderHistoryLimits {
req.StartDate = time.Now().Add(-ok.Features.Supports.MaximumOrderHistory)
} else {
return nil, fmt.Errorf("%w max lookup %v", futures.ErrOrderHistoryTooLarge, time.Now().Add(-ok.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 = ok.GetFuturesContractDetails(ctx, req.Asset)
if err != nil {
return nil, err
}
for i := range req.Pairs {
fPair, err := ok.FormatExchangeCurrency(req.Pairs[i], req.Asset)
if err != nil {
return nil, err
}
instrumentType := GetInstrumentTypeFromAssetItem(req.Asset)
multiplier := 1.0
var contractSettlementType futures.ContractSettlementType
if req.Asset.IsFutures() {
for j := range contracts {
if !contracts[j].Name.Equal(fPair) {
continue
}
multiplier = contracts[j].Multiplier
contractSettlementType = contracts[j].SettlementType
break
}
}
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 = ok.Get7DayOrderHistory(ctx, historyRequest)
} else {
positions, err = ok.Get3MonthOrderHistory(ctx, historyRequest)
}
if err != nil {
return nil, err
}
for j := range positions {
if req.Pairs[i].String() != positions[j].InstrumentID {
continue
}
var orderStatus order.Status
orderStatus, err = order.StringToOrderStatus(strings.ToUpper(positions[j].State))
if err != nil {
log.Errorf(log.ExchangeSys, "%s %v", ok.Name, err)
}
orderSide := positions[j].Side
var oType order.Type
oType, 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: ok.Name,
OrderID: positions[j].OrderID,
ClientOrderID: positions[j].ClientOrderID,
Type: oType,
Side: orderSide,
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),
})
}
}
return resp, nil
}
// SetLeverage sets the account's initial leverage for the asset type and pair
func (ok *Okx) 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 := ok.FormatSymbol(pair, item)
if err != nil {
return err
}
marginMode := ok.marginTypeToString(marginType)
_, err = ok.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 (ok *Okx) 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 := ok.FormatSymbol(pair, item)
if err != nil {
return -1, err
}
marginMode := ok.marginTypeToString(marginType)
lev, err := ok.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 (ok *Okx) 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 := ok.GetInstruments(ctx, &InstrumentsFetchParams{
InstrumentType: instType,
})
if err != nil {
return nil, err
}
resp := make([]futures.Contract, len(result))
for i := range result {
var cp, underlying currency.Pair
underlying, err = currency.NewPairFromString(result[i].Underlying)
if err != nil {
return nil, err
}
cp, err = currency.NewPairFromString(result[i].InstrumentID)
if err != nil {
return nil, err
}
settleCurr := currency.NewCode(result[i].SettlementCurrency)
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
}
}
contractSettlementType := futures.Linear
if result[i].SettlementCurrency == result[i].BaseCurrency {
contractSettlementType = futures.Inverse
}
resp[i] = futures.Contract{
Exchange: ok.Name,
Name: cp,
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,
SettlementCurrencies: currency.Currencies{settleCurr},
MarginCurrency: settleCurr,
Multiplier: result[i].ContractValue.Float64(),
MaxLeverage: result[i].MaxLeverage.Float64(),
}
}
return resp, nil
case asset.Spread:
results, err := ok.GetPublicSpreads(ctx, "", "", "", "")
if err != nil {
return nil, err
}
resp := make([]futures.Contract, len(results))
for s := range results {
var cp currency.Pair
cp, err = currency.NewPairFromString(results[s].SpreadID)
if err != nil {
return nil, err
}
contractSettlementType, err := futures.StringToContractSettlementType(results[s].SpreadType)
if err != nil {
return nil, err
}
resp[s] = futures.Contract{
Exchange: ok.Name,
Name: cp,
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 (ok *Okx) 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 = ok.GetPublicUnderlyings(context.Background(), instTypeOption)
if err != nil {
return nil, err
}
for u := range underlyings {
var incOID []OpenInterest
incOID, err = ok.GetOpenInterestData(ctx, instType, underlyings[u], "", "")
if err != nil {
return nil, err
}
oid = append(oid, incOID...)
}
case instTypeSwap,
instTypeFutures:
oid, err = ok.GetOpenInterestData(ctx, instType, "", "", "")
if err != nil {
return nil, err
}
}
for j := range oid {
var isEnabled bool
var p currency.Pair
p, isEnabled, err = ok.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.ExchangePairAsset{
Exchange: ok.Name,
Base: p.Base.Item,
Quote: p.Quote.Item,
Asset: v,
},
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 := ok.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 = ok.GetPublicUnderlyings(context.Background(), instTypeOption)
if err != nil {
return nil, err
}
for u := range underlyings {
var incOID []OpenInterest
incOID, err = ok.GetOpenInterestData(ctx, instTypes[k[0].Asset], underlyings[u], "", "")
if err != nil {
return nil, err
}
oid = append(oid, incOID...)
}
case instTypeSwap, instTypeFutures:
oid, err = ok.GetOpenInterestData(ctx, instTypes[k[0].Asset], "", "", pFmt)
if err != nil {
return nil, err
}
}
for i := range oid {
p, isEnabled, err := ok.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.ExchangePairAsset{
Exchange: ok.Name,
Base: p.Base.Item,
Quote: p.Quote.Item,
Asset: k[0].Asset,
},
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 (ok *Okx) GetCurrencyTradeURL(ctx context.Context, a asset.Item, cp currency.Pair) (string, error) {
_, err := ok.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 = ok.FormatExchangeCurrency(cp, a)
if err != nil {
return "", err
}
insts, err := ok.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 %v", asset.ErrNotSupported, a)
}
}
func (ok *Okx) underlyingFromInstID(instrumentType, instID string) (string, error) {
ok.instrumentsInfoMapLock.Lock()
defer ok.instrumentsInfoMapLock.Unlock()
if instrumentType != "" {
insts, okay := ok.instrumentsInfoMap[instrumentType]
if !okay {
return "", errInvalidInstrumentType
}
for a := range insts {
if insts[a].InstrumentID == instID {
return insts[a].Underlying, nil
}
}
} else {
for _, insts := range ok.instrumentsInfoMap {
for a := range insts {
if insts[a].InstrumentID == instID {
return insts[a].Underlying, nil
}
}
}
}
return "", fmt.Errorf("underlying not found for instrument %s", instID)
}