Skip to content

Commit

Permalink
Merge pull request c9s#1498 from c9s/edwin/okx/query-open-orders
Browse files Browse the repository at this point in the history
FEATURE: [okx] support query open orders
  • Loading branch information
bailantaotao authored Jan 14, 2024
2 parents c01be14 + 228bfba commit 03449d0
Show file tree
Hide file tree
Showing 9 changed files with 628 additions and 117 deletions.
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
}

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

0 comments on commit 03449d0

Please sign in to comment.