Merge pull request #1238 from MengShue/add_unit_test_for_okex

TEST: add unit test for okex exchange
This commit is contained in:
c9s 2023-08-24 12:44:45 +08:00 committed by GitHub
commit 2c4b6e8cd1
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 270 additions and 60 deletions

View File

@ -7,6 +7,7 @@ import (
"strings" "strings"
"github.com/pkg/errors" "github.com/pkg/errors"
"go.uber.org/multierr"
"github.com/c9s/bbgo/pkg/exchange/okex/okexapi" "github.com/c9s/bbgo/pkg/exchange/okex/okexapi"
"github.com/c9s/bbgo/pkg/fixedpoint" "github.com/c9s/bbgo/pkg/fixedpoint"
@ -18,6 +19,7 @@ func toGlobalSymbol(symbol string) string {
} }
// //go:generate sh -c "echo \"package okex\nvar spotSymbolMap = map[string]string{\n\" $(curl -s -L 'https://okex.com/api/v5/public/instruments?instType=SPOT' | jq -r '.data[] | \"\\(.instId | sub(\"-\" ; \"\") | tojson ): \\( .instId | tojson),\n\"') \"\n}\" > symbols.go" // //go:generate sh -c "echo \"package okex\nvar spotSymbolMap = map[string]string{\n\" $(curl -s -L 'https://okex.com/api/v5/public/instruments?instType=SPOT' | jq -r '.data[] | \"\\(.instId | sub(\"-\" ; \"\") | tojson ): \\( .instId | tojson),\n\"') \"\n}\" > symbols.go"
//
//go:generate go run gensymbols.go //go:generate go run gensymbols.go
func toLocalSymbol(symbol string) string { func toLocalSymbol(symbol string) string {
if s, ok := spotSymbolMap[symbol]; ok { if s, ok := spotSymbolMap[symbol]; ok {
@ -163,64 +165,18 @@ func toGlobalTrades(orderDetails []okexapi.OrderDetails) ([]types.Trade, error)
func toGlobalOrders(orderDetails []okexapi.OrderDetails) ([]types.Order, error) { func toGlobalOrders(orderDetails []okexapi.OrderDetails) ([]types.Order, error) {
var orders []types.Order var orders []types.Order
var err error
for _, orderDetail := range orderDetails { for _, orderDetail := range orderDetails {
orderID, err := strconv.ParseInt(orderDetail.OrderID, 10, 64)
if err != nil { o, err2 := toGlobalOrder(&orderDetail)
return orders, err if err2 != nil {
err = multierr.Append(err, err2)
continue
} }
orders = append(orders, *o)
side := types.SideType(strings.ToUpper(string(orderDetail.Side)))
orderType, err := toGlobalOrderType(orderDetail.OrderType)
if err != nil {
return orders, err
}
timeInForce := types.TimeInForceGTC
switch orderDetail.OrderType {
case okexapi.OrderTypeFOK:
timeInForce = types.TimeInForceFOK
case okexapi.OrderTypeIOC:
timeInForce = types.TimeInForceIOC
}
orderStatus, err := toGlobalOrderStatus(orderDetail.State)
if err != nil {
return orders, err
}
isWorking := false
switch orderStatus {
case types.OrderStatusNew, types.OrderStatusPartiallyFilled:
isWorking = true
}
orders = append(orders, types.Order{
SubmitOrder: types.SubmitOrder{
ClientOrderID: orderDetail.ClientOrderID,
Symbol: toGlobalSymbol(orderDetail.InstrumentID),
Side: side,
Type: orderType,
Price: orderDetail.Price,
Quantity: orderDetail.Quantity,
StopPrice: fixedpoint.Zero, // not supported yet
TimeInForce: timeInForce,
},
Exchange: types.ExchangeOKEx,
OrderID: uint64(orderID),
Status: orderStatus,
ExecutedQuantity: orderDetail.FilledQuantity,
IsWorking: isWorking,
CreationTime: types.Time(orderDetail.CreationTime),
UpdateTime: types.Time(orderDetail.UpdateTime),
IsMargin: false,
IsIsolated: false,
})
} }
return orders, nil return orders, err
} }
func toGlobalOrderStatus(state okexapi.OrderState) (types.OrderStatus, error) { func toGlobalOrderStatus(state okexapi.OrderState) (types.OrderStatus, error) {
@ -256,18 +212,19 @@ func toLocalOrderType(orderType types.OrderType) (okexapi.OrderType, error) {
} }
func toGlobalOrderType(orderType okexapi.OrderType) (types.OrderType, error) { func toGlobalOrderType(orderType okexapi.OrderType) (types.OrderType, error) {
// IOC, FOK are only allowed with limit order type, so we assume the order type is always limit order for FOK, IOC orders
switch orderType { switch orderType {
case okexapi.OrderTypeMarket: case okexapi.OrderTypeMarket:
return types.OrderTypeMarket, nil return types.OrderTypeMarket, nil
case okexapi.OrderTypeLimit:
case okexapi.OrderTypeLimit, okexapi.OrderTypeFOK, okexapi.OrderTypeIOC:
return types.OrderTypeLimit, nil return types.OrderTypeLimit, nil
case okexapi.OrderTypePostOnly: case okexapi.OrderTypePostOnly:
return types.OrderTypeLimitMaker, nil return types.OrderTypeLimitMaker, nil
case okexapi.OrderTypeFOK:
case okexapi.OrderTypeIOC:
} }
return "", fmt.Errorf("unknown or unsupported okex order type: %s", orderType) return "", fmt.Errorf("unknown or unsupported okex order type: %s", orderType)
} }
@ -277,3 +234,75 @@ func toLocalInterval(src string) string {
return strings.ToUpper(w) return strings.ToUpper(w)
}) })
} }
func toGlobalSide(side okexapi.SideType) (s types.SideType) {
switch string(side) {
case "sell":
s = types.SideTypeSell
case "buy":
s = types.SideTypeBuy
}
return s
}
func toGlobalOrder(okexOrder *okexapi.OrderDetails) (*types.Order, error) {
orderID, err := strconv.ParseInt(okexOrder.OrderID, 10, 64)
if err != nil {
return nil, err
}
side := toGlobalSide(okexOrder.Side)
orderType, err := toGlobalOrderType(okexOrder.OrderType)
if err != nil {
return nil, err
}
timeInForce := types.TimeInForceGTC
switch okexOrder.OrderType {
case okexapi.OrderTypeFOK:
timeInForce = types.TimeInForceFOK
case okexapi.OrderTypeIOC:
timeInForce = types.TimeInForceIOC
}
orderStatus, err := toGlobalOrderStatus(okexOrder.State)
if err != nil {
return nil, err
}
isWorking := false
switch orderStatus {
case types.OrderStatusNew, types.OrderStatusPartiallyFilled:
isWorking = true
}
isMargin := false
if okexOrder.InstrumentType == string(okexapi.InstrumentTypeMARGIN) {
isMargin = true
}
return &types.Order{
SubmitOrder: types.SubmitOrder{
ClientOrderID: okexOrder.ClientOrderID,
Symbol: toGlobalSymbol(okexOrder.InstrumentID),
Side: side,
Type: orderType,
Price: okexOrder.Price,
Quantity: okexOrder.Quantity,
StopPrice: fixedpoint.Zero, // not supported yet
TimeInForce: timeInForce,
},
Exchange: types.ExchangeOKEx,
OrderID: uint64(orderID),
Status: orderStatus,
ExecutedQuantity: okexOrder.FilledQuantity,
IsWorking: isWorking,
CreationTime: types.Time(okexOrder.CreationTime),
UpdateTime: types.Time(okexOrder.UpdateTime),
IsMargin: isMargin,
IsIsolated: false,
}, nil
}

