mirror of
https://github.com/d0zingcat/gocryptotrader.git
synced 2026-05-20 23:16:49 +00:00
* Sonic: Add sonic_on build tag Thinking about other ways to do this, but they amount to the same thing. It's messy, but I don't have another idea. * Okx: Remove redundant useAsIs on slices Slices are automatically used-as-is, so passing true for these types was unnecessary. Removing the field simplifies rationalising inverting the flag * Okx: Unify timestamp response types * Okx: Change ResetRFQMMPStatus to return a types.Time * Okx: Move withdrawData type to types * Okx: Fix AccountConfiguration slice return * Okx: Improve SendHTTPRequest documentation * Okx: Extend tests for UseAsIs non-defaults * Okx: Fix GetPublicUnderlying with sonic 1.12.9 Using **result for slices with useAsItIs causes sonic to fail. This might be addressed upstream, but it's also not clear what the unmarshal behaviour for an untyped reference to a typed reference should be in the RFC, so we could get a golang.org/encoding/json regression on this too. There's no harm in fixing this, for consistency, to match our handling for non-slice []any wrapping to just use the pointer as is. Note: As of today this requires sonic:main for this to work, because of the other bug: ``` M go.mod - github.com/bytedance/sonic v1.12.9 + github.com/bytedance/sonic v1.12.10-0.20250224121557-e30ac4f2e4fe ``` * Okx: Remove redundant slice check This code didn't work, and if I make it look at rv.Elem().Kind() it errors. Haven't dug too deeply but right now I think we just remove it. * Okx: Simplify SendHTTPRequest by removing UseAsIs Looks to be entirely derivable * Okx: Remove http check that resps must contain data GetAnnouncementTyeps failing in US because of empty response. But also any situation where there really is no data. e.g. GetCandlesticks might return no candlesticks for a period and instrument, because there aren't any. That shouldn't be an error. More saliently if you request orders, or something similar. So, since that check wasn't working before, and it's causing issues now, I'm going to remove it. * Okx: Fix TestGetAnnouncementTypes failing in US announcement-types returns empty in the US, where our github actions run. That's kinda okay. Just don't test we get any back * Sonic: Default to sonic off We've seen too many fatal panics and races with sonic, both in GCT runs and being reported in sonic, to default to it being turned on right now. Whilst we have faith sonic will get through these with time, for now the sensible thing to do for our users is make sonic opt-in. This also removes any of the conditions around 386, etc. If someone wants to run with sonic, they can. Most notably if they're trying out an experimental sonic branch that supports 386, etc.
3056 lines
99 KiB
Go
3056 lines
99 KiB
Go
package okx
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"fmt"
|
|
"math"
|
|
"sort"
|
|
"strconv"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/shopspring/decimal"
|
|
"github.com/thrasher-corp/gocryptotrader/common"
|
|
"github.com/thrasher-corp/gocryptotrader/common/key"
|
|
"github.com/thrasher-corp/gocryptotrader/config"
|
|
"github.com/thrasher-corp/gocryptotrader/currency"
|
|
exchange "github.com/thrasher-corp/gocryptotrader/exchanges"
|
|
"github.com/thrasher-corp/gocryptotrader/exchanges/account"
|
|
"github.com/thrasher-corp/gocryptotrader/exchanges/asset"
|
|
"github.com/thrasher-corp/gocryptotrader/exchanges/collateral"
|
|
"github.com/thrasher-corp/gocryptotrader/exchanges/deposit"
|
|
"github.com/thrasher-corp/gocryptotrader/exchanges/fundingrate"
|
|
"github.com/thrasher-corp/gocryptotrader/exchanges/futures"
|
|
"github.com/thrasher-corp/gocryptotrader/exchanges/kline"
|
|
"github.com/thrasher-corp/gocryptotrader/exchanges/margin"
|
|
"github.com/thrasher-corp/gocryptotrader/exchanges/order"
|
|
"github.com/thrasher-corp/gocryptotrader/exchanges/orderbook"
|
|
"github.com/thrasher-corp/gocryptotrader/exchanges/protocol"
|
|
"github.com/thrasher-corp/gocryptotrader/exchanges/request"
|
|
"github.com/thrasher-corp/gocryptotrader/exchanges/stream"
|
|
"github.com/thrasher-corp/gocryptotrader/exchanges/stream/buffer"
|
|
"github.com/thrasher-corp/gocryptotrader/exchanges/ticker"
|
|
"github.com/thrasher-corp/gocryptotrader/exchanges/trade"
|
|
"github.com/thrasher-corp/gocryptotrader/log"
|
|
"github.com/thrasher-corp/gocryptotrader/portfolio/withdraw"
|
|
)
|
|
|
|
const (
|
|
websocketResponseMaxLimit = time.Second * 3
|
|
)
|
|
|
|
// SetDefaults sets the basic defaults for Okx
|
|
func (ok *Okx) SetDefaults() {
|
|
ok.Name = "Okx"
|
|
ok.Enabled = true
|
|
ok.Verbose = true
|
|
|
|
ok.WsRequestSemaphore = make(chan int, 20)
|
|
ok.API.CredentialsValidator.RequiresKey = true
|
|
ok.API.CredentialsValidator.RequiresSecret = true
|
|
ok.API.CredentialsValidator.RequiresClientID = true
|
|
|
|
ok.instrumentsInfoMap = make(map[string][]Instrument)
|
|
|
|
cpf := ¤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.AccountLevel {
|
|
case "1":
|
|
if item != asset.Spot {
|
|
return 0, fmt.Errorf("%w %v", asset.ErrNotSupported, item)
|
|
}
|
|
fallthrough
|
|
case "2":
|
|
return collateral.SpotFuturesMode, nil
|
|
case "3":
|
|
return collateral.MultiMode, nil
|
|
case "4":
|
|
return collateral.PortfolioMode, nil
|
|
default:
|
|
return collateral.UnknownMode, fmt.Errorf("%w %v", order.ErrCollateralInvalid, cfg.AccountLevel)
|
|
}
|
|
}
|
|
|
|
// ChangePositionMargin will modify a position/currencies margin parameters
|
|
func (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)
|
|
}
|