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 := ¤cy.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 ok.WsResponseMultiplexer = wsRequestDataChannelsMultiplexer{ WsResponseChannelsMap: make(map[string]*wsRequestInfo), Register: make(chan *wsRequestInfo), Unregister: make(chan string), Message: make(chan *wsIncomingData), shutdown: make(chan bool), } } // 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 } 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) }