View File

@ -26,6 +26,8 @@ var log = logrus.WithFields(logrus.Fields{
"exchange": ID, "exchange": ID,
}) })
var ErrSymbolRequired = errors.New("symbol is a required parameter")
type Exchange struct { type Exchange struct {
key, secret, passphrase string key, secret, passphrase string
@ -273,7 +275,7 @@ func (e *Exchange) CancelOrders(ctx context.Context, orders ...types.Order) erro
var reqs []*okexapi.CancelOrderRequest var reqs []*okexapi.CancelOrderRequest
for _, order := range orders { for _, order := range orders {
if len(order.Symbol) == 0 { if len(order.Symbol) == 0 {
return errors.New("symbol is required for canceling an okex order") return ErrSymbolRequired
} }
req := e.client.TradeService.NewCancelOrderRequest() req := e.client.TradeService.NewCancelOrderRequest()
@ -339,3 +341,25 @@ func (e *Exchange) QueryKLines(ctx context.Context, symbol string, interval type
return klines, nil return klines, nil
} }
func (e *Exchange) QueryOrder(ctx context.Context, q types.OrderQuery) (*types.Order, error) {
if len(q.Symbol) == 0 {
return nil, ErrSymbolRequired
}
if len(q.OrderID) == 0 && len(q.ClientOrderID) == 0 {
return nil, errors.New("okex.QueryOrder: OrderId or ClientOrderId is required parameter")
}
req := e.client.TradeService.NewGetOrderDetailsRequest()
req.InstrumentID(q.Symbol).
OrderID(q.OrderID).
ClientOrderID(q.ClientOrderID)
var order *okexapi.OrderDetails
order, err := req.Do(ctx)
if err != nil {
return nil, err
}
return toGlobalOrder(order)
}

View File

@ -47,6 +47,7 @@ const (
InstrumentTypeSwap InstrumentType = "SWAP" InstrumentTypeSwap InstrumentType = "SWAP"
InstrumentTypeFutures InstrumentType = "FUTURES" InstrumentTypeFutures InstrumentType = "FUTURES"
InstrumentTypeOption InstrumentType = "OPTION" InstrumentTypeOption InstrumentType = "OPTION"
InstrumentTypeMARGIN InstrumentType = "MARGIN"
) )
type OrderState string type OrderState string

View File

@ -0,0 +1,107 @@
package okexapi
import (
"context"
"os"
"strconv"
"testing"
"github.com/stretchr/testify/assert"
"github.com/c9s/bbgo/pkg/testutil"
)
func getTestClientOrSkip(t *testing.T) *RestClient {
if b, _ := strconv.ParseBool(os.Getenv("CI")); b {
t.Skip("skip test for CI")
}
key, secret, passphrase, ok := testutil.IntegrationTestWithPassphraseConfigured(t, "OKEX")
if !ok {
t.Skip("Please configure all credentials about OKEX")
return nil
}
client := NewClient()
client.Auth(key, secret, passphrase)
return client
}
func TestClient_GetInstrumentsRequest(t *testing.T) {
client := NewClient()
ctx := context.Background()
srv := &PublicDataService{client: client}
req := srv.NewGetInstrumentsRequest()
instruments, err := req.
InstrumentType(InstrumentTypeSpot).
Do(ctx)
assert.NoError(t, err)
assert.NotEmpty(t, instruments)
t.Logf("instruments: %+v", instruments)
}
func TestClient_GetFundingRateRequest(t *testing.T) {
client := NewClient()
ctx := context.Background()
srv := &PublicDataService{client: client}
req := srv.NewGetFundingRate()
instrument, err := req.
InstrumentID("BTC-USDT-SWAP").
Do(ctx)
assert.NoError(t, err)
assert.NotEmpty(t, instrument)
t.Logf("instrument: %+v", instrument)
}
func TestClient_PlaceOrderRequest(t *testing.T) {
client := getTestClientOrSkip(t)
ctx := context.Background()
srv := &TradeService{client: client}
req := srv.NewPlaceOrderRequest()
order, err := req.
InstrumentID("BTC-USDT").
TradeMode("cash").
Side(SideTypeBuy).
OrderType(OrderTypeLimit).
Price("15000").
Quantity("0.0001").
Do(ctx)
assert.NoError(t, err)
assert.NotEmpty(t, order)
t.Logf("place order: %+v", order)
}
func TestClient_GetPendingOrderRequest(t *testing.T) {
client := getTestClientOrSkip(t)
ctx := context.Background()
srv := &TradeService{client: client}
req := srv.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()
srv := &TradeService{client: client}
req := srv.NewGetOrderDetailsRequest()
orderDetail, err := req.
InstrumentID("BTC-USDT").
OrderID("609869603774656544").
Do(ctx)
assert.NoError(t, err)
assert.NotEmpty(t, orderDetail)
t.Logf("order detail: %+v", orderDetail)
}

View File

@ -363,7 +363,7 @@ func (r *GetOrderDetailsRequest) Do(ctx context.Context) (*OrderDetails, error)
} }
if len(orderResponse.Data) == 0 { if len(orderResponse.Data) == 0 {
return nil, errors.New("order create error") return nil, errors.New("get order details error")
} }
return &orderResponse.Data[0], nil return &orderResponse.Data[0], nil

