Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

FEATURE: [okx] support query open orders #1498

Merged
merged 2 commits into from
Jan 14, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
43 changes: 43 additions & 0 deletions pkg/exchange/okex/convert.go
Original file line number Diff line number Diff line change
Expand Up @@ -149,6 +149,49 @@ func toGlobalTrades(orderDetails []okexapi.OrderDetails) ([]types.Trade, error)
return trades, nil
}

func openOrderToGlobal(order *okexapi.OpenOrder) (*types.Order, error) {
side := toGlobalSide(order.Side)

orderType, err := toGlobalOrderType(order.OrderType)
if err != nil {
return nil, err
}

timeInForce := types.TimeInForceGTC
switch order.OrderType {
case okexapi.OrderTypeFOK:
timeInForce = types.TimeInForceFOK
case okexapi.OrderTypeIOC:
timeInForce = types.TimeInForceIOC
}

orderStatus, err := toGlobalOrderStatus(order.State)
if err != nil {
return nil, err
}

return &types.Order{
SubmitOrder: types.SubmitOrder{
ClientOrderID: order.ClientOrderId,
Symbol: toGlobalSymbol(order.InstrumentID),
Side: side,
Type: orderType,
Price: order.Price,
Quantity: order.Size,
TimeInForce: timeInForce,
},
Exchange: types.ExchangeOKEx,
OrderID: uint64(order.OrderId),
UUID: strconv.FormatInt(int64(order.OrderId), 10),
Status: orderStatus,
OriginalStatus: string(order.State),
ExecutedQuantity: order.AccumulatedFillSize,
IsWorking: order.State.IsWorking(),
CreationTime: types.Time(order.CreatedTime),
UpdateTime: types.Time(order.UpdatedTime),
}, nil
}

