mirror of
https://github.com/c9s/bbgo.git
synced 2024-11-21 22:43:52 +00:00
Merge pull request #1238 from MengShue/add_unit_test_for_okex
TEST: add unit test for okex exchange
This commit is contained in:
commit
2c4b6e8cd1
|
@ -7,6 +7,7 @@ import (
|
|||
"strings"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
"go.uber.org/multierr"
|
||||
|
||||
"github.com/c9s/bbgo/pkg/exchange/okex/okexapi"
|
||||
"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 go run gensymbols.go
|
||||
func toLocalSymbol(symbol string) string {
|
||||
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) {
|
||||
var orders []types.Order
|
||||
var err error
|
||||
for _, orderDetail := range orderDetails {
|
||||
orderID, err := strconv.ParseInt(orderDetail.OrderID, 10, 64)
|
||||
if err != nil {
|
||||
return orders, err
|
||||
|
||||
o, err2 := toGlobalOrder(&orderDetail)
|
||||
if err2 != nil {
|
||||
err = multierr.Append(err, err2)
|
||||
continue
|
||||
}
|
||||
|
||||
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,
|
||||
})
|
||||
orders = append(orders, *o)
|
||||
}
|
||||
|
||||
return orders, nil
|
||||
return orders, err
|
||||
}
|
||||
|
||||
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) {
|
||||
// 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 {
|
||||
case okexapi.OrderTypeMarket:
|
||||
return types.OrderTypeMarket, nil
|
||||
case okexapi.OrderTypeLimit:
|
||||
|
||||
case okexapi.OrderTypeLimit, okexapi.OrderTypeFOK, okexapi.OrderTypeIOC:
|
||||
return types.OrderTypeLimit, nil
|
||||
|
||||
case okexapi.OrderTypePostOnly:
|
||||
return types.OrderTypeLimitMaker, nil
|
||||
|
||||
case okexapi.OrderTypeFOK:
|
||||
case okexapi.OrderTypeIOC:
|
||||
|
||||
}
|
||||
|
||||
return "", fmt.Errorf("unknown or unsupported okex order type: %s", orderType)
|
||||
}
|
||||
|
||||
|
@ -277,3 +234,75 @@ func toLocalInterval(src string) string {
|
|||
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
|
||||
}
|
||||
|
|
|
@ -26,6 +26,8 @@ var log = logrus.WithFields(logrus.Fields{
|
|||
"exchange": ID,
|
||||
})
|
||||
|
||||
var ErrSymbolRequired = errors.New("symbol is a required parameter")
|
||||
|
||||
type Exchange struct {
|
||||
key, secret, passphrase string
|
||||
|
||||
|
@ -273,7 +275,7 @@ func (e *Exchange) CancelOrders(ctx context.Context, orders ...types.Order) erro
|
|||
var reqs []*okexapi.CancelOrderRequest
|
||||
for _, order := range orders {
|
||||
if len(order.Symbol) == 0 {
|
||||
return errors.New("symbol is required for canceling an okex order")
|
||||
return ErrSymbolRequired
|
||||
}
|
||||
|
||||
req := e.client.TradeService.NewCancelOrderRequest()
|
||||
|
@ -339,3 +341,25 @@ func (e *Exchange) QueryKLines(ctx context.Context, symbol string, interval type
|
|||
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)
|
||||
}
|
||||
|
|
|
@ -47,6 +47,7 @@ const (
|
|||
InstrumentTypeSwap InstrumentType = "SWAP"
|
||||
InstrumentTypeFutures InstrumentType = "FUTURES"
|
||||
InstrumentTypeOption InstrumentType = "OPTION"
|
||||
InstrumentTypeMARGIN InstrumentType = "MARGIN"
|
||||
)
|
||||
|
||||
type OrderState string
|
||||
|
|
107
pkg/exchange/okex/okexapi/client_test.go
Normal file
107
pkg/exchange/okex/okexapi/client_test.go
Normal 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)
|
||||
}
|
|
@ -363,7 +363,7 @@ func (r *GetOrderDetailsRequest) Do(ctx context.Context) (*OrderDetails, error)
|
|||
}
|
||||
|
||||
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
|
||||
|
|
36
pkg/exchange/okex/query_order_test.go
Normal file
36
pkg/exchange/okex/query_order_test.go
Normal 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)
|
||||
}
|
|
@ -23,3 +23,16 @@ func IntegrationTestConfigured(t *testing.T, prefix string) (key, secret string,
|
|||
|
||||
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
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue
Block a user