View File

@ -0,0 +1,36 @@
package okex
import (
"context"
"os"
"testing"
"github.com/c9s/bbgo/pkg/types"
"github.com/stretchr/testify/assert"
)
func Test_QueryOrder(t *testing.T) {
key := os.Getenv("OKEX_API_KEY")
secret := os.Getenv("OKEX_API_SECRET")
passphrase := os.Getenv("OKEX_API_PASSPHRASE")
if len(key) == 0 && len(secret) == 0 {
t.Skip("api key/secret are not configured")
return
}
if len(passphrase) == 0 {
t.Skip("passphrase are not configured")
return
}
e := New(key, secret, passphrase)
queryOrder := types.OrderQuery{
Symbol: "BTC-USDT",
OrderID: "609869603774656544",
}
orderDetail, err := e.QueryOrder(context.Background(), queryOrder)
if assert.NoError(t, err) {
assert.NotEmpty(t, orderDetail)
}
t.Logf("order detail: %+v", orderDetail)
}

View File

@ -23,3 +23,16 @@ func IntegrationTestConfigured(t *testing.T, prefix string) (key, secret string,
return key, secret, ok return key, secret, ok
} }
func IntegrationTestWithPassphraseConfigured(t *testing.T, prefix string) (key, secret, passphrase string, ok bool) {
var hasKey, hasSecret, hasPassphrase bool
key, hasKey = os.LookupEnv(prefix + "_API_KEY")
secret, hasSecret = os.LookupEnv(prefix + "_API_SECRET")
passphrase, hasPassphrase = os.LookupEnv(prefix + "_API_PASSPHRASE")
ok = hasKey && hasSecret && hasPassphrase && os.Getenv("TEST_"+prefix) == "1"
if ok {
t.Logf(prefix+" api integration test enabled, key = %s, secret = %s, passphrase= %s", maskSecret(key), maskSecret(secret), maskSecret(passphrase))
}
return key, secret, passphrase, ok
}