mirror of
https://github.com/c9s/bbgo.git
synced 2024-11-25 16:25:16 +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"
|
"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)
|
||||||
|
if err2 != nil {
|
||||||
|
err = multierr.Append(err, err2)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
orders = append(orders, *o)
|
||||||
|
}
|
||||||
|
|
||||||
return orders, err
|
return orders, err
|
||||||
}
|
|
||||||
|
|
||||||
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
|
|
||||||
}
|
}
|
||||||
|
|
||||||
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
|
||||||
|
}
|
||||||
|
|
|
@ -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)
|
||||||
|
}
|
||||||
|
|
|
@ -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
|
||||||
|
|
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 {
|
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
|
||||||
|
|
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
|
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