Merge pull request #1494 from c9s/edwin/okx/place-order

FEATURE: [okx] generate place order request by requestgen
This commit is contained in:
bailantaotao 2024-01-11 10:33:31 +08:00 committed by GitHub
commit 8eb555619f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 427 additions and 264 deletions

View File

@ -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
/*

View File

@ -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) {

View File

@ -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}
}

View File

@ -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)
}

View File

@ -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
}

View File

@ -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