mirror of
https://github.com/d0zingcat/gocryptotrader.git
synced 2026-05-13 23:16:45 +00:00
* public endpoints methods added * Completing mapping REST endpoints * Binanceus Wrapper methods -Partially * Binanceus Wrapper methods -Partially * BinaWra functions with test funs; Not Completed * Finalizing wrapper methods & test * Fix & Complete wrapper functions * Finalizing wrapper methods & test * Adding Stream Datas * WS Test functions * CI: Fix golangci-lint linter issues * CI: Fix reverting unnessesary changes and type conversion issues * CI: Fix reverting unnessesary changes and type conversion issues * Adding Public endpoints and tests * Adding Market and Public Endpoints * Adding Public endoints * Public Trading Endpoints & Authenticated Trade order methods * Adding Authenticated Methods and Tests * Adding algo and Funding Authenticated endpoints * Adding funding trading endpoints and correspondint tests * adding authenticated endpoints * Completing Block Trading endpoints and added subaccount endpoints * Completing sub account and grid Trading endpoints * Adding Rate Limit and missing endpoints * Wrapper and Websocket handlers * Fixing Websocket Test and Push Data Handler Issues * Fixing Websocket Test and Push Data Handler Issues * Fixing linter issues, package dependency, and other slight tempos * Fixing linter and slight tempos * Update on test functions, and Rest and Websocket Endpoint handlers * Remove okex, adding comments, and slight fixes on endpoints. * Fixing linter issues and adding comments * Slight code changes, updating documentation, and n and linter issues * Fix context and configuration endpoint issues * slight fixes on config and test files * adding some missing test and fix linter issues * fix linter issue * Slight fixes on code structure, shorthand exp,and ot and other * Fix slight linter issue * Slight code fixes and fixing linter issues * fixing linter iissues * fixing linter iissues * slight linter issue fix * slight linter issue fix * Fix on models, type convert funcs and endpoints * Adding Error messages map and update of models * Fix on error message string and linter issues * Fix slight linter issue * Fix slight linter issue * Fixing type converts, models, and linter issues * Adding Ws fixes * Slight fix on websocket and other issues * Adding slight websocket fixes * Remove 'books5' channel default subscription * Small changes on default subscription and checksum * Fix slight websocket tempos * Fix Wrapper function tempost and linter issues * Resolving slight naming and other issues * Resolve slight pointer issues * resolve slight linter issues * Resolve config files issue * Update websocket and wrapper funcs with test and docs * fixs on websocket multiplexer, types, and other slight issues * fix slight linter issues * slight update on web-socket orderbook and tickers * fix slight issues and websocket runtime errors * Slight unit test fix and assing simple semaphore * FIx race issue * Update on authenticated endpoints * Fix wsSetupRun check in websocket 'setupWsAuth' func * Update wsSetupRun check in websocket 'setupWsAuth' func * Slight update on websocket handling * Fix some race conditions * fix slight tempos * fix authenticated test issues * Update on conditional statements * slight update on unit test * fix unit test tempos * Fix slight tempos * Change check map from struct valued to bool valued * slight fix trial * Slight unit test update * Fix websocket timeout error * Updating websocket subscription endpoints, and unit tests * update unit tests * Slight issue on wrapper method 'GetActiveOrders' * Overall code update * Addressing missing review comments * Fix unit test tempo and linter issue * Monor fix * Slight update * Slight unit test fix * Slight fixes * Slight fixes * Fixing on missing review comments * Adding WS Fixes * slight fix * Monor fix on unit test * Minor convert issue * Minor change on WS * Monor logic fix * Fix code structure and logic issues * Fixing small typos * fix slight data format issue * Update on trade and order wrapper methods * Adding slight update * fix on order detail * Slight update on FetchTradablePairs wrapper method * Slight update on wrapper * Update on deserialization and other slight issues * Final update * Resolve missing review comments * Slight update on config and unit test * minor fix on GetDepositAddress param * Minor fix
1472 lines
45 KiB
Go
1472 lines
45 KiB
Go
package okx
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"fmt"
|
|
"math"
|
|
"sort"
|
|
"strconv"
|
|
"strings"
|
|
"sync"
|
|
"time"
|
|
|
|
"github.com/thrasher-corp/gocryptotrader/common"
|
|
"github.com/thrasher-corp/gocryptotrader/config"
|
|
"github.com/thrasher-corp/gocryptotrader/currency"
|
|
exchange "github.com/thrasher-corp/gocryptotrader/exchanges"
|
|
"github.com/thrasher-corp/gocryptotrader/exchanges/account"
|
|
"github.com/thrasher-corp/gocryptotrader/exchanges/asset"
|
|
"github.com/thrasher-corp/gocryptotrader/exchanges/deposit"
|
|
"github.com/thrasher-corp/gocryptotrader/exchanges/kline"
|
|
"github.com/thrasher-corp/gocryptotrader/exchanges/order"
|
|
"github.com/thrasher-corp/gocryptotrader/exchanges/orderbook"
|
|
"github.com/thrasher-corp/gocryptotrader/exchanges/protocol"
|
|
"github.com/thrasher-corp/gocryptotrader/exchanges/request"
|
|
"github.com/thrasher-corp/gocryptotrader/exchanges/stream"
|
|
"github.com/thrasher-corp/gocryptotrader/exchanges/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 (
|
|
okxWebsocketResponseMaxLimit = time.Second * 3
|
|
)
|
|
|
|
// GetDefaultConfig returns a default exchange config
|
|
func (ok *Okx) GetDefaultConfig() (*config.Exchange, error) {
|
|
ok.SetDefaults()
|
|
exchCfg := new(config.Exchange)
|
|
exchCfg.Name = ok.Name
|
|
exchCfg.HTTPTimeout = exchange.DefaultHTTPTimeout
|
|
exchCfg.BaseCurrencies = ok.BaseCurrencies
|
|
|
|
err := ok.Setup(exchCfg)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
if ok.Features.Supports.RESTCapabilities.AutoPairUpdates {
|
|
err := ok.UpdateTradablePairs(context.TODO(), false)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
return exchCfg, nil
|
|
}
|
|
|
|
// 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
|
|
|
|
fmt1 := currency.PairStore{
|
|
RequestFormat: ¤cy.PairFormat{Uppercase: true},
|
|
ConfigFormat: ¤cy.PairFormat{
|
|
Delimiter: currency.DashDelimiter,
|
|
Uppercase: true,
|
|
},
|
|
}
|
|
|
|
err := ok.SetGlobalPairsManager(fmt1.RequestFormat, fmt1.ConfigFormat, asset.Spot, asset.Futures, asset.PerpetualSwap, asset.Option, asset.Margin)
|
|
if err != nil {
|
|
log.Errorln(log.ExchangeSys, err)
|
|
}
|
|
|
|
// Fill out the capabilities/features that the exchange supports
|
|
ok.Features = exchange.Features{
|
|
Supports: exchange.FeaturesSupported{
|
|
REST: true,
|
|
Websocket: true,
|
|
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,
|
|
},
|
|
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,
|
|
},
|
|
Enabled: exchange.FeaturesEnabled{
|
|
AutoPairUpdates: true,
|
|
Kline: kline.ExchangeCapabilitiesEnabled{
|
|
Intervals: map[string]bool{
|
|
kline.OneMin.Word(): true,
|
|
kline.ThreeMin.Word(): true,
|
|
kline.FiveMin.Word(): true,
|
|
kline.FifteenMin.Word(): true,
|
|
kline.ThirtyMin.Word(): true,
|
|
kline.OneHour.Word(): true,
|
|
kline.TwoHour.Word(): true,
|
|
kline.FourHour.Word(): true,
|
|
kline.SixHour.Word(): true,
|
|
kline.TwelveHour.Word(): true,
|
|
kline.OneDay.Word(): true,
|
|
kline.ThreeDay.Word(): true,
|
|
kline.OneWeek.Word(): true,
|
|
kline.OneMonth.Word(): true,
|
|
kline.ThreeMonth.Word(): true,
|
|
kline.SixMonth.Word(): true,
|
|
kline.OneYear.Word(): true,
|
|
},
|
|
ResultLimit: 300,
|
|
},
|
|
},
|
|
}
|
|
ok.Requester, err = request.New(ok.Name,
|
|
common.NewHTTPClientWithTimeout(exchange.DefaultHTTPTimeout),
|
|
request.WithLimiter(SetRateLimit()))
|
|
if err != nil {
|
|
log.Errorln(log.ExchangeSys, err)
|
|
}
|
|
|
|
ok.API.Endpoints = ok.NewEndpoints()
|
|
err = ok.API.Endpoints.SetDefaultEndpoints(map[exchange.URL]string{
|
|
exchange.RestSpot: okxAPIURL,
|
|
exchange.WebsocketSpot: okxAPIWebsocketPublicURL,
|
|
})
|
|
if err != nil {
|
|
log.Errorln(log.ExchangeSys, err)
|
|
}
|
|
|
|
ok.Websocket = stream.New()
|
|
ok.WebsocketResponseMaxLimit = okxWebsocketResponseMaxLimit
|
|
ok.WebsocketResponseCheckTimeout = okxWebsocketResponseMaxLimit
|
|
ok.WebsocketOrderbookBufferLimit = exchange.DefaultWebsocketOrderbookBufferLimit
|
|
}
|
|
|
|
// Setup takes in the supplied exchange configuration details and sets params
|
|
func (ok *Okx) Setup(exch *config.Exchange) error {
|
|
err := exch.Validate()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if !exch.Enabled {
|
|
ok.SetEnabled(false)
|
|
return nil
|
|
}
|
|
err = ok.SetupDefaults(exch)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
wsRunningEndpoint, err := ok.API.Endpoints.GetURL(exchange.WebsocketSpot)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
err = ok.Websocket.Setup(&stream.WebsocketSetup{
|
|
ExchangeConfig: exch,
|
|
DefaultURL: okxAPIWebsocketPublicURL,
|
|
RunningURL: wsRunningEndpoint,
|
|
Connector: ok.WsConnect,
|
|
Subscriber: ok.Subscribe,
|
|
Unsubscriber: ok.Unsubscribe,
|
|
GenerateSubscriptions: ok.GenerateDefaultSubscriptions,
|
|
Features: &ok.Features.Supports.WebsocketCapabilities,
|
|
OrderbookBufferConfig: buffer.Config{
|
|
Checksum: ok.CalculateUpdateOrderbookChecksum,
|
|
},
|
|
})
|
|
|
|
if err != nil {
|
|
return err
|
|
}
|
|
err = ok.Websocket.SetupNewConnection(stream.ConnectionSetup{
|
|
URL: okxAPIWebsocketPublicURL,
|
|
ResponseCheckTimeout: exch.WebsocketResponseCheckTimeout,
|
|
ResponseMaxLimit: okxWebsocketResponseMaxLimit,
|
|
RateLimit: 500,
|
|
})
|
|
if err != nil {
|
|
return err
|
|
}
|
|
return ok.Websocket.SetupNewConnection(stream.ConnectionSetup{
|
|
URL: okxAPIWebsocketPrivateURL,
|
|
ResponseCheckTimeout: exch.WebsocketResponseCheckTimeout,
|
|
ResponseMaxLimit: okxWebsocketResponseMaxLimit,
|
|
Authenticated: true,
|
|
RateLimit: 500,
|
|
})
|
|
}
|
|
|
|
// Start starts the Okx go routine
|
|
func (ok *Okx) Start(wg *sync.WaitGroup) error {
|
|
if wg == nil {
|
|
return fmt.Errorf("%T %w", wg, common.ErrNilPointer)
|
|
}
|
|
wg.Add(1)
|
|
go func() {
|
|
ok.Run()
|
|
wg.Done()
|
|
}()
|
|
return nil
|
|
}
|
|
|
|
// Run implements the Okx wrapper
|
|
func (ok *Okx) Run() {
|
|
if ok.Verbose {
|
|
log.Debugf(log.ExchangeSys,
|
|
"%s Websocket: %s.",
|
|
ok.Name,
|
|
common.IsEnabled(ok.Websocket.IsEnabled()))
|
|
ok.PrintEnabledPairs()
|
|
}
|
|
|
|
if !ok.GetEnabledFeatures().AutoPairUpdates {
|
|
return
|
|
}
|
|
err := ok.UpdateTradablePairs(context.TODO(), false)
|
|
if err != nil {
|
|
log.Errorf(log.ExchangeSys,
|
|
"%s failed to update tradable pairs. Err: %s",
|
|
ok.Name,
|
|
err)
|
|
}
|
|
}
|
|
|
|
// GetServerTime returns the current exchange server time.
|
|
func (ok *Okx) GetServerTime(ctx context.Context, ai asset.Item) (time.Time, error) {
|
|
return ok.GetSystemTime(ctx)
|
|
}
|
|
|
|
// FetchTradablePairs returns a list of the exchanges tradable pairs
|
|
func (ok *Okx) FetchTradablePairs(ctx context.Context, a asset.Item) ([]string, error) {
|
|
if !ok.SupportsAsset(a) {
|
|
return nil, fmt.Errorf("asset type of %s is not supported by %s", a, ok.Name)
|
|
}
|
|
format, err := ok.GetPairFormat(a, false)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
insts := []Instrument{}
|
|
switch a {
|
|
case asset.Spot:
|
|
insts, err = ok.GetInstruments(ctx, &InstrumentsFetchParams{
|
|
InstrumentType: okxInstTypeSpot,
|
|
})
|
|
case asset.Futures:
|
|
insts, err = ok.GetInstruments(ctx, &InstrumentsFetchParams{
|
|
InstrumentType: okxInstTypeFutures,
|
|
})
|
|
case asset.PerpetualSwap:
|
|
insts, err = ok.GetInstruments(ctx, &InstrumentsFetchParams{
|
|
InstrumentType: okxInstTypeSwap,
|
|
})
|
|
case asset.Option:
|
|
var underlyings []string
|
|
underlyings, err = ok.GetPublicUnderlyings(context.Background(), okxInstTypeOption)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
var instruments []Instrument
|
|
for x := range underlyings {
|
|
instruments, err = ok.GetInstruments(ctx, &InstrumentsFetchParams{
|
|
InstrumentType: okxInstTypeOption,
|
|
Underlying: underlyings[x],
|
|
})
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
insts = append(insts, instruments...)
|
|
}
|
|
case asset.Margin:
|
|
insts, err = ok.GetInstruments(ctx, &InstrumentsFetchParams{
|
|
InstrumentType: okxInstTypeMargin,
|
|
})
|
|
}
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if len(insts) == 0 {
|
|
return nil, errNoInstrumentFound
|
|
}
|
|
pairs := make([]string, len(insts))
|
|
for x := range insts {
|
|
c, err := currency.NewPairFromString(insts[x].InstrumentID)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
pairs[x] = format.Format(c)
|
|
}
|
|
return pairs, nil
|
|
}
|
|
|
|
// 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(false)
|
|
for i := range assetTypes {
|
|
p, err := ok.FetchTradablePairs(ctx, assetTypes[i])
|
|
if err != nil {
|
|
return err
|
|
}
|
|
pairs, err := currency.NewPairsFromStrings(p)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
err = ok.UpdatePairs(pairs, assetTypes[i], false, forceUpdate)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// 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) {
|
|
format, err := ok.GetPairFormat(a, false)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if !p.IsPopulated() {
|
|
return nil, errIncompleteCurrencyPair
|
|
}
|
|
instrumentID := format.Format(p)
|
|
if !ok.SupportsAsset(a) {
|
|
return nil, fmt.Errorf("%w: %v", asset.ErrNotSupported, a)
|
|
}
|
|
mdata, err := ok.GetTicker(ctx, instrumentID)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
var baseVolume float64
|
|
var quoteVolume float64
|
|
switch a {
|
|
case asset.Spot, asset.Margin:
|
|
baseVolume = mdata.Vol24H.Float64()
|
|
quoteVolume = mdata.VolCcy24H.Float64()
|
|
case asset.PerpetualSwap, asset.Futures, asset.Option:
|
|
baseVolume = mdata.VolCcy24H.Float64()
|
|
quoteVolume = mdata.Vol24H.Float64()
|
|
default:
|
|
return nil, fmt.Errorf("%w, asset type %s is not supported", errInvalidInstrumentType, a.String())
|
|
}
|
|
err = ticker.ProcessTicker(&ticker.Price{
|
|
Last: mdata.LastTradePrice.Float64(),
|
|
High: mdata.High24H.Float64(),
|
|
Low: mdata.Low24H.Float64(),
|
|
Bid: mdata.BidPrice.Float64(),
|
|
Ask: mdata.BestAskPrice.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 {
|
|
pairs, err := ok.GetEnabledPairs(assetType)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
instrumentType := ok.GetInstrumentTypeFromAssetItem(assetType)
|
|
ticks, err := ok.GetTickers(ctx, strings.ToUpper(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].BidPrice.Float64(),
|
|
Ask: ticks[y].BestAskPrice.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
|
|
}
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// FetchTicker returns the ticker for a currency pair
|
|
func (ok *Okx) FetchTicker(ctx context.Context, p currency.Pair, assetType asset.Item) (*ticker.Price, error) {
|
|
formattedPair, err := ok.FormatExchangeCurrency(p, assetType)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
tickerNew, err := ticker.GetTicker(ok.Name, formattedPair, assetType)
|
|
if err != nil {
|
|
return ok.UpdateTicker(ctx, p, assetType)
|
|
}
|
|
return tickerNew, nil
|
|
}
|
|
|
|
// FetchOrderbook returns orderbook base on the currency pair
|
|
func (ok *Okx) FetchOrderbook(ctx context.Context, pair currency.Pair, assetType asset.Item) (*orderbook.Base, error) {
|
|
ob, err := orderbook.Get(ok.Name, pair, assetType)
|
|
if err != nil {
|
|
return ok.UpdateOrderbook(ctx, pair, assetType)
|
|
}
|
|
return ob, 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) {
|
|
book := &orderbook.Base{
|
|
Exchange: ok.Name,
|
|
Pair: pair,
|
|
Asset: assetType,
|
|
VerifyOrderbook: ok.CanVerifyOrderbook,
|
|
}
|
|
var orderbookNew *OrderBookResponse
|
|
var err error
|
|
if !ok.SupportsAsset(assetType) {
|
|
return nil, fmt.Errorf("%w: %v", asset.ErrNotSupported, assetType)
|
|
}
|
|
var instrumentID string
|
|
format, err := ok.GetPairFormat(assetType, false)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if !pair.IsPopulated() {
|
|
return nil, errIncompleteCurrencyPair
|
|
}
|
|
instrumentID = format.Format(pair)
|
|
orderbookNew, err = ok.GetOrderBookDepth(ctx, instrumentID, 0)
|
|
if err != nil {
|
|
return book, err
|
|
}
|
|
|
|
orderBookD, err := orderbookNew.GetOrderBookResponseDetail()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
book.Bids = make(orderbook.Items, len(orderBookD.Bids))
|
|
for x := range orderBookD.Bids {
|
|
book.Bids[x] = orderbook.Item{
|
|
Amount: orderBookD.Bids[x].BaseCurrencies,
|
|
Price: orderBookD.Bids[x].DepthPrice,
|
|
}
|
|
}
|
|
book.Asks = make(orderbook.Items, len(orderBookD.Asks))
|
|
for x := range orderBookD.Asks {
|
|
book.Asks[x] = orderbook.Item{
|
|
Amount: orderBookD.Asks[x].NumberOfContracts,
|
|
Price: orderBookD.Asks[x].DepthPrice,
|
|
}
|
|
}
|
|
err = book.Process()
|
|
if err != nil {
|
|
return book, err
|
|
}
|
|
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) {
|
|
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)
|
|
}
|
|
balances, err := ok.GetBalance(ctx, "")
|
|
if err != nil {
|
|
return info, err
|
|
}
|
|
currencyBalance := make([]account.Balance, len(balances))
|
|
for i := range balances {
|
|
free := balances[i].AvailBal
|
|
locked := balances[i].FrozenBalance
|
|
currencyBalance[i] = account.Balance{
|
|
CurrencyName: currency.NewCode(balances[i].Currency),
|
|
Total: balances[i].Balance,
|
|
Hold: locked,
|
|
Free: free,
|
|
}
|
|
}
|
|
acc.Currencies = currencyBalance
|
|
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
|
|
}
|
|
|
|
// FetchAccountInfo retrieves balances for all enabled currencies
|
|
func (ok *Okx) FetchAccountInfo(ctx context.Context, assetType asset.Item) (account.Holdings, error) {
|
|
creds, err := ok.GetCredentials(ctx)
|
|
if err != nil {
|
|
return account.Holdings{}, err
|
|
}
|
|
acc, err := account.GetHoldings(ok.Name, creds, assetType)
|
|
if err != nil {
|
|
return ok.UpdateAccountInfo(ctx, assetType)
|
|
}
|
|
return acc, nil
|
|
}
|
|
|
|
// GetFundingHistory returns funding history, deposits and withdrawals
|
|
func (ok *Okx) GetFundingHistory(ctx context.Context) ([]exchange.FundHistory, error) {
|
|
depositHistories, err := ok.GetCurrencyDepositHistory(ctx, "", "", "", time.Time{}, time.Time{}, -1, 0)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
withdrawalHistories, err := ok.GetWithdrawalHistory(ctx, "", "", "", "", "", time.Time{}, time.Time{}, -5)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
resp := make([]exchange.FundHistory, 0, len(depositHistories)+len(withdrawalHistories))
|
|
for x := range depositHistories {
|
|
resp = append(resp, exchange.FundHistory{
|
|
ExchangeName: ok.Name,
|
|
Status: strconv.Itoa(depositHistories[x].State),
|
|
Timestamp: depositHistories[x].Timestamp.Time(),
|
|
Currency: depositHistories[x].Currency,
|
|
Amount: depositHistories[x].Amount,
|
|
TransferType: "deposit",
|
|
CryptoToAddress: depositHistories[x].ToDepositAddress,
|
|
CryptoTxID: depositHistories[x].TransactionID,
|
|
})
|
|
}
|
|
for x := range withdrawalHistories {
|
|
resp = append(resp, exchange.FundHistory{
|
|
ExchangeName: ok.Name,
|
|
Status: withdrawalHistories[x].StateOfWithdrawal,
|
|
Timestamp: withdrawalHistories[x].Timestamp.Time(),
|
|
Currency: withdrawalHistories[x].Currency,
|
|
Amount: withdrawalHistories[x].Amount,
|
|
TransferType: "withdrawal",
|
|
CryptoToAddress: withdrawalHistories[x].ToReceivingAddress,
|
|
CryptoTxID: withdrawalHistories[x].TransactionID,
|
|
TransferID: withdrawalHistories[x].WithdrawalID,
|
|
Fee: withdrawalHistories[x].WithdrawalFee,
|
|
CryptoChain: withdrawalHistories[x].ChainName,
|
|
})
|
|
}
|
|
return resp, nil
|
|
}
|
|
|
|
// GetWithdrawalsHistory returns previous withdrawals data
|
|
func (ok *Okx) GetWithdrawalsHistory(ctx context.Context, c currency.Code, _ asset.Item) (resp []exchange.WithdrawalHistory, err error) {
|
|
withdrawals, err := ok.GetWithdrawalHistory(ctx, c.String(), "", "", "", "", 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,
|
|
TransferType: "withdrawal",
|
|
CryptoToAddress: withdrawals[x].ToReceivingAddress,
|
|
CryptoTxID: withdrawals[x].TransactionID,
|
|
CryptoChain: withdrawals[x].ChainName,
|
|
TransferID: withdrawals[x].WithdrawalID,
|
|
Fee: withdrawals[x].WithdrawalFee,
|
|
})
|
|
}
|
|
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, false)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if !p.IsPopulated() {
|
|
return nil, errIncompleteCurrencyPair
|
|
}
|
|
instrumentID := format.Format(p)
|
|
tradeData, err := ok.GetTrades(ctx, instrumentID, 1000)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
resp := make([]trade.Data, len(tradeData))
|
|
var side order.Side
|
|
for x := range tradeData {
|
|
side, err = order.StringToOrderSide(tradeData[x].Side)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
resp[x] = trade.Data{
|
|
TID: tradeData[x].TradeID,
|
|
Exchange: ok.Name,
|
|
CurrencyPair: p,
|
|
AssetType: assetType,
|
|
Side: side,
|
|
Price: tradeData[x].Price,
|
|
Amount: tradeData[x].Quantity,
|
|
Timestamp: tradeData[x].Timestamp.Time(),
|
|
}
|
|
}
|
|
if ok.IsSaveTradeDataEnabled() {
|
|
err = trade.AddTradesToBuffer(ok.Name, resp...)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
sort.Sort(trade.ByDate(resp))
|
|
return resp, nil
|
|
}
|
|
|
|
// GetHistoricTrades retrives 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
|
|
format, err := ok.GetPairFormat(assetType, false)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if !p.IsPopulated() {
|
|
return nil, errIncompleteCurrencyPair
|
|
}
|
|
var resp []trade.Data
|
|
instrumentID := format.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 := 0; i < len(trades); i++ {
|
|
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
|
|
}
|
|
var tradeSide order.Side
|
|
tradeSide, err = order.StringToOrderSide(trades[i].Side)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
resp = append(resp, trade.Data{
|
|
TID: trades[i].TradeID,
|
|
Exchange: ok.Name,
|
|
CurrencyPair: p,
|
|
AssetType: assetType,
|
|
Price: trades[i].Price,
|
|
Amount: trades[i].Quantity,
|
|
Timestamp: trades[i].Timestamp.Time(),
|
|
Side: tradeSide,
|
|
})
|
|
}
|
|
tradeIDEnd = trades[len(trades)-1].TradeID
|
|
}
|
|
if ok.IsSaveTradeDataEnabled() {
|
|
err = trade.AddTradesToBuffer(ok.Name, 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 err := s.Validate(); err != nil {
|
|
return nil, err
|
|
}
|
|
if !ok.SupportsAsset(s.AssetType) {
|
|
return nil, fmt.Errorf("%w: %v", asset.ErrNotSupported, s.AssetType)
|
|
}
|
|
if s.Amount <= 0 {
|
|
return nil, fmt.Errorf("amount, or size (sz) of quantity to buy or sell hast to be greater than zero ")
|
|
}
|
|
format, err := ok.GetPairFormat(s.AssetType, false)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if !s.Pair.IsPopulated() {
|
|
return nil, errIncompleteCurrencyPair
|
|
}
|
|
instrumentID := format.Format(s.Pair)
|
|
var tradeMode string
|
|
if s.AssetType != asset.Margin {
|
|
tradeMode = "cash"
|
|
}
|
|
var sideType string
|
|
if s.Side.IsLong() {
|
|
sideType = order.Buy.Lower()
|
|
} else {
|
|
sideType = order.Sell.Lower()
|
|
}
|
|
var orderRequest = &PlaceOrderRequestParam{
|
|
InstrumentID: instrumentID,
|
|
TradeMode: tradeMode,
|
|
Side: sideType,
|
|
OrderType: s.Type.Lower(),
|
|
Amount: s.Amount,
|
|
ClientSupplierOrderID: s.ClientOrderID,
|
|
Price: s.Price,
|
|
}
|
|
switch s.Type.Lower() {
|
|
case OkxOrderLimit, OkxOrderPostOnly, OkxOrderFOK, OkxOrderIOC:
|
|
orderRequest.Price = s.Price
|
|
}
|
|
var placeOrderResponse *OrderData
|
|
if s.AssetType == asset.PerpetualSwap || s.AssetType == asset.Futures {
|
|
if s.Type.Lower() == "" {
|
|
orderRequest.OrderType = OkxOrderOptimalLimitIOC
|
|
}
|
|
// 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(orderRequest)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
} else {
|
|
placeOrderResponse, err = ok.PlaceOrder(ctx, orderRequest, s.AssetType)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return s.DeriveSubmitResponse(placeOrderResponse.OrderID)
|
|
}
|
|
|
|
// 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.Mod(action.Amount, 1) != 0 {
|
|
return nil, errors.New("okx contract amount can not be decimal")
|
|
}
|
|
format, err := ok.GetPairFormat(action.AssetType, false)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if !action.Pair.IsPopulated() {
|
|
return nil, errIncompleteCurrencyPair
|
|
}
|
|
instrumentID := format.Format(action.Pair)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
amendRequest := AmendOrderRequestParams{
|
|
InstrumentID: instrumentID,
|
|
NewQuantity: action.Amount,
|
|
OrderID: action.OrderID,
|
|
ClientSuppliedOrderID: action.ClientOrderID,
|
|
}
|
|
if ok.Websocket.CanUseAuthenticatedWebsocketForWrapper() {
|
|
_, err = ok.WsAmendOrder(&amendRequest)
|
|
} else {
|
|
_, err = ok.AmendOrder(ctx, &amendRequest)
|
|
}
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return action.DeriveModifyResponse()
|
|
}
|
|
|
|
// CancelOrder cancels an order by its corresponding ID number
|
|
func (ok *Okx) CancelOrder(ctx context.Context, ord *order.Cancel) error {
|
|
if err := ord.Validate(ord.StandardCancel()); err != nil {
|
|
return err
|
|
}
|
|
if !ok.SupportsAsset(ord.AssetType) {
|
|
return fmt.Errorf("%w: %v", asset.ErrNotSupported, ord.AssetType)
|
|
}
|
|
format, err := ok.GetPairFormat(ord.AssetType, false)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if !ord.Pair.IsPopulated() {
|
|
return errIncompleteCurrencyPair
|
|
}
|
|
instrumentID := format.Format(ord.Pair)
|
|
req := CancelOrderRequestParam{
|
|
InstrumentID: instrumentID,
|
|
OrderID: ord.OrderID,
|
|
ClientSupplierOrderID: ord.ClientOrderID,
|
|
}
|
|
if ok.Websocket.CanUseAuthenticatedWebsocketForWrapper() {
|
|
_, err = ok.WsCancelOrder(req)
|
|
} else {
|
|
_, err = ok.CancelSingleOrder(ctx, req)
|
|
}
|
|
return err
|
|
}
|
|
|
|
// CancelBatchOrders cancels orders by their corresponding ID numbers
|
|
func (ok *Okx) CancelBatchOrders(ctx context.Context, orders []order.Cancel) (order.CancelBatchResponse, error) {
|
|
var cancelBatchResponse order.CancelBatchResponse
|
|
if len(orders) > 20 {
|
|
return cancelBatchResponse, fmt.Errorf("%w, cannot cancel more than 20 orders", errExceedLimit)
|
|
} else if len(orders) == 0 {
|
|
return cancelBatchResponse, fmt.Errorf("%w, must have at least 1 cancel order", errNoOrderParameterPassed)
|
|
}
|
|
cancelOrderParams := []CancelOrderRequestParam{}
|
|
var err error
|
|
for x := range orders {
|
|
ord := orders[x]
|
|
err = ord.Validate(ord.StandardCancel())
|
|
if err != nil {
|
|
return cancelBatchResponse, err
|
|
}
|
|
if !ok.SupportsAsset(ord.AssetType) {
|
|
return cancelBatchResponse, fmt.Errorf("%w: %v", asset.ErrNotSupported, ord.AssetType)
|
|
}
|
|
var instrumentID string
|
|
var format currency.PairFormat
|
|
format, err = ok.GetPairFormat(ord.AssetType, false)
|
|
if err != nil {
|
|
return cancelBatchResponse, err
|
|
}
|
|
if !ord.Pair.IsPopulated() {
|
|
return cancelBatchResponse, errIncompleteCurrencyPair
|
|
}
|
|
instrumentID = format.Format(ord.Pair)
|
|
if err != nil {
|
|
return cancelBatchResponse, err
|
|
}
|
|
req := CancelOrderRequestParam{
|
|
InstrumentID: instrumentID,
|
|
OrderID: ord.OrderID,
|
|
ClientSupplierOrderID: ord.ClientOrderID,
|
|
}
|
|
cancelOrderParams = append(cancelOrderParams, req)
|
|
}
|
|
var canceledOrders []OrderData
|
|
if ok.Websocket.CanUseAuthenticatedWebsocketForWrapper() {
|
|
canceledOrders, err = ok.WsCancelMultipleOrder(cancelOrderParams)
|
|
} else {
|
|
canceledOrders, err = ok.CancelMultipleOrders(ctx, cancelOrderParams)
|
|
}
|
|
if err != nil {
|
|
return cancelBatchResponse, err
|
|
}
|
|
for x := range canceledOrders {
|
|
cancelBatchResponse.Status[canceledOrders[x].OrderID] = func() string {
|
|
if canceledOrders[x].SCode != "0" && canceledOrders[x].SCode != "2" {
|
|
return ""
|
|
}
|
|
return order.Cancelled.String()
|
|
}()
|
|
}
|
|
return cancelBatchResponse, 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{},
|
|
}
|
|
var instrumentType string
|
|
if orderCancellation.AssetType.IsValid() {
|
|
err = ok.CurrencyPairs.IsAssetEnabled(orderCancellation.AssetType)
|
|
if err != nil {
|
|
return order.CancelAllResponse{}, err
|
|
}
|
|
instrumentType = ok.GetInstrumentTypeFromAssetItem(orderCancellation.AssetType)
|
|
}
|
|
var oType string
|
|
if orderCancellation.Type != order.UnknownType && orderCancellation.Type != order.AnyType {
|
|
oType, err = ok.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].ClientSupplierOrderID == orderCancellation.ClientOrderID {
|
|
cancelAllOrdersRequestParams[x] = CancelOrderRequestParam{
|
|
OrderID: myOrders[x].OrderID,
|
|
ClientSupplierOrderID: myOrders[x].ClientSupplierOrderID,
|
|
}
|
|
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,
|
|
ClientSupplierOrderID: myOrders[x].ClientSupplierOrderID,
|
|
}
|
|
continue
|
|
}
|
|
default:
|
|
cancelAllOrdersRequestParams[x] = CancelOrderRequestParam{
|
|
OrderID: myOrders[x].OrderID,
|
|
ClientSupplierOrderID: myOrders[x].ClientSupplierOrderID,
|
|
}
|
|
}
|
|
}
|
|
remaining := cancelAllOrdersRequestParams
|
|
loop := int(math.Ceil(float64(len(remaining)) / 20.0))
|
|
for b := 0; b < loop; b++ {
|
|
var response []OrderData
|
|
if len(remaining) > 20 {
|
|
if ok.Websocket.CanUseAuthenticatedWebsocketForWrapper() {
|
|
response, err = ok.WsCancelMultipleOrder(remaining[:20])
|
|
} else {
|
|
response, err = ok.CancelMultipleOrders(ctx, remaining[:20])
|
|
}
|
|
remaining = remaining[20:]
|
|
} else {
|
|
if ok.Websocket.CanUseAuthenticatedWebsocketForWrapper() {
|
|
response, err = ok.WsCancelMultipleOrder(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].SCode == "0" {
|
|
cancelAllResponse.Status[response[y].OrderID] = order.Cancelled.String()
|
|
} else {
|
|
cancelAllResponse.Status[response[y].OrderID] = response[y].SMessage
|
|
}
|
|
}
|
|
}
|
|
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) {
|
|
var respData order.Detail
|
|
format, err := ok.GetPairFormat(assetType, false)
|
|
if err != nil {
|
|
return respData, err
|
|
}
|
|
if !pair.IsPopulated() {
|
|
return respData, errIncompleteCurrencyPair
|
|
}
|
|
instrumentID := format.Format(pair)
|
|
if !ok.SupportsAsset(assetType) {
|
|
return respData, fmt.Errorf("%w: %v", asset.ErrNotSupported, assetType)
|
|
}
|
|
orderDetail, err := ok.GetOrderDetail(ctx, &OrderDetailRequestParam{
|
|
InstrumentID: instrumentID,
|
|
OrderID: orderID,
|
|
})
|
|
if err != nil {
|
|
return respData, err
|
|
}
|
|
status, err := order.StringToOrderStatus(orderDetail.State)
|
|
if err != nil {
|
|
return respData, err
|
|
}
|
|
orderType, err := ok.OrderTypeFromString(orderDetail.OrderType)
|
|
if err != nil {
|
|
return respData, err
|
|
}
|
|
|
|
return order.Detail{
|
|
Amount: orderDetail.Size.Float64(),
|
|
Exchange: ok.Name,
|
|
OrderID: orderDetail.OrderID,
|
|
ClientOrderID: orderDetail.ClientSupplierOrderID,
|
|
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,
|
|
LastUpdated: orderDetail.UpdateTime,
|
|
}, err
|
|
}
|
|
|
|
// 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.String())
|
|
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, errDepositAddressNotFound
|
|
}
|
|
|
|
// 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.String(),
|
|
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(ctx context.Context, withdrawRequest *withdraw.Request) (*withdraw.ExchangeResponse, error) {
|
|
return nil, common.ErrFunctionNotSupported
|
|
}
|
|
|
|
// WithdrawFiatFundsToInternationalBank returns a withdrawal ID when a withdrawal is submitted
|
|
func (ok *Okx) WithdrawFiatFundsToInternationalBank(ctx context.Context, withdrawRequest *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.GetOrdersRequest) (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)
|
|
}
|
|
instrumentType := ok.GetInstrumentTypeFromAssetItem(req.AssetType)
|
|
var orderType string
|
|
if req.Type != order.UnknownType && req.Type != order.AnyType {
|
|
orderType, err = ok.OrderTypeString(req.Type)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
endTime := req.EndTime
|
|
var resp []order.Detail
|
|
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) ||
|
|
orderList[i].CreationTime.Before(req.StartTime) ||
|
|
endTime == orderList[i].CreationTime {
|
|
// reached end of orders to crawl
|
|
break allOrders
|
|
}
|
|
orderSide := orderList[i].Side
|
|
var pair currency.Pair
|
|
pair, err = ok.GetPairFromInstrumentID(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 = ok.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,
|
|
RemainingAmount: orderList[i].Size.Float64() - orderList[i].FillSize,
|
|
Fee: orderList[i].TransactionFee.Float64(),
|
|
FeeAsset: currency.NewCode(orderList[i].FeeCurrency),
|
|
Exchange: ok.Name,
|
|
OrderID: orderList[i].OrderID,
|
|
ClientOrderID: orderList[i].ClientSupplierOrderID,
|
|
Type: oType,
|
|
Side: orderSide,
|
|
Status: orderStatus,
|
|
AssetType: req.AssetType,
|
|
Date: orderList[i].CreationTime,
|
|
LastUpdated: orderList[i].UpdateTime,
|
|
})
|
|
}
|
|
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
|
|
}
|
|
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.GetOrdersRequest) (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 len(req.Pairs) == 0 {
|
|
return nil, errMissingAtLeast1CurrencyPair
|
|
}
|
|
if !ok.SupportsAsset(req.AssetType) {
|
|
return nil, fmt.Errorf("%w: %v", asset.ErrNotSupported, req.AssetType)
|
|
}
|
|
instrumentType := strings.ToUpper(ok.GetInstrumentTypeFromAssetItem(req.AssetType))
|
|
endTime := req.EndTime
|
|
var resp []order.Detail
|
|
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) ||
|
|
orderList[i].CreationTime.Before(req.StartTime) ||
|
|
endTime == orderList[i].CreationTime {
|
|
// reached end of orders to crawl
|
|
break allOrders
|
|
}
|
|
var pair currency.Pair
|
|
pair, err = ok.GetPairFromInstrumentID(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 = ok.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].ClientSupplierOrderID,
|
|
Type: oType,
|
|
Side: orderSide,
|
|
Status: orderStatus,
|
|
AssetType: req.AssetType,
|
|
Date: orderList[i].CreationTime,
|
|
LastUpdated: orderList[i].UpdateTime,
|
|
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
|
|
}
|
|
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)
|
|
}
|
|
|
|
// ValidateCredentials validates current credentials used for wrapper
|
|
func (ok *Okx) ValidateCredentials(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, start, end time.Time, interval kline.Interval) (kline.Item, error) {
|
|
if err := ok.ValidateKline(pair, a, interval); err != nil {
|
|
return kline.Item{}, err
|
|
}
|
|
if kline.TotalCandlesPerInterval(start, end, interval) > 100 {
|
|
return kline.Item{}, errors.New(kline.ErrRequestExceedsExchangeLimits)
|
|
}
|
|
format, err := ok.GetPairFormat(a, false)
|
|
if err != nil {
|
|
return kline.Item{}, err
|
|
}
|
|
if !pair.IsPopulated() {
|
|
return kline.Item{}, errIncompleteCurrencyPair
|
|
}
|
|
instrumentID := format.Format(pair)
|
|
candles, err := ok.GetCandlesticksHistory(ctx, instrumentID, interval, start, end, 100)
|
|
if err != nil {
|
|
return kline.Item{}, err
|
|
}
|
|
response := kline.Item{
|
|
Exchange: ok.Name,
|
|
Pair: pair,
|
|
Asset: a,
|
|
Interval: interval,
|
|
}
|
|
for x := range candles {
|
|
response.Candles = append(response.Candles, kline.Candle{
|
|
Time: candles[x].OpenTime,
|
|
Open: candles[x].OpenPrice,
|
|
High: candles[x].HighestPrice,
|
|
Low: candles[x].LowestPrice,
|
|
Close: candles[x].ClosePrice,
|
|
Volume: candles[x].Volume,
|
|
})
|
|
}
|
|
response.SortCandlesByTimestamp(false)
|
|
return response, nil
|
|
}
|
|
|
|
// 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, start, end time.Time, interval kline.Interval) (kline.Item, error) {
|
|
format, err := ok.GetPairFormat(a, false)
|
|
if err != nil {
|
|
return kline.Item{}, err
|
|
}
|
|
err = ok.ValidateKline(pair, a, interval)
|
|
if err != nil {
|
|
return kline.Item{}, err
|
|
}
|
|
instrumentID := format.Format(pair)
|
|
if err != nil {
|
|
return kline.Item{}, err
|
|
}
|
|
ret := kline.Item{
|
|
Exchange: ok.Name,
|
|
Pair: pair,
|
|
Interval: interval,
|
|
}
|
|
dates, err := kline.CalculateCandleDateRanges(start, end, interval, 100)
|
|
if err != nil {
|
|
return kline.Item{}, err
|
|
}
|
|
for y := range dates.Ranges {
|
|
candles, err := ok.GetCandlesticksHistory(ctx, instrumentID, interval, dates.Ranges[y].Start.Time, dates.Ranges[y].End.Time, 100)
|
|
if err != nil {
|
|
return kline.Item{}, err
|
|
}
|
|
for x := range candles {
|
|
ret.Candles = append(ret.Candles, kline.Candle{
|
|
Time: candles[x].OpenTime,
|
|
Open: candles[x].OpenPrice,
|
|
High: candles[x].HighestPrice,
|
|
Low: candles[x].LowestPrice,
|
|
Close: candles[x].ClosePrice,
|
|
Volume: candles[x].Volume,
|
|
})
|
|
}
|
|
}
|
|
ret.RemoveDuplicates()
|
|
ret.RemoveOutsideRange(start, end)
|
|
ret.SortCandlesByTimestamp(false)
|
|
return ret, nil
|
|
}
|
|
|
|
// 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)
|
|
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) {
|
|
continue
|
|
}
|
|
chains = append(chains, currencyChains[x].Chain)
|
|
}
|
|
return chains, nil
|
|
}
|