From cba5663fac69fda27b07ccd66b66868b97fbea97 Mon Sep 17 00:00:00 2001 From: "Alan.sung" Date: Fri, 21 Jul 2023 17:05:19 +0800 Subject: [PATCH 01/10] add unit test for okex exchange --- pkg/exchange/okex/okexapi/client_test.go | 76 ++++++++++++++++++++++++ pkg/testutil/auth.go | 13 ++++ 2 files changed, 89 insertions(+) create mode 100644 pkg/exchange/okex/okexapi/client_test.go diff --git a/pkg/exchange/okex/okexapi/client_test.go b/pkg/exchange/okex/okexapi/client_test.go new file mode 100644 index 000000000..f2c9d3e68 --- /dev/null +++ b/pkg/exchange/okex/okexapi/client_test.go @@ -0,0 +1,76 @@ +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.SkipNow() + return nil + } + + client := NewClient() + client.Auth(key, secret, passphrase) + return client +} + +func TestClient_GetInstrumentsRequest(t *testing.T) { + client := NewClient() + ctx := context.Background() + + ser := PublicDataService{client: client} + req := ser.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() + ser := PublicDataService{client: client} + req := ser.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() + ser := TradeService{client: client} + req := ser.NewPlaceOrderRequest() + + order, err := req. + InstrumentID("XTZ-BTC"). + TradeMode("cash"). + Side(SideTypeSell). + OrderType(OrderTypeLimit). + Price("0.001"). + Quantity("0.01"). + Do(ctx) + assert.NoError(t, err) + assert.NotEmpty(t, order) + t.Logf("order: %+v", order) // Right now account has no money +} diff --git a/pkg/testutil/auth.go b/pkg/testutil/auth.go index 164207e29..8e5bd43c7 100644 --- a/pkg/testutil/auth.go +++ b/pkg/testutil/auth.go @@ -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 +} From b0ccc7e51bdd9c0f3f42cd54c53630fec7255514 Mon Sep 17 00:00:00 2001 From: "Alan.sung" Date: Mon, 31 Jul 2023 11:00:38 +0900 Subject: [PATCH 02/10] use &PublicDataService{} to create it as a pointer object and rename ser to srv --- pkg/exchange/okex/okexapi/client_test.go | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/pkg/exchange/okex/okexapi/client_test.go b/pkg/exchange/okex/okexapi/client_test.go index f2c9d3e68..a5abe4210 100644 --- a/pkg/exchange/okex/okexapi/client_test.go +++ b/pkg/exchange/okex/okexapi/client_test.go @@ -31,8 +31,8 @@ func TestClient_GetInstrumentsRequest(t *testing.T) { client := NewClient() ctx := context.Background() - ser := PublicDataService{client: client} - req := ser.NewGetInstrumentsRequest() + srv := &PublicDataService{client: client} + req := srv.NewGetInstrumentsRequest() instruments, err := req. InstrumentType(InstrumentTypeSpot). @@ -45,8 +45,8 @@ func TestClient_GetInstrumentsRequest(t *testing.T) { func TestClient_GetFundingRateRequest(t *testing.T) { client := NewClient() ctx := context.Background() - ser := PublicDataService{client: client} - req := ser.NewGetFundingRate() + srv := &PublicDataService{client: client} + req := srv.NewGetFundingRate() instrument, err := req. InstrumentID("BTC-USDT-SWAP"). @@ -59,8 +59,8 @@ func TestClient_GetFundingRateRequest(t *testing.T) { func TestClient_PlaceOrderRequest(t *testing.T) { client := getTestClientOrSkip(t) ctx := context.Background() - ser := TradeService{client: client} - req := ser.NewPlaceOrderRequest() + srv := &TradeService{client: client} + req := srv.NewPlaceOrderRequest() order, err := req. InstrumentID("XTZ-BTC"). From 1c5d2dc7591a82d781d55f032d4eb26d139fbe3e Mon Sep 17 00:00:00 2001 From: "Alan.sung" Date: Wed, 9 Aug 2023 15:05:26 +0800 Subject: [PATCH 03/10] add QueryOrder in okex exchange.go --- go.sum | 1 + pkg/exchange/okex/convert.go | 29 ++++++++------- pkg/exchange/okex/exchange.go | 46 ++++++++++++++++++++++++ pkg/exchange/okex/okexapi/client_test.go | 31 ++++++++++++++++ pkg/exchange/okex/okexapi/trade.go | 2 +- 5 files changed, 95 insertions(+), 14 deletions(-) diff --git a/go.sum b/go.sum index cdc1af5fe..b39b94663 100644 --- a/go.sum +++ b/go.sum @@ -1000,6 +1000,7 @@ golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8T golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20220517211312-f3a8303e98df/go.mod h1:K8+ghG5WaK9qNqU5K3HdILfMLy1f3aNYFI/wnl100a8= gonum.org/v1/gonum v0.0.0-20180816165407-929014505bf4/go.mod h1:Y+Yx5eoAFn32cQvJDxZx5Dpnq+c3wtXuadVZAcxbbBo= gonum.org/v1/gonum v0.0.0-20190226202314-149afe6ec0b6/go.mod h1:jevfED4GnIEnJrWW55YmY9DMhajHcnkqVnEXmEtMyNI= gonum.org/v1/gonum v0.0.0-20190902003836-43865b531bee/go.mod h1:9mxDZsDKxgMAuccQkewq682L+0eCu4dCN2yonUJTCLU= diff --git a/pkg/exchange/okex/convert.go b/pkg/exchange/okex/convert.go index 968544729..2977c12c2 100644 --- a/pkg/exchange/okex/convert.go +++ b/pkg/exchange/okex/convert.go @@ -18,6 +18,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 { @@ -223,20 +224,20 @@ func toGlobalOrders(orderDetails []okexapi.OrderDetails) ([]types.Order, error) return orders, nil } -func toGlobalOrderStatus(state okexapi.OrderState) (types.OrderStatus, error) { +func toGlobalOrderStatus(state okexapi.OrderState) types.OrderStatus { switch state { case okexapi.OrderStateCanceled: - return types.OrderStatusCanceled, nil + return types.OrderStatusCanceled case okexapi.OrderStateLive: - return types.OrderStatusNew, nil + return types.OrderStatusNew case okexapi.OrderStatePartiallyFilled: - return types.OrderStatusPartiallyFilled, nil + return types.OrderStatusPartiallyFilled case okexapi.OrderStateFilled: - return types.OrderStatusFilled, nil + return types.OrderStatusFilled } - return "", fmt.Errorf("unknown or unsupported okex order state: %s", state) + return types.OrderStatus(state) } func toLocalOrderType(orderType types.OrderType) (okexapi.OrderType, error) { @@ -255,20 +256,22 @@ func toLocalOrderType(orderType types.OrderType) (okexapi.OrderType, error) { return "", fmt.Errorf("unknown or unsupported okex order type: %s", orderType) } -func toGlobalOrderType(orderType okexapi.OrderType) (types.OrderType, error) { +func toGlobalOrderType(orderType okexapi.OrderType) types.OrderType { switch orderType { case okexapi.OrderTypeMarket: - return types.OrderTypeMarket, nil + return types.OrderTypeMarket case okexapi.OrderTypeLimit: - return types.OrderTypeLimit, nil + return types.OrderTypeLimit case okexapi.OrderTypePostOnly: - return types.OrderTypeLimitMaker, nil - + return types.OrderTypeLimitMaker case okexapi.OrderTypeFOK: + return types.OrderTypeFillOrKill case okexapi.OrderTypeIOC: - + return types.OrderTypeIOC + default: + log.Errorf("unsupported order type: %v", orderType) + return "" } - return "", fmt.Errorf("unknown or unsupported okex order type: %s", orderType) } func toLocalInterval(src string) string { diff --git a/pkg/exchange/okex/exchange.go b/pkg/exchange/okex/exchange.go index 0ad66181f..1214ed527 100644 --- a/pkg/exchange/okex/exchange.go +++ b/pkg/exchange/okex/exchange.go @@ -339,3 +339,49 @@ 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, errors.New("okex.QueryOrder: InstrumentID is required parameter") + } + if len(q.OrderID) == 0 && len(q.ClientOrderID) == 0 { + return nil, errors.New("okex.QueryOrder: ordId or clOrdId is required parameter") + } + req := e.client.TradeService.NewGetOrderDetailsRequest() + req.InstrumentID(q.Symbol). + OrderID(q.OrderID). + ClientOrderID(q.ClientOrderID) + orderID, err := strconv.ParseInt(q.OrderID, 10, 64) + if err != nil { + return nil, err + } + + var order *okexapi.OrderDetails + order, err = req.Do(ctx) + + if err != nil { + return nil, err + } + + timeInForce := types.TimeInForceFOK + if order.OrderType == okexapi.OrderTypeIOC { + timeInForce = types.TimeInForceIOC + } + return &types.Order{ + SubmitOrder: types.SubmitOrder{ + ClientOrderID: order.ClientOrderID, + Symbol: order.InstrumentID, + Side: types.SideType(order.Side), + Type: toGlobalOrderType(order.OrderType), + Quantity: order.Quantity, + Price: order.Price, + TimeInForce: types.TimeInForce(timeInForce), + }, + Exchange: types.ExchangeOKEx, + OrderID: uint64(orderID), + Status: toGlobalOrderStatus(order.State), + ExecutedQuantity: order.FilledQuantity, + CreationTime: types.Time(order.CreationTime), + UpdateTime: types.Time(order.UpdateTime), + }, nil +} diff --git a/pkg/exchange/okex/okexapi/client_test.go b/pkg/exchange/okex/okexapi/client_test.go index a5abe4210..04fc69e1d 100644 --- a/pkg/exchange/okex/okexapi/client_test.go +++ b/pkg/exchange/okex/okexapi/client_test.go @@ -74,3 +74,34 @@ func TestClient_PlaceOrderRequest(t *testing.T) { assert.NotEmpty(t, order) t.Logf("order: %+v", order) // Right now account has no money } + +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("XTZ-BTC"). + OrderTypes(odr_type). + Do(ctx) + assert.NoError(t, err) + assert.Empty(t, pending_order) + t.Logf("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("xxx-test-order-id"). + Do(ctx) + assert.Error(t, err) // Right now account has no orders + assert.Empty(t, orderDetail) + t.Logf("err: %+v", err) +} diff --git a/pkg/exchange/okex/okexapi/trade.go b/pkg/exchange/okex/okexapi/trade.go index 7adc37c98..8a814057b 100644 --- a/pkg/exchange/okex/okexapi/trade.go +++ b/pkg/exchange/okex/okexapi/trade.go @@ -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 From ea5b45bfe44c3389cf418d004b0ded74edf712a6 Mon Sep 17 00:00:00 2001 From: "Alan.sung" Date: Fri, 11 Aug 2023 09:28:58 +0800 Subject: [PATCH 04/10] queryOrder() and test for it --- go.sum | 1 - pkg/exchange/okex/convert.go | 57 ++++++++++++++++++------ pkg/exchange/okex/exchange.go | 31 +++---------- pkg/exchange/okex/okexapi/client.go | 13 +++--- pkg/exchange/okex/okexapi/client_test.go | 24 +++++----- pkg/exchange/okex/query_order_test.go | 36 +++++++++++++++ 6 files changed, 106 insertions(+), 56 deletions(-) create mode 100644 pkg/exchange/okex/query_order_test.go diff --git a/go.sum b/go.sum index b39b94663..cdc1af5fe 100644 --- a/go.sum +++ b/go.sum @@ -1000,7 +1000,6 @@ golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8T golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -golang.org/x/xerrors v0.0.0-20220517211312-f3a8303e98df/go.mod h1:K8+ghG5WaK9qNqU5K3HdILfMLy1f3aNYFI/wnl100a8= gonum.org/v1/gonum v0.0.0-20180816165407-929014505bf4/go.mod h1:Y+Yx5eoAFn32cQvJDxZx5Dpnq+c3wtXuadVZAcxbbBo= gonum.org/v1/gonum v0.0.0-20190226202314-149afe6ec0b6/go.mod h1:jevfED4GnIEnJrWW55YmY9DMhajHcnkqVnEXmEtMyNI= gonum.org/v1/gonum v0.0.0-20190902003836-43865b531bee/go.mod h1:9mxDZsDKxgMAuccQkewq682L+0eCu4dCN2yonUJTCLU= diff --git a/pkg/exchange/okex/convert.go b/pkg/exchange/okex/convert.go index 2977c12c2..175dcf731 100644 --- a/pkg/exchange/okex/convert.go +++ b/pkg/exchange/okex/convert.go @@ -2,6 +2,7 @@ package okex import ( "fmt" + "hash/fnv" "regexp" "strconv" "strings" @@ -172,10 +173,7 @@ func toGlobalOrders(orderDetails []okexapi.OrderDetails) ([]types.Order, error) side := types.SideType(strings.ToUpper(string(orderDetail.Side))) - orderType, err := toGlobalOrderType(orderDetail.OrderType) - if err != nil { - return orders, err - } + orderType := toGlobalOrderType(orderDetail.OrderType) timeInForce := types.TimeInForceGTC switch orderDetail.OrderType { @@ -186,10 +184,7 @@ func toGlobalOrders(orderDetails []okexapi.OrderDetails) ([]types.Order, error) } - orderStatus, err := toGlobalOrderStatus(orderDetail.State) - if err != nil { - return orders, err - } + orderStatus := toGlobalOrderStatus(orderDetail.State) isWorking := false switch orderStatus { @@ -258,16 +253,21 @@ func toLocalOrderType(orderType types.OrderType) (okexapi.OrderType, error) { func toGlobalOrderType(orderType okexapi.OrderType) types.OrderType { switch orderType { - case okexapi.OrderTypeMarket: + case okexapi.OrderTypeMarket, okexapi.OrderTypeMarketMakerProtection: return types.OrderTypeMarket + case okexapi.OrderTypeLimit: return types.OrderTypeLimit + case okexapi.OrderTypePostOnly: return types.OrderTypeLimitMaker - case okexapi.OrderTypeFOK: - return types.OrderTypeFillOrKill - case okexapi.OrderTypeIOC: - return types.OrderTypeIOC + + case okexapi.OrderTypeIOC, okexapi.OrderTypeFOK: + return types.OrderTypeMarket + + case okexapi.OrderTypeMarektMakerProtectionPostOnly: + return types.OrderTypeLimitMaker + default: log.Errorf("unsupported order type: %v", orderType) return "" @@ -280,3 +280,34 @@ func toLocalInterval(src string) string { return strings.ToUpper(w) }) } + +func hashStringID(s string) uint64 { + h := fnv.New64a() + h.Write([]byte(s)) + return h.Sum64() +} + +func toGlobalOrder(okexOrder *okexapi.OrderDetails, isMargin bool) (*types.Order, error) { + timeInForce := types.TimeInForceFOK + if okexOrder.OrderType == okexapi.OrderTypeIOC { + timeInForce = types.TimeInForceIOC + } + return &types.Order{ + SubmitOrder: types.SubmitOrder{ + ClientOrderID: okexOrder.ClientOrderID, + Symbol: okexOrder.InstrumentID, + Side: types.SideType(okexOrder.Side), + Type: toGlobalOrderType(okexOrder.OrderType), + Quantity: okexOrder.Quantity, + Price: okexOrder.Price, + TimeInForce: types.TimeInForce(timeInForce), + }, + Exchange: types.ExchangeOKEx, + OrderID: hashStringID(okexOrder.OrderID), + Status: toGlobalOrderStatus(okexOrder.State), + ExecutedQuantity: okexOrder.FilledQuantity, + CreationTime: types.Time(okexOrder.CreationTime), + UpdateTime: types.Time(okexOrder.UpdateTime), + IsMargin: isMargin, + }, nil +} diff --git a/pkg/exchange/okex/exchange.go b/pkg/exchange/okex/exchange.go index 1214ed527..02b48af64 100644 --- a/pkg/exchange/okex/exchange.go +++ b/pkg/exchange/okex/exchange.go @@ -351,37 +351,18 @@ func (e *Exchange) QueryOrder(ctx context.Context, q types.OrderQuery) (*types.O req.InstrumentID(q.Symbol). OrderID(q.OrderID). ClientOrderID(q.ClientOrderID) - orderID, err := strconv.ParseInt(q.OrderID, 10, 64) - if err != nil { - return nil, err - } var order *okexapi.OrderDetails - order, err = req.Do(ctx) + order, err := req.Do(ctx) if err != nil { return nil, err } - timeInForce := types.TimeInForceFOK - if order.OrderType == okexapi.OrderTypeIOC { - timeInForce = types.TimeInForceIOC + isMargin := false + if order.InstrumentType == string(okexapi.InstrumentTypeMARGIN) { + isMargin = true } - return &types.Order{ - SubmitOrder: types.SubmitOrder{ - ClientOrderID: order.ClientOrderID, - Symbol: order.InstrumentID, - Side: types.SideType(order.Side), - Type: toGlobalOrderType(order.OrderType), - Quantity: order.Quantity, - Price: order.Price, - TimeInForce: types.TimeInForce(timeInForce), - }, - Exchange: types.ExchangeOKEx, - OrderID: uint64(orderID), - Status: toGlobalOrderStatus(order.State), - ExecutedQuantity: order.FilledQuantity, - CreationTime: types.Time(order.CreationTime), - UpdateTime: types.Time(order.UpdateTime), - }, nil + + return toGlobalOrder(order, isMargin) } diff --git a/pkg/exchange/okex/okexapi/client.go b/pkg/exchange/okex/okexapi/client.go index 626e84160..1138fd41a 100644 --- a/pkg/exchange/okex/okexapi/client.go +++ b/pkg/exchange/okex/okexapi/client.go @@ -33,11 +33,13 @@ const ( type OrderType string const ( - OrderTypeMarket OrderType = "market" - OrderTypeLimit OrderType = "limit" - OrderTypePostOnly OrderType = "post_only" - OrderTypeFOK OrderType = "fok" - OrderTypeIOC OrderType = "ioc" + OrderTypeMarket OrderType = "market" + OrderTypeLimit OrderType = "limit" + OrderTypePostOnly OrderType = "post_only" + OrderTypeFOK OrderType = "fok" + OrderTypeIOC OrderType = "ioc" + OrderTypeMarketMakerProtection OrderType = "mmp" + OrderTypeMarektMakerProtectionPostOnly OrderType = "mmp_and_post_only" ) type InstrumentType string @@ -47,6 +49,7 @@ const ( InstrumentTypeSwap InstrumentType = "SWAP" InstrumentTypeFutures InstrumentType = "FUTURES" InstrumentTypeOption InstrumentType = "OPTION" + InstrumentTypeMARGIN InstrumentType = "MARGIN" ) type OrderState string diff --git a/pkg/exchange/okex/okexapi/client_test.go b/pkg/exchange/okex/okexapi/client_test.go index 04fc69e1d..0f670d334 100644 --- a/pkg/exchange/okex/okexapi/client_test.go +++ b/pkg/exchange/okex/okexapi/client_test.go @@ -63,16 +63,16 @@ func TestClient_PlaceOrderRequest(t *testing.T) { req := srv.NewPlaceOrderRequest() order, err := req. - InstrumentID("XTZ-BTC"). + InstrumentID("BTC-USDT"). TradeMode("cash"). - Side(SideTypeSell). + Side(SideTypeBuy). OrderType(OrderTypeLimit). - Price("0.001"). - Quantity("0.01"). + Price("15000"). + Quantity("0.0001"). Do(ctx) assert.NoError(t, err) assert.NotEmpty(t, order) - t.Logf("order: %+v", order) // Right now account has no money + t.Logf("place order: %+v", order) } func TestClient_GetPendingOrderRequest(t *testing.T) { @@ -83,12 +83,12 @@ func TestClient_GetPendingOrderRequest(t *testing.T) { odr_type := []string{string(OrderTypeLimit), string(OrderTypeIOC)} pending_order, err := req. - InstrumentID("XTZ-BTC"). + InstrumentID("BTC-USDT"). OrderTypes(odr_type). Do(ctx) assert.NoError(t, err) - assert.Empty(t, pending_order) - t.Logf("order: %+v", pending_order) + assert.NotEmpty(t, pending_order) + t.Logf("pending order: %+v", pending_order) } func TestClient_GetOrderDetailsRequest(t *testing.T) { @@ -99,9 +99,9 @@ func TestClient_GetOrderDetailsRequest(t *testing.T) { orderDetail, err := req. InstrumentID("BTC-USDT"). - OrderID("xxx-test-order-id"). + OrderID("609869603774656544"). Do(ctx) - assert.Error(t, err) // Right now account has no orders - assert.Empty(t, orderDetail) - t.Logf("err: %+v", err) + assert.NoError(t, err) + assert.NotEmpty(t, orderDetail) + t.Logf("order detail: %+v", orderDetail) } diff --git a/pkg/exchange/okex/query_order_test.go b/pkg/exchange/okex/query_order_test.go new file mode 100644 index 000000000..3c32da40c --- /dev/null +++ b/pkg/exchange/okex/query_order_test.go @@ -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) +} From 672a878194d52875b80fd029641a673af9bca8a1 Mon Sep 17 00:00:00 2001 From: "Alan.sung" Date: Tue, 15 Aug 2023 14:26:27 +0800 Subject: [PATCH 05/10] update toGlobalOrder by referencing toGlobalOrders --- pkg/exchange/okex/convert.go | 149 +++++++++++++++-------------------- 1 file changed, 65 insertions(+), 84 deletions(-) diff --git a/pkg/exchange/okex/convert.go b/pkg/exchange/okex/convert.go index 175dcf731..7a008a2c2 100644 --- a/pkg/exchange/okex/convert.go +++ b/pkg/exchange/okex/convert.go @@ -2,7 +2,6 @@ package okex import ( "fmt" - "hash/fnv" "regexp" "strconv" "strings" @@ -166,73 +165,36 @@ func toGlobalTrades(orderDetails []okexapi.OrderDetails) ([]types.Trade, error) func toGlobalOrders(orderDetails []okexapi.OrderDetails) ([]types.Order, error) { var orders []types.Order for _, orderDetail := range orderDetails { - orderID, err := strconv.ParseInt(orderDetail.OrderID, 10, 64) + isMargin := false + if orderDetail.InstrumentType == string(okexapi.InstrumentTypeMARGIN) { + isMargin = true + } + + o, err := toGlobalOrder(&orderDetail, isMargin) if err != nil { - return orders, err + log.WithError(err).Error("order convert error") + } else { + orders = append(orders, *o) } - - side := types.SideType(strings.ToUpper(string(orderDetail.Side))) - - orderType := toGlobalOrderType(orderDetail.OrderType) - - timeInForce := types.TimeInForceGTC - switch orderDetail.OrderType { - case okexapi.OrderTypeFOK: - timeInForce = types.TimeInForceFOK - case okexapi.OrderTypeIOC: - timeInForce = types.TimeInForceIOC - - } - - orderStatus := toGlobalOrderStatus(orderDetail.State) - - 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 { +func toGlobalOrderStatus(state okexapi.OrderState) (types.OrderStatus, error) { switch state { case okexapi.OrderStateCanceled: - return types.OrderStatusCanceled + return types.OrderStatusCanceled, nil case okexapi.OrderStateLive: - return types.OrderStatusNew + return types.OrderStatusNew, nil case okexapi.OrderStatePartiallyFilled: - return types.OrderStatusPartiallyFilled + return types.OrderStatusPartiallyFilled, nil case okexapi.OrderStateFilled: - return types.OrderStatusFilled + return types.OrderStatusFilled, nil } - return types.OrderStatus(state) + return "", fmt.Errorf("unknown or unsupported okex order state: %s", state) } func toLocalOrderType(orderType types.OrderType) (okexapi.OrderType, error) { @@ -251,27 +213,20 @@ func toLocalOrderType(orderType types.OrderType) (okexapi.OrderType, error) { return "", fmt.Errorf("unknown or unsupported okex order type: %s", orderType) } -func toGlobalOrderType(orderType okexapi.OrderType) types.OrderType { +func toGlobalOrderType(orderType okexapi.OrderType) (types.OrderType, error) { switch orderType { - case okexapi.OrderTypeMarket, okexapi.OrderTypeMarketMakerProtection: - return types.OrderTypeMarket + case okexapi.OrderTypeMarket: + return types.OrderTypeMarket, nil - case okexapi.OrderTypeLimit: - return types.OrderTypeLimit + case okexapi.OrderTypeLimit, okexapi.OrderTypeMarketMakerProtection: + return types.OrderTypeLimit, nil - case okexapi.OrderTypePostOnly: - return types.OrderTypeLimitMaker + case okexapi.OrderTypePostOnly, okexapi.OrderTypeIOC, okexapi.OrderTypeFOK, okexapi.OrderTypeMarektMakerProtectionPostOnly: + return types.OrderTypeLimitMaker, nil - case okexapi.OrderTypeIOC, okexapi.OrderTypeFOK: - return types.OrderTypeMarket - - case okexapi.OrderTypeMarektMakerProtectionPostOnly: - return types.OrderTypeLimitMaker - - default: - log.Errorf("unsupported order type: %v", orderType) - return "" } + + return "", fmt.Errorf("unknown or unsupported okex order type: %s", orderType) } func toLocalInterval(src string) string { @@ -281,33 +236,59 @@ func toLocalInterval(src string) string { }) } -func hashStringID(s string) uint64 { - h := fnv.New64a() - h.Write([]byte(s)) - return h.Sum64() -} - func toGlobalOrder(okexOrder *okexapi.OrderDetails, isMargin bool) (*types.Order, error) { - timeInForce := types.TimeInForceFOK - if okexOrder.OrderType == okexapi.OrderTypeIOC { + + orderID, err := strconv.ParseInt(okexOrder.OrderID, 10, 64) + if err != nil { + return &types.Order{}, err + } + + side := types.SideType(strings.ToUpper(string(okexOrder.Side))) + + orderType, err := toGlobalOrderType(okexOrder.OrderType) + if err != nil { + return &types.Order{}, 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 &types.Order{}, err + } + + isWorking := false + switch orderStatus { + case types.OrderStatusNew, types.OrderStatusPartiallyFilled: + isWorking = true + + } + return &types.Order{ SubmitOrder: types.SubmitOrder{ ClientOrderID: okexOrder.ClientOrderID, - Symbol: okexOrder.InstrumentID, - Side: types.SideType(okexOrder.Side), - Type: toGlobalOrderType(okexOrder.OrderType), - Quantity: okexOrder.Quantity, + Symbol: toGlobalSymbol(okexOrder.InstrumentID), + Side: side, + Type: orderType, Price: okexOrder.Price, - TimeInForce: types.TimeInForce(timeInForce), + Quantity: okexOrder.Quantity, + StopPrice: fixedpoint.Zero, // not supported yet + TimeInForce: timeInForce, }, Exchange: types.ExchangeOKEx, - OrderID: hashStringID(okexOrder.OrderID), - Status: toGlobalOrderStatus(okexOrder.State), + 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 } From 3dce63710aa739330d0ffd5201b140db75cfb75c Mon Sep 17 00:00:00 2001 From: "Alan.sung" Date: Mon, 21 Aug 2023 15:31:30 +0800 Subject: [PATCH 06/10] Fix to fit all reviews last time --- pkg/exchange/okex/convert.go | 37 +++++++++++++++--------- pkg/exchange/okex/exchange.go | 15 ++++------ pkg/exchange/okex/okexapi/client.go | 2 +- pkg/exchange/okex/okexapi/client_test.go | 2 +- 4 files changed, 32 insertions(+), 24 deletions(-) diff --git a/pkg/exchange/okex/convert.go b/pkg/exchange/okex/convert.go index 7a008a2c2..272152783 100644 --- a/pkg/exchange/okex/convert.go +++ b/pkg/exchange/okex/convert.go @@ -165,14 +165,10 @@ func toGlobalTrades(orderDetails []okexapi.OrderDetails) ([]types.Trade, error) func toGlobalOrders(orderDetails []okexapi.OrderDetails) ([]types.Order, error) { var orders []types.Order for _, orderDetail := range orderDetails { - isMargin := false - if orderDetail.InstrumentType == string(okexapi.InstrumentTypeMARGIN) { - isMargin = true - } - o, err := toGlobalOrder(&orderDetail, isMargin) + o, err := toGlobalOrder(&orderDetail) if err != nil { - log.WithError(err).Error("order convert error") + return nil, err } else { orders = append(orders, *o) } @@ -218,10 +214,10 @@ func toGlobalOrderType(orderType okexapi.OrderType) (types.OrderType, error) { case okexapi.OrderTypeMarket: return types.OrderTypeMarket, nil - case okexapi.OrderTypeLimit, okexapi.OrderTypeMarketMakerProtection: + case okexapi.OrderTypeLimit, okexapi.OrderTypeFOK, okexapi.OrderTypeIOC, okexapi.OrderTypeMarketMakerProtection: return types.OrderTypeLimit, nil - case okexapi.OrderTypePostOnly, okexapi.OrderTypeIOC, okexapi.OrderTypeFOK, okexapi.OrderTypeMarektMakerProtectionPostOnly: + case okexapi.OrderTypePostOnly, okexapi.OrderTypeMarketMakerProtectionPostOnly: return types.OrderTypeLimitMaker, nil } @@ -236,18 +232,28 @@ func toLocalInterval(src string) string { }) } -func toGlobalOrder(okexOrder *okexapi.OrderDetails, isMargin bool) (*types.Order, error) { +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 &types.Order{}, err + return nil, err } - side := types.SideType(strings.ToUpper(string(okexOrder.Side))) + side := toGlobalSide(okexOrder.Side) orderType, err := toGlobalOrderType(okexOrder.OrderType) if err != nil { - return &types.Order{}, err + return nil, err } timeInForce := types.TimeInForceGTC @@ -260,7 +266,7 @@ func toGlobalOrder(okexOrder *okexapi.OrderDetails, isMargin bool) (*types.Order orderStatus, err := toGlobalOrderStatus(okexOrder.State) if err != nil { - return &types.Order{}, err + return nil, err } isWorking := false @@ -270,6 +276,11 @@ func toGlobalOrder(okexOrder *okexapi.OrderDetails, isMargin bool) (*types.Order } + isMargin := false + if okexOrder.InstrumentType == string(okexapi.InstrumentTypeMARGIN) { + isMargin = true + } + return &types.Order{ SubmitOrder: types.SubmitOrder{ ClientOrderID: okexOrder.ClientOrderID, diff --git a/pkg/exchange/okex/exchange.go b/pkg/exchange/okex/exchange.go index 02b48af64..3affd9cad 100644 --- a/pkg/exchange/okex/exchange.go +++ b/pkg/exchange/okex/exchange.go @@ -26,6 +26,8 @@ var log = logrus.WithFields(logrus.Fields{ "exchange": ID, }) +var ErrSymbolRequired = errors.New("Symbol is 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() @@ -342,10 +344,10 @@ func (e *Exchange) QueryKLines(ctx context.Context, symbol string, interval type func (e *Exchange) QueryOrder(ctx context.Context, q types.OrderQuery) (*types.Order, error) { if len(q.Symbol) == 0 { - return nil, errors.New("okex.QueryOrder: InstrumentID is required parameter") + return nil, ErrSymbolRequired } if len(q.OrderID) == 0 && len(q.ClientOrderID) == 0 { - return nil, errors.New("okex.QueryOrder: ordId or clOrdId is required parameter") + return nil, errors.New("okex.QueryOrder: OrderId or ClientOrderId is required parameter") } req := e.client.TradeService.NewGetOrderDetailsRequest() req.InstrumentID(q.Symbol). @@ -359,10 +361,5 @@ func (e *Exchange) QueryOrder(ctx context.Context, q types.OrderQuery) (*types.O return nil, err } - isMargin := false - if order.InstrumentType == string(okexapi.InstrumentTypeMARGIN) { - isMargin = true - } - - return toGlobalOrder(order, isMargin) + return toGlobalOrder(order) } diff --git a/pkg/exchange/okex/okexapi/client.go b/pkg/exchange/okex/okexapi/client.go index 1138fd41a..3730b3252 100644 --- a/pkg/exchange/okex/okexapi/client.go +++ b/pkg/exchange/okex/okexapi/client.go @@ -39,7 +39,7 @@ const ( OrderTypeFOK OrderType = "fok" OrderTypeIOC OrderType = "ioc" OrderTypeMarketMakerProtection OrderType = "mmp" - OrderTypeMarektMakerProtectionPostOnly OrderType = "mmp_and_post_only" + OrderTypeMarketMakerProtectionPostOnly OrderType = "mmp_and_post_only" ) type InstrumentType string diff --git a/pkg/exchange/okex/okexapi/client_test.go b/pkg/exchange/okex/okexapi/client_test.go index 0f670d334..ec6841642 100644 --- a/pkg/exchange/okex/okexapi/client_test.go +++ b/pkg/exchange/okex/okexapi/client_test.go @@ -18,7 +18,7 @@ func getTestClientOrSkip(t *testing.T) *RestClient { key, secret, passphrase, ok := testutil.IntegrationTestWithPassphraseConfigured(t, "OKEX") if !ok { - t.SkipNow() + t.Skip("Please configure all credentials about OKEX") return nil } From a4aa9c2edaa273964826d32cbb2bace63dd5db98 Mon Sep 17 00:00:00 2001 From: "Alan.sung" Date: Tue, 22 Aug 2023 15:14:18 +0800 Subject: [PATCH 07/10] remove mmp and mmp_post_only --- pkg/exchange/okex/convert.go | 4 ++-- pkg/exchange/okex/okexapi/client.go | 12 +++++------- 2 files changed, 7 insertions(+), 9 deletions(-) diff --git a/pkg/exchange/okex/convert.go b/pkg/exchange/okex/convert.go index 272152783..622171e43 100644 --- a/pkg/exchange/okex/convert.go +++ b/pkg/exchange/okex/convert.go @@ -214,10 +214,10 @@ func toGlobalOrderType(orderType okexapi.OrderType) (types.OrderType, error) { case okexapi.OrderTypeMarket: return types.OrderTypeMarket, nil - case okexapi.OrderTypeLimit, okexapi.OrderTypeFOK, okexapi.OrderTypeIOC, okexapi.OrderTypeMarketMakerProtection: + case okexapi.OrderTypeLimit, okexapi.OrderTypeFOK, okexapi.OrderTypeIOC: return types.OrderTypeLimit, nil - case okexapi.OrderTypePostOnly, okexapi.OrderTypeMarketMakerProtectionPostOnly: + case okexapi.OrderTypePostOnly: return types.OrderTypeLimitMaker, nil } diff --git a/pkg/exchange/okex/okexapi/client.go b/pkg/exchange/okex/okexapi/client.go index 3730b3252..ccb81f70a 100644 --- a/pkg/exchange/okex/okexapi/client.go +++ b/pkg/exchange/okex/okexapi/client.go @@ -33,13 +33,11 @@ const ( type OrderType string const ( - OrderTypeMarket OrderType = "market" - OrderTypeLimit OrderType = "limit" - OrderTypePostOnly OrderType = "post_only" - OrderTypeFOK OrderType = "fok" - OrderTypeIOC OrderType = "ioc" - OrderTypeMarketMakerProtection OrderType = "mmp" - OrderTypeMarketMakerProtectionPostOnly OrderType = "mmp_and_post_only" + OrderTypeMarket OrderType = "market" + OrderTypeLimit OrderType = "limit" + OrderTypePostOnly OrderType = "post_only" + OrderTypeFOK OrderType = "fok" + OrderTypeIOC OrderType = "ioc" ) type InstrumentType string From a0946fbd4241e15e492eb12e30ec2f899b108597 Mon Sep 17 00:00:00 2001 From: "Alan.sung" Date: Tue, 22 Aug 2023 17:23:16 +0800 Subject: [PATCH 08/10] use lower case in error string and add comment for IOC, FOK --- pkg/exchange/okex/convert.go | 7 ++++--- pkg/exchange/okex/exchange.go | 2 +- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/pkg/exchange/okex/convert.go b/pkg/exchange/okex/convert.go index 622171e43..4716b750c 100644 --- a/pkg/exchange/okex/convert.go +++ b/pkg/exchange/okex/convert.go @@ -168,10 +168,9 @@ func toGlobalOrders(orderDetails []okexapi.OrderDetails) ([]types.Order, error) o, err := toGlobalOrder(&orderDetail) if err != nil { - return nil, err - } else { - orders = append(orders, *o) + log.WithError(err).Error("order convert error") } + orders = append(orders, *o) } return orders, nil @@ -210,6 +209,8 @@ func toLocalOrderType(orderType types.OrderType) (okexapi.OrderType, error) { } func toGlobalOrderType(orderType okexapi.OrderType) (types.OrderType, error) { + // Okex IOC and FOK only implement limit order + // reference: https://www.okx.com/cn/help-center/360025135731 switch orderType { case okexapi.OrderTypeMarket: return types.OrderTypeMarket, nil diff --git a/pkg/exchange/okex/exchange.go b/pkg/exchange/okex/exchange.go index 3affd9cad..08069a9b4 100644 --- a/pkg/exchange/okex/exchange.go +++ b/pkg/exchange/okex/exchange.go @@ -26,7 +26,7 @@ var log = logrus.WithFields(logrus.Fields{ "exchange": ID, }) -var ErrSymbolRequired = errors.New("Symbol is required parameter") +var ErrSymbolRequired = errors.New("symbol is a required parameter") type Exchange struct { key, secret, passphrase string From 26cde5d57c7d306c5ced5d293acbaf149549f256 Mon Sep 17 00:00:00 2001 From: "Alan.sung" Date: Wed, 23 Aug 2023 15:44:51 +0800 Subject: [PATCH 09/10] use multierr to handle err return from toGlobalOrder --- pkg/exchange/okex/convert.go | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/pkg/exchange/okex/convert.go b/pkg/exchange/okex/convert.go index 4716b750c..1c903f8a1 100644 --- a/pkg/exchange/okex/convert.go +++ b/pkg/exchange/okex/convert.go @@ -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" @@ -164,16 +165,17 @@ 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 { - o, err := toGlobalOrder(&orderDetail) - if err != nil { - log.WithError(err).Error("order convert error") + o, err2 := toGlobalOrder(&orderDetail) + if err2 != nil { + err = multierr.Append(err, err2) } orders = append(orders, *o) } - return orders, nil + return orders, err } func toGlobalOrderStatus(state okexapi.OrderState) (types.OrderStatus, error) { @@ -209,8 +211,7 @@ func toLocalOrderType(orderType types.OrderType) (okexapi.OrderType, error) { } func toGlobalOrderType(orderType okexapi.OrderType) (types.OrderType, error) { - // Okex IOC and FOK only implement limit order - // reference: https://www.okx.com/cn/help-center/360025135731 + // 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 From f8ae408fad6625f0eaa4c39b3d09fe0d644ba115 Mon Sep 17 00:00:00 2001 From: "Alan.sung" Date: Wed, 23 Aug 2023 16:16:38 +0800 Subject: [PATCH 10/10] add continue in err != nil --- pkg/exchange/okex/convert.go | 1 + 1 file changed, 1 insertion(+) diff --git a/pkg/exchange/okex/convert.go b/pkg/exchange/okex/convert.go index 1c903f8a1..881f66ab9 100644 --- a/pkg/exchange/okex/convert.go +++ b/pkg/exchange/okex/convert.go @@ -171,6 +171,7 @@ func toGlobalOrders(orderDetails []okexapi.OrderDetails) ([]types.Order, error) o, err2 := toGlobalOrder(&orderDetail) if err2 != nil { err = multierr.Append(err, err2) + continue } orders = append(orders, *o) }