okx: Remove WsResponseMultiplexer and various refactors (#1851)

* rm WsResponseMultiplexer with added fixes

* linter: fix

* use const and testnet ctx update

* rename error to status for field name

* rm verbosity for random test

* gk: nits v1

* glorious/gk: nits

* linter: fix

* fix and consolidate this direction

* fix linter

* gk: nits cont

* gk: nits I missed

* gk: counter name change to messageIDSeq

* gk/glorious: nits untested

* glorious: nits and tested live endpoints

* Update exchanges/okx/ws_requests.go

Co-authored-by: Adrian Gallagher <adrian.gallagher@thrasher.io>

* Update exchanges/okx/ws_requests.go

Co-authored-by: Adrian Gallagher <adrian.gallagher@thrasher.io>

* Update exchanges/okx/ws_requests.go

Co-authored-by: Adrian Gallagher <adrian.gallagher@thrasher.io>

* Update exchanges/okx/okx.go

Co-authored-by: Adrian Gallagher <adrian.gallagher@thrasher.io>

* Update exchanges/okx/okx.go

Co-authored-by: Adrian Gallagher <adrian.gallagher@thrasher.io>

* thrasher-: nits!

---------

Co-authored-by: Ryan O'Hara-Reid <ryan.oharareid@thrasher.io>
Co-authored-by: Adrian Gallagher <adrian.gallagher@thrasher.io>
This commit is contained in:
Ryan O'Hara-Reid
2025-05-14 13:37:41 +10:00
committed by GitHub
parent 3a55387005
commit 61fc778818
11 changed files with 988 additions and 2172 deletions

View File

@@ -2,10 +2,8 @@ package okx
import (
"fmt"
"slices"
"strings"
"github.com/thrasher-corp/gocryptotrader/common"
"github.com/thrasher-corp/gocryptotrader/currency"
"github.com/thrasher-corp/gocryptotrader/exchanges/asset"
"github.com/thrasher-corp/gocryptotrader/exchanges/order"
@@ -139,41 +137,6 @@ func assetTypeFromInstrumentType(instrumentType string) (asset.Item, error) {
}
}
func (ok *Okx) validatePlaceOrderParams(arg *PlaceOrderRequestParam) error {
if arg == nil {
return common.ErrNilPointer
}
if arg.InstrumentID == "" {
return errMissingInstrumentID
}
if arg.AssetType == asset.Spot || arg.AssetType == asset.Margin || arg.AssetType == asset.Empty {
arg.Side = strings.ToLower(arg.Side)
if arg.Side != order.Buy.Lower() && arg.Side != order.Sell.Lower() {
return fmt.Errorf("%w %s", order.ErrSideIsInvalid, arg.Side)
}
}
if !slices.Contains([]string{"", TradeModeCross, TradeModeIsolated, TradeModeCash}, arg.TradeMode) {
return fmt.Errorf("%w %s", errInvalidTradeModeValue, arg.TradeMode)
}
if arg.AssetType == asset.Futures || arg.AssetType == asset.PerpetualSwap {
arg.PositionSide = strings.ToLower(arg.PositionSide)
if !slices.Contains([]string{"long", "short"}, arg.PositionSide) {
return fmt.Errorf("%w: `%s`, 'long' or 'short' supported", order.ErrSideIsInvalid, arg.PositionSide)
}
}
arg.OrderType = strings.ToLower(arg.OrderType)
if !slices.Contains([]string{orderMarket, orderLimit, orderPostOnly, orderFOK, orderIOC, orderOptimalLimitIOC, "mmp", "mmp_and_post_only"}, arg.OrderType) {
return fmt.Errorf("%w: '%v'", order.ErrTypeIsInvalid, arg.OrderType)
}
if arg.Amount <= 0 {
return order.ErrAmountBelowMin
}
if !slices.Contains([]string{"", "base_ccy", "quote_ccy"}, arg.QuantityType) {
return errCurrencyQuantityTypeRequired
}
return nil
}
// assetTypeString returns a string representation of asset type
func assetTypeString(assetType asset.Item) (string, error) {
switch assetType {

View File

@@ -30,15 +30,8 @@ import (
// Okx is the overarching type across this package
type Okx struct {
exchange.Base
WsResponseMultiplexer wsRequestDataChannelsMultiplexer
// WsRequestSemaphore channel is used to block write operation on the websocket connection to reduce contention; a kind of bounded parallelism.
// it is made to hold up to 20 integers so that up to 20 write operations can be called over the websocket connection at a time.
// and when the operation is completed the thread releases (consumes) one value from the channel so that the other waiting operation can enter.
// ok.WsRequestSemaphore <- 1
// defer func() { <-ok.WsRequestSemaphore }()
WsRequestSemaphore chan int
messageIDSeq common.Counter
instrumentsInfoMapLock sync.Mutex
instrumentsInfoMap map[string][]Instrument
}
@@ -58,15 +51,14 @@ const (
// PlaceOrder places an order
func (ok *Okx) PlaceOrder(ctx context.Context, arg *PlaceOrderRequestParam) (*OrderData, error) {
err := ok.validatePlaceOrderParams(arg)
if err != nil {
if err := arg.Validate(); err != nil {
return nil, err
}
var resp *OrderData
err = ok.SendHTTPRequest(ctx, exchange.RestSpot, placeOrderEPL, http.MethodPost, "trade/order", &arg, &resp, request.AuthenticatedRequest)
err := ok.SendHTTPRequest(ctx, exchange.RestSpot, placeOrderEPL, http.MethodPost, "trade/order", &arg, &resp, request.AuthenticatedRequest)
if err != nil {
if resp != nil && resp.StatusMessage != "" {
return nil, fmt.Errorf("%w, error code: %s error message: %s", err, resp.StatusCode, resp.StatusMessage)
return nil, fmt.Errorf("%w; %w", err, getStatusError(resp.StatusCode, resp.StatusMessage))
}
return nil, err
}
@@ -78,24 +70,22 @@ func (ok *Okx) PlaceMultipleOrders(ctx context.Context, args []PlaceOrderRequest
if len(args) == 0 {
return nil, order.ErrSubmissionIsNil
}
var err error
for x := range args {
err = ok.validatePlaceOrderParams(&args[x])
if err != nil {
if err := args[x].Validate(); err != nil {
return nil, err
}
}
var resp []OrderData
err = ok.SendHTTPRequest(ctx, exchange.RestSpot, placeMultipleOrdersEPL, http.MethodPost, "trade/batch-orders", &args, &resp, request.AuthenticatedRequest)
err := ok.SendHTTPRequest(ctx, exchange.RestSpot, placeMultipleOrdersEPL, http.MethodPost, "trade/batch-orders", &args, &resp, request.AuthenticatedRequest)
if err != nil {
if len(resp) == 0 {
return nil, err
}
var errs error
for x := range resp {
errs = common.AppendError(errs, fmt.Errorf("error code:%s error message: %v", resp[x].StatusCode, resp[x].StatusMessage))
errs = common.AppendError(errs, getStatusError(resp[x].StatusCode, resp[x].StatusMessage))
}
return nil, errs
return nil, common.AppendError(err, errs)
}
return resp, nil
}
@@ -115,7 +105,7 @@ func (ok *Okx) CancelSingleOrder(ctx context.Context, arg *CancelOrderRequestPar
err := ok.SendHTTPRequest(ctx, exchange.RestSpot, cancelOrderEPL, http.MethodPost, "trade/cancel-order", &arg, &resp, request.AuthenticatedRequest)
if err != nil {
if resp != nil && resp.StatusMessage != "" {
return nil, fmt.Errorf("%w, error code: %s and error message: %s", err, resp.StatusCode, resp.StatusMessage)
return nil, fmt.Errorf("%w; %w", err, getStatusError(resp.StatusCode, resp.StatusMessage))
}
return nil, err
}
@@ -124,7 +114,7 @@ func (ok *Okx) CancelSingleOrder(ctx context.Context, arg *CancelOrderRequestPar
// CancelMultipleOrders cancel incomplete orders in batches. Maximum 20 orders can be canceled at a time.
// Request parameters should be passed in the form of an array
func (ok *Okx) CancelMultipleOrders(ctx context.Context, args []CancelOrderRequestParam) ([]OrderData, error) {
func (ok *Okx) CancelMultipleOrders(ctx context.Context, args []CancelOrderRequestParam) ([]*OrderData, error) {
if len(args) == 0 {
return nil, common.ErrEmptyParams
}
@@ -137,20 +127,19 @@ func (ok *Okx) CancelMultipleOrders(ctx context.Context, args []CancelOrderReque
return nil, order.ErrOrderIDNotSet
}
}
var resp []OrderData
err := ok.SendHTTPRequest(ctx, exchange.RestSpot, cancelMultipleOrdersEPL,
http.MethodPost, "trade/cancel-batch-orders", args, &resp, request.AuthenticatedRequest)
var resp []*OrderData
err := ok.SendHTTPRequest(ctx, exchange.RestSpot, cancelMultipleOrdersEPL, http.MethodPost, "trade/cancel-batch-orders", args, &resp, request.AuthenticatedRequest)
if err != nil {
if len(resp) == 0 {
return nil, err
}
var errs error
for x := range resp {
if resp[x].StatusCode != "0" {
errs = common.AppendError(errs, fmt.Errorf("error code:%s message: %v", resp[x].StatusCode, resp[x].StatusMessage))
if resp[x].StatusCode != 0 {
errs = common.AppendError(errs, getStatusError(resp[x].StatusCode, resp[x].StatusMessage))
}
}
return nil, errs
return nil, common.AppendError(err, errs)
}
return resp, nil
}
@@ -556,7 +545,7 @@ func (ok *Okx) cancelAlgoOrder(ctx context.Context, args []AlgoOrderCancelParams
err := ok.SendHTTPRequest(ctx, exchange.RestSpot, rateLimit, http.MethodPost, route, &args, &resp, request.AuthenticatedRequest)
if err != nil {
if resp != nil && resp.StatusMessage != "" {
return nil, fmt.Errorf("%w, error code: %s, error message: %s", err, resp.StatusCode, resp.StatusMessage)
return nil, fmt.Errorf("%w; %w", err, getStatusError(resp.StatusCode, resp.StatusMessage))
}
return nil, err
}
@@ -2981,7 +2970,7 @@ func (ok *Okx) PlaceGridAlgoOrder(ctx context.Context, arg *GridAlgoOrder) (*Gri
err := ok.SendHTTPRequest(ctx, exchange.RestSpot, gridTradingEPL, http.MethodPost, "tradingBot/grid/order-algo", &arg, &resp, request.AuthenticatedRequest)
if err != nil {
if resp != nil && resp.StatusMessage != "" {
return nil, fmt.Errorf("%w, error code: %s error message: %s", err, resp.StatusCode, resp.StatusMessage)
return nil, fmt.Errorf("%w; %w", err, getStatusError(resp.StatusCode, resp.StatusMessage))
}
return nil, err
}
@@ -3003,7 +2992,7 @@ func (ok *Okx) AmendGridAlgoOrder(ctx context.Context, arg *GridAlgoOrderAmend)
err := ok.SendHTTPRequest(ctx, exchange.RestSpot, amendGridAlgoOrderEPL, http.MethodPost, "tradingBot/grid/amend-order-algo", &arg, &resp, request.AuthenticatedRequest)
if err != nil {
if resp != nil && resp.StatusMessage == "" {
return nil, fmt.Errorf("%w, error code: %s and error message: %s", err, resp.StatusMessage, resp.StatusCode)
return nil, fmt.Errorf("%w; %w", err, getStatusError(resp.StatusCode, resp.StatusMessage))
}
return nil, err
}
@@ -3040,7 +3029,13 @@ func (ok *Okx) StopGridAlgoOrder(ctx context.Context, arg []StopGridAlgoOrderReq
if len(resp) == 0 {
return nil, err
}
return nil, fmt.Errorf("error code:%s error message: %v", resp[0].StatusCode, resp[0].StatusMessage)
var errs error
for x := range resp {
if resp[x].StatusMessage != "" {
errs = common.AppendError(errs, getStatusError(resp[x].StatusCode, resp[x].StatusMessage))
}
}
return nil, common.AppendError(err, errs)
}
return resp, nil
}
@@ -3718,22 +3713,22 @@ func (ok *Okx) GetUnrealizedProfitSharingDetails(ctx context.Context, instrument
}
// SetFirstCopySettings set first copy settings for the certain lead trader. You need to first copy settings after stopping copying
func (ok *Okx) SetFirstCopySettings(ctx context.Context, arg *FirstCopySettings) (*ResponseSuccess, error) {
func (ok *Okx) SetFirstCopySettings(ctx context.Context, arg *FirstCopySettings) (*ResponseResult, error) {
err := validateFirstCopySettings(arg)
if err != nil {
return nil, err
}
var resp *ResponseSuccess
var resp *ResponseResult
return resp, ok.SendHTTPRequest(ctx, exchange.RestSpot, setFirstCopySettingsEPL, http.MethodPost, "copytrading/first-copy-settings", arg, &resp, request.AuthenticatedRequest)
}
// AmendCopySettings amends need to use this endpoint for amending copy settings
func (ok *Okx) AmendCopySettings(ctx context.Context, arg *FirstCopySettings) (*ResponseSuccess, error) {
func (ok *Okx) AmendCopySettings(ctx context.Context, arg *FirstCopySettings) (*ResponseResult, error) {
err := validateFirstCopySettings(arg)
if err != nil {
return nil, err
}
var resp *ResponseSuccess
var resp *ResponseResult
return resp, ok.SendHTTPRequest(ctx, exchange.RestSpot, amendFirstCopySettingsEPL, http.MethodPost, "copytrading/amend-copy-settings", arg, &resp, request.AuthenticatedRequest)
}
@@ -3757,7 +3752,7 @@ func validateFirstCopySettings(arg *FirstCopySettings) error {
}
// StopCopying need to use this endpoint for amending copy settings
func (ok *Okx) StopCopying(ctx context.Context, arg *StopCopyingParameter) (*ResponseSuccess, error) {
func (ok *Okx) StopCopying(ctx context.Context, arg *StopCopyingParameter) (*ResponseResult, error) {
if *arg == (StopCopyingParameter{}) {
return nil, common.ErrEmptyParams
}
@@ -3767,7 +3762,7 @@ func (ok *Okx) StopCopying(ctx context.Context, arg *StopCopyingParameter) (*Res
if arg.SubPositionCloseType == "" {
return nil, errSubPositionCloseTypeRequired
}
var resp *ResponseSuccess
var resp *ResponseResult
return resp, ok.SendHTTPRequest(ctx, exchange.RestSpot, stopCopyingEPL, http.MethodPost, "copytrading/stop-copy-trading", arg, &resp, request.AuthenticatedRequest)
}
@@ -5868,7 +5863,7 @@ func (ok *Okx) SendHTTPRequest(ctx context.Context, ep exchange.URL, f request.E
path := endpoint + requestPath
headers := make(map[string]string)
headers["Content-Type"] = "application/json"
if _, okay := ctx.Value(testNetVal).(bool); okay {
if simulate, okay := ctx.Value(testNetVal).(bool); okay && simulate {
headers["x-simulated-trading"] = "1"
}
if authenticated == request.AuthenticatedRequest {
@@ -5905,13 +5900,16 @@ func (ok *Okx) SendHTTPRequest(ctx context.Context, ep exchange.URL, f request.E
return err
}
if err == nil && resp.Code.Int64() != 0 {
if authenticated == request.AuthenticatedRequest {
err = request.ErrAuthRequestFailed
}
if resp.Msg != "" {
return fmt.Errorf("%w error code: %d message: %s", request.ErrAuthRequestFailed, resp.Code.Int64(), resp.Msg)
return common.AppendError(err, fmt.Errorf("error code: `%d`; message: %q", resp.Code.Int64(), resp.Msg))
}
if err, ok := ErrorCodes[resp.Code.String()]; ok {
return err
if mErr, ok := ErrorCodes[resp.Code.String()]; ok {
return common.AppendError(err, mErr)
}
return fmt.Errorf("%w error code: %d", request.ErrAuthRequestFailed, resp.Code.Int64())
return common.AppendError(err, fmt.Errorf("error code: `%d`", resp.Code.Int64()))
}
// First see if resp.Data can unmarshal into a slice of result, which is true for most APIs
@@ -5924,3 +5922,10 @@ func (ok *Okx) SendHTTPRequest(ctx context.Context, ep exchange.URL, f request.E
return nil
}
func getStatusError(statusCode int64, statusMessage string) error {
if statusCode == 0 {
return nil
}
return fmt.Errorf("status code: `%d` status message: %q", statusCode, statusMessage)
}

View File

@@ -8,7 +8,6 @@ import (
"time"
gws "github.com/gorilla/websocket"
"github.com/thrasher-corp/gocryptotrader/common"
"github.com/thrasher-corp/gocryptotrader/common/crypto"
"github.com/thrasher-corp/gocryptotrader/currency"
"github.com/thrasher-corp/gocryptotrader/encoding/json"
@@ -98,56 +97,15 @@ func (ok *Okx) WsSpreadAuth(ctx context.Context) error {
return err
}
base64Sign := crypto.Base64Encode(hmac)
wsReq := WebsocketEventRequest{
Operation: operationLogin,
Arguments: []WebsocketLoginData{
{
APIKey: creds.Key,
Passphrase: creds.ClientID,
Timestamp: timeUnix.Unix(),
Sign: base64Sign,
},
args := []WebsocketLoginData{
{
APIKey: creds.Key,
Passphrase: creds.ClientID,
Timestamp: timeUnix.Unix(),
Sign: base64Sign,
},
}
err = ok.Websocket.AuthConn.SendJSONMessage(ctx, request.UnAuth, wsReq)
if err != nil {
return err
}
timer := time.NewTimer(ok.WebsocketResponseCheckTimeout)
randomID, err := common.GenerateRandomString(16)
if err != nil {
return fmt.Errorf("%w, generating random string for incoming websocket response failed", err)
}
wsResponse := make(chan *wsIncomingData)
ok.WsResponseMultiplexer.Register <- &wsRequestInfo{
ID: randomID,
Chan: wsResponse,
Event: operationLogin,
}
ok.WsRequestSemaphore <- 1
defer func() {
<-ok.WsRequestSemaphore
}()
defer func() { ok.WsResponseMultiplexer.Unregister <- randomID }()
for {
select {
case data := <-wsResponse:
if data.Event == operationLogin && data.StatusCode == "0" {
ok.Websocket.SetCanUseAuthenticatedEndpoints(true)
return nil
} else if data.Event == "error" &&
(data.StatusCode == "60022" || data.StatusCode == "60009") {
ok.Websocket.SetCanUseAuthenticatedEndpoints(false)
return fmt.Errorf("authentication failed with error: %v", ErrorCodes[data.StatusCode])
}
continue
case <-timer.C:
timer.Stop()
return fmt.Errorf("%s websocket connection: timeout waiting for response with an operation: %v",
ok.Name,
wsReq.Operation)
}
}
return ok.SendAuthenticatedWebsocketRequest(ctx, request.Unset, "login-response", operationLogin, args, nil)
}
// GenerateDefaultBusinessSubscriptions returns a list of default subscriptions to business websocket.
@@ -213,8 +171,6 @@ func (ok *Okx) BusinessUnsubscribe(channelsToUnsubscribe subscription.List) erro
// as of the okx, exchange this endpoint sends subscription and unsubscription messages but with a list of json objects.
func (ok *Okx) handleBusinessSubscription(operation string, subscriptions subscription.List) error {
wsSubscriptionReq := WSSubscriptionInformationList{Operation: operation}
ok.WsRequestSemaphore <- 1
defer func() { <-ok.WsRequestSemaphore }()
var channels subscription.List
var authChannels subscription.List
var err error

File diff suppressed because it is too large Load Diff

View File

@@ -2,8 +2,10 @@ package okx
import (
"errors"
"reflect"
"fmt"
"slices"
"strconv"
"strings"
"time"
"github.com/thrasher-corp/gocryptotrader/common"
@@ -81,7 +83,6 @@ var (
errMissingExpiryTimeParameter = errors.New("missing expiry date parameter")
errInvalidTradeModeValue = errors.New("invalid trade mode value")
errCurrencyQuantityTypeRequired = errors.New("only base_ccy and quote_ccy quantity types are supported")
errWebsocketStreamNotAuthenticated = errors.New("websocket stream not authenticated")
errInvalidNewSizeOrPriceInformation = errors.New("invalid new size or price information")
errSizeOrPriceIsRequired = errors.New("either size or price is required")
errInvalidPriceLimit = errors.New("invalid price limit value")
@@ -114,8 +115,6 @@ var (
errMissingSubOrderType = errors.New("missing sub order type")
errMissingQuantity = errors.New("invalid quantity to buy or sell")
errAddressRequired = errors.New("address is required")
errInvalidWebsocketEvent = errors.New("invalid websocket event")
errMissingValidChannelInformation = errors.New("missing channel information")
errMaxRFQOrdersToCancel = errors.New("no more than 100 RFQ cancel order parameter is allowed")
errInvalidUnderlying = errors.New("invalid underlying")
errInstrumentFamilyOrUnderlyingRequired = errors.New("either underlying or instrument family is required")
@@ -127,7 +126,6 @@ var (
errInvalidAlgoOrderType = errors.New("invalid algo order type")
errInvalidIPAddress = errors.New("invalid ip address")
errInvalidAPIKeyPermission = errors.New("invalid API Key permission")
errInvalidResponseParam = errors.New("invalid response parameter, response must be non-nil pointer")
errInvalidDuration = errors.New("invalid grid contract duration, only '7D', '30D', and '180D' are allowed")
errInvalidProtocolType = errors.New("invalid protocol type, only 'staking' and 'defi' allowed")
errExceedLimit = errors.New("limit exceeded")
@@ -231,8 +229,7 @@ type OrderbookItemDetail struct {
// UnmarshalJSON deserializes byte data into OrderbookItemDetail instance
func (o *OrderbookItemDetail) UnmarshalJSON(data []byte) error {
target := [4]any{&o.DepthPrice, &o.Amount, &o.LiquidationOrders, &o.NumberOfOrders}
return json.Unmarshal(data, &target)
return json.Unmarshal(data, &[4]any{&o.DepthPrice, &o.Amount, &o.LiquidationOrders, &o.NumberOfOrders})
}
// CandlestickHistoryItem retrieves historical candlestick charts for the index or mark price from recent years.
@@ -248,9 +245,7 @@ type CandlestickHistoryItem struct {
// UnmarshalJSON converts the data slice into a CandlestickHistoryItem instance.
func (c *CandlestickHistoryItem) UnmarshalJSON(data []byte) error {
var state string
target := []any{&c.Timestamp, &c.OpenPrice, &c.HighestPrice, &c.LowestPrice, &c.ClosePrice, &state}
err := json.Unmarshal(data, &target)
if err != nil {
if err := json.Unmarshal(data, &[6]any{&c.Timestamp, &c.OpenPrice, &c.HighestPrice, &c.LowestPrice, &c.ClosePrice, &state}); err != nil {
return err
}
if state == "1" {
@@ -274,8 +269,7 @@ type CandleStick struct {
// UnmarshalJSON deserializes slice of data into Candlestick structure
func (c *CandleStick) UnmarshalJSON(data []byte) error {
target := [7]any{&c.OpenTime, &c.OpenPrice, &c.HighestPrice, &c.LowestPrice, &c.ClosePrice, &c.Volume, &c.QuoteAssetVolume}
return json.Unmarshal(data, &target)
return json.Unmarshal(data, &[7]any{&c.OpenTime, &c.OpenPrice, &c.HighestPrice, &c.LowestPrice, &c.ClosePrice, &c.Volume, &c.QuoteAssetVolume})
}
// TradeResponse represents the recent transaction instance
@@ -634,8 +628,7 @@ type TakerVolume struct {
// UnmarshalJSON deserializes a slice of data into TakerVolume
func (t *TakerVolume) UnmarshalJSON(data []byte) error {
deploy := [3]any{&t.Timestamp, &t.SellVolume, &t.BuyVolume}
return json.Unmarshal(data, &deploy)
return json.Unmarshal(data, &[3]any{&t.Timestamp, &t.SellVolume, &t.BuyVolume})
}
// MarginLendRatioItem represents margin lend ration information and creation timestamp
@@ -646,8 +639,7 @@ type MarginLendRatioItem struct {
// UnmarshalJSON deserializes a slice of data into MarginLendRatio
func (m *MarginLendRatioItem) UnmarshalJSON(data []byte) error {
target := [2]any{&m.Timestamp, &m.MarginLendRatio}
return json.Unmarshal(data, &target)
return json.Unmarshal(data, &[2]any{&m.Timestamp, &m.MarginLendRatio})
}
// LongShortRatio represents the ratio of users with net long vs net short positions for futures and perpetual swaps
@@ -658,8 +650,7 @@ type LongShortRatio struct {
// UnmarshalJSON deserializes a slice of data into LongShortRatio
func (l *LongShortRatio) UnmarshalJSON(data []byte) error {
target := [2]any{&l.Timestamp, &l.MarginLendRatio}
return json.Unmarshal(data, &target)
return json.Unmarshal(data, &[2]any{&l.Timestamp, &l.MarginLendRatio})
}
// OpenInterestVolume represents open interest and trading volume item for currencies of futures and perpetual swaps
@@ -671,8 +662,7 @@ type OpenInterestVolume struct {
// UnmarshalJSON deserializes json data into OpenInterestVolume struct
func (p *OpenInterestVolume) UnmarshalJSON(data []byte) error {
deploy := [3]any{&p.Timestamp, &p.OpenInterest, &p.Volume}
return json.Unmarshal(data, &deploy)
return json.Unmarshal(data, &[3]any{&p.Timestamp, &p.OpenInterest, &p.Volume})
}
// OpenInterestVolumeRatio represents open interest and trading volume ratio for currencies of futures and perpetual swaps
@@ -684,8 +674,7 @@ type OpenInterestVolumeRatio struct {
// UnmarshalJSON deserializes json data into OpenInterestVolumeRatio
func (o *OpenInterestVolumeRatio) UnmarshalJSON(data []byte) error {
deploy := [3]any{&o.Timestamp, &o.OpenInterestRatio, &o.VolumeRatio}
return json.Unmarshal(data, &deploy)
return json.Unmarshal(data, &[3]any{&o.Timestamp, &o.OpenInterestRatio, &o.VolumeRatio})
}
// ExpiryOpenInterestAndVolume represents open interest and trading volume of calls and puts for each upcoming expiration
@@ -751,8 +740,7 @@ type StrikeOpenInterestAndVolume struct {
// UnmarshalJSON deserializes slice of byte data into StrikeOpenInterestAndVolume
func (s *StrikeOpenInterestAndVolume) UnmarshalJSON(data []byte) error {
target := [6]any{&s.Timestamp, &s.Strike, &s.CallOpenInterest, &s.PutOpenInterest, &s.CallVolume, &s.PutVolume}
return json.Unmarshal(data, &target)
return json.Unmarshal(data, &[6]any{&s.Timestamp, &s.Strike, &s.CallOpenInterest, &s.PutOpenInterest, &s.CallVolume, &s.PutVolume})
}
// CurrencyTakerFlow holds the taker volume information for a single currency
@@ -768,46 +756,93 @@ type CurrencyTakerFlow struct {
// UnmarshalJSON deserializes a slice of byte data into CurrencyTakerFlow
func (c *CurrencyTakerFlow) UnmarshalJSON(data []byte) error {
target := [7]any{&c.Timestamp, &c.CallBuyVolume, &c.CallSellVolume, &c.PutBuyVolume, &c.PutSellVolume, &c.CallBlockVolume, &c.PutBlockVolume}
return json.Unmarshal(data, &target)
return json.Unmarshal(data, &[7]any{&c.Timestamp, &c.CallBuyVolume, &c.CallSellVolume, &c.PutBuyVolume, &c.PutSellVolume, &c.CallBlockVolume, &c.PutBlockVolume})
}
// PlaceOrderRequestParam requesting parameter for placing an order
type PlaceOrderRequestParam struct {
AssetType asset.Item `json:"-"`
InstrumentID string `json:"instId"`
TradeMode string `json:"tdMode,omitempty"` // cash isolated
TradeMode string `json:"tdMode"` // cash isolated
ClientOrderID string `json:"clOrdId,omitempty"`
Currency string `json:"ccy,omitempty"` // Only applicable to cross MARGIN orders in Single-currency margin.
OrderTag string `json:"tag,omitempty"`
Side string `json:"side,omitempty"`
PositionSide string `json:"posSide,omitempty"`
OrderType string `json:"ordType,omitempty"`
Amount float64 `json:"sz,string,omitempty"`
Price float64 `json:"px,string,omitempty"`
ReduceOnly bool `json:"reduceOnly,string,omitempty"`
QuantityType string `json:"tgtCcy,omitempty"` // values base_ccy and quote_ccy
Side string `json:"side"`
PositionSide string `json:"posSide,omitempty"` // long/short only for FUTURES and SWAP
OrderType string `json:"ordType"` // Time in force for the order
Amount float64 `json:"sz,string"`
Price float64 `json:"px,string,omitempty"` // Only applicable to limit,post_only,fok,ioc,mmp,mmp_and_post_only order.
// Options orders
PlaceOptionsOrder string `json:"pxUsd,omitempty"` // Place options orders in USD
PlaceOptionsOrderOnImpliedVolatility string `json:"pxVol,omitempty"` // Place options orders based on implied volatility, where 1 represents 100%
ReduceOnly bool `json:"reduceOnly,string,omitempty"`
TargetCurrency string `json:"tgtCcy,omitempty"` // values base_ccy and quote_ccy for spot market orders
SelfTradePreventionMode string `json:"stpMode,omitempty"` // Default to cancel maker, `cancel_maker`,`cancel_taker`, `cancel_both``
// Added in the websocket requests
BanAmend bool `json:"banAmend,omitempty"` // Whether the SPOT Market Order size can be amended by the system.
ExpiryTime types.Time `json:"expTime,omitzero"`
BanAmend bool `json:"banAmend,omitempty"` // Whether the SPOT Market Order size can be amended by the system.
}
// Validate validates the PlaceOrderRequestParam
func (arg *PlaceOrderRequestParam) Validate() error {
if arg == nil {
return fmt.Errorf("%T: %w", arg, common.ErrNilPointer)
}
if arg.InstrumentID == "" {
return errMissingInstrumentID
}
if arg.AssetType == asset.Spot || arg.AssetType == asset.Margin || arg.AssetType == asset.Empty {
arg.Side = strings.ToLower(arg.Side)
if arg.Side != order.Buy.Lower() && arg.Side != order.Sell.Lower() {
return fmt.Errorf("%w %s", order.ErrSideIsInvalid, arg.Side)
}
}
if !slices.Contains([]string{"", TradeModeCross, TradeModeIsolated, TradeModeCash}, arg.TradeMode) {
return fmt.Errorf("%w %s", errInvalidTradeModeValue, arg.TradeMode)
}
if arg.AssetType == asset.Futures || arg.AssetType == asset.PerpetualSwap {
arg.PositionSide = strings.ToLower(arg.PositionSide)
if !slices.Contains([]string{"long", "short"}, arg.PositionSide) {
return fmt.Errorf("%w: `%s`, 'long' or 'short' supported", order.ErrSideIsInvalid, arg.PositionSide)
}
}
arg.OrderType = strings.ToLower(arg.OrderType)
if !slices.Contains([]string{orderMarket, orderLimit, orderPostOnly, orderFOK, orderIOC, orderOptimalLimitIOC, "mmp", "mmp_and_post_only"}, arg.OrderType) {
return fmt.Errorf("%w: '%v'", order.ErrTypeIsInvalid, arg.OrderType)
}
if arg.Amount <= 0 {
return order.ErrAmountBelowMin
}
if !slices.Contains([]string{"", "base_ccy", "quote_ccy"}, arg.TargetCurrency) {
return errCurrencyQuantityTypeRequired
}
return nil
}
// OrderData response message for place, cancel, and amend an order requests.
type OrderData struct {
OrderID string `json:"ordId,omitempty"`
RequestID string `json:"reqId,omitempty"`
ClientOrderID string `json:"clOrdId,omitempty"`
Tag string `json:"tag,omitempty"`
StatusCode string `json:"sCode,omitempty"`
StatusMessage string `json:"sMsg,omitempty"`
OrderID string `json:"ordId"`
RequestID string `json:"reqId"`
ClientOrderID string `json:"clOrdId"`
Tag string `json:"tag"`
StatusCode int64 `json:"sCode,string"` // Anything above 0 is an error with an attached message
StatusMessage string `json:"sMsg"`
Timestamp string `json:"ts"`
}
// ResponseSuccess holds responses having a status result value
type ResponseSuccess struct {
Result bool `json:"result"`
func (o *OrderData) Error() error {
return getStatusError(o.StatusCode, o.StatusMessage)
}
StatusCode string `json:"sCode,omitempty"`
StatusMessage string `json:"sMsg,omitempty"`
// ResponseResult holds responses having a status result value
type ResponseResult struct {
Result bool `json:"result"`
StatusCode int64 `json:"sCode,string"`
StatusMessage string `json:"sMsg"`
}
func (r *ResponseResult) Error() error {
return getStatusError(r.StatusCode, r.StatusMessage)
}
// CancelOrderRequestParam represents order parameters to cancel an order
@@ -1106,7 +1141,7 @@ type AlgoOrderParams struct {
// AlgoOrder algo order requests response
type AlgoOrder struct {
AlgoID string `json:"algoId"`
StatusCode string `json:"sCode"`
StatusCode int64 `json:"sCode,string"`
StatusMessage string `json:"sMsg"`
ClientOrderID string `json:"clOrdId"`
AlgoClientOrderID string `json:"algoClOrdId"`
@@ -2732,7 +2767,7 @@ type GridAlgoOrder struct {
// GridAlgoOrderIDResponse represents grid algo order
type GridAlgoOrderIDResponse struct {
AlgoOrderID string `json:"algoId"`
StatusCode string `json:"sCode"`
StatusCode int64 `json:"sCode,string"`
StatusMessage string `json:"sMsg"`
}
@@ -2925,9 +2960,35 @@ type SpreadOrderParam struct {
Tag string `json:"tag,omitempty"`
}
// Validate checks if the parameters are valid
func (arg *SpreadOrderParam) Validate() error {
if arg == nil {
return fmt.Errorf("%T: %w", arg, common.ErrNilPointer)
}
if arg.SpreadID == "" {
return fmt.Errorf("%w, spread ID missing", errMissingInstrumentID)
}
if arg.OrderType == "" {
return fmt.Errorf("%w spread order type is required", order.ErrTypeIsInvalid)
}
if arg.Size <= 0 {
return order.ErrAmountBelowMin
}
if arg.Price <= 0 {
return order.ErrPriceBelowMin
}
arg.Side = strings.ToLower(arg.Side)
switch arg.Side {
case order.Buy.Lower(), order.Sell.Lower():
default:
return fmt.Errorf("%w %s", order.ErrSideIsInvalid, arg.Side)
}
return nil
}
// SpreadOrderResponse represents a spread create order response
type SpreadOrderResponse struct {
StatusCode string `json:"sCode"`
StatusCode int64 `json:"sCode,string"` // Anything above 0 is an error with an attached message
StatusMessage string `json:"sMsg"`
ClientOrderID string `json:"clOrdId"`
OrderID string `json:"ordId"`
@@ -2937,6 +2998,10 @@ type SpreadOrderResponse struct {
RequestID string `json:"reqId"`
}
func (arg *SpreadOrderResponse) Error() error {
return getStatusError(arg.StatusCode, arg.StatusMessage)
}
// AmendSpreadOrderParam holds amend parameters for spread order
type AmendSpreadOrderParam struct {
OrderID string `json:"ordId"`
@@ -3104,20 +3169,6 @@ type WSSubscriptionInformationList struct {
Arguments []SubscriptionInfo `json:"args"`
}
// OperationResponse holds common operation identification
type OperationResponse struct {
ID string `json:"id"`
Operation string `json:"op"`
Code string `json:"code"`
Msg string `json:"msg"`
}
// WsPlaceOrderResponse place order response thought the websocket connection
type WsPlaceOrderResponse struct {
OperationResponse
Data []OrderData `json:"data"`
}
// SpreadOrderInfo holds spread order response information
type SpreadOrderInfo struct {
ClientOrderID string `json:"clOrdId"`
@@ -3127,15 +3178,6 @@ type SpreadOrderInfo struct {
StatusMessage string `json:"sMsg"`
}
type wsRequestInfo struct {
ID string
Chan chan *wsIncomingData
Event string
Channel string
InstrumentType string
InstrumentID string
}
type wsIncomingData struct {
Event string `json:"event"`
Argument SubscriptionInfo `json:"arg"`
@@ -3148,37 +3190,6 @@ type wsIncomingData struct {
Data json.RawMessage `json:"data"`
}
// copyToPlaceOrderResponse returns WSPlaceOrderResponse struct instance
func (w *wsIncomingData) copyToPlaceOrderResponse() (*WsPlaceOrderResponse, error) {
if len(w.Data) == 0 {
return nil, common.ErrNoResponse
}
var placeOrds []OrderData
err := json.Unmarshal(w.Data, &placeOrds)
if err != nil {
return nil, err
}
return &WsPlaceOrderResponse{
OperationResponse: OperationResponse{
Operation: w.Operation,
ID: w.ID,
},
Data: placeOrds,
}, nil
}
// copyResponseToInterface unmarshals the response data into the dataHolder interface.
func (w *wsIncomingData) copyResponseToInterface(dataHolder any) error {
rv := reflect.ValueOf(dataHolder)
if rv.Kind() != reflect.Pointer {
return errInvalidResponseParam
}
if len(w.Data) == 0 {
return common.ErrNoResponse
}
return json.Unmarshal(w.Data, &[]any{dataHolder})
}
// WSInstrumentResponse represents websocket instruments push message
type WSInstrumentResponse struct {
Argument SubscriptionInfo `json:"arg"`
@@ -3213,17 +3224,6 @@ type WsOrderActionResponse struct {
Msg string `json:"msg"`
}
func (a *WsOrderActionResponse) populateFromIncomingData(incoming *wsIncomingData) error {
if incoming == nil {
return common.ErrNilPointer
}
a.ID = incoming.ID
a.Code = incoming.StatusCode
a.Operation = incoming.Operation
a.Msg = incoming.Message
return nil
}
// SubscriptionOperationInput represents the account channel input data
type SubscriptionOperationInput struct {
Operation string `json:"op"`
@@ -4307,24 +4307,6 @@ type APYItem struct {
Timestamp types.Time `json:"ts"`
}
// wsRequestDataChannelsMultiplexer a single multiplexer instance to multiplex websocket messages multiplexer channels
type wsRequestDataChannelsMultiplexer struct {
// To Synchronize incoming messages coming through the websocket channel
WsResponseChannelsMap map[string]*wsRequestInfo
Register chan *wsRequestInfo
Unregister chan string
Message chan *wsIncomingData
shutdown chan bool
}
// wsSubscriptionParameters represents toggling boolean values for subscription parameters
type wsSubscriptionParameters struct {
InstrumentType bool
InstrumentID bool
Underlying bool
Currency bool
}
// WsOrderbook5 stores the orderbook data for orderbook 5 websocket
type WsOrderbook5 struct {
Argument struct {
@@ -4930,8 +4912,7 @@ type ContractTakerVolume struct {
// UnmarshalJSON deserializes a slice data into ContractTakerVolume
func (c *ContractTakerVolume) UnmarshalJSON(data []byte) error {
target := [3]any{&c.Timestamp, &c.TakerSellVolume, &c.TakerBuyVolume}
return json.Unmarshal(data, &target)
return json.Unmarshal(data, &[3]any{&c.Timestamp, &c.TakerSellVolume, &c.TakerBuyVolume})
}
// ContractOpenInterestHistoryItem represents an open interest information for contract
@@ -4944,8 +4925,7 @@ type ContractOpenInterestHistoryItem struct {
// UnmarshalJSON deserializes slice data into ContractOpenInterestHistoryItem instance
func (c *ContractOpenInterestHistoryItem) UnmarshalJSON(data []byte) error {
target := [4]any{&c.Timestamp, &c.OpenInterestInContract, &c.OpenInterestInCurrency, &c.OpenInterestInUSD}
return json.Unmarshal(data, &target)
return json.Unmarshal(data, &[4]any{&c.Timestamp, &c.OpenInterestInContract, &c.OpenInterestInCurrency, &c.OpenInterestInUSD})
}
// TopTraderContractsLongShortRatio represents the timestamp and ratio information of top traders long and short accounts/positions
@@ -4956,8 +4936,7 @@ type TopTraderContractsLongShortRatio struct {
// UnmarshalJSON deserializes slice data into TopTraderContractsLongShortRatio instance
func (t *TopTraderContractsLongShortRatio) UnmarshalJSON(data []byte) error {
target := [2]any{&t.Timestamp, &t.Ratio}
return json.Unmarshal(data, &target)
return json.Unmarshal(data, &[2]any{&t.Timestamp, &t.Ratio})
}
// AccountInstrument represents an account instrument

File diff suppressed because it is too large Load Diff

View File

@@ -46,7 +46,6 @@ func (ok *Okx) SetDefaults() {
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
@@ -163,7 +162,7 @@ func (ok *Okx) SetDefaults() {
}
ok.Requester, err = request.New(ok.Name,
common.NewHTTPClientWithTimeout(exchange.DefaultHTTPTimeout),
request.WithLimiter(GetRateLimit()))
request.WithLimiter(rateLimits))
if err != nil {
log.Errorln(log.ExchangeSys, err)
}
@@ -181,14 +180,6 @@ func (ok *Okx) SetDefaults() {
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
@@ -218,46 +209,32 @@ func (ok *Okx) Setup(exch *config.Exchange) error {
GenerateSubscriptions: ok.generateSubscriptions,
Features: &ok.Features.Supports.WebsocketCapabilities,
MaxWebsocketSubscriptionsPerConnection: 240,
OrderbookBufferConfig: buffer.Config{
Checksum: ok.CalculateUpdateOrderbookChecksum,
},
RateLimitDefinitions: ok.Requester.GetRateLimiterDefinitions(),
OrderbookBufferConfig: buffer.Config{Checksum: ok.CalculateUpdateOrderbookChecksum},
RateLimitDefinitions: rateLimits,
}); err != nil {
return err
}
go ok.WsResponseMultiplexer.Run()
if err := ok.Websocket.SetupNewConnection(&websocket.ConnectionSetup{
URL: apiWebsocketPublicURL,
ResponseCheckTimeout: exch.WebsocketResponseCheckTimeout,
ResponseMaxLimit: websocketResponseMaxLimit,
RateLimit: request.NewRateLimitWithWeight(time.Second, 2, 1),
URL: apiWebsocketPublicURL,
ResponseCheckTimeout: exch.WebsocketResponseCheckTimeout,
ResponseMaxLimit: websocketResponseMaxLimit,
RateLimit: request.NewRateLimitWithWeight(time.Second, 2, 1),
BespokeGenerateMessageID: func(bool) int64 { return ok.messageIDSeq.IncrementAndGet() },
}); err != nil {
return err
}
return ok.Websocket.SetupNewConnection(&websocket.ConnectionSetup{
URL: apiWebsocketPrivateURL,
ResponseCheckTimeout: exch.WebsocketResponseCheckTimeout,
ResponseMaxLimit: websocketResponseMaxLimit,
Authenticated: true,
RateLimit: request.NewRateLimitWithWeight(time.Second, 2, 1),
URL: apiWebsocketPrivateURL,
ResponseCheckTimeout: exch.WebsocketResponseCheckTimeout,
ResponseMaxLimit: websocketResponseMaxLimit,
Authenticated: true,
RateLimit: request.NewRateLimitWithWeight(time.Second, 2, 1),
BespokeGenerateMessageID: func(bool) int64 { return ok.messageIDSeq.IncrementAndGet() },
})
}
// 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)
@@ -904,7 +881,7 @@ func (ok *Okx) SubmitOrder(ctx context.Context, s *order.Submit) (*order.SubmitR
}
var placeSpreadOrderResponse *SpreadOrderResponse
if ok.Websocket.CanUseAuthenticatedWebsocketForWrapper() {
placeSpreadOrderResponse, err = ok.WsPlaceSpreadOrder(ctx, spreadParam)
placeSpreadOrderResponse, err = ok.WSPlaceSpreadOrder(ctx, spreadParam)
if err != nil {
return nil, err
}
@@ -925,16 +902,16 @@ func (ok *Okx) SubmitOrder(ctx context.Context, s *order.Submit) (*order.SubmitR
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,
InstrumentID: pairString,
TradeMode: tradeMode,
Side: sideType,
PositionSide: positionSide,
OrderType: orderTypeString,
Amount: amount,
ClientOrderID: s.ClientOrderID,
Price: s.Price,
TargetCurrency: targetCurrency,
AssetType: s.AssetType,
}
switch s.Type.Lower() {
case orderLimit, orderPostOnly, orderFOK, orderIOC:
@@ -952,15 +929,12 @@ func (ok *Okx) SubmitOrder(ctx context.Context, s *order.Submit) (*order.SubmitR
}
}
if ok.Websocket.CanUseAuthenticatedWebsocketForWrapper() {
placeOrderResponse, err = ok.WsPlaceOrder(ctx, orderRequest)
if err != nil {
return nil, err
}
placeOrderResponse, err = ok.WSPlaceOrder(ctx, orderRequest)
} else {
placeOrderResponse, err = ok.PlaceOrder(ctx, orderRequest)
if err != nil {
return nil, err
}
}
if err != nil {
return nil, err
}
return s.DeriveSubmitResponse(placeOrderResponse.OrderID)
case "trigger":
@@ -1130,7 +1104,7 @@ func (ok *Okx) ModifyOrder(ctx context.Context, action *order.Modify) (*order.Mo
NewPrice: action.Price,
}
if ok.Websocket.CanUseAuthenticatedWebsocketForWrapper() {
_, err = ok.WsAmandSpreadOrder(ctx, amendSpreadOrder)
_, err = ok.WSAmendSpreadOrder(ctx, amendSpreadOrder)
} else {
_, err = ok.AmendSpreadOrder(ctx, amendSpreadOrder)
}
@@ -1158,7 +1132,7 @@ func (ok *Okx) ModifyOrder(ctx context.Context, action *order.Modify) (*order.Mo
ClientOrderID: action.ClientOrderID,
}
if ok.Websocket.CanUseAuthenticatedWebsocketForWrapper() {
_, err = ok.WsAmendOrder(ctx, &amendRequest)
_, err = ok.WSAmendOrder(ctx, &amendRequest)
} else {
_, err = ok.AmendOrder(ctx, &amendRequest)
}
@@ -1239,7 +1213,7 @@ func (ok *Okx) CancelOrder(ctx context.Context, ord *order.Cancel) error {
var err error
if ord.AssetType == asset.Spread {
if ok.Websocket.CanUseAuthenticatedWebsocketForWrapper() {
_, err = ok.WsCancelSpreadOrder(ctx, ord.OrderID, ord.ClientOrderID)
_, err = ok.WSCancelSpreadOrder(ctx, ord.OrderID, ord.ClientOrderID)
} else {
_, err = ok.CancelSpreadOrder(ctx, ord.OrderID, ord.ClientOrderID)
}
@@ -1262,12 +1236,11 @@ func (ok *Okx) CancelOrder(ctx context.Context, ord *order.Cancel) error {
ClientOrderID: ord.ClientOrderID,
}
if ok.Websocket.CanUseAuthenticatedWebsocketForWrapper() {
_, err = ok.WsCancelOrder(ctx, &req)
_, err = ok.WSCancelOrder(ctx, &req)
} else {
_, err = ok.CancelSingleOrder(ctx, &req)
}
case order.Trigger, order.OCO, order.ConditionalStop,
order.TWAP, order.TrailingStop, order.Chase:
case order.Trigger, order.OCO, order.ConditionalStop, order.TWAP, order.TrailingStop, order.Chase:
var response *AlgoOrder
response, err = ok.CancelAdvanceAlgoOrder(ctx, []AlgoOrderCancelParams{
{
@@ -1278,10 +1251,7 @@ func (ok *Okx) CancelOrder(ctx context.Context, ord *order.Cancel) error {
if err != nil {
return err
}
if response.StatusCode != "0" {
return fmt.Errorf("sCode: %s sMessage: %s", response.StatusCode, response.StatusMessage)
}
return nil
return getStatusError(response.StatusCode, response.StatusMessage)
default:
return fmt.Errorf("%w, order type %v", order.ErrUnsupportedOrderType, ord.Type)
}
@@ -1337,9 +1307,9 @@ func (ok *Okx) CancelBatchOrders(ctx context.Context, o []order.Cancel) (*order.
}
resp := &order.CancelBatchResponse{Status: make(map[string]string)}
if len(cancelOrderParams) > 0 {
var canceledOrders []OrderData
var canceledOrders []*OrderData
if ok.Websocket.CanUseAuthenticatedWebsocketForWrapper() {
canceledOrders, err = ok.WsCancelMultipleOrder(ctx, cancelOrderParams)
canceledOrders, err = ok.WSCancelMultipleOrders(ctx, cancelOrderParams)
} else {
canceledOrders, err = ok.CancelMultipleOrders(ctx, cancelOrderParams)
}
@@ -1348,7 +1318,7 @@ func (ok *Okx) CancelBatchOrders(ctx context.Context, o []order.Cancel) (*order.
}
for x := range canceledOrders {
resp.Status[canceledOrders[x].OrderID] = func() string {
if canceledOrders[x].StatusCode != "0" && canceledOrders[x].StatusCode != "2" {
if canceledOrders[x].StatusCode != 0 {
return ""
}
return order.Cancelled.String()
@@ -1362,11 +1332,11 @@ func (ok *Okx) CancelBatchOrders(ctx context.Context, o []order.Cancel) (*order.
return resp, nil
}
return nil, err
} else if cancelationResponse.StatusCode != "0" {
} 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)
return resp, getStatusError(cancelationResponse.StatusCode, cancelationResponse.StatusMessage)
}
for x := range cancelAlgoOrderParams {
resp.Status[cancelAlgoOrderParams[x].AlgoOrderID] = order.Cancelled.String()
@@ -1454,17 +1424,17 @@ ordersLoop:
remaining := cancelAllOrdersRequestParams
loop := int(math.Ceil(float64(len(remaining)) / 20.0))
for range loop {
var response []OrderData
var response []*OrderData
if len(remaining) > 20 {
if ok.Websocket.CanUseAuthenticatedWebsocketForWrapper() {
response, err = ok.WsCancelMultipleOrder(ctx, remaining[:20])
response, err = ok.WSCancelMultipleOrders(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)
response, err = ok.WSCancelMultipleOrders(ctx, remaining)
} else {
response, err = ok.CancelMultipleOrders(ctx, remaining)
}
@@ -1475,7 +1445,7 @@ ordersLoop:
}
}
for y := range response {
if response[y].StatusCode == "0" {
if response[y].StatusCode == 0 {
cancelAllResponse.Status[response[y].OrderID] = order.Cancelled.String()
} else {
cancelAllResponse.Status[response[y].OrderID] = response[y].StatusMessage
@@ -3020,28 +2990,3 @@ func (ok *Okx) GetCurrencyTradeURL(ctx context.Context, a asset.Item, cp currenc
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)
}

View File

@@ -47,7 +47,7 @@ const (
getOneClickRepayHistoryEPL
oneClickRepayCurrencyListEPL
tradeOneClickRepayEPL
massCancemMMPOrderEPL
massCancelMMPOrderEPL
getCounterpartiesEPL
createRFQEPL
cancelRFQEPL
@@ -323,8 +323,7 @@ const (
getFiatDepositPaymentMethodsEPL
)
// GetRateLimit returns a RateLimit instance, which implements the request.Limiter interface.
func GetRateLimit() request.RateLimitDefinitions {
var rateLimits = func() request.RateLimitDefinitions {
return request.RateLimitDefinitions{
// Trade Endpoints
placeOrderEPL: request.NewRateLimitWithWeight(twoSecondsInterval, 60, 1),
@@ -358,7 +357,7 @@ func GetRateLimit() request.RateLimitDefinitions {
getOneClickRepayHistoryEPL: request.NewRateLimitWithWeight(twoSecondsInterval, 1, 1),
oneClickRepayCurrencyListEPL: request.NewRateLimitWithWeight(twoSecondsInterval, 1, 1),
tradeOneClickRepayEPL: request.NewRateLimitWithWeight(twoSecondsInterval, 1, 1),
massCancemMMPOrderEPL: request.NewRateLimitWithWeight(twoSecondsInterval, 5, 1),
massCancelMMPOrderEPL: request.NewRateLimitWithWeight(twoSecondsInterval, 5, 1),
// Block Trading endpoints
getCounterpartiesEPL: request.NewRateLimitWithWeight(twoSecondsInterval, 5, 1),
@@ -661,4 +660,4 @@ func GetRateLimit() request.RateLimitDefinitions {
getWithdrawalPaymentMethodsEPL: request.NewRateLimitWithWeight(oneSecondInterval, 3, 1),
getFiatDepositPaymentMethodsEPL: request.NewRateLimitWithWeight(oneSecondInterval, 3, 1),
}
}
}()

View File

@@ -40,7 +40,7 @@ func TestRateLimit_LimitStatic(t *testing.T) {
"getOneClickRepayHistory": getOneClickRepayHistoryEPL,
"oneClickRepayCurrencyList": oneClickRepayCurrencyListEPL,
"tradeOneClickRepay": tradeOneClickRepayEPL,
"massCancemMMPOrder": massCancemMMPOrderEPL,
"massCancelMMPOrder": massCancelMMPOrderEPL,
"getCounterparties": getCounterpartiesEPL,
"createRFQ": createRFQEPL,
"cancelRFQ": cancelRFQEPL,
@@ -267,7 +267,7 @@ func TestRateLimit_LimitStatic(t *testing.T) {
"getUserAffilateRebateInformation": getUserAffiliateRebateInformationEPL,
}
rl, err := request.New("RateLimit_Static", http.DefaultClient, request.WithLimiter(GetRateLimit()))
rl, err := request.New("RateLimit_Static", http.DefaultClient, request.WithLimiter(rateLimits))
require.NoError(t, err)
for name, tt := range testTable {

View File

@@ -0,0 +1,344 @@
package okx
import (
"context"
"errors"
"fmt"
"reflect"
"strconv"
"github.com/thrasher-corp/gocryptotrader/common"
"github.com/thrasher-corp/gocryptotrader/encoding/json"
"github.com/thrasher-corp/gocryptotrader/exchanges/order"
"github.com/thrasher-corp/gocryptotrader/exchanges/request"
)
var (
errInvalidWebsocketRequest = errors.New("invalid websocket request")
errOperationFailed = errors.New("operation failed")
errPartialSuccess = errors.New("bulk operation partially succeeded")
errMassCancelFailed = errors.New("mass cancel failed")
errCancelAllSpreadOrdersFailed = errors.New("cancel all spread orders failed")
errMultipleItemsReturned = errors.New("multiple items returned")
)
// WSPlaceOrder submits an order
func (ok *Okx) WSPlaceOrder(ctx context.Context, arg *PlaceOrderRequestParam) (*OrderData, error) {
if err := arg.Validate(); err != nil {
return nil, err
}
id := strconv.FormatInt(ok.Websocket.AuthConn.GenerateMessageID(false), 10)
var resp []*OrderData
if err := ok.SendAuthenticatedWebsocketRequest(ctx, placeOrderEPL, id, "order", []PlaceOrderRequestParam{*arg}, &resp); err != nil {
return nil, err
}
return singleItem(resp)
}
// WSPlaceMultipleOrders submits multiple orders
func (ok *Okx) WSPlaceMultipleOrders(ctx context.Context, args []PlaceOrderRequestParam) ([]*OrderData, error) {
if len(args) == 0 {
return nil, fmt.Errorf("%T: %w", args, order.ErrSubmissionIsNil)
}
for i := range args {
if err := args[i].Validate(); err != nil {
return nil, err
}
}
id := strconv.FormatInt(ok.Websocket.AuthConn.GenerateMessageID(false), 10)
var resp []*OrderData
return resp, ok.SendAuthenticatedWebsocketRequest(ctx, placeMultipleOrdersEPL, id, "batch-orders", args, &resp)
}
// WSCancelOrder cancels an order
func (ok *Okx) WSCancelOrder(ctx context.Context, arg *CancelOrderRequestParam) (*OrderData, error) {
if arg == nil {
return nil, fmt.Errorf("%T: %w", arg, common.ErrNilPointer)
}
if arg.InstrumentID == "" {
return nil, errMissingInstrumentID
}
if arg.OrderID == "" && arg.ClientOrderID == "" {
return nil, order.ErrOrderIDNotSet
}
id := strconv.FormatInt(ok.Websocket.AuthConn.GenerateMessageID(false), 10)
var resp []*OrderData
if err := ok.SendAuthenticatedWebsocketRequest(ctx, cancelOrderEPL, id, "cancel-order", []CancelOrderRequestParam{*arg}, &resp); err != nil {
return nil, err
}
return singleItem(resp)
}
// WSCancelMultipleOrders cancels multiple orders
func (ok *Okx) WSCancelMultipleOrders(ctx context.Context, args []CancelOrderRequestParam) ([]*OrderData, error) {
if len(args) == 0 {
return nil, fmt.Errorf("%T: %w", args, order.ErrSubmissionIsNil)
}
for i := range args {
if args[i].InstrumentID == "" {
return nil, errMissingInstrumentID
}
if args[i].OrderID == "" && args[i].ClientOrderID == "" {
return nil, order.ErrOrderIDNotSet
}
}
id := strconv.FormatInt(ok.Websocket.AuthConn.GenerateMessageID(false), 10)
var resp []*OrderData
return resp, ok.SendAuthenticatedWebsocketRequest(ctx, cancelMultipleOrdersEPL, id, "batch-cancel-orders", args, &resp)
}
// WSAmendOrder amends an order
func (ok *Okx) WSAmendOrder(ctx context.Context, arg *AmendOrderRequestParams) (*OrderData, error) {
if arg == nil {
return nil, fmt.Errorf("%T: %w", arg, common.ErrNilPointer)
}
if arg.InstrumentID == "" {
return nil, errMissingInstrumentID
}
if arg.ClientOrderID == "" && arg.OrderID == "" {
return nil, order.ErrOrderIDNotSet
}
if arg.NewQuantity <= 0 && arg.NewPrice <= 0 {
return nil, errInvalidNewSizeOrPriceInformation
}
id := strconv.FormatInt(ok.Websocket.AuthConn.GenerateMessageID(false), 10)
var resp []*OrderData
if err := ok.SendAuthenticatedWebsocketRequest(ctx, amendOrderEPL, id, "amend-order", []AmendOrderRequestParams{*arg}, &resp); err != nil {
return nil, err
}
return singleItem(resp)
}
// WSAmendMultipleOrders amends multiple orders
func (ok *Okx) WSAmendMultipleOrders(ctx context.Context, args []AmendOrderRequestParams) ([]*OrderData, error) {
if len(args) == 0 {
return nil, fmt.Errorf("%T: %w", args, order.ErrSubmissionIsNil)
}
for x := range args {
if args[x].InstrumentID == "" {
return nil, errMissingInstrumentID
}
if args[x].ClientOrderID == "" && args[x].OrderID == "" {
return nil, order.ErrOrderIDNotSet
}
if args[x].NewQuantity <= 0 && args[x].NewPrice <= 0 {
return nil, errInvalidNewSizeOrPriceInformation
}
}
id := strconv.FormatInt(ok.Websocket.AuthConn.GenerateMessageID(false), 10)
var resp []*OrderData
return resp, ok.SendAuthenticatedWebsocketRequest(ctx, amendMultipleOrdersEPL, id, "batch-amend-orders", args, &resp)
}
// WSMassCancelOrders cancels all MMP pending orders of an instrument family. Only applicable to Option in Portfolio Margin mode, and MMP privilege is required.
func (ok *Okx) WSMassCancelOrders(ctx context.Context, args []CancelMassReqParam) error {
if len(args) == 0 {
return fmt.Errorf("%T: %w", args, order.ErrSubmissionIsNil)
}
for x := range args {
if args[x].InstrumentType == "" {
return fmt.Errorf("%w, instrument type can not be empty", errInvalidInstrumentType)
}
if args[x].InstrumentFamily == "" {
return errInstrumentFamilyRequired
}
}
id := strconv.FormatInt(ok.Websocket.AuthConn.GenerateMessageID(false), 10)
var resps []*struct {
Result bool `json:"result"`
}
if err := ok.SendAuthenticatedWebsocketRequest(ctx, amendOrderEPL, id, "mass-cancel", args, &resps); err != nil {
return err
}
resp, err := singleItem(resps)
if err != nil {
return err
}
if !resp.Result {
return errMassCancelFailed
}
return nil
}
// WSPlaceSpreadOrder submits a spread order
func (ok *Okx) WSPlaceSpreadOrder(ctx context.Context, arg *SpreadOrderParam) (*SpreadOrderResponse, error) {
if err := arg.Validate(); err != nil {
return nil, err
}
id := strconv.FormatInt(ok.Websocket.AuthConn.GenerateMessageID(false), 10)
var resp []*SpreadOrderResponse
if err := ok.SendAuthenticatedWebsocketRequest(ctx, placeSpreadOrderEPL, id, "sprd-order", []SpreadOrderParam{*arg}, &resp); err != nil {
return nil, err
}
return singleItem(resp)
}
// WSAmendSpreadOrder amends a spread order
func (ok *Okx) WSAmendSpreadOrder(ctx context.Context, arg *AmendSpreadOrderParam) (*SpreadOrderResponse, error) {
if arg == nil {
return nil, fmt.Errorf("%T: %w", arg, common.ErrNilPointer)
}
if arg.OrderID == "" && arg.ClientOrderID == "" {
return nil, order.ErrOrderIDNotSet
}
if arg.NewPrice == 0 && arg.NewSize == 0 {
return nil, errSizeOrPriceIsRequired
}
id := strconv.FormatInt(ok.Websocket.AuthConn.GenerateMessageID(false), 10)
var resp []*SpreadOrderResponse
if err := ok.SendAuthenticatedWebsocketRequest(ctx, amendSpreadOrderEPL, id, "sprd-amend-order", []AmendSpreadOrderParam{*arg}, &resp); err != nil {
return nil, err
}
return singleItem(resp)
}
// WSCancelSpreadOrder cancels an incomplete spread order through the websocket connection.
func (ok *Okx) WSCancelSpreadOrder(ctx context.Context, orderID, clientOrderID string) (*SpreadOrderResponse, error) {
if orderID == "" && clientOrderID == "" {
return nil, order.ErrOrderIDNotSet
}
arg := make(map[string]string)
if orderID != "" {
arg["ordId"] = orderID
}
if clientOrderID != "" {
arg["clOrdId"] = clientOrderID
}
id := strconv.FormatInt(ok.Websocket.AuthConn.GenerateMessageID(false), 10)
var resp []*SpreadOrderResponse
if err := ok.SendAuthenticatedWebsocketRequest(ctx, cancelSpreadOrderEPL, id, "sprd-cancel-order", []map[string]string{arg}, &resp); err != nil {
return nil, err
}
return singleItem(resp)
}
// WSCancelAllSpreadOrders cancels all spread orders and return success message through the websocket channel.
func (ok *Okx) WSCancelAllSpreadOrders(ctx context.Context, spreadID string) error {
arg := make(map[string]string, 1)
if spreadID != "" {
arg["sprdId"] = spreadID
}
id := strconv.FormatInt(ok.Websocket.AuthConn.GenerateMessageID(false), 10)
var resps []*ResponseResult
if err := ok.SendAuthenticatedWebsocketRequest(ctx, cancelAllSpreadOrderEPL, id, "sprd-mass-cancel", []map[string]string{arg}, &resps); err != nil {
return err
}
resp, err := singleItem(resps)
if err != nil {
return err
}
if !resp.Result {
return errCancelAllSpreadOrdersFailed
}
return nil
}
// SendAuthenticatedWebsocketRequest sends a websocket request to the server
func (ok *Okx) SendAuthenticatedWebsocketRequest(ctx context.Context, epl request.EndpointLimit, id, operation string, payload, result any) error {
if operation == "" || payload == nil {
return errInvalidWebsocketRequest
}
outbound := &struct {
ID string `json:"id"`
Operation string `json:"op"`
Arguments any `json:"args"`
// TODO: Add ExpTime to the struct, the struct should look like this:
// ExpTime string `json:"expTime,omitempty"` so a deadline can be set
// Request effective deadline. Unix timestamp format in milliseconds, e.g. 1597026383085
}{
ID: id,
Operation: operation,
Arguments: payload,
}
incoming, err := ok.Websocket.AuthConn.SendMessageReturnResponse(ctx, epl, id, outbound)
if err != nil {
return err
}
intermediary := struct {
ID string `json:"id"`
Operation string `json:"op"`
Code int64 `json:"code,string"`
Message string `json:"msg"`
Data any `json:"data"`
InTime string `json:"inTime"`
OutTime string `json:"outTime"`
}{
Data: result,
}
if err := json.Unmarshal(incoming, &intermediary); err != nil {
return err
}
switch intermediary.Code {
case 0:
return nil
case 1:
return parseWSResponseErrors(result, errOperationFailed)
case 2:
return parseWSResponseErrors(result, errPartialSuccess)
default:
return getStatusError(intermediary.Code, intermediary.Message)
}
}
func parseWSResponseErrors(result any, err error) error {
s := reflect.ValueOf(result).Elem()
for i := range s.Len() {
v := s.Index(i)
if subErr, ok := v.Interface().(interface{ Error() error }); ok && subErr.Error() != nil {
err = common.AppendError(err, fmt.Errorf("%s[%d]: %w", v.Type(), i+1, subErr.Error()))
}
}
return err
}
func singleItem[T any](resp []*T) (*T, error) {
if len(resp) == 0 {
return nil, common.ErrNoResponse
}
if len(resp) > 1 {
return nil, fmt.Errorf("%w, received %d", errMultipleItemsReturned, len(resp))
}
return resp[0], nil
}

View File

@@ -0,0 +1,266 @@
package okx
import (
"errors"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/thrasher-corp/gocryptotrader/common"
"github.com/thrasher-corp/gocryptotrader/exchanges/order"
"github.com/thrasher-corp/gocryptotrader/exchanges/request"
"github.com/thrasher-corp/gocryptotrader/exchanges/sharedtestvalues"
)
func TestWSPlaceOrder(t *testing.T) {
t.Parallel()
_, err := ok.WSPlaceOrder(t.Context(), nil)
require.ErrorIs(t, err, common.ErrNilPointer)
sharedtestvalues.SkipTestIfCredentialsUnset(t, ok, canManipulateRealOrders)
out := &PlaceOrderRequestParam{
InstrumentID: btcusdt,
TradeMode: TradeModeIsolated, // depending on portfolio settings this can also be TradeModeCash
Side: "Buy",
OrderType: "post_only",
Amount: 0.0001,
Price: 20000,
Currency: "USDT",
}
got, err := ok.WSPlaceOrder(request.WithVerbose(t.Context()), out)
require.NoError(t, err)
require.NotEmpty(t, got)
}
func TestWSPlaceMultipleOrders(t *testing.T) {
t.Parallel()
_, err := ok.WSPlaceMultipleOrders(t.Context(), nil)
require.ErrorIs(t, err, order.ErrSubmissionIsNil)
_, err = ok.WSPlaceMultipleOrders(t.Context(), []PlaceOrderRequestParam{{}})
require.ErrorIs(t, err, errMissingInstrumentID)
sharedtestvalues.SkipTestIfCredentialsUnset(t, ok, canManipulateRealOrders)
out := PlaceOrderRequestParam{
InstrumentID: btcusdt,
TradeMode: TradeModeIsolated, // depending on portfolio settings this can also be TradeModeCash
Side: "Buy",
OrderType: "post_only",
Amount: 0.0001,
Price: 20000,
Currency: "USDT",
}
got, err := ok.WSPlaceMultipleOrders(request.WithVerbose(t.Context()), []PlaceOrderRequestParam{out})
require.NoError(t, err)
require.NotEmpty(t, got)
}
func TestWSCancelOrder(t *testing.T) {
t.Parallel()
_, err := ok.WSCancelOrder(t.Context(), nil)
require.ErrorIs(t, err, common.ErrNilPointer)
_, err = ok.WSCancelOrder(t.Context(), &CancelOrderRequestParam{})
require.ErrorIs(t, err, errMissingInstrumentID)
_, err = ok.WSCancelOrder(t.Context(), &CancelOrderRequestParam{InstrumentID: btcusdt})
require.ErrorIs(t, err, order.ErrOrderIDNotSet)
sharedtestvalues.SkipTestIfCredentialsUnset(t, ok, canManipulateRealOrders)
got, err := ok.WSCancelOrder(request.WithVerbose(t.Context()), &CancelOrderRequestParam{InstrumentID: btcusdt, OrderID: "2341161427393388544"})
require.NoError(t, err)
require.NotEmpty(t, got)
}
func TestWSCancelMultipleOrders(t *testing.T) {
t.Parallel()
_, err := ok.WSCancelMultipleOrders(t.Context(), nil)
require.ErrorIs(t, err, order.ErrSubmissionIsNil)
_, err = ok.WSCancelMultipleOrders(t.Context(), []CancelOrderRequestParam{{}})
require.ErrorIs(t, err, errMissingInstrumentID)
_, err = ok.WSCancelMultipleOrders(t.Context(), []CancelOrderRequestParam{{InstrumentID: btcusdt}})
require.ErrorIs(t, err, order.ErrOrderIDNotSet)
sharedtestvalues.SkipTestIfCredentialsUnset(t, ok, canManipulateRealOrders)
got, err := ok.WSCancelMultipleOrders(request.WithVerbose(t.Context()), []CancelOrderRequestParam{{InstrumentID: btcusdt, OrderID: "2341184920998715392"}})
require.NoError(t, err)
require.NotEmpty(t, got)
}
func TestWSAmendOrder(t *testing.T) {
t.Parallel()
_, err := ok.WSAmendOrder(t.Context(), nil)
require.ErrorIs(t, err, common.ErrNilPointer)
out := &AmendOrderRequestParams{}
_, err = ok.WSAmendOrder(t.Context(), out)
require.ErrorIs(t, err, errMissingInstrumentID)
out.InstrumentID = btcusdt
_, err = ok.WSAmendOrder(t.Context(), out)
require.ErrorIs(t, err, order.ErrOrderIDNotSet)
out.OrderID = "2341200629875154944"
_, err = ok.WSAmendOrder(t.Context(), out)
require.ErrorIs(t, err, errInvalidNewSizeOrPriceInformation)
sharedtestvalues.SkipTestIfCredentialsUnset(t, ok, canManipulateRealOrders)
out.NewPrice = 21000
got, err := ok.WSAmendOrder(request.WithVerbose(t.Context()), out)
require.NoError(t, err)
require.NotEmpty(t, got)
}
func TestWSAmendMultipleOrders(t *testing.T) {
t.Parallel()
_, err := ok.WSAmendMultipleOrders(t.Context(), nil)
require.ErrorIs(t, err, order.ErrSubmissionIsNil)
out := AmendOrderRequestParams{}
_, err = ok.WSAmendMultipleOrders(t.Context(), []AmendOrderRequestParams{out})
require.ErrorIs(t, err, errMissingInstrumentID)
out.InstrumentID = btcusdt
_, err = ok.WSAmendMultipleOrders(t.Context(), []AmendOrderRequestParams{out})
require.ErrorIs(t, err, order.ErrOrderIDNotSet)
out.OrderID = "2341200629875154944"
_, err = ok.WSAmendMultipleOrders(t.Context(), []AmendOrderRequestParams{out})
require.ErrorIs(t, err, errInvalidNewSizeOrPriceInformation)
sharedtestvalues.SkipTestIfCredentialsUnset(t, ok, canManipulateRealOrders)
out.NewPrice = 20000
got, err := ok.WSAmendMultipleOrders(request.WithVerbose(t.Context()), []AmendOrderRequestParams{out})
require.NoError(t, err)
require.NotEmpty(t, got)
}
func TestWSMassCancelOrders(t *testing.T) {
t.Parallel()
err := ok.WSMassCancelOrders(t.Context(), nil)
require.ErrorIs(t, err, order.ErrSubmissionIsNil)
err = ok.WSMassCancelOrders(t.Context(), []CancelMassReqParam{{}})
require.ErrorIs(t, err, errInvalidInstrumentType)
err = ok.WSMassCancelOrders(t.Context(), []CancelMassReqParam{{InstrumentType: "OPTION"}})
require.ErrorIs(t, err, errInstrumentFamilyRequired)
sharedtestvalues.SkipTestIfCredentialsUnset(t, ok, canManipulateRealOrders)
err = ok.WSMassCancelOrders(request.WithVerbose(t.Context()), []CancelMassReqParam{
{
InstrumentType: "OPTION",
InstrumentFamily: "BTC-USD",
},
})
require.NoError(t, err)
}
func TestWSPlaceSpreadOrder(t *testing.T) {
t.Parallel()
_, err := ok.WSPlaceSpreadOrder(t.Context(), nil)
require.ErrorIs(t, err, common.ErrNilPointer)
sharedtestvalues.SkipTestIfCredentialsUnset(t, ok, canManipulateRealOrders)
result, err := ok.WSPlaceSpreadOrder(request.WithVerbose(t.Context()), &SpreadOrderParam{
SpreadID: "BTC-USDT_BTC-USDT-SWAP",
ClientOrderID: "b15",
Side: order.Buy.Lower(),
OrderType: "limit",
Price: 2.15,
Size: 2,
})
require.NoError(t, err)
assert.NotNil(t, result)
}
func TestWSAmendSpreadOrder(t *testing.T) {
t.Parallel()
_, err := ok.WSAmendSpreadOrder(t.Context(), nil)
require.ErrorIs(t, err, common.ErrNilPointer)
_, err = ok.WSAmendSpreadOrder(t.Context(), &AmendSpreadOrderParam{NewSize: 2})
require.ErrorIs(t, err, order.ErrOrderIDNotSet)
_, err = ok.WSAmendSpreadOrder(t.Context(), &AmendSpreadOrderParam{OrderID: "2510789768709120"})
require.ErrorIs(t, err, errSizeOrPriceIsRequired)
sharedtestvalues.SkipTestIfCredentialsUnset(t, ok, canManipulateRealOrders)
result, err := ok.WSAmendSpreadOrder(request.WithVerbose(t.Context()), &AmendSpreadOrderParam{
OrderID: "2510789768709120",
NewSize: 2,
})
require.NoError(t, err)
assert.NotNil(t, result)
}
func TestWSCancelSpreadOrder(t *testing.T) {
t.Parallel()
_, err := ok.WSCancelSpreadOrder(t.Context(), "", "")
require.ErrorIs(t, err, order.ErrOrderIDNotSet)
sharedtestvalues.SkipTestIfCredentialsUnset(t, ok, canManipulateRealOrders)
result, err := ok.WSCancelSpreadOrder(request.WithVerbose(t.Context()), "1234", "")
require.NoError(t, err)
assert.NotNil(t, result)
}
func TestWSCancelAllSpreadOrders(t *testing.T) {
t.Parallel()
sharedtestvalues.SkipTestIfCredentialsUnset(t, ok, canManipulateRealOrders)
err := ok.WSCancelAllSpreadOrders(request.WithVerbose(t.Context()), "BTC-USDT_BTC-USDT-SWAP")
require.NoError(t, err)
}
type mockHasError struct {
err error
}
func (m *mockHasError) Error() error {
return m.err
}
func TestParseWSResponseErrors(t *testing.T) {
t.Parallel()
require.Panics(t, func() { _ = parseWSResponseErrors(123, nil) }, "result must be a pointer")
require.Panics(t, func() { _ = parseWSResponseErrors(&mockHasError{}, nil) }, "result must be a slice")
var emptySlice []*mockHasError
require.NoError(t, parseWSResponseErrors(&emptySlice, nil))
require.ErrorIs(t, parseWSResponseErrors(&emptySlice, errOperationFailed), errOperationFailed)
err1 := errors.New("error 1")
err2 := errors.New("error 2")
mockSlice := []*mockHasError{{err: nil}, {err: err1}, {err: err2}}
err := parseWSResponseErrors(&mockSlice, errPartialSuccess)
require.ErrorIs(t, err, errPartialSuccess)
require.ErrorIs(t, err, err1)
require.ErrorIs(t, err, err2)
}
func TestSingleItem(t *testing.T) {
t.Parallel()
_, err := singleItem([]*any(nil))
require.ErrorIs(t, err, common.ErrNoResponse)
_, err = singleItem([]*mockHasError{{}, {}})
require.ErrorIs(t, err, errMultipleItemsReturned)
got, err := singleItem([]*mockHasError{{}})
require.NoError(t, err)
require.NotNil(t, got)
}