diff --git a/pkg/exchange/okex/exchange.go b/pkg/exchange/okex/exchange.go index 9d3eac7fb..d3ca2e939 100644 --- a/pkg/exchange/okex/exchange.go +++ b/pkg/exchange/okex/exchange.go @@ -28,6 +28,7 @@ var ( queryTickerLimiter = rate.NewLimiter(rate.Every(100*time.Millisecond), 10) queryTickersLimiter = rate.NewLimiter(rate.Every(100*time.Millisecond), 10) queryAccountLimiter = rate.NewLimiter(rate.Every(200*time.Millisecond), 5) + placeOrderLimiter = rate.NewLimiter(rate.Every(30*time.Millisecond), 30) ) const ID = "okex" @@ -198,63 +199,68 @@ func (e *Exchange) QueryAccountBalances(ctx context.Context) (types.BalanceMap, func (e *Exchange) SubmitOrder(ctx context.Context, order types.SubmitOrder) (*types.Order, error) { orderReq := e.client.NewPlaceOrderRequest() + orderReq.InstrumentID(toLocalSymbol(order.Symbol)) + orderReq.Side(toLocalSideType(order.Side)) + orderReq.Size(order.Market.FormatQuantity(order.Quantity)) + + // set price field for limit orders + switch order.Type { + case types.OrderTypeStopLimit, types.OrderTypeLimit: + orderReq.Price(order.Market.FormatPrice(order.Price)) + case types.OrderTypeMarket: + // Because our order.Quantity unit is base coin, so we indicate the target currency to Base. + if order.Side == types.SideTypeBuy { + orderReq.Size(order.Market.FormatQuantity(order.Quantity)) + orderReq.TargetCurrency(okexapi.TargetCurrencyBase) + } else { + orderReq.Size(order.Market.FormatQuantity(order.Quantity)) + orderReq.TargetCurrency(okexapi.TargetCurrencyQuote) + } + } + orderType, err := toLocalOrderType(order.Type) if err != nil { return nil, err } - orderReq.InstrumentID(toLocalSymbol(order.Symbol)) - orderReq.Side(toLocalSideType(order.Side)) - - if order.Market.Symbol != "" { - orderReq.Quantity(order.Market.FormatQuantity(order.Quantity)) - } else { - // TODO report error - orderReq.Quantity(order.Quantity.FormatString(8)) - } - - // set price field for limit orders - switch order.Type { - case types.OrderTypeStopLimit, types.OrderTypeLimit: - if order.Market.Symbol != "" { - orderReq.Price(order.Market.FormatPrice(order.Price)) - } else { - // TODO report error - orderReq.Price(order.Price.FormatString(8)) - } - } - switch order.TimeInForce { - case "FOK": + case types.TimeInForceFOK: orderReq.OrderType(okexapi.OrderTypeFOK) - case "IOC": + case types.TimeInForceIOC: orderReq.OrderType(okexapi.OrderTypeIOC) default: orderReq.OrderType(orderType) } - orderHead, err := orderReq.Do(ctx) + if err := placeOrderLimiter.Wait(ctx); err != nil { + return nil, fmt.Errorf("place order rate limiter wait error: %w", err) + } + + _, err = strconv.ParseInt(order.ClientOrderID, 10, 64) + if err != nil { + return nil, fmt.Errorf("client order id should be numberic: %s, err: %w", order.ClientOrderID, err) + } + orderReq.ClientOrderID(order.ClientOrderID) + + orders, err := orderReq.Do(ctx) if err != nil { return nil, err } - orderID, err := strconv.ParseInt(orderHead.OrderID, 10, 64) - if err != nil { - return nil, err + if len(orders) != 1 { + return nil, fmt.Errorf("unexpected length of order response: %v", orders) } - return &types.Order{ - SubmitOrder: order, - Exchange: types.ExchangeOKEx, - OrderID: uint64(orderID), - Status: types.OrderStatusNew, - ExecutedQuantity: fixedpoint.Zero, - IsWorking: true, - CreationTime: types.Time(time.Now()), - UpdateTime: types.Time(time.Now()), - IsMargin: false, - IsIsolated: false, - }, nil + orderRes, err := e.QueryOrder(ctx, types.OrderQuery{ + Symbol: order.Symbol, + OrderID: orders[0].OrderID, + ClientOrderID: orders[0].ClientOrderID, + }) + if err != nil { + return nil, fmt.Errorf("failed to query order by id: %s, clientOrderId: %s, err: %w", orders[0].OrderID, orders[0].ClientOrderID, err) + } + + return orderRes, nil // TODO: move this to batch place orders interface /* diff --git a/pkg/exchange/okex/okexapi/client_test.go b/pkg/exchange/okex/okexapi/client_test.go index 218dcfd67..91fe26395 100644 --- a/pkg/exchange/okex/okexapi/client_test.go +++ b/pkg/exchange/okex/okexapi/client_test.go @@ -91,15 +91,21 @@ func TestClient_PlaceOrderRequest(t *testing.T) { order, err := req. InstrumentID("BTC-USDT"). - TradeMode("cash"). - Side(SideTypeBuy). + TradeMode(TradeModeCash). + Side(SideTypeSell). OrderType(OrderTypeLimit). - Price("15000"). - Quantity("0.0001"). + TargetCurrency(TargetCurrencyBase). + Price("48000"). + Size("0.001"). Do(ctx) assert.NoError(t, err) assert.NotEmpty(t, order) t.Logf("place order: %+v", order) + + c := client.NewGetOrderDetailsRequest().OrderID(order[0].OrderID).InstrumentID("BTC-USDT") + res, err := c.Do(ctx) + assert.NoError(t, err) + t.Log(res) } func TestClient_GetPendingOrderRequest(t *testing.T) { diff --git a/pkg/exchange/okex/okexapi/place_order_request.go b/pkg/exchange/okex/okexapi/place_order_request.go new file mode 100644 index 000000000..c45a3d0ea --- /dev/null +++ b/pkg/exchange/okex/okexapi/place_order_request.go @@ -0,0 +1,67 @@ +package okexapi + +import "github.com/c9s/requestgen" + +type TradeMode string + +const ( + TradeModeCash TradeMode = "cash" + TradeModeIsolated TradeMode = "isolated" + TradeModeCross TradeMode = "cross" +) + +type TargetCurrency string + +const ( + TargetCurrencyBase TargetCurrency = "base_ccy" + TargetCurrencyQuote TargetCurrency = "quote_ccy" +) + +//go:generate -command GetRequest requestgen -method GET -responseType .APIResponse -responseDataField Data +//go:generate -command PostRequest requestgen -method POST -responseType .APIResponse -responseDataField Data + +type OrderResponse struct { + OrderID string `json:"ordId"` + ClientOrderID string `json:"clOrdId"` + Tag string `json:"tag"` + Code string `json:"sCode"` + Message string `json:"sMsg"` +} + +//go:generate PostRequest -url "/api/v5/trade/order" -type PlaceOrderRequest -responseDataType []OrderResponse +type PlaceOrderRequest struct { + client requestgen.AuthenticatedAPIClient + + instrumentID string `param:"instId"` + + // tdMode + // margin mode: "cross", "isolated" + // non-margin mode cash + tradeMode TradeMode `param:"tdMode" validValues:"cross,isolated,cash"` + + // A combination of case-sensitive alphanumerics, all numbers, or all letters of up to 32 characters. + clientOrderID *string `param:"clOrdId"` + + // A combination of case-sensitive alphanumerics, all numbers, or all letters of up to 8 characters. + tag *string `param:"tag"` + + // "buy" or "sell" + side SideType `param:"side" validValues:"buy,sell"` + + orderType OrderType `param:"ordType"` + + size string `param:"sz"` + + // price + price *string `param:"px"` + + // Whether the target currency uses the quote or base currency. + // base_ccy: Base currency ,quote_ccy: Quote currency + // Only applicable to SPOT Market Orders + // Default is quote_ccy for buy, base_ccy for sell + targetCurrency *TargetCurrency `param:"tgtCcy" validValues:"quote_ccy,base_ccy"` +} + +func (c *RestClient) NewPlaceOrderRequest() *PlaceOrderRequest { + return &PlaceOrderRequest{client: c} +} diff --git a/pkg/exchange/okex/okexapi/place_order_request_accessors.go b/pkg/exchange/okex/okexapi/place_order_request_accessors.go deleted file mode 100644 index b272cee11..000000000 --- a/pkg/exchange/okex/okexapi/place_order_request_accessors.go +++ /dev/null @@ -1,151 +0,0 @@ -// Code generated by "requestgen -type PlaceOrderRequest"; DO NOT EDIT. - -package okexapi - -import ( - "encoding/json" - "fmt" - "net/url" -) - -func (p *PlaceOrderRequest) InstrumentID(instrumentID string) *PlaceOrderRequest { - p.instrumentID = instrumentID - return p -} - -func (p *PlaceOrderRequest) TradeMode(tradeMode string) *PlaceOrderRequest { - p.tradeMode = tradeMode - return p -} - -func (p *PlaceOrderRequest) ClientOrderID(clientOrderID string) *PlaceOrderRequest { - p.clientOrderID = &clientOrderID - return p -} - -func (p *PlaceOrderRequest) Tag(tag string) *PlaceOrderRequest { - p.tag = &tag - return p -} - -func (p *PlaceOrderRequest) Side(side SideType) *PlaceOrderRequest { - p.side = side - return p -} - -func (p *PlaceOrderRequest) OrderType(orderType OrderType) *PlaceOrderRequest { - p.orderType = orderType - return p -} - -func (p *PlaceOrderRequest) Quantity(quantity string) *PlaceOrderRequest { - p.quantity = quantity - return p -} - -func (p *PlaceOrderRequest) Price(price string) *PlaceOrderRequest { - p.price = &price - return p -} - -func (p *PlaceOrderRequest) GetParameters() (map[string]interface{}, error) { - var params = map[string]interface{}{} - - // check instrumentID field -> json key instId - instrumentID := p.instrumentID - - // assign parameter of instrumentID - params["instId"] = instrumentID - - // check tradeMode field -> json key tdMode - tradeMode := p.tradeMode - - switch tradeMode { - case "cross", "isolated", "cash": - params["tdMode"] = tradeMode - - default: - return params, fmt.Errorf("tdMode value %v is invalid", tradeMode) - - } - - // assign parameter of tradeMode - params["tdMode"] = tradeMode - - // check clientOrderID field -> json key clOrdId - if p.clientOrderID != nil { - clientOrderID := *p.clientOrderID - - // assign parameter of clientOrderID - params["clOrdId"] = clientOrderID - } - - // check tag field -> json key tag - if p.tag != nil { - tag := *p.tag - - // assign parameter of tag - params["tag"] = tag - } - - // check side field -> json key side - side := p.side - - switch side { - case "buy", "sell": - params["side"] = side - - default: - return params, fmt.Errorf("side value %v is invalid", side) - - } - - // assign parameter of side - params["side"] = side - - // check orderType field -> json key ordType - orderType := p.orderType - - // assign parameter of orderType - params["ordType"] = orderType - - // check quantity field -> json key sz - quantity := p.quantity - - // assign parameter of quantity - params["sz"] = quantity - - // check price field -> json key px - if p.price != nil { - price := *p.price - - // assign parameter of price - params["px"] = price - } - - return params, nil -} - -func (p *PlaceOrderRequest) GetParametersQuery() (url.Values, error) { - query := url.Values{} - - params, err := p.GetParameters() - if err != nil { - return query, err - } - - for k, v := range params { - query.Add(k, fmt.Sprintf("%v", v)) - } - - return query, nil -} - -func (p *PlaceOrderRequest) GetParametersJSON() ([]byte, error) { - params, err := p.GetParameters() - if err != nil { - return nil, err - } - - return json.Marshal(params) -} diff --git a/pkg/exchange/okex/okexapi/place_order_request_requestgen.go b/pkg/exchange/okex/okexapi/place_order_request_requestgen.go new file mode 100644 index 000000000..3303d5961 --- /dev/null +++ b/pkg/exchange/okex/okexapi/place_order_request_requestgen.go @@ -0,0 +1,305 @@ +// Code generated by "requestgen -method POST -responseType .APIResponse -responseDataField Data -url /api/v5/trade/order -type PlaceOrderRequest -responseDataType []OrderResponse"; DO NOT EDIT. + +package okexapi + +import ( + "context" + "encoding/json" + "fmt" + "net/url" + "reflect" + "regexp" +) + +func (r *PlaceOrderRequest) InstrumentID(instrumentID string) *PlaceOrderRequest { + r.instrumentID = instrumentID + return r +} + +func (r *PlaceOrderRequest) TradeMode(tradeMode TradeMode) *PlaceOrderRequest { + r.tradeMode = tradeMode + return r +} + +func (r *PlaceOrderRequest) ClientOrderID(clientOrderID string) *PlaceOrderRequest { + r.clientOrderID = &clientOrderID + return r +} + +func (r *PlaceOrderRequest) Tag(tag string) *PlaceOrderRequest { + r.tag = &tag + return r +} + +func (r *PlaceOrderRequest) Side(side SideType) *PlaceOrderRequest { + r.side = side + return r +} + +func (r *PlaceOrderRequest) OrderType(orderType OrderType) *PlaceOrderRequest { + r.orderType = orderType + return r +} + +func (r *PlaceOrderRequest) Size(size string) *PlaceOrderRequest { + r.size = size + return r +} + +func (r *PlaceOrderRequest) Price(price string) *PlaceOrderRequest { + r.price = &price + return r +} + +func (r *PlaceOrderRequest) TargetCurrency(targetCurrency TargetCurrency) *PlaceOrderRequest { + r.targetCurrency = &targetCurrency + return r +} + +// GetQueryParameters builds and checks the query parameters and returns url.Values +func (r *PlaceOrderRequest) GetQueryParameters() (url.Values, error) { + var params = map[string]interface{}{} + + query := url.Values{} + for _k, _v := range params { + query.Add(_k, fmt.Sprintf("%v", _v)) + } + + return query, nil +} + +// GetParameters builds and checks the parameters and return the result in a map object +func (r *PlaceOrderRequest) GetParameters() (map[string]interface{}, error) { + var params = map[string]interface{}{} + // check instrumentID field -> json key instId + instrumentID := r.instrumentID + + // assign parameter of instrumentID + params["instId"] = instrumentID + // check tradeMode field -> json key tdMode + tradeMode := r.tradeMode + + // TEMPLATE check-valid-values + switch tradeMode { + case "cross", "isolated", "cash": + params["tdMode"] = tradeMode + + default: + return nil, fmt.Errorf("tdMode value %v is invalid", tradeMode) + + } + // END TEMPLATE check-valid-values + + // assign parameter of tradeMode + params["tdMode"] = tradeMode + // check clientOrderID field -> json key clOrdId + if r.clientOrderID != nil { + clientOrderID := *r.clientOrderID + + // assign parameter of clientOrderID + params["clOrdId"] = clientOrderID + } else { + } + // check tag field -> json key tag + if r.tag != nil { + tag := *r.tag + + // assign parameter of tag + params["tag"] = tag + } else { + } + // check side field -> json key side + side := r.side + + // TEMPLATE check-valid-values + switch side { + case "buy", "sell": + params["side"] = side + + default: + return nil, fmt.Errorf("side value %v is invalid", side) + + } + // END TEMPLATE check-valid-values + + // assign parameter of side + params["side"] = side + // check orderType field -> json key ordType + orderType := r.orderType + + // TEMPLATE check-valid-values + switch orderType { + case OrderTypeMarket, OrderTypeLimit, OrderTypePostOnly, OrderTypeFOK, OrderTypeIOC: + params["ordType"] = orderType + + default: + return nil, fmt.Errorf("ordType value %v is invalid", orderType) + + } + // END TEMPLATE check-valid-values + + // assign parameter of orderType + params["ordType"] = orderType + // check size field -> json key sz + size := r.size + + // assign parameter of size + params["sz"] = size + // check price field -> json key px + if r.price != nil { + price := *r.price + + // assign parameter of price + params["px"] = price + } else { + } + // check targetCurrency field -> json key tgtCcy + if r.targetCurrency != nil { + targetCurrency := *r.targetCurrency + + // TEMPLATE check-valid-values + switch targetCurrency { + case "quote_ccy", "base_ccy": + params["tgtCcy"] = targetCurrency + + default: + return nil, fmt.Errorf("tgtCcy value %v is invalid", targetCurrency) + + } + // END TEMPLATE check-valid-values + + // assign parameter of targetCurrency + params["tgtCcy"] = targetCurrency + } else { + } + + return params, nil +} + +// GetParametersQuery converts the parameters from GetParameters into the url.Values format +func (r *PlaceOrderRequest) GetParametersQuery() (url.Values, error) { + query := url.Values{} + + params, err := r.GetParameters() + if err != nil { + return query, err + } + + for _k, _v := range params { + if r.isVarSlice(_v) { + r.iterateSlice(_v, func(it interface{}) { + query.Add(_k+"[]", fmt.Sprintf("%v", it)) + }) + } else { + query.Add(_k, fmt.Sprintf("%v", _v)) + } + } + + return query, nil +} + +// GetParametersJSON converts the parameters from GetParameters into the JSON format +func (r *PlaceOrderRequest) GetParametersJSON() ([]byte, error) { + params, err := r.GetParameters() + if err != nil { + return nil, err + } + + return json.Marshal(params) +} + +// GetSlugParameters builds and checks the slug parameters and return the result in a map object +func (r *PlaceOrderRequest) GetSlugParameters() (map[string]interface{}, error) { + var params = map[string]interface{}{} + + return params, nil +} + +func (r *PlaceOrderRequest) applySlugsToUrl(url string, slugs map[string]string) string { + for _k, _v := range slugs { + needleRE := regexp.MustCompile(":" + _k + "\\b") + url = needleRE.ReplaceAllString(url, _v) + } + + return url +} + +func (r *PlaceOrderRequest) iterateSlice(slice interface{}, _f func(it interface{})) { + sliceValue := reflect.ValueOf(slice) + for _i := 0; _i < sliceValue.Len(); _i++ { + it := sliceValue.Index(_i).Interface() + _f(it) + } +} + +func (r *PlaceOrderRequest) isVarSlice(_v interface{}) bool { + rt := reflect.TypeOf(_v) + switch rt.Kind() { + case reflect.Slice: + return true + } + return false +} + +func (r *PlaceOrderRequest) GetSlugsMap() (map[string]string, error) { + slugs := map[string]string{} + params, err := r.GetSlugParameters() + if err != nil { + return slugs, nil + } + + for _k, _v := range params { + slugs[_k] = fmt.Sprintf("%v", _v) + } + + return slugs, nil +} + +// GetPath returns the request path of the API +func (r *PlaceOrderRequest) GetPath() string { + return "/api/v5/trade/order" +} + +// Do generates the request object and send the request object to the API endpoint +func (r *PlaceOrderRequest) Do(ctx context.Context) ([]OrderResponse, error) { + + params, err := r.GetParameters() + if err != nil { + return nil, err + } + query := url.Values{} + + var apiURL string + + apiURL = r.GetPath() + + req, err := r.client.NewAuthenticatedRequest(ctx, "POST", apiURL, query, params) + if err != nil { + return nil, err + } + + response, err := r.client.SendRequest(req) + if err != nil { + return nil, err + } + + var apiResponse APIResponse + if err := response.DecodeJSON(&apiResponse); err != nil { + return nil, err + } + + type responseValidator interface { + Validate() error + } + validator, ok := interface{}(apiResponse).(responseValidator) + if ok { + if err := validator.Validate(); err != nil { + return nil, err + } + } + var data []OrderResponse + if err := json.Unmarshal(apiResponse.Data, &data); err != nil { + return nil, err + } + return data, nil +} diff --git a/pkg/exchange/okex/okexapi/trade.go b/pkg/exchange/okex/okexapi/trade.go index f42553c51..09f03e360 100644 --- a/pkg/exchange/okex/okexapi/trade.go +++ b/pkg/exchange/okex/okexapi/trade.go @@ -11,20 +11,6 @@ import ( "github.com/pkg/errors" ) -type OrderResponse struct { - OrderID string `json:"ordId"` - ClientOrderID string `json:"clOrdId"` - Tag string `json:"tag"` - Code string `json:"sCode"` - Message string `json:"sMsg"` -} - -func (c *RestClient) NewPlaceOrderRequest() *PlaceOrderRequest { - return &PlaceOrderRequest{ - client: c, - } -} - func (c *RestClient) NewBatchPlaceOrderRequest() *BatchPlaceOrderRequest { return &BatchPlaceOrderRequest{ client: c, @@ -61,67 +47,11 @@ func (c *RestClient) NewGetTransactionDetailsRequest() *GetTransactionDetailsReq } } -//go:generate requestgen -type PlaceOrderRequest -type PlaceOrderRequest struct { - client *RestClient - - instrumentID string `param:"instId"` - - // tdMode - // margin mode: "cross", "isolated" - // non-margin mode cash - tradeMode string `param:"tdMode" validValues:"cross,isolated,cash"` - - // A combination of case-sensitive alphanumerics, all numbers, or all letters of up to 32 characters. - clientOrderID *string `param:"clOrdId"` - - // A combination of case-sensitive alphanumerics, all numbers, or all letters of up to 8 characters. - tag *string `param:"tag"` - - // "buy" or "sell" - side SideType `param:"side" validValues:"buy,sell"` - - orderType OrderType `param:"ordType"` - - quantity string `param:"sz"` - - // price - price *string `param:"px"` -} - func (r *PlaceOrderRequest) Parameters() map[string]interface{} { params, _ := r.GetParameters() return params } -func (r *PlaceOrderRequest) Do(ctx context.Context) (*OrderResponse, error) { - payload := r.Parameters() - req, err := r.client.NewAuthenticatedRequest(ctx, "POST", "/api/v5/trade/order", nil, payload) - if err != nil { - return nil, err - } - - response, err := r.client.SendRequest(req) - if err != nil { - return nil, err - } - - var apiResponse APIResponse - if err := response.DecodeJSON(&apiResponse); err != nil { - return nil, err - } - var data []OrderResponse - if err := json.Unmarshal(apiResponse.Data, &data); err != nil { - return nil, err - } - - if len(data) == 0 { - return nil, errors.New("order create error") - } - - return &data[0], nil -} - //go:generate requestgen -type CancelOrderRequest type CancelOrderRequest struct { client *RestClient