From 61fc778818152c445e37c8f4d1c4f878a9e784db Mon Sep 17 00:00:00 2001 From: Ryan O'Hara-Reid Date: Wed, 14 May 2025 13:37:41 +1000 Subject: [PATCH] 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 * Update exchanges/okx/ws_requests.go Co-authored-by: Adrian Gallagher * Update exchanges/okx/ws_requests.go Co-authored-by: Adrian Gallagher * Update exchanges/okx/okx.go Co-authored-by: Adrian Gallagher * Update exchanges/okx/okx.go Co-authored-by: Adrian Gallagher * thrasher-: nits! --------- Co-authored-by: Ryan O'Hara-Reid Co-authored-by: Adrian Gallagher --- exchanges/okx/helpers.go | 37 - exchanges/okx/okx.go | 87 +- exchanges/okx/okx_business_websocket.go | 58 +- exchanges/okx/okx_test.go | 770 +++------------ exchanges/okx/okx_types.go | 257 +++-- exchanges/okx/okx_websocket.go | 1185 +---------------------- exchanges/okx/okx_wrapper.go | 143 +-- exchanges/okx/ratelimit.go | 9 +- exchanges/okx/ratelimiter_test.go | 4 +- exchanges/okx/ws_requests.go | 344 +++++++ exchanges/okx/ws_requests_test.go | 266 +++++ 11 files changed, 988 insertions(+), 2172 deletions(-) create mode 100644 exchanges/okx/ws_requests.go create mode 100644 exchanges/okx/ws_requests_test.go diff --git a/exchanges/okx/helpers.go b/exchanges/okx/helpers.go index 6f79a4ae..18c79a19 100644 --- a/exchanges/okx/helpers.go +++ b/exchanges/okx/helpers.go @@ -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 { diff --git a/exchanges/okx/okx.go b/exchanges/okx/okx.go index 84abe861..dda02c7e 100644 --- a/exchanges/okx/okx.go +++ b/exchanges/okx/okx.go @@ -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) +} diff --git a/exchanges/okx/okx_business_websocket.go b/exchanges/okx/okx_business_websocket.go index 65a6d090..d096e3b7 100644 --- a/exchanges/okx/okx_business_websocket.go +++ b/exchanges/okx/okx_business_websocket.go @@ -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 diff --git a/exchanges/okx/okx_test.go b/exchanges/okx/okx_test.go index 20fcaab2..c14c51ef 100644 --- a/exchanges/okx/okx_test.go +++ b/exchanges/okx/okx_test.go @@ -45,6 +45,8 @@ const ( passphrase = "" canManipulateRealOrders = false useTestNet = false + + btcusdt = "BTC-USDT" ) var ( @@ -162,11 +164,7 @@ func syncLeadTraderUniqueID() { // contextGenerate sends an optional value to allow test requests // named this way, so it shows up in auto-complete and reminds you to use it func contextGenerate() context.Context { - ctx := context.Background() - if useTestNet { - ctx = context.WithValue(ctx, testNetKey("testnet"), useTestNet) - } - return ctx + return context.WithValue(context.Background(), testNetVal, useTestNet) } func TestGetTickers(t *testing.T) { @@ -337,7 +335,7 @@ func TestGetBlockTicker(t *testing.T) { _, err := ok.GetBlockTicker(contextGenerate(), "") require.ErrorIs(t, err, errMissingInstrumentID) - result, err := ok.GetBlockTicker(contextGenerate(), "BTC-USDT") + result, err := ok.GetBlockTicker(contextGenerate(), btcusdt) require.NoError(t, err) assert.NotNil(t, result) } @@ -347,11 +345,11 @@ func TestGetBlockTrade(t *testing.T) { _, err := ok.GetPublicBlockTrades(contextGenerate(), "") require.ErrorIs(t, err, errMissingInstrumentID) - trades, err := ok.GetPublicBlockTrades(contextGenerate(), "BTC-USDT") + trades, err := ok.GetPublicBlockTrades(contextGenerate(), btcusdt) require.NoError(t, err) if assert.NotEmpty(t, trades, "Should get some block trades") { trade := trades[0] - assert.Equal(t, "BTC-USDT", trade.InstrumentID, "InstrumentID should have correct value") + assert.Equal(t, btcusdt, trade.InstrumentID, "InstrumentID should have correct value") assert.NotEmpty(t, trade.TradeID, "TradeID should not be empty") assert.Positive(t, trade.Price, "Price should have a positive value") assert.Positive(t, trade.Size, "Size should have a positive value") @@ -431,23 +429,23 @@ func TestGetInstrument(t *testing.T) { func TestGetDeliveryHistory(t *testing.T) { t.Parallel() - _, err := ok.GetDeliveryHistory(contextGenerate(), "", "BTC-USDT", "", time.Time{}, time.Time{}, 3) + _, err := ok.GetDeliveryHistory(contextGenerate(), "", btcusdt, "", time.Time{}, time.Time{}, 3) require.ErrorIs(t, err, errInvalidInstrumentType) _, err = ok.GetDeliveryHistory(contextGenerate(), instTypeFutures, "", "", time.Time{}, time.Time{}, 3) require.ErrorIs(t, err, errInstrumentFamilyOrUnderlyingRequired) - _, err = ok.GetDeliveryHistory(contextGenerate(), instTypeFutures, "BTC-USDT", "", time.Time{}, time.Time{}, 345) + _, err = ok.GetDeliveryHistory(contextGenerate(), instTypeFutures, btcusdt, "", time.Time{}, time.Time{}, 345) require.ErrorIs(t, err, errLimitValueExceedsMaxOf100) - result, err := ok.GetDeliveryHistory(contextGenerate(), instTypeFutures, "BTC-USDT", "", time.Time{}, time.Time{}, 3) + result, err := ok.GetDeliveryHistory(contextGenerate(), instTypeFutures, btcusdt, "", time.Time{}, time.Time{}, 3) require.NoError(t, err) assert.NotNil(t, result) } func TestGetOpenInterestData(t *testing.T) { t.Parallel() - _, err := ok.GetOpenInterestData(contextGenerate(), "", "BTC-USDT", "", "") + _, err := ok.GetOpenInterestData(contextGenerate(), "", btcusdt, "", "") require.ErrorIs(t, err, errInvalidInstrumentType) _, err = ok.GetOpenInterestData(contextGenerate(), instTypeOption, "", "", "") @@ -461,6 +459,32 @@ func TestGetOpenInterestData(t *testing.T) { assert.NotNil(t, result) } +// Only being used for testing purposes and unexported +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) +} + func TestGetSingleFundingRate(t *testing.T) { t.Parallel() _, err := ok.GetSingleFundingRate(contextGenerate(), "") @@ -546,7 +570,7 @@ func TestGetLiquidationOrders(t *testing.T) { func TestGetMarkPrice(t *testing.T) { t.Parallel() - _, err := ok.GetMarkPrice(contextGenerate(), "", "", "", "BTC-USDT") + _, err := ok.GetMarkPrice(contextGenerate(), "", "", "", btcusdt) require.ErrorIs(t, err, errInvalidInstrumentType) result, err := ok.GetMarkPrice(contextGenerate(), "MARGIN", "", "", "") @@ -556,19 +580,19 @@ func TestGetMarkPrice(t *testing.T) { func TestGetPositionTiers(t *testing.T) { t.Parallel() - _, err := ok.GetPositionTiers(contextGenerate(), "", "cross", "BTC-USDT", "", "", "", currency.ETH) + _, err := ok.GetPositionTiers(contextGenerate(), "", "cross", btcusdt, "", "", "", currency.ETH) require.ErrorIs(t, err, errInvalidInstrumentType) - _, err = ok.GetPositionTiers(contextGenerate(), instTypeFutures, "", "BTC-USDT", "", "", "", currency.ETH) + _, err = ok.GetPositionTiers(contextGenerate(), instTypeFutures, "", btcusdt, "", "", "", currency.ETH) require.ErrorIs(t, err, errInvalidTradeMode) _, err = ok.GetPositionTiers(contextGenerate(), instTypeFutures, "cross", "", "", "", "", currency.EMPTYCODE) require.ErrorIs(t, err, errInstrumentFamilyOrUnderlyingRequired) - _, err = ok.GetPositionTiers(contextGenerate(), instTypeFutures, "cross", "BTC-USDT", "", "", "", currency.EMPTYCODE) + _, err = ok.GetPositionTiers(contextGenerate(), instTypeFutures, "cross", btcusdt, "", "", "", currency.EMPTYCODE) require.ErrorIs(t, err, errEitherInstIDOrCcyIsRequired) - result, err := ok.GetPositionTiers(contextGenerate(), instTypeFutures, "cross", "BTC-USDT", "", "", "", currency.ETH) + result, err := ok.GetPositionTiers(contextGenerate(), instTypeFutures, "cross", btcusdt, "", "", "", currency.ETH) require.NoError(t, err) assert.NotNil(t, result) } @@ -611,7 +635,7 @@ func TestGetInsuranceFundInformation(t *testing.T) { _, err = ok.GetInsuranceFundInformation(contextGenerate(), arg) require.ErrorIs(t, err, errInstrumentFamilyOrUnderlyingRequired) - arg.Underlying = "BTC-USDT" + arg.Underlying = btcusdt r, err := ok.GetInsuranceFundInformation(contextGenerate(), arg) assert.NoError(t, err) assert.Positive(t, r.Total, "Total should be positive") @@ -624,7 +648,7 @@ func TestGetInsuranceFundInformation(t *testing.T) { r, err = ok.GetInsuranceFundInformation(contextGenerate(), &InsuranceFundInformationRequestParams{ InstrumentType: instTypeFutures, - Underlying: "BTC-USDT", + Underlying: btcusdt, Limit: 2, }) assert.NoError(t, err) @@ -767,7 +791,7 @@ func TestPlaceOrder(t *testing.T) { require.ErrorIs(t, err, order.ErrAmountBelowMin) arg.Amount = 1 - arg.QuantityType = "abcd" + arg.TargetCurrency = "abcd" _, err = ok.PlaceOrder(contextGenerate(), arg) require.ErrorIs(t, err, errCurrencyQuantityTypeRequired) @@ -860,7 +884,7 @@ func TestCancelSingleOrder(t *testing.T) { require.ErrorIs(t, err, common.ErrEmptyParams) _, err = ok.CancelSingleOrder(contextGenerate(), &CancelOrderRequestParam{OrderID: "12321312312"}) require.ErrorIs(t, err, errMissingInstrumentID) - _, err = ok.CancelSingleOrder(contextGenerate(), &CancelOrderRequestParam{InstrumentID: "BTC-USDT"}) + _, err = ok.CancelSingleOrder(contextGenerate(), &CancelOrderRequestParam{InstrumentID: btcusdt}) require.ErrorIs(t, err, order.ErrOrderIDNotSet) sharedtestvalues.SkipTestIfCredentialsUnset(t, ok, canManipulateRealOrders) @@ -958,7 +982,7 @@ func TestClosePositions(t *testing.T) { require.ErrorIs(t, err, common.ErrEmptyParams) _, err = ok.ClosePositions(contextGenerate(), &ClosePositionsRequestParams{MarginMode: "cross"}) require.ErrorIs(t, err, errMissingInstrumentID) - _, err = ok.ClosePositions(contextGenerate(), &ClosePositionsRequestParams{InstrumentID: "BTC-USDT", MarginMode: "abc"}) + _, err = ok.ClosePositions(contextGenerate(), &ClosePositionsRequestParams{InstrumentID: btcusdt, MarginMode: "abc"}) require.ErrorIs(t, err, margin.ErrMarginTypeUnsupported) sharedtestvalues.SkipTestIfCredentialsUnset(t, ok, canManipulateRealOrders) @@ -977,7 +1001,7 @@ func TestGetOrderDetail(t *testing.T) { assert.ErrorIs(t, err, common.ErrEmptyParams) _, err = ok.GetOrderDetail(contextGenerate(), &OrderDetailRequestParam{OrderID: "1234"}) assert.ErrorIs(t, err, errMissingInstrumentID) - _, err = ok.GetOrderDetail(contextGenerate(), &OrderDetailRequestParam{InstrumentID: "BTC-USDT"}) + _, err = ok.GetOrderDetail(contextGenerate(), &OrderDetailRequestParam{InstrumentID: btcusdt}) assert.ErrorIs(t, err, order.ErrOrderIDNotSet) sharedtestvalues.SkipTestIfCredentialsUnset(t, ok) @@ -1111,7 +1135,7 @@ func TestStopOrder(t *testing.T) { result, err := ok.PlaceStopOrder(contextGenerate(), &AlgoOrderParams{ AlgoClientOrderID: "681096944655273984", TakeProfitTriggerPriceType: "index", - InstrumentID: "BTC-USDT", + InstrumentID: btcusdt, OrderType: "conditional", Side: order.Sell.Lower(), TradeMode: "isolated", @@ -1139,7 +1163,7 @@ func TestPlaceIcebergOrder(t *testing.T) { result, err := ok.PlaceIcebergOrder(contextGenerate(), &AlgoOrderParams{ AlgoClientOrderID: "681096944655273984", LimitPrice: 100.22, SizeLimit: 9999.9, - PriceSpread: 0.04, InstrumentID: "BTC-USDT", + PriceSpread: 0.04, InstrumentID: btcusdt, OrderType: "iceberg", Side: order.Buy.Lower(), TradeMode: "isolated", Size: 6, }) @@ -1168,7 +1192,7 @@ func TestPlaceTWAPOrder(t *testing.T) { sharedtestvalues.SkipTestIfCredentialsUnset(t, ok, canManipulateRealOrders) result, err := ok.PlaceTWAPOrder(contextGenerate(), &AlgoOrderParams{ AlgoClientOrderID: "681096944655273984", - InstrumentID: "BTC-USDT", + InstrumentID: btcusdt, LimitPrice: 100.22, SizeLimit: 9999.9, OrderType: "twap", @@ -1204,7 +1228,7 @@ func TestPlaceTakeProfitStopLossOrder(t *testing.T) { StopLossTriggerPrice: 1234, StopLossTriggerPriceType: "last", AlgoClientOrderID: "681096944655273984", - InstrumentID: "BTC-USDT", + InstrumentID: btcusdt, LimitPrice: 100.22, SizeLimit: 9999.9, PriceSpread: 0.4, @@ -1237,7 +1261,7 @@ func TestPlaceChaseAlgoOrder(t *testing.T) { _, err = ok.PlaceChaseAlgoOrder(contextGenerate(), arg) require.ErrorIs(t, err, errMissingInstrumentID) - arg.InstrumentID = "BTC-USDT" + arg.InstrumentID = btcusdt _, err = ok.PlaceChaseAlgoOrder(contextGenerate(), arg) require.ErrorIs(t, err, errInvalidTradeModeValue) @@ -1253,7 +1277,7 @@ func TestPlaceChaseAlgoOrder(t *testing.T) { sharedtestvalues.SkipTestIfCredentialsUnset(t, ok, canManipulateRealOrders) result, err := ok.PlaceChaseAlgoOrder(contextGenerate(), &AlgoOrderParams{ AlgoClientOrderID: "681096944655273984", - InstrumentID: "BTC-USDT", + InstrumentID: btcusdt, LimitPrice: 100.22, OrderType: "chase", TradeMode: "cross", @@ -1286,7 +1310,7 @@ func TestTriggerAlgoOrder(t *testing.T) { AlgoClientOrderID: "681096944655273984", TriggerPriceType: "mark", TriggerPrice: 1234, - InstrumentID: "BTC-USDT", + InstrumentID: btcusdt, OrderType: "trigger", Side: order.Buy.Lower(), TradeMode: "cross", @@ -1309,7 +1333,7 @@ func TestPlaceTrailingStopOrder(t *testing.T) { sharedtestvalues.SkipTestIfCredentialsUnset(t, ok, canManipulateRealOrders) result, err := ok.PlaceTrailingStopOrder(contextGenerate(), &AlgoOrderParams{ AlgoClientOrderID: "681096944655273984", CallbackRatio: 0.01, - InstrumentID: "BTC-USDT", OrderType: "move_order_stop", + InstrumentID: btcusdt, OrderType: "move_order_stop", Side: order.Buy.Lower(), TradeMode: "isolated", Size: 2, ActivePrice: 1234, }) @@ -1327,7 +1351,7 @@ func TestCancelAlgoOrder(t *testing.T) { _, err = ok.CancelAlgoOrder(contextGenerate(), []AlgoOrderCancelParams{arg}) require.ErrorIs(t, err, errMissingInstrumentID) - arg.InstrumentID = "BTC-USDT" + arg.InstrumentID = btcusdt arg.AlgoOrderID = "" _, err = ok.CancelAlgoOrder(contextGenerate(), []AlgoOrderCancelParams{arg}) require.ErrorIs(t, err, order.ErrOrderIDNotSet) @@ -1335,7 +1359,7 @@ func TestCancelAlgoOrder(t *testing.T) { sharedtestvalues.SkipTestIfCredentialsUnset(t, ok, canManipulateRealOrders) result, err := ok.CancelAlgoOrder(contextGenerate(), []AlgoOrderCancelParams{ { - InstrumentID: "BTC-USDT", + InstrumentID: btcusdt, AlgoOrderID: "90994943", }, }) @@ -1356,7 +1380,7 @@ func TestCancelAdvanceAlgoOrder(t *testing.T) { sharedtestvalues.SkipTestIfCredentialsUnset(t, ok, canManipulateRealOrders) result, err := ok.CancelAdvanceAlgoOrder(contextGenerate(), []AlgoOrderCancelParams{{ - InstrumentID: "BTC-USDT", + InstrumentID: btcusdt, AlgoOrderID: "90994943", }}) require.NoError(t, err) @@ -2260,7 +2284,7 @@ func TestSetLeverageRate(t *testing.T) { Currency: currency.USDT, Leverage: 5, MarginMode: "isolated", - InstrumentID: "BTC-USDT", + InstrumentID: btcusdt, AssetType: asset.Futures, }) require.ErrorIs(t, err, order.ErrSideIsInvalid) @@ -2270,7 +2294,7 @@ func TestSetLeverageRate(t *testing.T) { Currency: currency.USDT, Leverage: 5, MarginMode: "cross", - InstrumentID: "BTC-USDT", + InstrumentID: btcusdt, }) assert.True(t, err == nil || errors.Is(err, common.ErrNoResponse)) } @@ -2279,11 +2303,11 @@ func TestGetMaximumBuySellAmountOROpenAmount(t *testing.T) { t.Parallel() _, err := ok.GetMaximumBuySellAmountOROpenAmount(contextGenerate(), currency.BTC, "", "cross", "", 5, true) require.ErrorIs(t, err, errMissingInstrumentID) - _, err = ok.GetMaximumBuySellAmountOROpenAmount(contextGenerate(), currency.BTC, "BTC-USDT", "", "", 5, true) + _, err = ok.GetMaximumBuySellAmountOROpenAmount(contextGenerate(), currency.BTC, btcusdt, "", "", 5, true) require.ErrorIs(t, err, errInvalidTradeModeValue) sharedtestvalues.SkipTestIfCredentialsUnset(t, ok) - result, err := ok.GetMaximumBuySellAmountOROpenAmount(contextGenerate(), currency.BTC, "BTC-USDT", "cross", "", 5, true) + result, err := ok.GetMaximumBuySellAmountOROpenAmount(contextGenerate(), currency.BTC, btcusdt, "cross", "", 5, true) require.NoError(t, err) assert.NotNil(t, result) } @@ -2292,11 +2316,11 @@ func TestGetMaximumAvailableTradableAmount(t *testing.T) { t.Parallel() _, err := ok.GetMaximumAvailableTradableAmount(contextGenerate(), currency.BTC, "", "cross", "", true, false, 123) require.ErrorIs(t, err, errMissingInstrumentID) - _, err = ok.GetMaximumAvailableTradableAmount(contextGenerate(), currency.BTC, "BTC-USDT", "", "", true, false, 123) + _, err = ok.GetMaximumAvailableTradableAmount(contextGenerate(), currency.BTC, btcusdt, "", "", true, false, 123) require.ErrorIs(t, err, errInvalidTradeModeValue) sharedtestvalues.SkipTestIfCredentialsUnset(t, ok) - result, err := ok.GetMaximumAvailableTradableAmount(contextGenerate(), currency.BTC, "BTC-USDT", "cross", "", true, false, 123) + result, err := ok.GetMaximumAvailableTradableAmount(contextGenerate(), currency.BTC, btcusdt, "cross", "", true, false, 123) require.NoError(t, err) assert.NotNil(t, result) } @@ -2310,7 +2334,7 @@ func TestIncreaseDecreaseMargin(t *testing.T) { _, err = ok.IncreaseDecreaseMargin(contextGenerate(), arg) require.ErrorIs(t, err, errMissingInstrumentID) - arg.InstrumentID = "BTC-USDT" + arg.InstrumentID = btcusdt _, err = ok.IncreaseDecreaseMargin(contextGenerate(), arg) require.ErrorIs(t, err, order.ErrSideIsInvalid) @@ -2324,7 +2348,7 @@ func TestIncreaseDecreaseMargin(t *testing.T) { sharedtestvalues.SkipTestIfCredentialsUnset(t, ok, canManipulateRealOrders) result, err := ok.IncreaseDecreaseMargin(contextGenerate(), &IncreaseDecreaseMarginInput{ - InstrumentID: "BTC-USDT", + InstrumentID: btcusdt, PositionSide: "long", MarginBalanceType: "add", Amount: 1000, @@ -2338,11 +2362,11 @@ func TestGetLeverageRate(t *testing.T) { t.Parallel() _, err := ok.GetLeverageRate(contextGenerate(), "", "cross", currency.EMPTYCODE) require.ErrorIs(t, err, errMissingInstrumentID) - _, err = ok.GetLeverageRate(contextGenerate(), "BTC-USDT", "", currency.EMPTYCODE) + _, err = ok.GetLeverageRate(contextGenerate(), btcusdt, "", currency.EMPTYCODE) require.ErrorIs(t, err, margin.ErrMarginTypeUnsupported) sharedtestvalues.SkipTestIfCredentialsUnset(t, ok) - result, err := ok.GetLeverageRate(contextGenerate(), "BTC-USDT", "cross", currency.EMPTYCODE) + result, err := ok.GetLeverageRate(contextGenerate(), btcusdt, "cross", currency.EMPTYCODE) require.NoError(t, err) assert.NotNil(t, result) } @@ -2657,13 +2681,13 @@ func TestGetGreeks(t *testing.T) { func TestGetPMLimitation(t *testing.T) { t.Parallel() - _, err := ok.GetPMPositionLimitation(contextGenerate(), "", "BTC-USDT", "") + _, err := ok.GetPMPositionLimitation(contextGenerate(), "", btcusdt, "") require.ErrorIs(t, err, errInvalidInstrumentType) _, err = ok.GetPMPositionLimitation(contextGenerate(), "SWAP", "", "") require.ErrorIs(t, err, errInstrumentFamilyOrUnderlyingRequired) sharedtestvalues.SkipTestIfCredentialsUnset(t, ok) - result, err := ok.GetPMPositionLimitation(contextGenerate(), "SWAP", "BTC-USDT", "") + result, err := ok.GetPMPositionLimitation(contextGenerate(), "SWAP", btcusdt, "") require.NoError(t, err) assert.NotNil(t, result) } @@ -3169,16 +3193,16 @@ func TestGetGridAIParameter(t *testing.T) { err := json.Unmarshal([]byte(gridAIParamJSON), &response) require.NoError(t, err) - _, err = ok.GetGridAIParameter(contextGenerate(), "", "BTC-USDT", "", "") + _, err = ok.GetGridAIParameter(contextGenerate(), "", btcusdt, "", "") require.ErrorIs(t, err, errInvalidAlgoOrderType) _, err = ok.GetGridAIParameter(contextGenerate(), "grid", "", "", "") require.ErrorIs(t, err, errMissingInstrumentID) - _, err = ok.GetGridAIParameter(contextGenerate(), "contract_grid", "BTC-USDT", "", "") + _, err = ok.GetGridAIParameter(contextGenerate(), "contract_grid", btcusdt, "", "") require.ErrorIs(t, err, errMissingRequiredArgumentDirection) - _, err = ok.GetGridAIParameter(contextGenerate(), "grid", "BTC-USDT", "", "12M") + _, err = ok.GetGridAIParameter(contextGenerate(), "grid", btcusdt, "", "12M") require.ErrorIs(t, err, errInvalidDuration) - result, err := ok.GetGridAIParameter(contextGenerate(), "grid", "BTC-USDT", "", "") + result, err := ok.GetGridAIParameter(contextGenerate(), "grid", btcusdt, "", "") require.NoError(t, err) assert.NotNil(t, result) } @@ -3800,7 +3824,7 @@ func TestWithdraw(t *testing.T) { func TestGetPairFromInstrumentID(t *testing.T) { t.Parallel() instruments := []string{ - "BTC-USDT", + btcusdt, "BTC-USDT-SWAP", "BTC-USDT-ER33234", } @@ -4080,7 +4104,7 @@ func TestWSProcessTrades(t *testing.T) { ok := new(Okx) //nolint:govet // Intentional shadow to avoid future copy/paste mistakes require.NoError(t, testexch.Setup(ok), "Test instance Setup must not error") - assets, err := ok.getAssetsFromInstrumentID("BTC-USDT") + assets, err := ok.getAssetsFromInstrumentID(btcusdt) require.NoError(t, err, "getAssetsFromInstrumentID must not error") p := currency.NewPairWithDelimiter("BTC", "USDT", currency.DashDelimiter) @@ -4145,520 +4169,6 @@ func TestWSProcessTrades(t *testing.T) { } } -// ************************** Public Channel Subscriptions ***************************** - -func TestInstrumentsSubscription(t *testing.T) { - t.Parallel() - err := ok.InstrumentsSubscription(contextGenerate(), "subscribe", asset.Spot, currency.NewBTCUSDT()) - assert.NoError(t, err) -} - -func TestTickersSubscription(t *testing.T) { - t.Parallel() - err := ok.TickersSubscription(contextGenerate(), "subscribe", asset.Margin, currency.NewBTCUSDT()) - require.NoError(t, err) - err = ok.TickersSubscription(contextGenerate(), "unsubscribe", asset.Spot, currency.NewBTCUSDT()) - assert.NoError(t, err) -} - -func TestOpenInterestSubscription(t *testing.T) { - t.Parallel() - err := ok.OpenInterestSubscription(contextGenerate(), "subscribe", asset.PerpetualSwap, currency.NewPair(currency.BTC, currency.NewCode("USD-SWAP"))) - assert.NoError(t, err) -} - -func TestCandlesticksSubscription(t *testing.T) { - t.Parallel() - enabled, err := ok.GetEnabledPairs(asset.PerpetualSwap) - require.NoError(t, err) - if len(enabled) == 0 { - t.SkipNow() - } - err = ok.CandlesticksSubscription(contextGenerate(), "subscribe", channelCandle1m, asset.Futures, enabled[0]) - assert.NoError(t, err) -} - -func TestTradesSubscription(t *testing.T) { - t.Parallel() - err := ok.TradesSubscription(contextGenerate(), "subscribe", asset.Spot, currency.NewBTCUSDT()) - assert.NoError(t, err) -} - -func TestEstimatedDeliveryExercisePriceSubscription(t *testing.T) { - t.Parallel() - futuresPairs, err := ok.FetchTradablePairs(contextGenerate(), asset.Futures) - require.NoErrorf(t, err, "%s error while fetching tradable pairs for instrument type %v: %v", ok.Name, asset.Futures, err) - if len(futuresPairs) == 0 { - t.SkipNow() - } - err = ok.EstimatedDeliveryExercisePriceSubscription(contextGenerate(), "subscribe", asset.Futures, futuresPairs[0]) - assert.NoError(t, err) -} - -func TestMarkPriceSubscription(t *testing.T) { - t.Parallel() - futuresPairs, err := ok.FetchTradablePairs(contextGenerate(), asset.Futures) - require.NoErrorf(t, err, "%s error while fetching tradable pairs for instrument type %v: %v", ok.Name, asset.Futures, err) - if len(futuresPairs) == 0 { - t.SkipNow() - } - err = ok.MarkPriceSubscription(contextGenerate(), "subscribe", asset.Futures, futuresPairs[0]) - assert.NoError(t, err) -} - -func TestMarkPriceCandlesticksSubscription(t *testing.T) { - t.Parallel() - enabled, err := ok.GetEnabledPairs(asset.Spot) - require.NoError(t, err) - if len(enabled) == 0 { - t.SkipNow() - } - err = ok.MarkPriceCandlesticksSubscription(contextGenerate(), "subscribe", channelMarkPriceCandle1Y, asset.Futures, enabled[0]) - assert.NoError(t, err) -} - -func TestPriceLimitSubscription(t *testing.T) { - t.Parallel() - err := ok.PriceLimitSubscription(contextGenerate(), "subscribe", asset.PerpetualSwap, currency.NewPairWithDelimiter("BTC", "USD-SWAP", currency.DashDelimiter)) - assert.NoError(t, err) -} - -func TestOrderBooksSubscription(t *testing.T) { - t.Parallel() - enabled, err := ok.GetEnabledPairs(asset.Spot) - require.NoError(t, err) - if len(enabled) == 0 { - t.SkipNow() - } - err = ok.OrderBooksSubscription(contextGenerate(), "subscribe", channelOrderBooks, asset.Futures, enabled[0]) - require.NoError(t, err) - err = ok.OrderBooksSubscription(contextGenerate(), "unsubscribe", channelOrderBooks, asset.Futures, enabled[0]) - assert.NoError(t, err) -} - -func TestOptionSummarySubscription(t *testing.T) { - t.Parallel() - err := ok.OptionSummarySubscription(contextGenerate(), "subscribe", currency.NewPair(currency.SOL, currency.USD)) - require.NoError(t, err) - err = ok.OptionSummarySubscription(contextGenerate(), "unsubscribe", currency.NewPair(currency.SOL, currency.USD)) - assert.NoError(t, err) -} - -func TestFundingRateSubscription(t *testing.T) { - t.Parallel() - err := ok.FundingRateSubscription(contextGenerate(), "subscribe", asset.Spot, currency.NewPair(currency.BTC, currency.NewCode("USDT-SWAP"))) - require.NoError(t, err) - err = ok.FundingRateSubscription(contextGenerate(), "unsubscribe", asset.Spot, currency.NewPair(currency.BTC, currency.NewCode("USDT-SWAP"))) - assert.NoError(t, err) -} - -func TestIndexCandlesticksSubscription(t *testing.T) { - t.Parallel() - err := ok.IndexCandlesticksSubscription(contextGenerate(), "subscribe", channelIndexCandle6M, asset.Spot, currency.NewPair(currency.SOL, currency.USD)) - require.NoError(t, err) - err = ok.IndexCandlesticksSubscription(contextGenerate(), "unsubscribe", channelIndexCandle6M, asset.Spot, currency.NewPair(currency.SOL, currency.USD)) - assert.NoError(t, err) -} - -func TestIndexTickerChannelIndexTickerChannel(t *testing.T) { - t.Parallel() - err := ok.IndexTickerChannel(contextGenerate(), "subscribe", asset.Spot, currency.NewPair(currency.SOL, currency.USD)) - require.NoError(t, err) - err = ok.IndexTickerChannel(contextGenerate(), "unsubscribe", asset.Spot, currency.NewPair(currency.SOL, currency.USD)) - assert.NoError(t, err) -} - -func TestStatusSubscription(t *testing.T) { - t.Parallel() - err := ok.StatusSubscription(contextGenerate(), "subscribe", asset.Spot, currency.NewPair(currency.SOL, currency.USD)) - require.NoError(t, err) - err = ok.StatusSubscription(contextGenerate(), "unsubscribe", asset.Spot, currency.NewPair(currency.SOL, currency.USD)) - assert.NoError(t, err) -} - -func TestPublicStructureBlockTradesSubscription(t *testing.T) { - t.Parallel() - err := ok.PublicStructureBlockTradesSubscription(contextGenerate(), "subscribe", asset.Spot, currency.NewPair(currency.SOL, currency.USD)) - require.NoError(t, err) - err = ok.PublicStructureBlockTradesSubscription(contextGenerate(), "unsubscribe", asset.Spot, currency.NewPair(currency.SOL, currency.USD)) - assert.NoError(t, err) -} - -func TestBlockTickerSubscription(t *testing.T) { - t.Parallel() - err := ok.BlockTickerSubscription(contextGenerate(), "subscribe", asset.Options, currency.NewBTCUSDT()) - require.NoError(t, err) - err = ok.BlockTickerSubscription(contextGenerate(), "unsubscribe", asset.Options, currency.NewBTCUSDT()) - assert.NoError(t, err) -} - -func TestPublicBlockTradesSubscription(t *testing.T) { - t.Parallel() - err := ok.PublicBlockTradesSubscription(contextGenerate(), "subscribe", asset.Options, currency.NewPairWithDelimiter("BTC", "USDT-SWAP", "-")) - require.NoError(t, err) - err = ok.PublicBlockTradesSubscription(contextGenerate(), "unsubscribe", asset.Options, currency.NewPairWithDelimiter("BTC", "USDT-SWAP", "-")) - assert.NoError(t, err) -} - -// ************ Authenticated Websocket endpoints Test ********************************************** - -func TestWsAccountSubscription(t *testing.T) { - t.Parallel() - sharedtestvalues.SkipTestIfCredentialsUnset(t, ok) - err := ok.WsAccountSubscription(contextGenerate(), "subscribe", asset.Spot, currency.NewBTCUSDT()) - assert.NoError(t, err) -} - -func TestWsPlaceOrder(t *testing.T) { - t.Parallel() - _, err := ok.WsPlaceOrder(contextGenerate(), &PlaceOrderRequestParam{}) - require.ErrorIs(t, err, common.ErrNilPointer) - - arg := &PlaceOrderRequestParam{ - ReduceOnly: true, - AssetType: asset.Margin, - } - _, err = ok.WsPlaceOrder(contextGenerate(), arg) - require.ErrorIs(t, err, errMissingInstrumentID) - - arg.InstrumentID = spotTP.String() - _, err = ok.WsPlaceOrder(contextGenerate(), arg) - require.ErrorIs(t, err, order.ErrSideIsInvalid) - - arg.Side = "Buy" - arg.TradeMode = "abc" - _, err = ok.WsPlaceOrder(contextGenerate(), arg) - require.ErrorIs(t, err, errInvalidTradeModeValue) - - arg.TradeMode = "cross" - _, err = ok.WsPlaceOrder(contextGenerate(), arg) - require.ErrorIs(t, err, order.ErrTypeIsInvalid) - - arg.OrderType = order.Limit.String() - _, err = ok.WsPlaceOrder(contextGenerate(), arg) - require.ErrorIs(t, err, order.ErrAmountBelowMin) - - arg.AssetType = asset.Futures - _, err = ok.WsPlaceOrder(contextGenerate(), arg) - require.ErrorIs(t, err, order.ErrSideIsInvalid) - - sharedtestvalues.SkipTestIfCredentialsUnset(t, ok, canManipulateRealOrders) - result, err := ok.WsPlaceOrder(contextGenerate(), &PlaceOrderRequestParam{ - InstrumentID: "BTC-USDC", - TradeMode: "cross", - Side: order.Buy.Lower(), - OrderType: "limit", - Amount: 2.6, - Price: 2.1, - Currency: "BTC", - }) - assert.NoError(t, err) - assert.NotNil(t, result) - - result, err = ok.WsPlaceOrder(contextGenerate(), &PlaceOrderRequestParam{ - InstrumentID: "BTC-USDC", - TradeMode: "cross", - Side: order.Buy.Lower(), - PositionSide: "long", - OrderType: "limit", - Amount: 2.6, - Price: 2.1, - Currency: "BTC", - AssetType: asset.Futures, - }) - require.NoError(t, err) - assert.NotNil(t, result) -} - -func TestWsPlaceMultipleOrder(t *testing.T) { - t.Parallel() - var resp []PlaceOrderRequestParam - err := json.Unmarshal([]byte(placeOrderArgs), &resp) - require.NoError(t, err) - - _, err = ok.WsPlaceMultipleOrders(contextGenerate(), []PlaceOrderRequestParam{}) - require.ErrorIs(t, err, order.ErrSubmissionIsNil) - - arg := PlaceOrderRequestParam{ - ReduceOnly: true, - } - _, err = ok.WsPlaceMultipleOrders(contextGenerate(), []PlaceOrderRequestParam{arg}) - require.ErrorIs(t, err, errMissingInstrumentID) - - arg.InstrumentID = spotTP.String() - _, err = ok.WsPlaceMultipleOrders(contextGenerate(), []PlaceOrderRequestParam{arg}) - require.ErrorIs(t, err, order.ErrSideIsInvalid) - - arg.Side = "buy" - arg.TradeMode = "abc" - _, err = ok.WsPlaceMultipleOrders(contextGenerate(), []PlaceOrderRequestParam{arg}) - require.ErrorIs(t, err, errInvalidTradeModeValue) - - arg.TradeMode = "cross" - _, err = ok.WsPlaceMultipleOrders(contextGenerate(), []PlaceOrderRequestParam{arg}) - require.ErrorIs(t, err, order.ErrTypeIsInvalid) - - arg.OrderType = "limit" - _, err = ok.WsPlaceMultipleOrders(contextGenerate(), []PlaceOrderRequestParam{arg}) - require.ErrorIs(t, err, order.ErrAmountBelowMin) - - arg.AssetType = asset.Futures - _, err = ok.WsPlaceMultipleOrders(contextGenerate(), []PlaceOrderRequestParam{arg}) - require.ErrorIs(t, err, order.ErrSideIsInvalid) - - arg.PositionSide = "long" - _, err = ok.WsPlaceMultipleOrders(contextGenerate(), []PlaceOrderRequestParam{arg}) - require.ErrorIs(t, err, order.ErrAmountBelowMin) - - sharedtestvalues.SkipTestIfCredentialsUnset(t, ok, canManipulateRealOrders) - _, err = ok.WsPlaceMultipleOrders(contextGenerate(), resp) - assert.False(t, (err != nil && !errors.Is(err, errWebsocketStreamNotAuthenticated)), err) -} - -func TestWsCancelOrder(t *testing.T) { - t.Parallel() - _, err := ok.WsCancelOrder(contextGenerate(), nil) - require.ErrorIs(t, err, common.ErrNilPointer) - - _, err = ok.WsCancelOrder(contextGenerate(), &CancelOrderRequestParam{OrderID: "1234"}) - require.ErrorIs(t, err, errMissingInstrumentID) - - _, err = ok.WsCancelOrder(contextGenerate(), &CancelOrderRequestParam{InstrumentID: "BTC-USD-190927"}) - require.ErrorIs(t, err, order.ErrOrderIDNotSet) - - sharedtestvalues.SkipTestIfCredentialsUnset(t, ok, canManipulateRealOrders) - result, err := ok.WsCancelOrder(contextGenerate(), &CancelOrderRequestParam{ - InstrumentID: "BTC-USD-190927", - OrderID: "2510789768709120", - }) - require.NoError(t, err) - assert.NotNil(t, result) -} - -func TestWsCancleMultipleOrder(t *testing.T) { - t.Parallel() - arg := CancelOrderRequestParam{ - OrderID: "2510789768709120", - } - _, err := ok.WsCancelMultipleOrder(contextGenerate(), []CancelOrderRequestParam{arg}) - require.ErrorIs(t, err, errMissingInstrumentID) - - arg.InstrumentID = "DCR-BTC" - arg.OrderID = "" - _, err = ok.WsCancelMultipleOrder(contextGenerate(), []CancelOrderRequestParam{arg}) - require.ErrorIs(t, err, order.ErrOrderIDNotSet) - - sharedtestvalues.SkipTestIfCredentialsUnset(t, ok, canManipulateRealOrders) - result, err := ok.WsCancelMultipleOrder(contextGenerate(), []CancelOrderRequestParam{{ - InstrumentID: "DCR-BTC", - OrderID: "2510789768709120", - }}) - require.NoError(t, err) - assert.NotNil(t, result) -} - -func TestWsAmendOrder(t *testing.T) { - t.Parallel() - _, err := ok.WsAmendOrder(contextGenerate(), nil) - require.ErrorIs(t, err, common.ErrNilPointer) - - arg := &AmendOrderRequestParams{} - _, err = ok.WsAmendOrder(contextGenerate(), arg) - require.ErrorIs(t, err, errMissingInstrumentID) - - arg.InstrumentID = spotTP.String() - _, err = ok.WsAmendOrder(contextGenerate(), arg) - require.ErrorIs(t, err, order.ErrOrderIDNotSet) - - arg.OrderID = "1234" - _, err = ok.WsAmendOrder(contextGenerate(), arg) - require.ErrorIs(t, err, errInvalidNewSizeOrPriceInformation) - - sharedtestvalues.SkipTestIfCredentialsUnset(t, ok, canManipulateRealOrders) - result, err := ok.WsAmendOrder(contextGenerate(), &AmendOrderRequestParams{ - InstrumentID: spotTP.String(), - OrderID: "2510789768709120", - NewQuantity: 1234, - }) - require.NoError(t, err) - assert.NotNil(t, result) -} - -func TestWsAmendMultipleOrders(t *testing.T) { - t.Parallel() - arg := AmendOrderRequestParams{ - CancelOnFail: true, - } - _, err := ok.WsAmendMultipleOrders(contextGenerate(), []AmendOrderRequestParams{arg}) - require.ErrorIs(t, err, errMissingInstrumentID) - - arg.InstrumentID = "DCR-BTC" - _, err = ok.WsAmendMultipleOrders(contextGenerate(), []AmendOrderRequestParams{arg}) - require.ErrorIs(t, err, order.ErrOrderIDNotSet) - - arg.OrderID = "2510789768709120" - _, err = ok.WsAmendMultipleOrders(contextGenerate(), []AmendOrderRequestParams{arg}) - require.ErrorIs(t, err, errInvalidNewSizeOrPriceInformation) - - sharedtestvalues.SkipTestIfCredentialsUnset(t, ok, canManipulateRealOrders) - result, err := ok.WsAmendMultipleOrders(contextGenerate(), []AmendOrderRequestParams{ - { - InstrumentID: "DCR-BTC", - OrderID: "2510789768709120", - NewPrice: 1233324.332, - NewQuantity: 1234, - }, - }) - require.NoError(t, err) - assert.NotNil(t, result) -} - -func TestWsMassCancelOrders(t *testing.T) { - t.Parallel() - _, err := ok.WsMassCancelOrders(contextGenerate(), []CancelMassReqParam{{}}) - require.ErrorIs(t, err, common.ErrEmptyParams) - - _, err = ok.WsMassCancelOrders(contextGenerate(), []CancelMassReqParam{{InstrumentFamily: "BTC-USD"}}) - require.ErrorIs(t, err, errInvalidInstrumentType) - - _, err = ok.WsMassCancelOrders(contextGenerate(), []CancelMassReqParam{{InstrumentType: "OPTION"}}) - require.ErrorIs(t, err, errInstrumentFamilyRequired) - - sharedtestvalues.SkipTestIfCredentialsUnset(t, ok, canManipulateRealOrders) - result, err := ok.WsMassCancelOrders(contextGenerate(), []CancelMassReqParam{ - { - InstrumentType: "OPTION", - InstrumentFamily: "BTC-USD", - }, - }) - require.NoError(t, err) - assert.NotNil(t, result) -} - -func TestWsPositionChannel(t *testing.T) { - t.Parallel() - sharedtestvalues.SkipTestIfCredentialsUnset(t, ok) - err := ok.WsPositionChannel(contextGenerate(), "subscribe", asset.Options, currency.NewPair(currency.USD, currency.BTC)) - assert.NoError(t, err) -} - -func TestBalanceAndPositionSubscription(t *testing.T) { - t.Parallel() - sharedtestvalues.SkipTestIfCredentialsUnset(t, ok) - err := ok.BalanceAndPositionSubscription(contextGenerate(), "subscribe", "1234") - require.NoError(t, err) - err = ok.BalanceAndPositionSubscription(contextGenerate(), "unsubscribe", "1234") - assert.NoError(t, err) -} - -func TestWsOrderChannel(t *testing.T) { - t.Parallel() - sharedtestvalues.SkipTestIfCredentialsUnset(t, ok) - err := ok.WsOrderChannel(contextGenerate(), "subscribe", asset.Margin, currency.NewPair(currency.SOL, currency.USDT), "") - require.NoError(t, err) - err = ok.WsOrderChannel(contextGenerate(), "unsubscribe", asset.Margin, currency.NewPair(currency.SOL, currency.USDT), "") - assert.NoError(t, err) -} - -func TestAlgoOrdersSubscription(t *testing.T) { - t.Parallel() - sharedtestvalues.SkipTestIfCredentialsUnset(t, ok) - err := ok.AlgoOrdersSubscription(contextGenerate(), "subscribe", asset.PerpetualSwap, currency.NewPair(currency.SOL, currency.NewCode("USD-SWAP"))) - require.NoError(t, err) - err = ok.AlgoOrdersSubscription(contextGenerate(), "unsubscribe", asset.PerpetualSwap, currency.NewPair(currency.SOL, currency.NewCode("USD-SWAP"))) - assert.NoError(t, err) -} - -func TestAdvanceAlgoOrdersSubscription(t *testing.T) { - t.Parallel() - sharedtestvalues.SkipTestIfCredentialsUnset(t, ok) - err := ok.AdvanceAlgoOrdersSubscription(contextGenerate(), "subscribe", asset.PerpetualSwap, currency.NewPair(currency.SOL, currency.NewCode("USD-SWAP")), "") - require.NoError(t, err) - err = ok.AdvanceAlgoOrdersSubscription(contextGenerate(), "unsubscribe", asset.PerpetualSwap, currency.NewPair(currency.SOL, currency.NewCode("USD-SWAP")), "") - assert.NoError(t, err) -} - -func TestPositionRiskWarningSubscription(t *testing.T) { - t.Parallel() - sharedtestvalues.SkipTestIfCredentialsUnset(t, ok) - err := ok.PositionRiskWarningSubscription(contextGenerate(), "subscribe", asset.PerpetualSwap, currency.NewPair(currency.SOL, currency.NewCode("USD-SWAP"))) - require.NoError(t, err) - err = ok.PositionRiskWarningSubscription(contextGenerate(), "unsubscribe", asset.PerpetualSwap, currency.NewPair(currency.SOL, currency.NewCode("USD-SWAP"))) - assert.NoError(t, err) -} - -func TestAccountGreeksSubscription(t *testing.T) { - t.Parallel() - sharedtestvalues.SkipTestIfCredentialsUnset(t, ok) - err := ok.AccountGreeksSubscription(contextGenerate(), "subscribe", currency.NewPair(currency.SOL, currency.USD)) - require.NoError(t, err) - err = ok.AccountGreeksSubscription(contextGenerate(), "unsubscribe", currency.NewPair(currency.SOL, currency.USD)) - assert.NoError(t, err) -} - -func TestRFQSubscription(t *testing.T) { - t.Parallel() - sharedtestvalues.SkipTestIfCredentialsUnset(t, ok) - err := ok.RFQSubscription(contextGenerate(), "subscribe", "") - require.NoError(t, err) - err = ok.RFQSubscription(contextGenerate(), "unsubscribe", "") - assert.NoError(t, err) -} - -func TestQuotesSubscription(t *testing.T) { - t.Parallel() - sharedtestvalues.SkipTestIfCredentialsUnset(t, ok) - err := ok.QuotesSubscription(contextGenerate(), "subscribe") - require.NoError(t, err) - err = ok.QuotesSubscription(contextGenerate(), "unsubscribe") - assert.NoError(t, err) -} - -func TestStructureBlockTradesSubscription(t *testing.T) { - t.Parallel() - sharedtestvalues.SkipTestIfCredentialsUnset(t, ok) - err := ok.StructureBlockTradesSubscription(contextGenerate(), "subscribe") - require.NoError(t, err) - err = ok.StructureBlockTradesSubscription(contextGenerate(), "unsubscribe") - assert.NoError(t, err) -} - -func TestSpotGridAlgoOrdersSubscription(t *testing.T) { - t.Parallel() - sharedtestvalues.SkipTestIfCredentialsUnset(t, ok) - err := ok.SpotGridAlgoOrdersSubscription(contextGenerate(), "subscribe", asset.Empty, currency.EMPTYPAIR, "") - require.NoError(t, err) - err = ok.SpotGridAlgoOrdersSubscription(contextGenerate(), "unsubscribe", asset.Empty, currency.EMPTYPAIR, "") - assert.NoError(t, err) -} - -func TestContractGridAlgoOrders(t *testing.T) { - t.Parallel() - sharedtestvalues.SkipTestIfCredentialsUnset(t, ok) - err := ok.ContractGridAlgoOrders(contextGenerate(), "subscribe", asset.Empty, currency.EMPTYPAIR, "") - require.NoError(t, err) - err = ok.ContractGridAlgoOrders(contextGenerate(), "unsubscribe", asset.Empty, currency.EMPTYPAIR, "") - assert.NoError(t, err) -} - -func TestGridPositionsSubscription(t *testing.T) { - t.Parallel() - sharedtestvalues.SkipTestIfCredentialsUnset(t, ok) - err := ok.GridPositionsSubscription(contextGenerate(), "subscribe", "1234") - require.NoError(t, err) - err = ok.GridPositionsSubscription(contextGenerate(), "unsubscribe", "1234") - assert.NoError(t, err) -} - -func TestGridSubOrders(t *testing.T) { - t.Parallel() - sharedtestvalues.SkipTestIfCredentialsUnset(t, ok) - err := ok.GridSubOrders(contextGenerate(), "subscribe", "") - require.NoError(t, err) - err = ok.GridSubOrders(contextGenerate(), "unsubscribe", "") - assert.NoError(t, err) -} - func TestGetServerTime(t *testing.T) { t.Parallel() result, err := ok.GetServerTime(contextGenerate(), asset.Empty) @@ -4994,7 +4504,7 @@ func TestGetFuturesContractDetails(t *testing.T) { require.ErrorIs(t, err, asset.ErrNotSupported) for _, a := range []asset.Item{asset.Futures, asset.PerpetualSwap, asset.Spread} { - result, err := ok.GetFuturesContractDetails(t.Context(), a) + result, err := ok.GetFuturesContractDetails(contextGenerate(), a) require.NoError(t, err) require.NotNil(t, result) } @@ -5021,17 +4531,17 @@ func TestWsProcessOrderbook5(t *testing.T) { func TestGetLeverateEstimatedInfo(t *testing.T) { t.Parallel() - _, err := ok.GetLeverageEstimatedInfo(contextGenerate(), "", "cross", "1", "", "BTC-USDT", currency.BTC) + _, err := ok.GetLeverageEstimatedInfo(contextGenerate(), "", "cross", "1", "", btcusdt, currency.BTC) require.ErrorIs(t, err, errInvalidInstrumentType) - _, err = ok.GetLeverageEstimatedInfo(contextGenerate(), "MARGIN", "", "1", "", "BTC-USDT", currency.BTC) + _, err = ok.GetLeverageEstimatedInfo(contextGenerate(), "MARGIN", "", "1", "", btcusdt, currency.BTC) require.ErrorIs(t, err, margin.ErrMarginTypeUnsupported) - _, err = ok.GetLeverageEstimatedInfo(contextGenerate(), "MARGIN", "cross", "", "", "BTC-USDT", currency.BTC) + _, err = ok.GetLeverageEstimatedInfo(contextGenerate(), "MARGIN", "cross", "", "", btcusdt, currency.BTC) require.ErrorIs(t, err, errInvalidLeverage) - _, err = ok.GetLeverageEstimatedInfo(contextGenerate(), "MARGIN", "cross", "1", "", "BTC-USDT", currency.EMPTYCODE) + _, err = ok.GetLeverageEstimatedInfo(contextGenerate(), "MARGIN", "cross", "1", "", btcusdt, currency.EMPTYCODE) require.ErrorIs(t, err, currency.ErrCurrencyCodeEmpty) sharedtestvalues.SkipTestIfCredentialsUnset(t, ok) - result, err := ok.GetLeverageEstimatedInfo(contextGenerate(), "MARGIN", "cross", "1", "", "BTC-USDT", currency.BTC) + result, err := ok.GetLeverageEstimatedInfo(contextGenerate(), "MARGIN", "cross", "1", "", btcusdt, currency.BTC) require.NoError(t, err) assert.NotNil(t, result) } @@ -5041,20 +4551,20 @@ func TestManualBorrowAndRepayInQuickMarginMode(t *testing.T) { _, err := ok.ManualBorrowAndRepayInQuickMarginMode(contextGenerate(), &BorrowAndRepay{}) require.ErrorIs(t, err, common.ErrEmptyParams) _, err = ok.ManualBorrowAndRepayInQuickMarginMode(contextGenerate(), &BorrowAndRepay{ - InstrumentID: "BTC-USDT", + InstrumentID: btcusdt, LoanCcy: currency.USDT, Side: "borrow", }) require.ErrorIs(t, err, order.ErrAmountBelowMin) _, err = ok.ManualBorrowAndRepayInQuickMarginMode(contextGenerate(), &BorrowAndRepay{ Amount: 1, - InstrumentID: "BTC-USDT", + InstrumentID: btcusdt, Side: "borrow", }) require.ErrorIs(t, err, currency.ErrCurrencyCodeEmpty) _, err = ok.ManualBorrowAndRepayInQuickMarginMode(contextGenerate(), &BorrowAndRepay{ Amount: 1, - InstrumentID: "BTC-USDT", + InstrumentID: btcusdt, LoanCcy: currency.USDT, }) require.ErrorIs(t, err, order.ErrSideIsInvalid) @@ -5068,7 +4578,7 @@ func TestManualBorrowAndRepayInQuickMarginMode(t *testing.T) { sharedtestvalues.SkipTestIfCredentialsUnset(t, ok, canManipulateRealOrders) result, err := ok.ManualBorrowAndRepayInQuickMarginMode(contextGenerate(), &BorrowAndRepay{ Amount: 1, - InstrumentID: "BTC-USDT", + InstrumentID: btcusdt, LoanCcy: currency.USDT, Side: "borrow", }) @@ -5236,7 +4746,7 @@ func TestPreCheckOrder(t *testing.T) { _, err = ok.PreCheckOrder(contextGenerate(), arg) require.ErrorIs(t, err, errMissingInstrumentID) - arg.InstrumentID = "BTC-USDT" + arg.InstrumentID = btcusdt _, err = ok.PreCheckOrder(contextGenerate(), arg) require.ErrorIs(t, err, errInvalidTradeModeValue) @@ -5254,7 +4764,7 @@ func TestPreCheckOrder(t *testing.T) { sharedtestvalues.SkipTestIfCredentialsUnset(t, ok, canManipulateRealOrders) result, err := ok.PreCheckOrder(contextGenerate(), &OrderPreCheckParams{ - InstrumentID: "BTC-USDT", + InstrumentID: btcusdt, TradeMode: "cash", ClientOrderID: "b15", Side: order.Buy.Lower(), @@ -5417,7 +4927,7 @@ func TestRSIBackTesting(t *testing.T) { t.Parallel() _, err := ok.RSIBackTesting(contextGenerate(), "", "", "", 50, 14, kline.FiveMin) require.ErrorIs(t, err, errMissingInstrumentID) - result, err := ok.RSIBackTesting(contextGenerate(), "BTC-USDT", "", "", 50, 14, kline.FiveMin) + result, err := ok.RSIBackTesting(contextGenerate(), btcusdt, "", "", 50, 14, kline.FiveMin) require.NoError(t, err) assert.NotNil(t, result) } @@ -5603,7 +5113,7 @@ func TestGetRecurringSubOrders(t *testing.T) { func TestGetExistingLeadingPositions(t *testing.T) { t.Parallel() sharedtestvalues.SkipTestIfCredentialsUnset(t, ok) - result, err := ok.GetExistingLeadingPositions(contextGenerate(), instTypeSpot, "BTC-USDT", time.Now(), time.Time{}, 0) + result, err := ok.GetExistingLeadingPositions(contextGenerate(), instTypeSpot, btcusdt, time.Now(), time.Time{}, 0) require.NoError(t, err) assert.NotNil(t, result) } @@ -5952,18 +5462,7 @@ func TestCancelSpreadOrder(t *testing.T) { require.ErrorIs(t, err, order.ErrOrderIDNotSet) sharedtestvalues.SkipTestIfCredentialsUnset(t, ok, canManipulateRealOrders) - result, err := ok.CancelSpreadOrder(contextGenerate(), "12345", "") - require.NoError(t, err) - assert.NotNil(t, result) -} - -func TestWsCancelSpreadOrder(t *testing.T) { - t.Parallel() - _, err := ok.WsCancelSpreadOrder(contextGenerate(), "", "") - require.ErrorIs(t, err, order.ErrOrderIDNotSet) - - sharedtestvalues.SkipTestIfCredentialsUnset(t, ok, canManipulateRealOrders) - result, err := ok.WsCancelSpreadOrder(contextGenerate(), "1234", "") + result, err := ok.CancelSpreadOrder(request.WithVerbose(contextGenerate()), "12345", "") require.NoError(t, err) assert.NotNil(t, result) } @@ -5976,14 +5475,6 @@ func TestCancelAllSpreadOrders(t *testing.T) { assert.NotNil(t, result) } -func TestWsCancelAllSpreadOrders(t *testing.T) { - t.Parallel() - sharedtestvalues.SkipTestIfCredentialsUnset(t, ok, canManipulateRealOrders) - result, err := ok.WsCancelAllSpreadOrders(contextGenerate(), "BTC-USDT_BTC-USDT-SWAP") - require.NoError(t, err) - assert.NotNil(t, result) -} - func TestAmendSpreadOrder(t *testing.T) { t.Parallel() _, err := ok.AmendSpreadOrder(contextGenerate(), &AmendSpreadOrderParam{}) @@ -6002,24 +5493,6 @@ func TestAmendSpreadOrder(t *testing.T) { assert.NotNil(t, result) } -func TestWsAmandSpreadOrder(t *testing.T) { - t.Parallel() - _, err := ok.WsAmandSpreadOrder(contextGenerate(), &AmendSpreadOrderParam{}) - require.ErrorIs(t, err, common.ErrEmptyParams) - _, err = ok.WsAmandSpreadOrder(contextGenerate(), &AmendSpreadOrderParam{NewSize: 2}) - require.ErrorIs(t, err, order.ErrOrderIDNotSet) - _, err = ok.WsAmandSpreadOrder(contextGenerate(), &AmendSpreadOrderParam{OrderID: "2510789768709120"}) - require.ErrorIs(t, err, errSizeOrPriceIsRequired) - - sharedtestvalues.SkipTestIfCredentialsUnset(t, ok, canManipulateRealOrders) - result, err := ok.WsAmandSpreadOrder(contextGenerate(), &AmendSpreadOrderParam{ - OrderID: "2510789768709120", - NewSize: 2, - }) - require.NoError(t, err) - assert.NotNil(t, result) -} - func TestGetSpreadOrderDetails(t *testing.T) { t.Parallel() _, err := ok.GetSpreadOrderDetails(contextGenerate(), "", "") @@ -6152,24 +5625,6 @@ func TestGetPublicExchangeList(t *testing.T) { assert.NotNil(t, result) } -func TestWsPlaceSpreadOrder(t *testing.T) { - t.Parallel() - _, err := ok.WsPlaceSpreadOrder(contextGenerate(), &SpreadOrderParam{}) - require.ErrorIs(t, err, common.ErrNilPointer) - - sharedtestvalues.SkipTestIfCredentialsUnset(t, ok, canManipulateRealOrders) - result, err := ok.WsPlaceSpreadOrder(contextGenerate(), &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 TestGetInviteesDetail(t *testing.T) { t.Parallel() _, err := ok.GetInviteesDetail(contextGenerate(), "") @@ -6758,3 +6213,46 @@ func TestMarginTypeToString(t *testing.T) { assert.Equal(t, marginTypeString, marginTypeToStringMap[m]) } } + +func TestValidatePlaceOrderRequestParam(t *testing.T) { + t.Parallel() + var p *PlaceOrderRequestParam + require.ErrorIs(t, p.Validate(), common.ErrNilPointer) + p = &PlaceOrderRequestParam{} + require.ErrorIs(t, p.Validate(), errMissingInstrumentID) + p.InstrumentID = btcusdt + require.ErrorIs(t, p.Validate(), order.ErrSideIsInvalid) + p.Side = order.Buy.String() + p.TradeMode = "abc" + require.ErrorIs(t, p.Validate(), errInvalidTradeModeValue) + p.TradeMode = TradeModeIsolated + p.AssetType = asset.Futures + require.ErrorIs(t, p.Validate(), order.ErrSideIsInvalid) + p.PositionSide = "long" + require.ErrorIs(t, p.Validate(), order.ErrTypeIsInvalid) + p.OrderType = order.Market.String() + require.ErrorIs(t, p.Validate(), order.ErrAmountBelowMin) + p.Amount = 1 + p.TargetCurrency = "moo cows" + require.ErrorIs(t, p.Validate(), errCurrencyQuantityTypeRequired) + p.TargetCurrency = "base_ccy" + require.NoError(t, p.Validate()) +} + +func TestValidateSpreadOrderParam(t *testing.T) { + t.Parallel() + var p *SpreadOrderParam + require.ErrorIs(t, p.Validate(), common.ErrNilPointer) + p = &SpreadOrderParam{} + require.ErrorIs(t, p.Validate(), errMissingInstrumentID) + p.SpreadID = "BTC-USDT_BTC-USDT-SWAP" + require.ErrorIs(t, p.Validate(), order.ErrTypeIsInvalid) + p.OrderType = order.Market.String() + require.ErrorIs(t, p.Validate(), order.ErrAmountBelowMin) + p.Size = 1 + require.ErrorIs(t, p.Validate(), order.ErrPriceBelowMin) + p.Price = 1 + require.ErrorIs(t, p.Validate(), order.ErrSideIsInvalid) + p.Side = order.Buy.String() + require.NoError(t, p.Validate()) +} diff --git a/exchanges/okx/okx_types.go b/exchanges/okx/okx_types.go index 6e43ac8b..b017b871 100644 --- a/exchanges/okx/okx_types.go +++ b/exchanges/okx/okx_types.go @@ -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 diff --git a/exchanges/okx/okx_websocket.go b/exchanges/okx/okx_websocket.go index ce4c3eca..6be2099c 100644 --- a/exchanges/okx/okx_websocket.go +++ b/exchanges/okx/okx_websocket.go @@ -7,11 +7,13 @@ import ( "fmt" "hash/crc32" "net/http" + "slices" "strconv" "strings" "text/template" "time" + "github.com/buger/jsonparser" gws "github.com/gorilla/websocket" "github.com/thrasher-corp/gocryptotrader/common" "github.com/thrasher-corp/gocryptotrader/common/crypto" @@ -31,15 +33,15 @@ import ( "github.com/thrasher-corp/gocryptotrader/types" ) -var ( - candlestickChannelsMap = map[string]bool{channelCandle1Y: true, channelCandle6M: true, channelCandle3M: true, channelCandle1M: true, channelCandle1W: true, channelCandle1D: true, channelCandle2D: true, channelCandle3D: true, channelCandle5D: true, channelCandle12H: true, channelCandle6H: true, channelCandle4H: true, channelCandle2H: true, channelCandle1H: true, channelCandle30m: true, channelCandle15m: true, channelCandle5m: true, channelCandle3m: true, channelCandle1m: true, channelCandle1Yutc: true, channelCandle3Mutc: true, channelCandle1Mutc: true, channelCandle1Wutc: true, channelCandle1Dutc: true, channelCandle2Dutc: true, channelCandle3Dutc: true, channelCandle5Dutc: true, channelCandle12Hutc: true, channelCandle6Hutc: true} - candlesticksMarkPriceMap = map[string]bool{channelMarkPriceCandle1Y: true, channelMarkPriceCandle6M: true, channelMarkPriceCandle3M: true, channelMarkPriceCandle1M: true, channelMarkPriceCandle1W: true, channelMarkPriceCandle1D: true, channelMarkPriceCandle2D: true, channelMarkPriceCandle3D: true, channelMarkPriceCandle5D: true, channelMarkPriceCandle12H: true, channelMarkPriceCandle6H: true, channelMarkPriceCandle4H: true, channelMarkPriceCandle2H: true, channelMarkPriceCandle1H: true, channelMarkPriceCandle30m: true, channelMarkPriceCandle15m: true, channelMarkPriceCandle5m: true, channelMarkPriceCandle3m: true, channelMarkPriceCandle1m: true, channelMarkPriceCandle1Yutc: true, channelMarkPriceCandle3Mutc: true, channelMarkPriceCandle1Mutc: true, channelMarkPriceCandle1Wutc: true, channelMarkPriceCandle1Dutc: true, channelMarkPriceCandle2Dutc: true, channelMarkPriceCandle3Dutc: true, channelMarkPriceCandle5Dutc: true, channelMarkPriceCandle12Hutc: true, channelMarkPriceCandle6Hutc: true} - candlesticksIndexPriceMap = map[string]bool{channelIndexCandle1Y: true, channelIndexCandle6M: true, channelIndexCandle3M: true, channelIndexCandle1M: true, channelIndexCandle1W: true, channelIndexCandle1D: true, channelIndexCandle2D: true, channelIndexCandle3D: true, channelIndexCandle5D: true, channelIndexCandle12H: true, channelIndexCandle6H: true, channelIndexCandle4H: true, channelIndexCandle2H: true, channelIndexCandle1H: true, channelIndexCandle30m: true, channelIndexCandle15m: true, channelIndexCandle5m: true, channelIndexCandle3m: true, channelIndexCandle1m: true, channelIndexCandle1Yutc: true, channelIndexCandle3Mutc: true, channelIndexCandle1Mutc: true, channelIndexCandle1Wutc: true, channelIndexCandle1Dutc: true, channelIndexCandle2Dutc: true, channelIndexCandle3Dutc: true, channelIndexCandle5Dutc: true, channelIndexCandle12Hutc: true, channelIndexCandle6Hutc: true} -) - var ( pingMsg = []byte("ping") pongMsg = []byte("pong") + + // See: https://www.okx.com/docs-v5/en/#error-code-websocket-public + authConnErrorCodes = []string{ + "60007", "60022", "60023", "60024", "60026", "63999", "60032", "60011", "60009", + "60005", "60021", "60031", "50110", + } ) const ( @@ -59,14 +61,6 @@ const ( indexCandlestick = "index-" candle = "candle" - // Spread Order - - // Operations - okxSpreadOrder = "sprd-order" - okxSpreadAmendOrder = "sprd-amend-order" - okxSpreadCancelOrder = "sprd-cancel-order" - okxSpreadCancelAllOrders = "sprd-mass-cancel" - // Subscriptions okxSpreadOrders = "sprd-orders" okxSpreadTrades = "sprd-trades" @@ -127,15 +121,6 @@ const ( channelOptSummary = "opt-summary" channelFundingRate = "funding-rate" - // Websocket trade endpoint operations - okxOpOrder = "order" - okxOpBatchOrders = "batch-orders" - okxOpCancelOrder = "cancel-order" - okxOpBatchCancelOrders = "batch-cancel-orders" - okxOpAmendOrder = "amend-order" - okxOpBatchAmendOrders = "batch-amend-orders" - okxOpMassCancelOrder = "mass-cancel" - // Candlestick lengths channelCandle1Y = candle + "1Y" channelCandle6M = candle + "6M" @@ -316,56 +301,17 @@ func (ok *Okx) WsAuth(ctx context.Context) error { return err } base64Sign := crypto.Base64Encode(hmac) - req := 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.Unset, req) - 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" || data.StatusCode == "60004") { - ok.Websocket.SetCanUseAuthenticatedEndpoints(false) - return fmt.Errorf("%w code: %s message: %s", errWebsocketStreamNotAuthenticated, data.StatusCode, data.Message) - } - continue - case <-timer.C: - timer.Stop() - return fmt.Errorf("%s websocket connection: timeout waiting for response with an operation: %v", - ok.Name, - req.Operation) - } - } + + return ok.SendAuthenticatedWebsocketRequest(ctx, request.Unset, "login-response", operationLogin, args, nil) } // wsReadData sends msgs from public and auth websockets to data handler @@ -397,8 +343,6 @@ func (ok *Okx) Unsubscribe(channelsToUnsubscribe subscription.List) error { func (ok *Okx) handleSubscription(operation string, subs subscription.List) error { reqs := WSSubscriptionInformationList{Operation: operation} authRequests := WSSubscriptionInformationList{Operation: operation} - ok.WsRequestSemaphore <- 1 - defer func() { <-ok.WsRequestSemaphore }() var channels subscription.List var authChannels subscription.List var errs error @@ -484,6 +428,10 @@ func (ok *Okx) handleSubscription(operation string, subs subscription.List) erro // WsHandleData will read websocket raw data and pass to appropriate handler func (ok *Okx) WsHandleData(respRaw []byte) error { + if id, _ := jsonparser.GetString(respRaw, "id"); id != "" { + return ok.Websocket.Match.RequireMatchWithData(id, respRaw) + } + var resp wsIncomingData err := json.Unmarshal(respRaw, &resp) if err != nil { @@ -492,9 +440,8 @@ func (ok *Okx) WsHandleData(respRaw []byte) error { } return fmt.Errorf("%w unmarshalling %v", err, respRaw) } - if (resp.Event != "" && (resp.Event == "login" || resp.Event == "error")) || resp.Operation != "" { - ok.WsResponseMultiplexer.Message <- &resp - return nil + if resp.Event == operationLogin || (resp.Event == "error" && slices.Contains(authConnErrorCodes, resp.StatusCode)) { + return ok.Websocket.Match.RequireMatchWithData("login-response", respRaw) } if len(resp.Data) == 0 { return nil @@ -1556,1092 +1503,6 @@ func (ok *Okx) wsProcessPushData(data []byte, resp any) error { return nil } -// Websocket Trade methods - -// WsPlaceOrder places an order thought the websocket connection stream, and returns a SubmitResponse and error message. -func (ok *Okx) WsPlaceOrder(ctx context.Context, arg *PlaceOrderRequestParam) (*OrderData, error) { - if arg == nil || *arg == (PlaceOrderRequestParam{}) { - return nil, common.ErrNilPointer - } - err := ok.validatePlaceOrderParams(arg) - if err != nil { - return nil, err - } - if !ok.AreCredentialsValid(ctx) || !ok.Websocket.CanUseAuthenticatedEndpoints() { - return nil, errWebsocketStreamNotAuthenticated - } - randomID, err := common.GenerateRandomString(32, common.SmallLetters, common.CapitalLetters, common.NumberCharacters) - if err != nil { - return nil, err - } - input := WsOperationInput{ - ID: randomID, - Arguments: []PlaceOrderRequestParam{*arg}, - Operation: okxOpOrder, - } - err = ok.Websocket.AuthConn.SendJSONMessage(context.TODO(), placeOrderEPL, input) - if err != nil { - return nil, err - } - timer := time.NewTimer(ok.WebsocketResponseMaxLimit) - wsResponse := make(chan *wsIncomingData) - ok.WsResponseMultiplexer.Register <- &wsRequestInfo{ - ID: randomID, - Chan: wsResponse, - } - defer func() { ok.WsResponseMultiplexer.Unregister <- randomID }() - for { - select { - case data := <-wsResponse: - if data.Operation == okxOpOrder && data.ID == input.ID { - var dataHolder *OrderData - err = ok.handleIncomingData(data, &dataHolder) - if err != nil { - return nil, err - } - if data.StatusCode == "1" { - return nil, fmt.Errorf("error code:%s error message: %s", dataHolder.StatusCode, dataHolder.StatusMessage) - } - return dataHolder, nil - } - continue - case <-timer.C: - timer.Stop() - return nil, fmt.Errorf("%s websocket connection: timeout waiting for response with an operation: %v", - ok.Name, - input.Operation) - } - } -} - -// WsPlaceMultipleOrders creates an order through the websocket -func (ok *Okx) WsPlaceMultipleOrders(ctx context.Context, args []PlaceOrderRequestParam) ([]OrderData, error) { - if len(args) == 0 { - return nil, order.ErrSubmissionIsNil - } - var err error - for x := range args { - arg := args[x] - err = ok.validatePlaceOrderParams(&arg) - if err != nil { - return nil, err - } - } - if !ok.AreCredentialsValid(ctx) || !ok.Websocket.CanUseAuthenticatedEndpoints() { - return nil, errWebsocketStreamNotAuthenticated - } - randomID, err := common.GenerateRandomString(4, common.NumberCharacters) - if err != nil { - return nil, err - } - input := WsOperationInput{ - ID: randomID, - Arguments: args, - Operation: okxOpBatchOrders, - } - err = ok.Websocket.AuthConn.SendJSONMessage(context.TODO(), placeMultipleOrdersEPL, input) - if err != nil { - return nil, err - } - timer := time.NewTimer(ok.WebsocketResponseMaxLimit) - wsResponse := make(chan *wsIncomingData) - ok.WsResponseMultiplexer.Register <- &wsRequestInfo{ - ID: randomID, - Chan: wsResponse, - } - defer func() { ok.WsResponseMultiplexer.Unregister <- randomID }() - for { - select { - case data := <-wsResponse: - if data.Operation == okxOpBatchOrders && data.ID == input.ID { - if data.StatusCode == "0" || data.StatusCode == "2" { - var resp *WsPlaceOrderResponse - resp, err = data.copyToPlaceOrderResponse() - if err != nil { - return nil, err - } - return resp.Data, nil - } - var resp WsOrderActionResponse - err = resp.populateFromIncomingData(data) - if err != nil { - return nil, err - } - err = json.Unmarshal(data.Data, &(resp.Data)) - if err != nil { - return nil, err - } - if len(data.Data) == 0 { - return nil, fmt.Errorf("error code:%s error message: %v", data.StatusCode, ErrorCodes[data.StatusCode]) - } - var errs error - for x := range resp.Data { - if resp.Data[x].StatusCode != "0" { - errs = common.AppendError(errs, fmt.Errorf("error code:%s error message: %s", resp.Data[x].StatusCode, resp.Data[x].StatusMessage)) - } - } - return nil, errs - } - continue - case <-timer.C: - timer.Stop() - return nil, fmt.Errorf("%s websocket connection: timeout waiting for response with an operation: %v", - ok.Name, - input.Operation) - } - } -} - -// WsCancelOrder websocket function to cancel a trade order -func (ok *Okx) WsCancelOrder(ctx context.Context, arg *CancelOrderRequestParam) (*OrderData, error) { - if arg == nil || *arg == (CancelOrderRequestParam{}) { - return nil, common.ErrNilPointer - } - if arg.InstrumentID == "" { - return nil, errMissingInstrumentID - } - if arg.OrderID == "" && arg.ClientOrderID == "" { - return nil, order.ErrOrderIDNotSet - } - if !ok.AreCredentialsValid(ctx) || !ok.Websocket.CanUseAuthenticatedEndpoints() { - return nil, errWebsocketStreamNotAuthenticated - } - randomID, err := common.GenerateRandomString(4, common.NumberCharacters) - if err != nil { - return nil, err - } - input := WsOperationInput{ - ID: randomID, - Arguments: []CancelOrderRequestParam{*arg}, - Operation: okxOpCancelOrder, - } - err = ok.Websocket.AuthConn.SendJSONMessage(ctx, cancelOrderEPL, input) - if err != nil { - return nil, err - } - timer := time.NewTimer(ok.WebsocketResponseMaxLimit) - wsResponse := make(chan *wsIncomingData) - ok.WsResponseMultiplexer.Register <- &wsRequestInfo{ - ID: randomID, - Chan: wsResponse, - } - defer func() { ok.WsResponseMultiplexer.Unregister <- randomID }() - for { - select { - case data := <-wsResponse: - if data.Operation == okxOpCancelOrder && data.ID == input.ID { - var dataHolder *OrderData - err = ok.handleIncomingData(data, &dataHolder) - if err != nil { - return nil, err - } - if data.StatusCode == "1" { - return nil, fmt.Errorf("error code:%s error message: %s", dataHolder.StatusCode, dataHolder.StatusMessage) - } - return dataHolder, nil - } - continue - case <-timer.C: - timer.Stop() - return nil, fmt.Errorf("%s websocket connection: timeout waiting for response with an operation: %v", - ok.Name, - input.Operation) - } - } -} - -// WsCancelMultipleOrder cancel multiple order through the websocket channel. -func (ok *Okx) WsCancelMultipleOrder(ctx context.Context, args []CancelOrderRequestParam) ([]OrderData, error) { - for x := range args { - arg := args[x] - if arg.InstrumentID == "" { - return nil, errMissingInstrumentID - } - if arg.OrderID == "" && arg.ClientOrderID == "" { - return nil, order.ErrOrderIDNotSet - } - } - if !ok.AreCredentialsValid(ctx) || !ok.Websocket.CanUseAuthenticatedEndpoints() { - return nil, errWebsocketStreamNotAuthenticated - } - randomID, err := common.GenerateRandomString(4, common.NumberCharacters) - if err != nil { - return nil, err - } - input := WsOperationInput{ - ID: randomID, - Arguments: args, - Operation: okxOpBatchCancelOrders, - } - err = ok.Websocket.AuthConn.SendJSONMessage(context.TODO(), cancelMultipleOrdersEPL, input) - if err != nil { - return nil, err - } - timer := time.NewTimer(ok.WebsocketResponseMaxLimit) - wsResponse := make(chan *wsIncomingData) - ok.WsResponseMultiplexer.Register <- &wsRequestInfo{ - ID: randomID, - Chan: wsResponse, - } - defer func() { ok.WsResponseMultiplexer.Unregister <- randomID }() - for { - select { - case data := <-wsResponse: - if data.Operation == okxOpBatchCancelOrders && data.ID == input.ID { - if data.StatusCode == "0" || data.StatusCode == "2" { - var resp *WsPlaceOrderResponse - resp, err = data.copyToPlaceOrderResponse() - if err != nil { - return nil, err - } - return resp.Data, nil - } - if len(data.Data) == 0 { - return nil, fmt.Errorf("error code:%s error message: %v", data.StatusCode, ErrorCodes[data.StatusCode]) - } - var resp WsOrderActionResponse - err = resp.populateFromIncomingData(data) - if err != nil { - return nil, err - } - err = json.Unmarshal(data.Data, &(resp.Data)) - if err != nil { - return nil, err - } - var errs error - for x := range resp.Data { - if resp.Data[x].StatusCode != "0" { - errs = common.AppendError(errs, fmt.Errorf("error code:%s error message: %v", resp.Data[x].StatusCode, resp.Data[x].StatusMessage)) - } - } - return nil, errs - } - continue - case <-timer.C: - timer.Stop() - return nil, fmt.Errorf("%s websocket connection: timeout waiting for response with an operation: %v", - ok.Name, - input.Operation) - } - } -} - -// WsAmendOrder method to amend trade order using a request thought the websocket channel. -func (ok *Okx) WsAmendOrder(ctx context.Context, arg *AmendOrderRequestParams) (*OrderData, error) { - if arg == nil { - return nil, 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 - } - if !ok.AreCredentialsValid(ctx) || !ok.Websocket.CanUseAuthenticatedEndpoints() { - return nil, errWebsocketStreamNotAuthenticated - } - randomID, err := common.GenerateRandomString(4, common.NumberCharacters) - if err != nil { - return nil, err - } - input := WsOperationInput{ - ID: randomID, - Operation: okxOpAmendOrder, - Arguments: []AmendOrderRequestParams{*arg}, - } - err = ok.Websocket.AuthConn.SendJSONMessage(ctx, amendOrderEPL, input) - if err != nil { - return nil, err - } - timer := time.NewTimer(ok.WebsocketResponseMaxLimit) - wsResponse := make(chan *wsIncomingData) - ok.WsResponseMultiplexer.Register <- &wsRequestInfo{ - ID: randomID, - Chan: wsResponse, - } - defer func() { ok.WsResponseMultiplexer.Unregister <- randomID }() - for { - select { - case data := <-wsResponse: - if data.Operation == okxOpAmendOrder && data.ID == input.ID { - var dataHolder *OrderData - err = ok.handleIncomingData(data, &dataHolder) - if err != nil { - return nil, err - } - if data.StatusCode == "1" { - return nil, fmt.Errorf("error code:%s error message: %s", dataHolder.StatusCode, dataHolder.StatusMessage) - } - return dataHolder, nil - } - continue - case <-timer.C: - timer.Stop() - return nil, fmt.Errorf("%s websocket connection: timeout waiting for response with an operation: %v", - ok.Name, - input.Operation) - } - } -} - -// WsAmendMultipleOrders a request through the websocket connection to amend multiple trade orders. -func (ok *Okx) WsAmendMultipleOrders(ctx context.Context, args []AmendOrderRequestParams) ([]OrderData, error) { - 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 - } - } - if !ok.AreCredentialsValid(ctx) || !ok.Websocket.CanUseAuthenticatedEndpoints() { - return nil, errWebsocketStreamNotAuthenticated - } - randomID, err := common.GenerateRandomString(4, common.NumberCharacters) - if err != nil { - return nil, err - } - input := &WsOperationInput{ - ID: randomID, - Operation: okxOpBatchAmendOrders, - Arguments: args, - } - err = ok.Websocket.AuthConn.SendJSONMessage(ctx, amendMultipleOrdersEPL, input) - if err != nil { - return nil, err - } - timer := time.NewTimer(ok.WebsocketResponseMaxLimit) - wsResponse := make(chan *wsIncomingData) - ok.WsResponseMultiplexer.Register <- &wsRequestInfo{ - ID: randomID, - Chan: wsResponse, - } - defer func() { ok.WsResponseMultiplexer.Unregister <- randomID }() - for { - select { - case data := <-wsResponse: - if data.Operation == okxOpBatchAmendOrders && data.ID == input.ID { - if data.StatusCode == "0" || data.StatusCode == "2" { - var resp *WsPlaceOrderResponse - resp, err = data.copyToPlaceOrderResponse() - if err != nil { - return nil, err - } - return resp.Data, nil - } - if len(data.Data) == 0 { - return nil, fmt.Errorf("error code:%s error message: %v", data.StatusCode, ErrorCodes[data.StatusCode]) - } - var resp WsOrderActionResponse - err = resp.populateFromIncomingData(data) - if err != nil { - return nil, err - } - err = json.Unmarshal(data.Data, &(resp.Data)) - if err != nil { - return nil, err - } - var errs error - for x := range resp.Data { - if resp.Data[x].StatusCode != "0" { - errs = common.AppendError(errs, fmt.Errorf("error code:%s error message: %v", resp.Data[x].StatusCode, resp.Data[x].StatusMessage)) - } - } - return nil, errs - } - continue - case <-timer.C: - timer.Stop() - return nil, fmt.Errorf("%s websocket connection: timeout waiting for response with an operation: %v", - ok.Name, - input.Operation) - } - } -} - -// WsMassCancelOrders cancel all the 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) (bool, error) { - for x := range args { - if args[x] == (CancelMassReqParam{}) { - return false, common.ErrEmptyParams - } - if args[x].InstrumentType == "" { - return false, fmt.Errorf("%w, instrument type can not be empty", errInvalidInstrumentType) - } - if args[x].InstrumentFamily == "" { - return false, errInstrumentFamilyRequired - } - } - if !ok.AreCredentialsValid(ctx) || !ok.Websocket.CanUseAuthenticatedEndpoints() { - return false, errWebsocketStreamNotAuthenticated - } - randomID, err := common.GenerateRandomString(4, common.NumberCharacters) - if err != nil { - return false, err - } - input := &WsOperationInput{ - ID: randomID, - Operation: okxOpMassCancelOrder, - Arguments: args, - } - err = ok.Websocket.AuthConn.SendJSONMessage(ctx, request.Unset, input) - if err != nil { - return false, err - } - timer := time.NewTimer(ok.WebsocketResponseMaxLimit) - wsResponse := make(chan *wsIncomingData) - ok.WsResponseMultiplexer.Register <- &wsRequestInfo{ - ID: randomID, - Chan: wsResponse, - } - defer func() { ok.WsResponseMultiplexer.Unregister <- randomID }() - for { - select { - case data := <-wsResponse: - if data.Operation == okxOpMassCancelOrder && data.ID == input.ID { - if data.StatusCode == "0" || data.StatusCode == "2" { - resp := []struct { - Result bool `json:"result"` - }{} - err := json.Unmarshal(data.Data, &resp) - if err != nil { - return false, err - } - if len(data.Data) == 0 { - return false, fmt.Errorf("error code:%s message: %v", data.StatusCode, ErrorCodes[data.StatusCode]) - } - return resp[0].Result, nil - } - return false, fmt.Errorf("error code:%s message: %v", data.StatusCode, data.Message) - } - continue - case <-timer.C: - timer.Stop() - return false, fmt.Errorf("%s websocket connection: timeout waiting for response with an operation: %v", - ok.Name, - input.Operation) - } - } -} - -// Run this functions distributes websocket request responses to -func (m *wsRequestDataChannelsMultiplexer) Run() { - tickerData := time.NewTicker(time.Second) - for { - select { - case <-m.shutdown: - // We've consumed the shutdown, so create a new chan for subsequent runs - m.shutdown = make(chan bool) - return - case <-tickerData.C: - for x, myChan := range m.WsResponseChannelsMap { - if myChan == nil { - delete(m.WsResponseChannelsMap, x) - } - } - case id := <-m.Unregister: - delete(m.WsResponseChannelsMap, id) - case reg := <-m.Register: - m.WsResponseChannelsMap[reg.ID] = reg - case msg := <-m.Message: - if msg.ID != "" && m.WsResponseChannelsMap[msg.ID] != nil { - m.WsResponseChannelsMap[msg.ID].Chan <- msg - continue - } - for _, myChan := range m.WsResponseChannelsMap { - if (msg.Event == "error" || myChan.Event == operationLogin) && - (msg.StatusCode == "60009" || msg.StatusCode == "60004" || msg.StatusCode == "60022" || msg.StatusCode == "0") && - strings.Contains(msg.Message, myChan.Channel) { - myChan.Chan <- msg - continue - } else if msg.Event != myChan.Event || - msg.Argument.Channel != myChan.Channel || - msg.Argument.InstrumentType != myChan.InstrumentType || - msg.Argument.InstrumentID != myChan.InstrumentID { - continue - } - myChan.Chan <- msg - break - } - } - } -} - -// Shutdown causes the multiplexer to exit its Run loop -// All channels are left open, but websocket shutdown first will ensure no more messages block on multiplexer reading -func (m *wsRequestDataChannelsMultiplexer) Shutdown() { - close(m.shutdown) -} - -// wsChannelSubscription sends a subscription or unsubscription request for different channels through the websocket -func (ok *Okx) wsChannelSubscription(ctx context.Context, operation, channel string, assetType asset.Item, pair currency.Pair, tInstrumentType, tInstrumentID, tUnderlying bool) error { - if operation != operationSubscribe && operation != operationUnsubscribe { - return errInvalidWebsocketEvent - } - if channel == "" { - return errMissingValidChannelInformation - } - var underlying string - var instrumentID string - var instrumentType string - var format currency.PairFormat - var err error - if tInstrumentType { - instrumentType = GetInstrumentTypeFromAssetItem(assetType) - if instrumentType != instTypeSpot && - instrumentType != instTypeMargin && - instrumentType != instTypeSwap && - instrumentType != instTypeFutures && - instrumentType != instTypeOption { - instrumentType = instTypeANY - } - } - if tUnderlying { - if !pair.IsEmpty() { - underlying, _ = ok.GetUnderlying(pair, assetType) - } - } - if tInstrumentID { - format, err = ok.GetPairFormat(assetType, false) - if err != nil { - return err - } - if !pair.IsPopulated() { - return currency.ErrCurrencyPairsEmpty - } - instrumentID = format.Format(pair) - } - input := &SubscriptionOperationInput{ - Operation: operation, - Arguments: []SubscriptionInfo{ - { - Channel: channel, - InstrumentType: instrumentType, - Underlying: underlying, - InstrumentID: instrumentID, - }, - }, - } - ok.WsRequestSemaphore <- 1 - defer func() { <-ok.WsRequestSemaphore }() - return ok.Websocket.Conn.SendJSONMessage(ctx, request.Unset, input) -} - -// Private Channel Websocket methods - -// wsAuthChannelSubscription send a subscription or unsubscription request for different channels through the websocket -func (ok *Okx) wsAuthChannelSubscription(ctx context.Context, operation, channel string, assetType asset.Item, pair currency.Pair, uid, algoID string, params wsSubscriptionParameters) error { - if operation != operationSubscribe && operation != operationUnsubscribe { - return errInvalidWebsocketEvent - } - var underlying string - var instrumentID string - var instrumentType string - var ccy string - if params.InstrumentType { - instrumentType = GetInstrumentTypeFromAssetItem(assetType) - if instrumentType != instTypeMargin && - instrumentType != instTypeSwap && - instrumentType != instTypeFutures && - instrumentType != instTypeOption { - instrumentType = instTypeANY - } - } - if params.Underlying { - if !pair.IsEmpty() { - underlying, _ = ok.GetUnderlying(pair, assetType) - } - } - if params.InstrumentID { - if !pair.IsPopulated() { - return currency.ErrCurrencyPairsEmpty - } - format, err := ok.GetPairFormat(assetType, false) - if err != nil { - return err - } - instrumentID = format.Format(pair) - } - if params.Currency { - if !pair.IsEmpty() { - if !pair.Base.IsEmpty() { - ccy = strings.ToUpper(pair.Base.String()) - } else { - ccy = strings.ToUpper(pair.Quote.String()) - } - } - } - if channel == "" { - return errMissingValidChannelInformation - } - input := &SubscriptionOperationInput{ - Operation: operation, - Arguments: []SubscriptionInfo{ - { - Channel: channel, - InstrumentType: instrumentType, - Underlying: underlying, - InstrumentID: instrumentID, - AlgoID: algoID, - Currency: ccy, - UID: uid, - }, - }, - } - ok.WsRequestSemaphore <- 1 - defer func() { <-ok.WsRequestSemaphore }() - return ok.Websocket.AuthConn.SendJSONMessage(ctx, request.Unset, input) -} - -// WsAccountSubscription retrieve account information. Data will be pushed when triggered by -// events such as placing order, canceling order, transaction execution, etc. -// It will also be pushed in regular interval according to subscription granularity. -func (ok *Okx) WsAccountSubscription(ctx context.Context, operation string, assetType asset.Item, pair currency.Pair) error { - return ok.wsAuthChannelSubscription(ctx, operation, channelAccount, assetType, pair, "", "", wsSubscriptionParameters{Currency: true}) -} - -// WsPositionChannel retrieve the position data. The first snapshot will be sent in accordance with the granularity of the subscription. Data will be pushed when certain actions, such placing or canceling an order, trigger it. It will also be pushed periodically based on the granularity of the subscription. -func (ok *Okx) WsPositionChannel(ctx context.Context, operation string, assetType asset.Item, pair currency.Pair) error { - return ok.wsAuthChannelSubscription(ctx, operation, channelPositions, assetType, pair, "", "", wsSubscriptionParameters{InstrumentType: true}) -} - -// BalanceAndPositionSubscription retrieve account balance and position information. Data will be pushed when triggered by events such as filled order, funding transfer. -func (ok *Okx) BalanceAndPositionSubscription(ctx context.Context, operation, uid string) error { - return ok.wsAuthChannelSubscription(ctx, operation, channelBalanceAndPosition, asset.Empty, currency.EMPTYPAIR, uid, "", wsSubscriptionParameters{}) -} - -// WsOrderChannel for subscribing for orders. -func (ok *Okx) WsOrderChannel(ctx context.Context, operation string, assetType asset.Item, pair currency.Pair, _ string) error { - return ok.wsAuthChannelSubscription(ctx, operation, channelOrders, assetType, pair, "", "", wsSubscriptionParameters{InstrumentType: true, InstrumentID: true, Underlying: true}) -} - -// AlgoOrdersSubscription for subscribing to algo - order channels -func (ok *Okx) AlgoOrdersSubscription(ctx context.Context, operation string, assetType asset.Item, pair currency.Pair) error { - return ok.wsAuthChannelSubscription(ctx, operation, channelAlgoOrders, assetType, pair, "", "", wsSubscriptionParameters{InstrumentType: true, InstrumentID: true, Underlying: true}) -} - -// AdvanceAlgoOrdersSubscription algo order subscription to retrieve advance algo orders (including Iceberg order, TWAP order, Trailing order). Data will be pushed when first subscribed. Data will be pushed when triggered by events such as placing/canceling order. -func (ok *Okx) AdvanceAlgoOrdersSubscription(ctx context.Context, operation string, assetType asset.Item, pair currency.Pair, algoID string) error { - return ok.wsAuthChannelSubscription(ctx, operation, channelAlgoAdvance, assetType, pair, "", algoID, wsSubscriptionParameters{InstrumentType: true, InstrumentID: true}) -} - -// PositionRiskWarningSubscription this push channel is only used as a risk warning, and is not recommended as a risk judgment for strategic trading -// In the case that the market is not moving violently, there may be the possibility that the position has been liquidated at the same time that this message is pushed. -func (ok *Okx) PositionRiskWarningSubscription(ctx context.Context, operation string, assetType asset.Item, pair currency.Pair) error { - return ok.wsAuthChannelSubscription(ctx, operation, channelLiquidationWarning, assetType, pair, "", "", wsSubscriptionParameters{InstrumentType: true, InstrumentID: true, Underlying: true}) -} - -// AccountGreeksSubscription algo order subscription to retrieve account greeks information. Data will be pushed when triggered by events such as increase/decrease positions or cash balance in account, and will also be pushed in regular interval according to subscription granularity. -func (ok *Okx) AccountGreeksSubscription(ctx context.Context, operation string, pair currency.Pair) error { - return ok.wsAuthChannelSubscription(ctx, operation, channelAccountGreeks, asset.Empty, pair, "", "", wsSubscriptionParameters{Currency: true}) -} - -// RFQSubscription subscription to retrieve RFQ updates on RFQ orders. -func (ok *Okx) RFQSubscription(ctx context.Context, operation, uid string) error { - return ok.wsAuthChannelSubscription(ctx, operation, channelRFQs, asset.Empty, currency.EMPTYPAIR, uid, "", wsSubscriptionParameters{}) -} - -// QuotesSubscription subscription to retrieve Quote subscription -func (ok *Okx) QuotesSubscription(ctx context.Context, operation string) error { - return ok.wsAuthChannelSubscription(ctx, operation, channelQuotes, asset.Empty, currency.EMPTYPAIR, "", "", wsSubscriptionParameters{}) -} - -// StructureBlockTradesSubscription to retrieve Structural block subscription -func (ok *Okx) StructureBlockTradesSubscription(ctx context.Context, operation string) error { - return ok.wsAuthChannelSubscription(ctx, operation, channelStructureBlockTrades, asset.Empty, currency.EMPTYPAIR, "", "", wsSubscriptionParameters{}) -} - -// SpotGridAlgoOrdersSubscription to retrieve spot grid algo orders. Data will be pushed when first subscribed. Data will be pushed when triggered by events such as placing/canceling order. -func (ok *Okx) SpotGridAlgoOrdersSubscription(ctx context.Context, operation string, assetType asset.Item, pair currency.Pair, algoID string) error { - return ok.wsAuthChannelSubscription(ctx, operation, channelSpotGridOrder, assetType, pair, "", algoID, wsSubscriptionParameters{InstrumentType: true, Underlying: true}) -} - -// ContractGridAlgoOrders to retrieve contract grid algo orders. Data will be pushed when first subscribed. Data will be pushed when triggered by events such as placing/canceling order. -func (ok *Okx) ContractGridAlgoOrders(ctx context.Context, operation string, assetType asset.Item, pair currency.Pair, algoID string) error { - return ok.wsAuthChannelSubscription(ctx, operation, channelGridOrdersContract, assetType, pair, "", algoID, wsSubscriptionParameters{InstrumentType: true, Underlying: true}) -} - -// GridPositionsSubscription to retrieve grid positions. Data will be pushed when first subscribed. Data will be pushed when triggered by events such as placing/canceling order. -func (ok *Okx) GridPositionsSubscription(ctx context.Context, operation, algoID string) error { - return ok.wsAuthChannelSubscription(ctx, operation, channelGridPositions, asset.Empty, currency.EMPTYPAIR, "", algoID, wsSubscriptionParameters{}) -} - -// GridSubOrders to retrieve grid sub orders. Data will be pushed when first subscribed. Data will be pushed when triggered by events such as placing order. -func (ok *Okx) GridSubOrders(ctx context.Context, operation, algoID string) error { - return ok.wsAuthChannelSubscription(ctx, operation, channelGridSubOrders, asset.Empty, currency.EMPTYPAIR, "", algoID, wsSubscriptionParameters{}) -} - -// Public Websocket stream subscription - -// InstrumentsSubscription to subscribe for instruments. The full instrument list will be pushed -// for the first time after subscription. Subsequently, the instruments will be pushed if there is any change to the instrument’s state (such as delivery of FUTURES, -// exercise of OPTION, listing of new contracts / trading pairs, trading suspension, etc.). -func (ok *Okx) InstrumentsSubscription(ctx context.Context, operation string, assetType asset.Item, pair currency.Pair) error { - return ok.wsChannelSubscription(ctx, operation, channelInstruments, assetType, pair, true, false, false) -} - -// TickersSubscription subscribing to "ticker" channel to retrieve the last traded price, bid price, ask price and 24-hour trading volume of instruments. Data will be pushed every 100 ms. -func (ok *Okx) TickersSubscription(ctx context.Context, operation string, assetType asset.Item, pair currency.Pair) error { - return ok.wsChannelSubscription(ctx, operation, channelTickers, assetType, pair, false, true, false) -} - -// OpenInterestSubscription to subscribe or unsubscribe to "open-interest" channel to retrieve the open interest. Data will be pushed every 3 seconds. -func (ok *Okx) OpenInterestSubscription(ctx context.Context, operation string, assetType asset.Item, pair currency.Pair) error { - if assetType != asset.Futures && assetType != asset.Options && assetType != asset.PerpetualSwap { - return fmt.Errorf("%w, received '%v' only FUTURES, SWAP and OPTION asset types are supported", errInvalidInstrumentType, assetType) - } - return ok.wsChannelSubscription(ctx, operation, channelOpenInterest, assetType, pair, false, true, false) -} - -// CandlesticksSubscription to subscribe or unsubscribe to "candle" channels to retrieve the candlesticks data of an instrument. the push frequency is the fastest interval 500ms push the data. -func (ok *Okx) CandlesticksSubscription(ctx context.Context, operation, channel string, assetType asset.Item, pair currency.Pair) error { - if _, okay := candlestickChannelsMap[channel]; !okay { - return errMissingValidChannelInformation - } - return ok.wsChannelSubscription(ctx, operation, channel, assetType, pair, false, true, false) -} - -// TradesSubscription to subscribe or unsubscribe to "trades" channel to retrieve the recent trades data. Data will be pushed whenever there is a trade. Every update contain only one trade. -func (ok *Okx) TradesSubscription(ctx context.Context, operation string, assetType asset.Item, pair currency.Pair) error { - return ok.wsChannelSubscription(ctx, operation, channelTrades, assetType, pair, false, true, false) -} - -// EstimatedDeliveryExercisePriceSubscription to subscribe or unsubscribe to "estimated-price" channel to retrieve the estimated delivery/exercise price of FUTURES contracts and OPTION. -func (ok *Okx) EstimatedDeliveryExercisePriceSubscription(ctx context.Context, operation string, assetType asset.Item, pair currency.Pair) error { - if assetType != asset.Futures && assetType != asset.Options { - return fmt.Errorf("%w, received '%v' only FUTURES and OPTION asset types are supported", errInvalidInstrumentType, assetType) - } - return ok.wsChannelSubscription(ctx, operation, channelEstimatedPrice, assetType, pair, true, true, false) -} - -// MarkPriceSubscription to subscribe or unsubscribe to the "mark-price" to retrieve the mark price. Data will be pushed every 200 ms when the mark price changes, and will be pushed every 10 seconds when the mark price does not change. -func (ok *Okx) MarkPriceSubscription(ctx context.Context, operation string, assetType asset.Item, pair currency.Pair) error { - return ok.wsChannelSubscription(ctx, operation, channelMarkPrice, assetType, pair, false, true, false) -} - -// MarkPriceCandlesticksSubscription to subscribe or unsubscribe to "mark-price-candles" channels to retrieve the candlesticks data of the mark price. Data will be pushed every 500 ms. -func (ok *Okx) MarkPriceCandlesticksSubscription(ctx context.Context, operation, channel string, assetType asset.Item, pair currency.Pair) error { - if _, okay := candlesticksMarkPriceMap[channel]; !okay { - return fmt.Errorf("%w channel: %v", errMissingValidChannelInformation, channel) - } - return ok.wsChannelSubscription(ctx, operation, channel, assetType, pair, false, true, false) -} - -// PriceLimitSubscription subscribe or unsubscribe to "price-limit" channel to retrieve the maximum buy price and minimum sell price of the instrument. Data will be pushed every 5 seconds when there are changes in limits, and will not be pushed when there is no changes on limit. -func (ok *Okx) PriceLimitSubscription(ctx context.Context, operation string, assetType asset.Item, pair currency.Pair) error { - if operation != operationSubscribe && operation != operationUnsubscribe { - return errInvalidWebsocketEvent - } - return ok.wsChannelSubscription(ctx, operation, channelPriceLimit, assetType, pair, false, true, false) -} - -// OrderBooksSubscription subscribe or unsubscribe to "books*" channel to retrieve order book data. -func (ok *Okx) OrderBooksSubscription(ctx context.Context, operation, channel string, assetType asset.Item, pair currency.Pair) error { - if channel != channelOrderBooks && channel != channelOrderBooks5 && channel != channelOrderBooks50TBT && channel != channelOrderBooksTBT && channel != channelBBOTBT { - return fmt.Errorf("%w channel: %v", errMissingValidChannelInformation, channel) - } - return ok.wsChannelSubscription(ctx, operation, channel, assetType, pair, false, true, false) -} - -// OptionSummarySubscription a method to subscribe or unsubscribe to "opt-summary" channel -// to retrieve detailed pricing information of all OPTION contracts. Data will be pushed at once. -func (ok *Okx) OptionSummarySubscription(ctx context.Context, operation string, pair currency.Pair) error { - return ok.wsChannelSubscription(ctx, operation, channelOptSummary, asset.Options, pair, false, false, true) -} - -// FundingRateSubscription a method to subscribe and unsubscribe to "funding-rate" channel. -// retrieve funding rate. Data will be pushed in 30s to 90s. -func (ok *Okx) FundingRateSubscription(ctx context.Context, operation string, assetType asset.Item, pair currency.Pair) error { - return ok.wsChannelSubscription(ctx, operation, channelFundingRate, assetType, pair, false, true, false) -} - -// IndexCandlesticksSubscription a method to subscribe and unsubscribe to "index-candle*" channel -// to retrieve the candlesticks data of the index. Data will be pushed every 500 ms. -func (ok *Okx) IndexCandlesticksSubscription(ctx context.Context, operation, channel string, assetType asset.Item, pair currency.Pair) error { - if _, okay := candlesticksIndexPriceMap[channel]; !okay { - return fmt.Errorf("%w channel: %v", errMissingValidChannelInformation, channel) - } - return ok.wsChannelSubscription(ctx, operation, channel, assetType, pair, false, true, false) -} - -// IndexTickerChannel a method to subscribe and unsubscribe to "index-tickers" channel -func (ok *Okx) IndexTickerChannel(ctx context.Context, operation string, assetType asset.Item, pair currency.Pair) error { - return ok.wsChannelSubscription(ctx, operation, channelIndexTickers, assetType, pair, false, true, false) -} - -// StatusSubscription get the status of system maintenance and push when the system maintenance status changes. -// First subscription: "Push the latest change data"; every time there is a state change, push the changed content -func (ok *Okx) StatusSubscription(ctx context.Context, operation string, assetType asset.Item, pair currency.Pair) error { - return ok.wsChannelSubscription(ctx, operation, channelStatus, assetType, pair, false, false, false) -} - -// PublicStructureBlockTradesSubscription a method to subscribe or unsubscribe to "public-struc-block-trades" channel -func (ok *Okx) PublicStructureBlockTradesSubscription(ctx context.Context, operation string, assetType asset.Item, pair currency.Pair) error { - return ok.wsChannelSubscription(ctx, operation, channelPublicStrucBlockTrades, assetType, pair, false, false, false) -} - -// BlockTickerSubscription a method to subscribe and unsubscribe to a "block-tickers" channel to retrieve the latest block trading volume in the last 24 hours. -// The data will be pushed when triggered by transaction execution event. In addition, it will also be pushed in 5 minutes interval according to subscription granularity. -func (ok *Okx) BlockTickerSubscription(ctx context.Context, operation string, assetType asset.Item, pair currency.Pair) error { - return ok.wsChannelSubscription(ctx, operation, channelBlockTickers, assetType, pair, false, true, false) -} - -// PublicBlockTradesSubscription a method to subscribe and unsubscribe to a "public-block-trades" channel to retrieve the recent block trades data by individual legs. -// Each leg in a block trade is pushed in a separate update. Data will be pushed whenever there is a block trade. -func (ok *Okx) PublicBlockTradesSubscription(ctx context.Context, operation string, assetType asset.Item, pair currency.Pair) error { - return ok.wsChannelSubscription(ctx, operation, channelPublicBlockTrades, assetType, pair, false, true, false) -} - -// Websocket Spread Trade methods - -// handleIncomingData extracts the incoming data to the dataHolder interface after few checks and return nil or return error message otherwise -func (ok *Okx) handleIncomingData(data *wsIncomingData, dataHolder any) error { - if data.StatusCode == "0" || data.StatusCode == "1" { - err := data.copyResponseToInterface(dataHolder) - if err != nil { - return err - } - if dataHolder == nil { - return fmt.Errorf("%w, invalid incoming data", common.ErrNoResponse) - } - return nil - } - return fmt.Errorf("error code:%s error message: %v", data.StatusCode, ErrorCodes[data.StatusCode]) -} - -// WsPlaceSpreadOrder places a spread order thought the websocket connection stream, and returns a SubmitResponse and error message. -func (ok *Okx) WsPlaceSpreadOrder(ctx context.Context, arg *SpreadOrderParam) (*SpreadOrderResponse, error) { - if arg == nil || *arg == (SpreadOrderParam{}) { - return nil, common.ErrNilPointer - } - err := ok.validatePlaceSpreadOrderParam(arg) - if err != nil { - return nil, err - } - if !ok.AreCredentialsValid(ctx) || !ok.Websocket.CanUseAuthenticatedEndpoints() { - return nil, errWebsocketStreamNotAuthenticated - } - randomID, err := common.GenerateRandomString(32, common.SmallLetters, common.CapitalLetters, common.NumberCharacters) - if err != nil { - return nil, err - } - input := WsOperationInput{ - ID: randomID, - Arguments: []SpreadOrderParam{*arg}, - Operation: okxSpreadOrder, - } - err = ok.Websocket.AuthConn.SendJSONMessage(ctx, request.UnAuth, input) - if err != nil { - return nil, err - } - timer := time.NewTimer(ok.WebsocketResponseMaxLimit) - wsResponse := make(chan *wsIncomingData) - ok.WsResponseMultiplexer.Register <- &wsRequestInfo{ - ID: randomID, - Chan: wsResponse, - } - defer func() { ok.WsResponseMultiplexer.Unregister <- randomID }() - for { - select { - case data := <-wsResponse: - if data.Operation == okxSpreadOrder && data.ID == input.ID { - var dataHolder *SpreadOrderResponse - err = ok.handleIncomingData(data, &dataHolder) - if err != nil { - return nil, err - } - if data.StatusCode == "1" { - return nil, fmt.Errorf("error code:%s message: %s", dataHolder.StatusCode, dataHolder.StatusMessage) - } - return dataHolder, nil - } - continue - case <-timer.C: - timer.Stop() - return nil, fmt.Errorf("%s websocket connection: timeout waiting for response with an operation: %v", - ok.Name, - input.Operation) - } - } -} - -// WsAmandSpreadOrder amends incomplete spread order through the websocket channel. -func (ok *Okx) WsAmandSpreadOrder(ctx context.Context, arg *AmendSpreadOrderParam) (*SpreadOrderResponse, error) { - if arg == nil || *arg == (AmendSpreadOrderParam{}) { - return nil, common.ErrEmptyParams - } - if arg.OrderID == "" && arg.ClientOrderID == "" { - return nil, order.ErrOrderIDNotSet - } - if arg.NewPrice == 0 && arg.NewSize == 0 { - return nil, errSizeOrPriceIsRequired - } - if !ok.AreCredentialsValid(ctx) || !ok.Websocket.CanUseAuthenticatedEndpoints() { - return nil, errWebsocketStreamNotAuthenticated - } - randomID, err := common.GenerateRandomString(32, common.SmallLetters, common.CapitalLetters, common.NumberCharacters) - if err != nil { - return nil, err - } - input := WsOperationInput{ - ID: randomID, - Arguments: []AmendSpreadOrderParam{*arg}, - Operation: okxSpreadAmendOrder, - } - err = ok.Websocket.AuthConn.SendJSONMessage(ctx, request.UnAuth, input) - if err != nil { - return nil, err - } - timer := time.NewTimer(ok.WebsocketResponseMaxLimit) - wsResponse := make(chan *wsIncomingData) - ok.WsResponseMultiplexer.Register <- &wsRequestInfo{ - ID: randomID, - Chan: wsResponse, - } - defer func() { ok.WsResponseMultiplexer.Unregister <- randomID }() - for { - select { - case data := <-wsResponse: - if data.Operation == okxSpreadAmendOrder && data.ID == input.ID { - var dataHolder *SpreadOrderResponse - err = ok.handleIncomingData(data, &dataHolder) - if err != nil { - return nil, err - } - if data.StatusCode == "1" { - return nil, fmt.Errorf("error code:%s message: %s", dataHolder.StatusCode, dataHolder.StatusMessage) - } - return dataHolder, nil - } - continue - case <-timer.C: - timer.Stop() - return nil, fmt.Errorf("%s websocket connection: timeout waiting for response with an operation: %v", - ok.Name, - input.Operation) - } - } -} - -// 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 - } - if !ok.AreCredentialsValid(ctx) || !ok.Websocket.CanUseAuthenticatedEndpoints() { - return nil, errWebsocketStreamNotAuthenticated - } - arg := make(map[string]string) - if orderID != "" { - arg["ordId"] = orderID - } - if clientOrderID != "" { - arg["clOrdId"] = clientOrderID - } - randomID, err := common.GenerateRandomString(32, common.SmallLetters, common.CapitalLetters, common.NumberCharacters) - if err != nil { - return nil, err - } - input := WsOperationInput{ - ID: randomID, - Arguments: []map[string]string{arg}, - Operation: okxSpreadCancelOrder, - } - err = ok.Websocket.AuthConn.SendJSONMessage(ctx, request.UnAuth, input) - if err != nil { - return nil, err - } - timer := time.NewTimer(ok.WebsocketResponseMaxLimit) - wsResponse := make(chan *wsIncomingData) - ok.WsResponseMultiplexer.Register <- &wsRequestInfo{ - ID: randomID, - Chan: wsResponse, - } - defer func() { ok.WsResponseMultiplexer.Unregister <- randomID }() - for { - select { - case data := <-wsResponse: - if data.Operation == okxSpreadCancelOrder && data.ID == input.ID { - var dataHolder *SpreadOrderResponse - err = ok.handleIncomingData(data, &dataHolder) - if err != nil { - return nil, err - } - if data.StatusCode == "1" { - return nil, fmt.Errorf("error code:%s message: %s", dataHolder.StatusCode, dataHolder.StatusMessage) - } - return dataHolder, nil - } - continue - case <-timer.C: - timer.Stop() - return nil, fmt.Errorf("%s websocket connection: timeout waiting for response with an operation: %v", - ok.Name, - input.Operation) - } - } -} - -// WsCancelAllSpreadOrders cancels all spread orders and return success message through the websocket channel. -func (ok *Okx) WsCancelAllSpreadOrders(ctx context.Context, spreadID string) (bool, error) { - if !ok.AreCredentialsValid(ctx) || !ok.Websocket.CanUseAuthenticatedEndpoints() { - return false, errWebsocketStreamNotAuthenticated - } - arg := make(map[string]string, 1) - if spreadID != "" { - arg["sprdId"] = spreadID - } - randomID, err := common.GenerateRandomString(32, common.SmallLetters, common.CapitalLetters, common.NumberCharacters) - if err != nil { - return false, err - } - input := WsOperationInput{ - ID: randomID, - Arguments: []map[string]string{arg}, - Operation: okxSpreadCancelAllOrders, - } - err = ok.Websocket.AuthConn.SendJSONMessage(ctx, request.UnAuth, input) - if err != nil { - return false, err - } - timer := time.NewTimer(ok.WebsocketResponseMaxLimit) - wsResponse := make(chan *wsIncomingData) - ok.WsResponseMultiplexer.Register <- &wsRequestInfo{ - ID: randomID, - Chan: wsResponse, - } - defer func() { ok.WsResponseMultiplexer.Unregister <- randomID }() - for { - select { - case data := <-wsResponse: - if data.Operation == okxSpreadCancelAllOrders && data.ID == input.ID { - dataHolder := &ResponseSuccess{} - err = ok.handleIncomingData(data, &dataHolder) - if err != nil { - return false, err - } - if data.StatusCode == "1" { - return false, fmt.Errorf("error code:%s message: %s", dataHolder.StatusCode, dataHolder.StatusMessage) - } - return dataHolder.Result, nil - } - continue - case <-timer.C: - timer.Stop() - return false, fmt.Errorf("%s websocket connection: timeout waiting for response with an operation: %v", - ok.Name, - input.Operation) - } - } -} - // channelName converts global subscription channel names to exchange specific names func channelName(s *subscription.Subscription) string { if s, ok := subscriptionNames[s.Channel]; ok { diff --git a/exchanges/okx/okx_wrapper.go b/exchanges/okx/okx_wrapper.go index ccc58ddb..7e468925 100644 --- a/exchanges/okx/okx_wrapper.go +++ b/exchanges/okx/okx_wrapper.go @@ -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) -} diff --git a/exchanges/okx/ratelimit.go b/exchanges/okx/ratelimit.go index e81fafea..81d199e1 100644 --- a/exchanges/okx/ratelimit.go +++ b/exchanges/okx/ratelimit.go @@ -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), } -} +}() diff --git a/exchanges/okx/ratelimiter_test.go b/exchanges/okx/ratelimiter_test.go index 614fc490..fc3d69e2 100644 --- a/exchanges/okx/ratelimiter_test.go +++ b/exchanges/okx/ratelimiter_test.go @@ -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 { diff --git a/exchanges/okx/ws_requests.go b/exchanges/okx/ws_requests.go new file mode 100644 index 00000000..73e7224f --- /dev/null +++ b/exchanges/okx/ws_requests.go @@ -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 +} diff --git a/exchanges/okx/ws_requests_test.go b/exchanges/okx/ws_requests_test.go new file mode 100644 index 00000000..6d08382d --- /dev/null +++ b/exchanges/okx/ws_requests_test.go @@ -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) +}