func toGlobalOrders(orderDetails []okexapi.OrderDetails) ([]types.Order, error) {
var orders []types.Order
var err error
Expand Down
84 changes: 84 additions & 0 deletions pkg/exchange/okex/convert_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
package okex

import (
"fmt"
"testing"

"github.com/stretchr/testify/assert"

"github.com/c9s/bbgo/pkg/exchange/okex/okexapi"
"github.com/c9s/bbgo/pkg/fixedpoint"
"github.com/c9s/bbgo/pkg/types"
)

func Test_openOrderToGlobal(t *testing.T) {
var (
assert = assert.New(t)

orderId = 665576973905014786
// {"accFillSz":"0","algoClOrdId":"","algoId":"","attachAlgoClOrdId":"","attachAlgoOrds":[],"avgPx":"","cTime":"1704957916401","cancelSource":"","cancelSourceReason":"","category":"normal","ccy":"","clOrdId":"","fee":"0","feeCcy":"USDT","fillPx":"","fillSz":"0","fillTime":"","instId":"BTC-USDT","instType":"SPOT","lever":"","ordId":"665576973905014786","ordType":"limit","pnl":"0","posSide":"net","px":"48174.5","pxType":"","pxUsd":"","pxVol":"","quickMgnType":"","rebate":"0","rebateCcy":"BTC","reduceOnly":"false","side":"sell","slOrdPx":"","slTriggerPx":"","slTriggerPxType":"","source":"","state":"live","stpId":"","stpMode":"","sz":"0.00001","tag":"","tdMode":"cash","tgtCcy":"","tpOrdPx":"","tpTriggerPx":"","tpTriggerPxType":"","tradeId":"","uTime":"1704957916401"}
openOrder = &okexapi.OpenOrder{
AccumulatedFillSize: fixedpoint.NewFromFloat(0),
AvgPrice: fixedpoint.NewFromFloat(0),
CreatedTime: types.NewMillisecondTimestampFromInt(1704957916401),
Category: "normal",
Currency: "BTC",
ClientOrderId: "",
Fee: fixedpoint.Zero,
FeeCurrency: "USDT",
FillTime: types.NewMillisecondTimestampFromInt(0),
InstrumentID: "BTC-USDT",
InstrumentType: okexapi.InstrumentTypeSpot,
OrderId: types.StrInt64(orderId),
OrderType: okexapi.OrderTypeLimit,
Price: fixedpoint.NewFromFloat(48174.5),
Side: okexapi.SideTypeBuy,
State: okexapi.OrderStateLive,
Size: fixedpoint.NewFromFloat(0.00001),
UpdatedTime: types.NewMillisecondTimestampFromInt(1704957916401),
}
expOrder = &types.Order{
SubmitOrder: types.SubmitOrder{
ClientOrderID: openOrder.ClientOrderId,
Symbol: toGlobalSymbol(openOrder.InstrumentID),
Side: types.SideTypeBuy,
Type: types.OrderTypeLimit,
Quantity: fixedpoint.NewFromFloat(0.00001),
Price: fixedpoint.NewFromFloat(48174.5),
AveragePrice: fixedpoint.Zero,
StopPrice: fixedpoint.Zero,
TimeInForce: types.TimeInForceGTC,
},
Exchange: types.ExchangeOKEx,
OrderID: uint64(orderId),
UUID: fmt.Sprintf("%d", orderId),
Status: types.OrderStatusNew,
OriginalStatus: string(okexapi.OrderStateLive),
ExecutedQuantity: fixedpoint.Zero,
IsWorking: true,
CreationTime: types.Time(types.NewMillisecondTimestampFromInt(1704957916401).Time()),
UpdateTime: types.Time(types.NewMillisecondTimestampFromInt(1704957916401).Time()),
}
)

t.Run("succeeds", func(t *testing.T) {
order, err := openOrderToGlobal(openOrder)
assert.NoError(err)
assert.Equal(expOrder, order)
})

t.Run("unexpected order status", func(t *testing.T) {
newOrder := *openOrder
newOrder.State = "xxx"
_, err := openOrderToGlobal(&newOrder)
assert.ErrorContains(err, "xxx")
})

t.Run("unexpected order type", func(t *testing.T) {
newOrder := *openOrder
newOrder.OrderType = "xxx"
_, err := openOrderToGlobal(&newOrder)
assert.ErrorContains(err, "xxx")
})

}
51 changes: 41 additions & 10 deletions pkg/exchange/okex/exchange.go
Original file line number Diff line number Diff line change
Expand Up @@ -30,15 +30,15 @@ var (
queryAccountLimiter = rate.NewLimiter(rate.Every(200*time.Millisecond), 5)
placeOrderLimiter = rate.NewLimiter(rate.Every(30*time.Millisecond), 30)
batchCancelOrderLimiter = rate.NewLimiter(rate.Every(5*time.Millisecond), 200)
queryOpenOrderLimiter = rate.NewLimiter(rate.Every(30*time.Millisecond), 30)
)

const ID = "okex"
const (
ID = "okex"

// PlatformToken is the platform currency of OKEx, pre-allocate static string here
const PlatformToken = "OKB"
// PlatformToken is the platform currency of OKEx, pre-allocate static string here
PlatformToken = "OKB"

const (
// Constant For query limit
defaultQueryLimit = 100
)

Expand Down Expand Up @@ -295,15 +295,46 @@ func (e *Exchange) SubmitOrder(ctx context.Context, order types.SubmitOrder) (*t
*/
}

// QueryOpenOrders retrieves the pending orders. The data returned is ordered by createdTime, and we utilized the
// `After` parameter to acquire all orders.
func (e *Exchange) QueryOpenOrders(ctx context.Context, symbol string) (orders []types.Order, err error) {
instrumentID := toLocalSymbol(symbol)
req := e.client.NewGetPendingOrderRequest().InstrumentType(okexapi.InstrumentTypeSpot).InstrumentID(instrumentID)
orderDetails, err := req.Do(ctx)
if err != nil {
return orders, err

nextCursor := int64(0)
for {
if err := queryOpenOrderLimiter.Wait(ctx); err != nil {
return nil, fmt.Errorf("query open orders rate limiter wait error: %w", err)
}

req := e.client.NewGetOpenOrdersRequest().
InstrumentID(instrumentID).
After(strconv.FormatInt(nextCursor, 10))
openOrders, err := req.Do(ctx)
if err != nil {
return nil, fmt.Errorf("failed to query open orders: %w", err)
}

for _, o := range openOrders {
o, err := openOrderToGlobal(&o)
if err != nil {
return nil, fmt.Errorf("failed to convert order, err: %v", err)
}

orders = append(orders, *o)
}

orderLen := len(openOrders)
// a defensive programming to ensure the length of order response is expected.
if orderLen > defaultQueryLimit {
return nil, fmt.Errorf("unexpected open orders length %d", orderLen)
}

if orderLen < defaultQueryLimit {
break
}
nextCursor = int64(openOrders[orderLen-1].OrderId)
}

orders, err = toGlobalOrders(orderDetails)
return orders, err
}

Expand Down
4 changes: 4 additions & 0 deletions pkg/exchange/okex/okexapi/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,10 @@ const (
OrderStateFilled OrderState = "filled"
)

func (o OrderState) IsWorking() bool {
return o == OrderStateLive || o == OrderStatePartiallyFilled
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I guess you can use switch case here

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why? The function represents only the working status- either live or partially filled.

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

like this:

switch o {  case OrderStateLive, OrderStatePartiallyFilled: return true;  default: return false  }

}

type RestClient struct {
requestgen.BaseAPIClient

Expand Down
35 changes: 20 additions & 15 deletions pkg/exchange/okex/okexapi/client_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -140,6 +140,26 @@ func TestClient_CancelOrderRequest(t *testing.T) {
t.Log(cancelResp)
}

func TestClient_OpenOrdersRequest(t *testing.T) {
client := getTestClientOrSkip(t)
ctx := context.Background()

orders := []OpenOrder{}
beforeId := int64(0)
for {
c := client.NewGetOpenOrdersRequest().InstrumentID("BTC-USDT").Limit("1").After(fmt.Sprintf("%d", beforeId))
res, err := c.Do(ctx)
assert.NoError(t, err)
if len(res) != 1 {
break
}
orders = append(orders, res...)
beforeId = int64(res[0].OrderId)
}

t.Log(orders)
}

func TestClient_BatchCancelOrderRequest(t *testing.T) {
client := getTestClientOrSkip(t)
ctx := context.Background()
Expand Down Expand Up @@ -170,21 +190,6 @@ func TestClient_BatchCancelOrderRequest(t *testing.T) {
t.Log(cancelResp)
}

func TestClient_GetPendingOrderRequest(t *testing.T) {
client := getTestClientOrSkip(t)
ctx := context.Background()
req := client.NewGetPendingOrderRequest()
odr_type := []string{string(OrderTypeLimit), string(OrderTypeIOC)}

pending_order, err := req.
InstrumentID("BTC-USDT").
OrderTypes(odr_type).
Do(ctx)
assert.NoError(t, err)
assert.NotEmpty(t, pending_order)
t.Logf("pending order: %+v", pending_order)
}

func TestClient_GetOrderDetailsRequest(t *testing.T) {
client := getTestClientOrSkip(t)
ctx := context.Background()
Expand Down
112 changes: 112 additions & 0 deletions pkg/exchange/okex/okexapi/get_open_orders_request.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
package okexapi

import (
"time"

"github.com/c9s/requestgen"

"github.com/c9s/bbgo/pkg/fixedpoint"
"github.com/c9s/bbgo/pkg/types"
)

//go:generate -command GetRequest requestgen -method GET -responseType .APIResponse -responseDataField Data
//go:generate -command PostRequest requestgen -method POST -responseType .APIResponse -responseDataField Data

type OpenOrder struct {
AccumulatedFillSize fixedpoint.Value `json:"accFillSz"`
// If none is filled, it will return "".
AvgPrice fixedpoint.Value `json:"avgPx"`
CreatedTime types.MillisecondTimestamp `json:"cTime"`
Category string `json:"category"`
ClientOrderId string `json:"clOrdId"`
Fee fixedpoint.Value `json:"fee"`
FeeCurrency string `json:"feeCcy"`
// Last filled time
FillTime types.MillisecondTimestamp `json:"fillTime"`
InstrumentID string `json:"instId"`
InstrumentType InstrumentType `json:"instType"`
OrderId types.StrInt64 `json:"ordId"`
OrderType OrderType `json:"ordType"`
Price fixedpoint.Value `json:"px"`
Side SideType `json:"side"`
State OrderState `json:"state"`
Size fixedpoint.Value `json:"sz"`
TargetCurrency string `json:"tgtCcy"`
UpdatedTime types.MillisecondTimestamp `json:"uTime"`

// Margin currency
// Only applicable to cross MARGIN orders in Single-currency margin.
Currency string `json:"ccy"`
TradeId string `json:"tradeId"`
// Last filled price
FillPrice fixedpoint.Value `json:"fillPx"`
// Last filled quantity
FillSize fixedpoint.Value `json:"fillSz"`
// Leverage, from 0.01 to 125.
// Only applicable to MARGIN/FUTURES/SWAP
Lever string `json:"lever"`
// Profit and loss, Applicable to orders which have a trade and aim to close position. It always is 0 in other conditions
Pnl fixedpoint.Value `json:"pnl"`
PositionSide string `json:"posSide"`
// Options price in USDOnly applicable to options; return "" for other instrument types
PriceUsd fixedpoint.Value `json:"pxUsd"`
// Implied volatility of the options orderOnly applicable to options; return "" for other instrument types
PriceVol fixedpoint.Value `json:"pxVol"`
// Price type of options
PriceType string `json:"pxType"`
// Rebate amount, only applicable to spot and margin, the reward of placing orders from the platform (rebate)
// given to user who has reached the specified trading level. If there is no rebate, this field is "".
Rebate fixedpoint.Value `json:"rebate"`
RebateCcy string `json:"rebateCcy"`
// Client-supplied Algo ID when placing order attaching TP/SL.
AttachAlgoClOrdId string `json:"attachAlgoClOrdId"`
SlOrdPx fixedpoint.Value `json:"slOrdPx"`
SlTriggerPx fixedpoint.Value `json:"slTriggerPx"`
SlTriggerPxType string `json:"slTriggerPxType"`
AttachAlgoOrds []interface{} `json:"attachAlgoOrds"`
Source string `json:"source"`
// Self trade prevention ID. Return "" if self trade prevention is not applicable
StpId string `json:"stpId"`
// Self trade prevention mode. Return "" if self trade prevention is not applicable
StpMode string `json:"stpMode"`
Tag string `json:"tag"`
TradeMode string `json:"tdMode"`
TpOrdPx fixedpoint.Value `json:"tpOrdPx"`
TpTriggerPx fixedpoint.Value `json:"tpTriggerPx"`
TpTriggerPxType string `json:"tpTriggerPxType"`
ReduceOnly string `json:"reduceOnly"`
QuickMgnType string `json:"quickMgnType"`
AlgoClOrdId string `json:"algoClOrdId"`
AlgoId string `json:"algoId"`
}

//go:generate GetRequest -url "/api/v5/trade/orders-pending" -type GetOpenOrdersRequest -responseDataType []OpenOrder
type GetOpenOrdersRequest struct {
client requestgen.AuthenticatedAPIClient

instrumentID *string `param:"instId,query"`

instrumentType InstrumentType `param:"instType,query"`

orderType *OrderType `param:"ordType,query"`

state *OrderState `param:"state,query"`
category *string `param:"category,query"`
// Pagination of data to return records earlier than the requested ordId
after *string `param:"after,query"`
// Pagination of data to return records newer than the requested ordId
before *string `param:"before,query"`
// Filter with a begin timestamp. Unix timestamp format in milliseconds, e.g. 1597026383085
begin *time.Time `param:"begin,query"`

// Filter with an end timestamp. Unix timestamp format in milliseconds, e.g. 1597026383085
end *time.Time `param:"end,query"`
limit *string `param:"limit,query"`
}

func (c *RestClient) NewGetOpenOrdersRequest() *GetOpenOrdersRequest {
return &GetOpenOrdersRequest{
client: c,
instrumentType: InstrumentTypeSpot,
}
}
Loading
Loading