From 640960aec128c035b356b9589472d1d5daada737 Mon Sep 17 00:00:00 2001 From: "Samuael A." <39623015+samuael@users.noreply.github.com> Date: Fri, 23 May 2025 02:07:09 +0300 Subject: [PATCH] exchanges/order: Add TimeInForce type (#1382) * Added TimeInForce type and updated related files * Linter issue fix and minor coinbasepro type update * Bitrex consts update * added unit test and minor changes in bittrex * Unit tests update * Fix minor linter issues * Update TestStringToTimeInForce unit test * fix conflict with gateio timeInForce * Update order tests * Complete updating the order unit tests * update kucoin and deribit wrapper to match the time in force change * fix time-in-force related test errors * linter issue fix * time in force constants, functions and unit tests update * shift tif policies to TimeInForce * Update time-in-force, related functions, and unit tests * fix linter issue and time-in-force processing * added a good till crossing tif value * order type fix and fix related tim-in-force entries * update time-in-force unmarshaling and unit test * fix time-in-force error in gateio * linter issue fix * update based on review comments * add unit test and fix missing issues * minor fix and added benchmark unit test * change GTT to GTC for limit * fix linter issue * added time-in-force value to place order param * fix minor issues based on review comment and move tif code to separate files * update on exchanges linked to time-in-force * resolve missing review comments * minor linter issues fix * added time-in-force handler and update timeInForce parametered endpoint * minor fixes based on review * nits fix * update based on review * linter fix * rm getTimeInForce func and minor change to time-in-force * minor change * update based on review comments * wrappers and time-in-force calling approach * minor change * update gateio string to timeInForce conversion and unit test * updated order test unit tes functions * minor fixes on unit tests * nits fix based on feedback * update TestDeriveCancel unit test assert messages * update TestDeriveCancel unit test assert messages * update timeInForceFromString method to return formatted error and update functions using it * restructure and fix minor exchanges time-in-force handling issues * replaced unused getTypeFromTimeInForce with inline switch-based order type check * separated the repeated timeInForce conversion code to a function * update exchanges time-in-force handling based on review comments * limter fix * edded comment to validTimesInForce var * added comment to gateio's timeInForceString func * added goodTillCancel switch case to gateio timeInForceString func --- cmd/exchange_wrapper_issues/main.go | 2 - .../exchange_wrapper_standards_test.go | 42 +- engine/order_manager.go | 7 +- exchanges/binance/binance.go | 2 +- exchanges/binance/binance_cfutures.go | 4 +- exchanges/binance/binance_test.go | 4 +- exchanges/binance/binance_types.go | 16 +- exchanges/binance/binance_wrapper.go | 14 +- exchanges/binance/cfutures_types.go | 2 +- exchanges/binanceus/binanceus.go | 2 +- exchanges/binanceus/binanceus_test.go | 4 +- exchanges/binanceus/binanceus_types.go | 16 +- exchanges/binanceus/binanceus_wrapper.go | 4 +- exchanges/btcmarkets/btcmarkets.go | 11 +- exchanges/btcmarkets/btcmarkets_test.go | 64 +- exchanges/btcmarkets/btcmarkets_wrapper.go | 4 +- exchanges/bybit/bybit_test.go | 1 - exchanges/coinbasepro/coinbasepro.go | 13 +- exchanges/coinbasepro/coinbasepro_types.go | 11 - exchanges/coinbasepro/coinbasepro_wrapper.go | 6 +- exchanges/deribit/deribit_test.go | 25 + exchanges/deribit/deribit_wrapper.go | 40 +- exchanges/gateio/gateio.go | 44 +- exchanges/gateio/gateio_test.go | 128 +-- exchanges/gateio/gateio_wrapper.go | 113 ++- exchanges/huobi/huobi_types.go | 9 +- exchanges/huobi/huobi_wrapper.go | 63 +- exchanges/kraken/futures_types.go | 10 +- exchanges/kraken/kraken.go | 2 +- exchanges/kraken/kraken_futures.go | 19 +- exchanges/kraken/kraken_test.go | 2 +- exchanges/kraken/kraken_types.go | 50 +- exchanges/kraken/kraken_wrapper.go | 11 +- exchanges/kucoin/kucoin.go | 2 +- exchanges/kucoin/kucoin_test.go | 21 + exchanges/kucoin/kucoin_wrapper.go | 47 +- exchanges/okx/helpers.go | 53 +- exchanges/okx/okx_test.go | 63 +- exchanges/okx/okx_wrapper.go | 47 +- exchanges/order/limits_test.go | 230 ++--- exchanges/order/order_test.go | 952 +++++------------- exchanges/order/order_types.go | 51 +- exchanges/order/orders.go | 102 +- exchanges/order/timeinforce.go | 140 +++ exchanges/order/timeinforce_test.go | 168 ++++ exchanges/poloniex/poloniex_wrapper.go | 4 +- 46 files changed, 1201 insertions(+), 1424 deletions(-) create mode 100644 exchanges/order/timeinforce.go create mode 100644 exchanges/order/timeinforce_test.go diff --git a/cmd/exchange_wrapper_issues/main.go b/cmd/exchange_wrapper_issues/main.go index 3edc18c6..803ab406 100644 --- a/cmd/exchange_wrapper_issues/main.go +++ b/cmd/exchange_wrapper_issues/main.go @@ -272,8 +272,6 @@ func parseOrderType(orderType string) order.Type { return order.Limit case order.Market.String(): return order.Market - case order.ImmediateOrCancel.String(): - return order.ImmediateOrCancel case order.Stop.String(): return order.Stop case order.TrailingStop.String(): diff --git a/cmd/exchange_wrapper_standards/exchange_wrapper_standards_test.go b/cmd/exchange_wrapper_standards/exchange_wrapper_standards_test.go index 6b364d15..6ccff329 100644 --- a/cmd/exchange_wrapper_standards/exchange_wrapper_standards_test.go +++ b/cmd/exchange_wrapper_standards/exchange_wrapper_standards_test.go @@ -448,30 +448,30 @@ func generateMethodArg(ctx context.Context, t *testing.T, argGenerator *MethodAr input = reflect.ValueOf(req) case argGenerator.MethodInputType.AssignableTo(orderSubmitParam): input = reflect.ValueOf(&order.Submit{ - Exchange: exchName, - Type: order.Limit, - Side: order.Buy, - Pair: argGenerator.AssetParams.Pair, - AssetType: argGenerator.AssetParams.Asset, - Price: 150, - Amount: 1, - ClientID: "1337", - ClientOrderID: "13371337", - ImmediateOrCancel: true, - Leverage: 1, + Exchange: exchName, + Type: order.Limit, + Side: order.Buy, + Pair: argGenerator.AssetParams.Pair, + AssetType: argGenerator.AssetParams.Asset, + Price: 150, + Amount: 1, + ClientID: "1337", + ClientOrderID: "13371337", + TimeInForce: order.ImmediateOrCancel, + Leverage: 1, }) case argGenerator.MethodInputType.AssignableTo(orderModifyParam): input = reflect.ValueOf(&order.Modify{ - Exchange: exchName, - Type: order.Limit, - Side: order.Buy, - Pair: argGenerator.AssetParams.Pair, - AssetType: argGenerator.AssetParams.Asset, - Price: 150, - Amount: 1, - ClientOrderID: "13371337", - OrderID: "1337", - ImmediateOrCancel: true, + Exchange: exchName, + Type: order.Limit, + Side: order.Buy, + Pair: argGenerator.AssetParams.Pair, + AssetType: argGenerator.AssetParams.Asset, + Price: 150, + Amount: 1, + ClientOrderID: "13371337", + OrderID: "1337", + TimeInForce: order.ImmediateOrCancel, }) case argGenerator.MethodInputType.AssignableTo(orderCancelParam): input = reflect.ValueOf(&order.Cancel{ diff --git a/engine/order_manager.go b/engine/order_manager.go index a6757805..1479f821 100644 --- a/engine/order_manager.go +++ b/engine/order_manager.go @@ -395,10 +395,9 @@ func (m *OrderManager) Modify(ctx context.Context, mod *order.Modify) (*order.Mo // Populate additional Modify fields as some of them are required by various // exchange implementations. - mod.Pair = det.Pair // Used by Bithumb. - mod.Side = det.Side // Used by Bithumb. - mod.PostOnly = det.PostOnly // Used by Poloniex. - mod.ImmediateOrCancel = det.ImmediateOrCancel // Used by Poloniex. + mod.Pair = det.Pair + mod.Side = det.Side + mod.TimeInForce = det.TimeInForce // Following is just a precaution to not modify orders by mistake if exchange // implementations do not check fields of the Modify struct for zero values. diff --git a/exchanges/binance/binance.go b/exchanges/binance/binance.go index b602a790..b86200c0 100644 --- a/exchanges/binance/binance.go +++ b/exchanges/binance/binance.go @@ -613,7 +613,7 @@ func (b *Binance) newOrder(ctx context.Context, api string, o *NewOrderRequest, params.Set("price", strconv.FormatFloat(o.Price, 'f', -1, 64)) } if o.TimeInForce != "" { - params.Set("timeInForce", string(o.TimeInForce)) + params.Set("timeInForce", o.TimeInForce) } if o.NewClientOrderID != "" { diff --git a/exchanges/binance/binance_cfutures.go b/exchanges/binance/binance_cfutures.go index 5a892c94..e8ebeea4 100644 --- a/exchanges/binance/binance_cfutures.go +++ b/exchanges/binance/binance_cfutures.go @@ -991,8 +991,8 @@ func (b *Binance) FuturesNewOrder(ctx context.Context, x *FuturesNewOrderRequest params.Set("positionSide", x.PositionSide) } params.Set("type", x.OrderType) - if string(x.TimeInForce) != "" { - params.Set("timeInForce", string(x.TimeInForce)) + if x.TimeInForce != "" { + params.Set("timeInForce", x.TimeInForce) } if x.ReduceOnly { params.Set("reduceOnly", "true") diff --git a/exchanges/binance/binance_test.go b/exchanges/binance/binance_test.go index 1e5dbddb..1bbb9874 100644 --- a/exchanges/binance/binance_test.go +++ b/exchanges/binance/binance_test.go @@ -865,7 +865,7 @@ func TestFuturesNewOrder(t *testing.T) { Symbol: currency.NewPairWithDelimiter("BTCUSD", "PERP", "_"), Side: "BUY", OrderType: "LIMIT", - TimeInForce: BinanceRequestParamsTimeGTC, + TimeInForce: order.GoodTillCancel.String(), Quantity: 1, Price: 1, }, @@ -1418,7 +1418,7 @@ func TestNewOrderTest(t *testing.T) { TradeType: BinanceRequestParamsOrderLimit, Price: 0.0025, Quantity: 100000, - TimeInForce: BinanceRequestParamsTimeGTC, + TimeInForce: order.GoodTillCancel.String(), } err := b.NewOrderTest(t.Context(), req) diff --git a/exchanges/binance/binance_types.go b/exchanges/binance/binance_types.go index 9e41b006..866ade6f 100644 --- a/exchanges/binance/binance_types.go +++ b/exchanges/binance/binance_types.go @@ -375,7 +375,7 @@ type NewOrderRequest struct { TradeType RequestParamsOrderType // TimeInForce specifies how long the order remains in effect. // Examples are (Good Till Cancel (GTC), Immediate or Cancel (IOC) and Fill Or Kill (FOK)) - TimeInForce RequestParamsTimeForceType + TimeInForce string // Quantity is the total base qty spent or received in an order. Quantity float64 // QuoteOrderQty is the total quote qty spent or received in a MARKET order. @@ -486,20 +486,6 @@ type MarginAccountAsset struct { NetAsset float64 `json:"netAsset,string"` } -// RequestParamsTimeForceType Time in force -type RequestParamsTimeForceType string - -var ( - // BinanceRequestParamsTimeGTC GTC - BinanceRequestParamsTimeGTC = RequestParamsTimeForceType("GTC") - - // BinanceRequestParamsTimeIOC IOC - BinanceRequestParamsTimeIOC = RequestParamsTimeForceType("IOC") - - // BinanceRequestParamsTimeFOK FOK - BinanceRequestParamsTimeFOK = RequestParamsTimeForceType("FOK") -) - // RequestParamsOrderType trade order type type RequestParamsOrderType string diff --git a/exchanges/binance/binance_wrapper.go b/exchanges/binance/binance_wrapper.go index 419c5dc2..9538c11f 100644 --- a/exchanges/binance/binance_wrapper.go +++ b/exchanges/binance/binance_wrapper.go @@ -864,15 +864,15 @@ func (b *Binance) SubmitOrder(ctx context.Context, s *order.Submit) (*order.Subm } else { sideType = order.Sell.String() } - timeInForce := BinanceRequestParamsTimeGTC + timeInForce := order.GoodTillCancel.String() var requestParamsOrderType RequestParamsOrderType switch s.Type { case order.Market: timeInForce = "" requestParamsOrderType = BinanceRequestParamsOrderMarket case order.Limit: - if s.ImmediateOrCancel { - timeInForce = BinanceRequestParamsTimeIOC + if s.TimeInForce.Is(order.ImmediateOrCancel) { + timeInForce = order.ImmediateOrCancel.String() } requestParamsOrderType = BinanceRequestParamsOrderLimit default: @@ -918,15 +918,11 @@ func (b *Binance) SubmitOrder(ctx context.Context, s *order.Submit) (*order.Subm return nil, errors.New("invalid side") } - var ( - oType string - timeInForce RequestParamsTimeForceType - ) - + var oType, timeInForce string switch s.Type { case order.Limit: oType = cfuturesLimit - timeInForce = BinanceRequestParamsTimeGTC + timeInForce = order.GoodTillCancel.String() case order.Market: oType = cfuturesMarket case order.Stop: diff --git a/exchanges/binance/cfutures_types.go b/exchanges/binance/cfutures_types.go index 80744d09..3ae2073f 100644 --- a/exchanges/binance/cfutures_types.go +++ b/exchanges/binance/cfutures_types.go @@ -213,7 +213,7 @@ type FuturesNewOrderRequest struct { Side string PositionSide string OrderType string - TimeInForce RequestParamsTimeForceType + TimeInForce string NewClientOrderID string ClosePosition string WorkingType string diff --git a/exchanges/binanceus/binanceus.go b/exchanges/binanceus/binanceus.go index 2faf63f8..91939219 100644 --- a/exchanges/binanceus/binanceus.go +++ b/exchanges/binanceus/binanceus.go @@ -1026,7 +1026,7 @@ func (bi *Binanceus) newOrder(ctx context.Context, api string, o *NewOrderReques params.Set("price", strconv.FormatFloat(o.Price, 'f', -1, 64)) } if o.TimeInForce != "" { - params.Set("timeInForce", string(o.TimeInForce)) + params.Set("timeInForce", o.TimeInForce) } if o.NewClientOrderID != "" { params.Set("newClientOrderId", o.NewClientOrderID) diff --git a/exchanges/binanceus/binanceus_test.go b/exchanges/binanceus/binanceus_test.go index 56732467..963e7e28 100644 --- a/exchanges/binanceus/binanceus_test.go +++ b/exchanges/binanceus/binanceus_test.go @@ -782,7 +782,7 @@ func TestNewOrderTest(t *testing.T) { TradeType: BinanceRequestParamsOrderLimit, Price: 0.0025, Quantity: 100000, - TimeInForce: BinanceRequestParamsTimeGTC, + TimeInForce: order.GoodTillCancel.String(), } _, err := bi.NewOrderTest(t.Context(), req) if err != nil { @@ -810,7 +810,7 @@ func TestNewOrder(t *testing.T) { TradeType: BinanceRequestParamsOrderLimit, Price: 0.0025, Quantity: 100000, - TimeInForce: BinanceRequestParamsTimeGTC, + TimeInForce: order.GoodTillCancel.String(), } if _, err := bi.NewOrder(t.Context(), req); err != nil && !strings.Contains(err.Error(), "Account has insufficient balance for requested action") { t.Error("Binanceus NewOrder() error", err) diff --git a/exchanges/binanceus/binanceus_types.go b/exchanges/binanceus/binanceus_types.go index b9bb3c19..905cafb5 100644 --- a/exchanges/binanceus/binanceus_types.go +++ b/exchanges/binanceus/binanceus_types.go @@ -404,26 +404,12 @@ type OrderRateLimit struct { // RequestParamsOrderType trade order type type RequestParamsOrderType string -// RequestParamsTimeForceType Time in force -type RequestParamsTimeForceType string - -var ( - // BinanceRequestParamsTimeGTC GTC - BinanceRequestParamsTimeGTC = RequestParamsTimeForceType("GTC") - - // BinanceRequestParamsTimeIOC IOC - BinanceRequestParamsTimeIOC = RequestParamsTimeForceType("IOC") - - // BinanceRequestParamsTimeFOK FOK - BinanceRequestParamsTimeFOK = RequestParamsTimeForceType("FOK") -) - // NewOrderRequest request type type NewOrderRequest struct { Symbol currency.Pair Side string TradeType RequestParamsOrderType - TimeInForce RequestParamsTimeForceType + TimeInForce string Quantity float64 QuoteOrderQty float64 Price float64 diff --git a/exchanges/binanceus/binanceus_wrapper.go b/exchanges/binanceus/binanceus_wrapper.go index f5c2acdf..4a7c9a78 100644 --- a/exchanges/binanceus/binanceus_wrapper.go +++ b/exchanges/binanceus/binanceus_wrapper.go @@ -490,7 +490,7 @@ func (bi *Binanceus) GetHistoricTrades(ctx context.Context, p currency.Pair, ass // SubmitOrder submits a new order func (bi *Binanceus) SubmitOrder(ctx context.Context, s *order.Submit) (*order.SubmitResponse, error) { var submitOrderResponse order.SubmitResponse - var timeInForce RequestParamsTimeForceType + var timeInForce string var sideType string err := s.Validate(bi.GetTradingRequirements()) if err != nil { @@ -509,7 +509,7 @@ func (bi *Binanceus) SubmitOrder(ctx context.Context, s *order.Submit) (*order.S case order.Market: requestParamOrderType = BinanceRequestParamsOrderMarket case order.Limit: - timeInForce = BinanceRequestParamsTimeGTC + timeInForce = order.GoodTillCancel.String() requestParamOrderType = BinanceRequestParamsOrderLimit default: return nil, fmt.Errorf("%w %v", order.ErrUnsupportedOrderType, s.Type) diff --git a/exchanges/btcmarkets/btcmarkets.go b/exchanges/btcmarkets/btcmarkets.go index 0856e12c..97205f31 100644 --- a/exchanges/btcmarkets/btcmarkets.go +++ b/exchanges/btcmarkets/btcmarkets.go @@ -78,10 +78,6 @@ const ( askSide = "Ask" bidSide = "Bid" - // time in force - immediateOrCancel = "IOC" - fillOrKill = "FOK" - subscribe = "subscribe" fundChange = "fundChange" orderChange = "orderChange" @@ -377,11 +373,8 @@ func (b *BTCMarkets) formatOrderSide(o order.Side) (string, error) { // getTimeInForce returns a string depending on the options in order.Submit func (b *BTCMarkets) getTimeInForce(s *order.Submit) string { - if s.ImmediateOrCancel { - return immediateOrCancel - } - if s.FillOrKill { - return fillOrKill + if s.TimeInForce.Is(order.ImmediateOrCancel) || s.TimeInForce.Is(order.FillOrKill) { + return s.TimeInForce.String() } return "" // GTC (good till cancelled, default value) } diff --git a/exchanges/btcmarkets/btcmarkets_test.go b/exchanges/btcmarkets/btcmarkets_test.go index 7fca7c1f..e1beacd8 100644 --- a/exchanges/btcmarkets/btcmarkets_test.go +++ b/exchanges/btcmarkets/btcmarkets_test.go @@ -196,27 +196,27 @@ func TestGetTradeByID(t *testing.T) { func TestSubmitOrder(t *testing.T) { t.Parallel() _, err := b.SubmitOrder(t.Context(), &order.Submit{ - Exchange: b.Name, - Price: 100, - Amount: 1, - Type: order.TrailingStop, - AssetType: asset.Spot, - Side: order.Bid, - Pair: currency.NewPair(currency.BTC, currency.AUD), - PostOnly: true, + Exchange: b.Name, + Price: 100, + Amount: 1, + Type: order.TrailingStop, + AssetType: asset.Spot, + Side: order.Bid, + Pair: currency.NewPair(currency.BTC, currency.AUD), + TimeInForce: order.PostOnly, }) if !errors.Is(err, order.ErrTypeIsInvalid) { t.Fatalf("received: '%v' but expected: '%v'", err, order.ErrTypeIsInvalid) } _, err = b.SubmitOrder(t.Context(), &order.Submit{ - Exchange: b.Name, - Price: 100, - Amount: 1, - Type: order.Limit, - AssetType: asset.Spot, - Side: order.AnySide, - Pair: currency.NewPair(currency.BTC, currency.AUD), - PostOnly: true, + Exchange: b.Name, + Price: 100, + Amount: 1, + Type: order.Limit, + AssetType: asset.Spot, + Side: order.AnySide, + Pair: currency.NewPair(currency.BTC, currency.AUD), + TimeInForce: order.PostOnly, }) if !errors.Is(err, order.ErrSideIsInvalid) { t.Fatalf("received: '%v' but expected: '%v'", err, order.ErrSideIsInvalid) @@ -225,14 +225,14 @@ func TestSubmitOrder(t *testing.T) { sharedtestvalues.SkipTestIfCredentialsUnset(t, b, canManipulateRealOrders) _, err = b.SubmitOrder(t.Context(), &order.Submit{ - Exchange: b.Name, - Price: 100, - Amount: 1, - Type: order.Limit, - AssetType: asset.Spot, - Side: order.Bid, - Pair: currency.NewPair(currency.BTC, currency.AUD), - PostOnly: true, + Exchange: b.Name, + Price: 100, + Amount: 1, + Type: order.Limit, + AssetType: asset.Spot, + Side: order.Bid, + Pair: currency.NewPair(currency.BTC, currency.AUD), + TimeInForce: order.PostOnly, }) if err != nil { t.Error(err) @@ -998,19 +998,13 @@ func TestFormatOrderSide(t *testing.T) { func TestGetTimeInForce(t *testing.T) { t.Parallel() f := b.getTimeInForce(&order.Submit{}) - if f != "" { - t.Fatal("unexpected value") - } + require.Empty(t, f) - f = b.getTimeInForce(&order.Submit{ImmediateOrCancel: true}) - if f != immediateOrCancel { - t.Fatalf("received: '%v' but expected: '%v'", f, immediateOrCancel) - } + f = b.getTimeInForce(&order.Submit{TimeInForce: order.ImmediateOrCancel}) + require.Equal(t, "IOC", f) - f = b.getTimeInForce(&order.Submit{FillOrKill: true}) - if f != fillOrKill { - t.Fatalf("received: '%v' but expected: '%v'", f, fillOrKill) - } + f = b.getTimeInForce(&order.Submit{TimeInForce: order.FillOrKill}) + assert.Equal(t, "FOK", f) } func TestReplaceOrder(t *testing.T) { diff --git a/exchanges/btcmarkets/btcmarkets_wrapper.go b/exchanges/btcmarkets/btcmarkets_wrapper.go index c41df944..56813c9e 100644 --- a/exchanges/btcmarkets/btcmarkets_wrapper.go +++ b/exchanges/btcmarkets/btcmarkets_wrapper.go @@ -465,7 +465,7 @@ func (b *BTCMarkets) SubmitOrder(ctx context.Context, s *order.Submit) (*order.S b.getTimeInForce(s), "", s.ClientID, - s.PostOnly) + s.TimeInForce.Is(order.PostOnly)) if err != nil { return nil, err } @@ -635,7 +635,7 @@ func (b *BTCMarkets) GetOrderInfo(ctx context.Context, orderID string, _ currenc case stop: resp.Type = order.Stop case takeProfit: - resp.Type = order.ImmediateOrCancel + resp.Type = order.TakeProfit default: resp.Type = order.UnknownType } diff --git a/exchanges/bybit/bybit_test.go b/exchanges/bybit/bybit_test.go index 658c85fc..464e4c63 100644 --- a/exchanges/bybit/bybit_test.go +++ b/exchanges/bybit/bybit_test.go @@ -291,7 +291,6 @@ func TestModifyOrder(t *testing.T) { Side: order.Buy, AssetType: asset.Options, Pair: spotTradablePair, - PostOnly: true, Price: 1234, Amount: 0.15, TriggerPrice: 1145, diff --git a/exchanges/coinbasepro/coinbasepro.go b/exchanges/coinbasepro/coinbasepro.go index 73e6f3f5..c2a4af00 100644 --- a/exchanges/coinbasepro/coinbasepro.go +++ b/exchanges/coinbasepro/coinbasepro.go @@ -335,15 +335,13 @@ func (c *CoinbasePro) GetHolds(ctx context.Context, accountID string) ([]Account // timeInforce - [optional] GTC, GTT, IOC, or FOK (default is GTC) // cancelAfter - [optional] min, hour, day * Requires time_in_force to be GTT // postOnly - [optional] Post only flag Invalid when time_in_force is IOC or FOK -func (c *CoinbasePro) PlaceLimitOrder(ctx context.Context, clientRef string, price, amount float64, side string, timeInforce RequestParamsTimeForceType, cancelAfter, productID, stp string, postOnly bool) (string, error) { - resp := GeneralizedOrderResponse{} +func (c *CoinbasePro) PlaceLimitOrder(ctx context.Context, clientRef string, price, amount float64, side, timeInforce, cancelAfter, productID, stp string, postOnly bool) (string, error) { req := make(map[string]any) req["type"] = order.Limit.Lower() req["price"] = strconv.FormatFloat(price, 'f', -1, 64) req["size"] = strconv.FormatFloat(amount, 'f', -1, 64) req["side"] = side req["product_id"] = productID - if cancelAfter != "" { req["cancel_after"] = cancelAfter } @@ -359,13 +357,8 @@ func (c *CoinbasePro) PlaceLimitOrder(ctx context.Context, clientRef string, pri if postOnly { req["post_only"] = postOnly } - - err := c.SendAuthenticatedHTTPRequest(ctx, exchange.RestSpot, http.MethodPost, coinbaseproOrders, req, &resp) - if err != nil { - return "", err - } - - return resp.ID, nil + resp := GeneralizedOrderResponse{} + return resp.ID, c.SendAuthenticatedHTTPRequest(ctx, exchange.RestSpot, http.MethodPost, coinbaseproOrders, req, &resp) } // PlaceMarketOrder places a new market order. diff --git a/exchanges/coinbasepro/coinbasepro_types.go b/exchanges/coinbasepro/coinbasepro_types.go index 60da085a..4274b0bc 100644 --- a/exchanges/coinbasepro/coinbasepro_types.go +++ b/exchanges/coinbasepro/coinbasepro_types.go @@ -489,17 +489,6 @@ type wsStatus struct { Type string `json:"type"` } -// RequestParamsTimeForceType Time in force -type RequestParamsTimeForceType string - -var ( - // CoinbaseRequestParamsTimeGTC GTC - CoinbaseRequestParamsTimeGTC = RequestParamsTimeForceType("GTC") - - // CoinbaseRequestParamsTimeIOC IOC - CoinbaseRequestParamsTimeIOC = RequestParamsTimeForceType("IOC") -) - // TransferHistory returns wallet transfer history type TransferHistory struct { ID string `json:"id"` diff --git a/exchanges/coinbasepro/coinbasepro_wrapper.go b/exchanges/coinbasepro/coinbasepro_wrapper.go index 883e1737..cdda50b3 100644 --- a/exchanges/coinbasepro/coinbasepro_wrapper.go +++ b/exchanges/coinbasepro/coinbasepro_wrapper.go @@ -430,9 +430,9 @@ func (c *CoinbasePro) SubmitOrder(ctx context.Context, s *order.Submit) (*order. fPair.String(), "") case order.Limit: - timeInForce := CoinbaseRequestParamsTimeGTC - if s.ImmediateOrCancel { - timeInForce = CoinbaseRequestParamsTimeIOC + timeInForce := order.GoodTillCancel.String() + if s.TimeInForce == order.ImmediateOrCancel { + timeInForce = order.ImmediateOrCancel.String() } orderID, err = c.PlaceLimitOrder(ctx, "", diff --git a/exchanges/deribit/deribit_test.go b/exchanges/deribit/deribit_test.go index 63795f4f..216f6d6c 100644 --- a/exchanges/deribit/deribit_test.go +++ b/exchanges/deribit/deribit_test.go @@ -4158,3 +4158,28 @@ func TestFormatChannelPair(t *testing.T) { pair.Delimiter = "-" assert.Equal(t, "BTC-PERPETUAL", formatChannelPair(pair)) } + +var timeInForceList = []struct { + String string + PostOnly bool + TIF order.TimeInForce + Error error +}{ + {"good_til_cancelled", false, order.GoodTillCancel, nil}, + {"good_til_cancelled", true, order.GoodTillCancel | order.PostOnly, nil}, + {"good_til_day", false, order.GoodTillDay, nil}, + {"good_til_day", true, order.GoodTillDay | order.PostOnly, nil}, + {"fill_or_kill", false, order.FillOrKill, nil}, + {"immediate_or_cancel", false, order.ImmediateOrCancel, nil}, + {"abcd", false, order.UnknownTIF, order.ErrInvalidTimeInForce}, + {"", false, order.UnknownTIF, nil}, +} + +func TestTimeInForceFromString(t *testing.T) { + t.Parallel() + for i := range timeInForceList { + result, err := timeInForceFromString(timeInForceList[i].String, timeInForceList[i].PostOnly) + assert.Equalf(t, timeInForceList[i].TIF, result, "expected %s, got %s", timeInForceList[i].TIF.String(), result.String()) + require.ErrorIs(t, err, timeInForceList[i].Error) + } +} diff --git a/exchanges/deribit/deribit_wrapper.go b/exchanges/deribit/deribit_wrapper.go index d66a86ad..80fea157 100644 --- a/exchanges/deribit/deribit_wrapper.go +++ b/exchanges/deribit/deribit_wrapper.go @@ -585,7 +585,7 @@ func (d *Deribit) SubmitOrder(ctx context.Context, s *order.Submit) (*order.Subm return nil, err } timeInForce := "" - if s.ImmediateOrCancel { + if s.TimeInForce.Is(order.ImmediateOrCancel) { timeInForce = "immediate_or_cancel" } var data *PrivateTradeData @@ -597,7 +597,7 @@ func (d *Deribit) SubmitOrder(ctx context.Context, s *order.Submit) (*order.Subm Amount: s.Amount, Price: s.Price, TriggerPrice: s.TriggerPrice, - PostOnly: s.PostOnly, + PostOnly: s.TimeInForce.Is(order.PostOnly), ReduceOnly: s.ReduceOnly, } switch { @@ -649,7 +649,7 @@ func (d *Deribit) ModifyOrder(ctx context.Context, action *order.Modify) (*order var err error reqParam := &OrderBuyAndSellParams{ TriggerPrice: action.TriggerPrice, - PostOnly: action.PostOnly, + PostOnly: action.TimeInForce.Is(order.PostOnly), Amount: action.Amount, OrderID: action.OrderID, Price: action.Price, @@ -773,10 +773,15 @@ func (d *Deribit) GetOrderInfo(ctx context.Context, orderID string, _ currency.P return nil, fmt.Errorf("%v: orderStatus %s not supported", d.Name, orderInfo.OrderState) } } + var tif order.TimeInForce + tif, err = timeInForceFromString(orderInfo.TimeInForce, orderInfo.PostOnly) + if err != nil { + return nil, err + } return &order.Detail{ AssetType: assetType, Exchange: d.Name, - PostOnly: orderInfo.PostOnly, + TimeInForce: tif, Price: orderInfo.Price, Amount: orderInfo.Amount, ExecutedAmount: orderInfo.FilledAmount, @@ -894,10 +899,15 @@ func (d *Deribit) GetActiveOrders(ctx context.Context, getOrdersRequest *order.M if ordersData[y].OrderState != "open" { continue } + + var tif order.TimeInForce + tif, err = timeInForceFromString(ordersData[y].TimeInForce, ordersData[y].PostOnly) + if err != nil { + return nil, err + } resp = append(resp, order.Detail{ AssetType: getOrdersRequest.AssetType, Exchange: d.Name, - PostOnly: ordersData[y].PostOnly, Price: ordersData[y].Price, Amount: ordersData[y].Amount, ExecutedAmount: ordersData[y].FilledAmount, @@ -909,6 +919,7 @@ func (d *Deribit) GetActiveOrders(ctx context.Context, getOrdersRequest *order.M Side: orderSide, Type: orderType, Status: orderStatus, + TimeInForce: tif, }) } } @@ -963,10 +974,15 @@ func (d *Deribit) GetOrderHistory(ctx context.Context, getOrdersRequest *order.M return resp, fmt.Errorf("%v: orderStatus %s not supported", d.Name, ordersData[y].OrderState) } } + + var tif order.TimeInForce + tif, err = timeInForceFromString(ordersData[y].TimeInForce, ordersData[y].PostOnly) + if err != nil { + return nil, err + } resp = append(resp, order.Detail{ AssetType: getOrdersRequest.AssetType, Exchange: d.Name, - PostOnly: ordersData[y].PostOnly, Price: ordersData[y].Price, Amount: ordersData[y].Amount, ExecutedAmount: ordersData[y].FilledAmount, @@ -978,6 +994,7 @@ func (d *Deribit) GetOrderHistory(ctx context.Context, getOrdersRequest *order.M Side: orderSide, Type: orderType, Status: orderStatus, + TimeInForce: tif, }) } } @@ -1555,3 +1572,14 @@ func (d *Deribit) formatPairString(assetType asset.Item, pair currency.Pair) str } return pair.String() } + +func timeInForceFromString(timeInForceString string, postOnly bool) (order.TimeInForce, error) { + tif, err := order.StringToTimeInForce(timeInForceString) + if err != nil { + return order.UnknownTIF, err + } + if postOnly { + tif |= order.PostOnly + } + return tif, nil +} diff --git a/exchanges/gateio/gateio.go b/exchanges/gateio/gateio.go index e5cbff13..f84087a6 100644 --- a/exchanges/gateio/gateio.go +++ b/exchanges/gateio/gateio.go @@ -152,7 +152,6 @@ var ( errInvalidLeverageValue = errors.New("invalid leverage value") errInvalidRiskLimit = errors.New("new position risk limit") errInvalidCountTotalValue = errors.New("invalid \"count_total\" value, supported \"count_total\" values are 0 and 1") - errInvalidTimeInForce = errors.New("invalid time in force value") errInvalidAutoSizeValue = errors.New("invalid \"auto_size\" value, only \"close_long\" and \"close_short\" are supported") errTooManyOrderRequest = errors.New("too many order creation request") errInvalidTimeout = errors.New("invalid timeout, should be in seconds At least 5 seconds, 0 means cancel the countdown") @@ -168,6 +167,24 @@ var ( errInvalidTextValue = errors.New("invalid text value, requires prefix `t-`") ) +// validTimesInForce holds a list of supported time-in-force values and corresponding string representations. +// slice iteration outperforms map with this few elements +var validTimesInForce = []struct { + String string + TimeInForce order.TimeInForce +}{ + {gtcTIF, order.GoodTillCancel}, {iocTIF, order.ImmediateOrCancel}, {pocTIF, order.PostOnly}, {fokTIF, order.FillOrKill}, +} + +func timeInForceFromString(tif string) (order.TimeInForce, error) { + for a := range validTimesInForce { + if validTimesInForce[a].String == tif { + return validTimesInForce[a].TimeInForce, nil + } + } + return order.UnknownTIF, fmt.Errorf("%w: %q", order.ErrUnsupportedTimeInForce, tif) +} + // Gateio is the overarching type across this package type Gateio struct { Counter common.Counter // Must be first due to alignment requirements @@ -829,7 +846,7 @@ func (g *Gateio) CreatePriceTriggeredOrder(ctx context.Context, arg *PriceTrigge return nil, errNilArgument } if arg.Put.TimeInForce != gtcTIF && arg.Put.TimeInForce != iocTIF { - return nil, fmt.Errorf("%w, only 'gct' and 'ioc' are supported", errInvalidTimeInForce) + return nil, fmt.Errorf("%w: %q only 'gct' and 'ioc' are supported", order.ErrUnsupportedTimeInForce, arg.Put.TimeInForce) } if arg.Market.IsEmpty() { return nil, fmt.Errorf("%w, %s", currency.ErrCurrencyPairEmpty, "field market is required") @@ -2294,14 +2311,14 @@ func (g *Gateio) PlaceFuturesOrder(ctx context.Context, arg *ContractOrderCreate if arg.Size == 0 { return nil, fmt.Errorf("%w, specify positive number to make a bid, and negative number to ask", order.ErrSideIsInvalid) } - if arg.TimeInForce != gtcTIF && arg.TimeInForce != iocTIF && arg.TimeInForce != pocTIF && arg.TimeInForce != fokTIF { - return nil, errInvalidTimeInForce + if _, err := timeInForceFromString(arg.TimeInForce); err != nil { + return nil, err } if arg.Price == "" { return nil, errInvalidPrice } if arg.Price == "0" && arg.TimeInForce != iocTIF && arg.TimeInForce != fokTIF { - return nil, errInvalidTimeInForce + return nil, fmt.Errorf("%w: %q; only 'IOC' and 'FOK' allowed for market order", order.ErrUnsupportedTimeInForce, arg.TimeInForce) } if arg.AutoSize != "" && (arg.AutoSize == "close_long" || arg.AutoSize == "close_short") { return nil, errInvalidAutoSizeValue @@ -2383,17 +2400,14 @@ func (g *Gateio) PlaceBatchFuturesOrders(ctx context.Context, settle currency.Co if args[x].Size == 0 { return nil, fmt.Errorf("%w, specify positive number to make a bid, and negative number to ask", order.ErrSideIsInvalid) } - if args[x].TimeInForce != gtcTIF && - args[x].TimeInForce != iocTIF && - args[x].TimeInForce != pocTIF && - args[x].TimeInForce != fokTIF { - return nil, errInvalidTimeInForce + if _, err := timeInForceFromString(args[x].TimeInForce); err != nil { + return nil, err } if args[x].Price == "" { return nil, errInvalidPrice } if args[x].Price == "0" && args[x].TimeInForce != iocTIF && args[x].TimeInForce != fokTIF { - return nil, errInvalidTimeInForce + return nil, fmt.Errorf("%w: %q; only 'ioc' and 'fok' allowed for market order", order.ErrUnsupportedTimeInForce, args[x].TimeInForce) } if args[x].Text != "" && !strings.HasPrefix(args[x].Text, "t-") { return nil, errInvalidTextValue @@ -2547,7 +2561,7 @@ func (g *Gateio) CreatePriceTriggeredFuturesOrder(ctx context.Context, settle cu return nil, fmt.Errorf("%w, price must be greater than 0", errInvalidPrice) } if arg.Initial.TimeInForce != "" && arg.Initial.TimeInForce != gtcTIF && arg.Initial.TimeInForce != iocTIF { - return nil, fmt.Errorf("%w, only time in force value 'gtc' and 'ioc' are supported", errInvalidTimeInForce) + return nil, fmt.Errorf("%w: %q; only 'gtc' and 'ioc' are allowed", order.ErrInvalidTimeInForce, arg.Initial.TimeInForce) } if arg.Trigger.StrategyType != 0 && arg.Trigger.StrategyType != 1 { return nil, errors.New("strategy type must be 0 or 1, 0: by price, and 1: by price gap") @@ -2870,8 +2884,8 @@ func (g *Gateio) PlaceDeliveryOrder(ctx context.Context, arg *ContractOrderCreat if arg.Size == 0 { return nil, fmt.Errorf("%w, specify positive number to make a bid, and negative number to ask", order.ErrSideIsInvalid) } - if arg.TimeInForce != gtcTIF && arg.TimeInForce != iocTIF && arg.TimeInForce != pocTIF && arg.TimeInForce != fokTIF { - return nil, errInvalidTimeInForce + if _, err := timeInForceFromString(arg.TimeInForce); err != nil { + return nil, err } if arg.Price == "" { return nil, errInvalidPrice @@ -3071,7 +3085,7 @@ func (g *Gateio) GetDeliveryPriceTriggeredOrder(ctx context.Context, settle curr } if arg.Initial.TimeInForce != "" && arg.Initial.TimeInForce != gtcTIF && arg.Initial.TimeInForce != iocTIF { - return nil, fmt.Errorf("%w, only time in force value 'gtc' and 'ioc' are supported", errInvalidTimeInForce) + return nil, fmt.Errorf("%w: %q; only 'gtc' and 'ioc' are allowed", order.ErrUnsupportedTimeInForce, arg.Initial.TimeInForce) } if arg.Trigger.StrategyType != 0 && arg.Trigger.StrategyType != 1 { return nil, errors.New("strategy type must be 0 or 1, 0: by price, and 1: by price gap") diff --git a/exchanges/gateio/gateio_test.go b/exchanges/gateio/gateio_test.go index 6b3cc7fa..1e0d8cdc 100644 --- a/exchanges/gateio/gateio_test.go +++ b/exchanges/gateio/gateio_test.go @@ -1816,13 +1816,14 @@ func TestSubmitOrder(t *testing.T) { sharedtestvalues.SkipTestIfCredentialsUnset(t, g, canManipulateRealOrders) for _, a := range g.GetAssetTypes(false) { _, err := g.SubmitOrder(t.Context(), &order.Submit{ - Exchange: g.Name, - Pair: getPair(t, a), - Side: order.Buy, - Type: order.Limit, - Price: 1, - Amount: 1, - AssetType: a, + Exchange: g.Name, + Pair: getPair(t, a), + Side: order.Buy, + Type: order.Limit, + Price: 1, + Amount: 1, + AssetType: a, + TimeInForce: order.GoodTillCancel, }) assert.NoErrorf(t, err, "SubmitOrder should not error for %s", a) } @@ -2738,25 +2739,6 @@ func TestGetClientOrderIDFromText(t *testing.T) { assert.Equal(t, "t-123", getClientOrderIDFromText("t-123"), "should return t-123") } -func TestGetTypeFromTimeInForce(t *testing.T) { - t.Parallel() - typeResp, postOnly := getTypeFromTimeInForce("gtc") - assert.Equal(t, order.Limit, typeResp, "should be a limit order") - assert.False(t, postOnly, "should return false") - - typeResp, postOnly = getTypeFromTimeInForce("ioc") - assert.Equal(t, order.Market, typeResp, "should be market order") - assert.False(t, postOnly, "should return false") - - typeResp, postOnly = getTypeFromTimeInForce("poc") - assert.Equal(t, order.Limit, typeResp, "should be limit order") - assert.True(t, postOnly, "should return true") - - typeResp, postOnly = getTypeFromTimeInForce("fok") - assert.Equal(t, order.Market, typeResp, "should be market order") - assert.False(t, postOnly, "should return false") -} - func TestGetSideAndAmountFromSize(t *testing.T) { t.Parallel() side, amount, remaining := getSideAndAmountFromSize(1, 1) @@ -2784,29 +2766,6 @@ func TestGetFutureOrderSize(t *testing.T) { assert.Equal(t, -1.0, ret) } -func TestGetTimeInForce(t *testing.T) { - t.Parallel() - - _, err := getTimeInForce(&order.Submit{Type: order.Market, PostOnly: true}) - assert.ErrorIs(t, err, errPostOnlyOrderTypeUnsupported) - - ret, err := getTimeInForce(&order.Submit{Type: order.Market}) - require.NoError(t, err) - assert.Equal(t, "ioc", ret) - - ret, err = getTimeInForce(&order.Submit{Type: order.Limit, PostOnly: true}) - require.NoError(t, err) - assert.Equal(t, "poc", ret) - - ret, err = getTimeInForce(&order.Submit{Type: order.Limit}) - require.NoError(t, err) - assert.Equal(t, "gtc", ret) - - ret, err = getTimeInForce(&order.Submit{Type: order.Market, FillOrKill: true}) - require.NoError(t, err) - assert.Equal(t, "fok", ret) -} - func TestProcessFuturesOrdersPushData(t *testing.T) { t.Parallel() testCases := []struct { @@ -2967,7 +2926,7 @@ func TestDeriveSpotWebsocketOrderResponse(t *testing.T) { Type: order.Market, Side: order.Sell, Status: order.Filled, - ImmediateOrCancel: true, + TimeInForce: order.ImmediateOrCancel, Cost: 0.0001, Purchased: 9.35033, }, got) @@ -3010,7 +2969,7 @@ func TestDeriveSpotWebsocketOrderResponses(t *testing.T) { Type: order.Market, Side: order.Sell, Status: order.Filled, - ImmediateOrCancel: true, + TimeInForce: order.ImmediateOrCancel, Cost: 0.0001, Purchased: 9.35033, }, @@ -3028,7 +2987,7 @@ func TestDeriveSpotWebsocketOrderResponses(t *testing.T) { Type: order.Market, Side: order.Buy, Status: order.Filled, - ImmediateOrCancel: true, + TimeInForce: order.ImmediateOrCancel, Cost: 9.991512, Purchased: 816.3, }, @@ -3046,7 +3005,7 @@ func TestDeriveSpotWebsocketOrderResponses(t *testing.T) { Type: order.Limit, Side: order.Buy, Status: order.Filled, - FillOrKill: true, + TimeInForce: order.FillOrKill, Cost: 7.346, Purchased: 200, }, @@ -3064,7 +3023,7 @@ func TestDeriveSpotWebsocketOrderResponses(t *testing.T) { Type: order.Limit, Side: order.Buy, Status: order.Open, - PostOnly: true, + TimeInForce: order.PostOnly, }, { Exchange: g.Name, @@ -3080,6 +3039,7 @@ func TestDeriveSpotWebsocketOrderResponses(t *testing.T) { Type: order.Limit, Side: order.Sell, Status: order.Open, + TimeInForce: order.GoodTillCancel, }, }, }, @@ -3127,7 +3087,7 @@ func TestDeriveFuturesWebsocketOrderResponse(t *testing.T) { Type: order.Market, Side: order.Long, Status: order.Filled, - ImmediateOrCancel: true, + TimeInForce: order.ImmediateOrCancel, ReduceOnly: true, }, got) } @@ -3170,7 +3130,7 @@ func TestDeriveFuturesWebsocketOrderResponses(t *testing.T) { Type: order.Market, Side: order.Long, Status: order.Filled, - ImmediateOrCancel: true, + TimeInForce: order.ImmediateOrCancel, ReduceOnly: true, }, { @@ -3186,7 +3146,7 @@ func TestDeriveFuturesWebsocketOrderResponses(t *testing.T) { Type: order.Market, Side: order.Short, Status: order.Filled, - ImmediateOrCancel: true, + TimeInForce: order.ImmediateOrCancel, }, { Exchange: g.Name, @@ -3201,6 +3161,7 @@ func TestDeriveFuturesWebsocketOrderResponses(t *testing.T) { Type: order.Limit, Side: order.Long, Status: order.Open, + TimeInForce: order.GoodTillCancel, }, { Exchange: g.Name, @@ -3215,6 +3176,7 @@ func TestDeriveFuturesWebsocketOrderResponses(t *testing.T) { Type: order.Limit, Side: order.Short, Status: order.Open, + TimeInForce: order.GoodTillCancel, }, { Exchange: g.Name, @@ -3228,7 +3190,7 @@ func TestDeriveFuturesWebsocketOrderResponses(t *testing.T) { Type: order.Market, Side: order.Long, Status: order.Filled, - ImmediateOrCancel: true, + TimeInForce: order.ImmediateOrCancel, }, { Exchange: g.Name, @@ -3242,7 +3204,7 @@ func TestDeriveFuturesWebsocketOrderResponses(t *testing.T) { Type: order.Market, Side: order.Short, Status: order.Filled, - ImmediateOrCancel: true, + TimeInForce: order.ImmediateOrCancel, ReduceOnly: true, }, }, @@ -3335,3 +3297,51 @@ func getPairs(tb testing.TB, a asset.Item) currency.Pairs { return enabledPairs } + +func BenchmarkTimeInForceFromString(b *testing.B) { + for b.Loop() { + for _, tifString := range []string{gtcTIF, iocTIF, pocTIF, fokTIF} { + if _, err := timeInForceFromString(tifString); err != nil { + b.Fatal(tifString) + } + } + } +} + +func TestTimeInForceFromString(t *testing.T) { + t.Parallel() + _, err := timeInForceFromString("abcdef") + assert.ErrorIs(t, err, order.ErrUnsupportedTimeInForce) + + for k, v := range map[string]order.TimeInForce{gtcTIF: order.GoodTillCancel, iocTIF: order.ImmediateOrCancel, pocTIF: order.PostOnly, fokTIF: order.FillOrKill} { + t.Run(k, func(t *testing.T) { + t.Parallel() + tif, err := timeInForceFromString(k) + require.NoError(t, err) + assert.Equal(t, v, tif) + }) + } +} + +func TestGetTypeFromTimeInForce(t *testing.T) { + t.Parallel() + typeResp := getTypeFromTimeInForce("gtc", 0) + assert.Equal(t, order.Limit, typeResp) + + typeResp = getTypeFromTimeInForce("ioc", 0) + assert.Equal(t, order.Market, typeResp, "should be market order") + + typeResp = getTypeFromTimeInForce("poc", 123) + assert.Equal(t, order.Limit, typeResp, "should be limit order") + + typeResp = getTypeFromTimeInForce("fok", 0) + assert.Equal(t, order.Market, typeResp, "should be market order") +} + +func TestTimeInForceString(t *testing.T) { + t.Parallel() + assert.Empty(t, timeInForceString(order.UnknownTIF)) + for _, valid := range validTimesInForce { + assert.Equal(t, valid.String, timeInForceString(valid.TimeInForce)) + } +} diff --git a/exchanges/gateio/gateio_wrapper.go b/exchanges/gateio/gateio_wrapper.go index fe0a5905..55641953 100644 --- a/exchanges/gateio/gateio_wrapper.go +++ b/exchanges/gateio/gateio_wrapper.go @@ -972,10 +972,6 @@ func (g *Gateio) SubmitOrder(ctx context.Context, s *order.Submit) (*order.Submi if err != nil { return nil, err } - timeInForce, err := getTimeInForce(s) - if err != nil { - return nil, err - } settle, err := getSettlementCurrency(s.Pair, s.AssetType) if err != nil { return nil, err @@ -986,7 +982,7 @@ func (g *Gateio) SubmitOrder(ctx context.Context, s *order.Submit) (*order.Submi Price: strconv.FormatFloat(s.Price, 'f', -1, 64), // Cannot be an empty string, requires "0" for market orders. Settle: settle, ReduceOnly: s.ReduceOnly, - TimeInForce: timeInForce, + TimeInForce: timeInForceString(s.TimeInForce), Text: s.ClientOrderID, } var o *Order @@ -1304,8 +1300,10 @@ func (g *Gateio) GetOrderInfo(ctx context.Context, orderID string, pair currency } side, amount, remaining := getSideAndAmountFromSize(fOrder.Size, fOrder.RemainingAmount) - - ordertype, postonly := getTypeFromTimeInForce(fOrder.TimeInForce) + tif, err := timeInForceFromString(fOrder.TimeInForce) + if err != nil { + return nil, err + } return &order.Detail{ Amount: amount, ExecutedAmount: amount - remaining, @@ -1320,8 +1318,8 @@ func (g *Gateio) GetOrderInfo(ctx context.Context, orderID string, pair currency LastUpdated: fOrder.FinishTime.Time(), Pair: pair, AssetType: a, - Type: ordertype, - PostOnly: postonly, + Type: getTypeFromTimeInForce(fOrder.TimeInForce, fOrder.OrderPrice.Float64()), + TimeInForce: tif, Side: side, }, nil case asset.Options: @@ -1507,8 +1505,11 @@ func (g *Gateio) GetActiveOrders(ctx context.Context, req *order.MultiOrderReque if futuresOrders[i].Status != statusOpen || (len(req.Pairs) > 0 && !req.Pairs.Contains(pair, true)) { continue } - side, amount, remaining := getSideAndAmountFromSize(futuresOrders[i].Size, futuresOrders[i].RemainingAmount) + tif, err := timeInForceFromString(futuresOrders[i].TimeInForce) + if err != nil { + return nil, err + } orders = append(orders, order.Detail{ Status: order.Open, Amount: amount, @@ -1527,7 +1528,7 @@ func (g *Gateio) GetActiveOrders(ctx context.Context, req *order.MultiOrderReque Type: order.Limit, SettlementCurrency: settle, ReduceOnly: futuresOrders[i].IsReduceOnly, - PostOnly: futuresOrders[i].TimeInForce == "poc", + TimeInForce: tif, AverageExecutedPrice: futuresOrders[i].FillPrice.Float64(), }) } @@ -2252,16 +2253,17 @@ func getClientOrderIDFromText(text string) string { } // getTypeFromTimeInForce returns the order type and if the order is post only -func getTypeFromTimeInForce(tif string) (orderType order.Type, postOnly bool) { +func getTypeFromTimeInForce(tif string, price float64) (orderType order.Type) { switch tif { - case iocTIF: - return order.Market, false - case fokTIF: - return order.Market, false - case pocTIF: - return order.Limit, true + case iocTIF, fokTIF: + return order.Market + case pocTIF, gtcTIF: + return order.Limit default: - return order.Limit, false + if price == 0 { + return order.Market + } + return order.Limit } } @@ -2285,27 +2287,6 @@ func getFutureOrderSize(s *order.Submit) (float64, error) { } } -var errPostOnlyOrderTypeUnsupported = errors.New("post only is only supported for limit orders") - -// getTimeInForce returns the time in force for a given order. If Market order -// IOC -func getTimeInForce(s *order.Submit) (string, error) { - timeInForce := "gtc" // limit order taker/maker - if s.Type == order.Market || s.ImmediateOrCancel { - timeInForce = iocTIF // market taker only - } - if s.PostOnly { - if s.Type != order.Limit { - return "", fmt.Errorf("%w not for %v", errPostOnlyOrderTypeUnsupported, s.Type) - } - timeInForce = pocTIF // limit order maker only - } - if s.FillOrKill { - timeInForce = fokTIF // limit order entire fill or kill - } - return timeInForce, nil -} - // GetCurrencyTradeURL returns the URL to the exchange's trade page for the given asset and currency pair func (g *Gateio) GetCurrencyTradeURL(_ context.Context, a asset.Item, cp currency.Pair) (string, error) { _, err := g.CurrencyPairs.IsPairEnabled(cp, a) @@ -2362,17 +2343,12 @@ func (g *Gateio) WebsocketSubmitOrder(ctx context.Context, s *order.Submit) (*or return nil, err } - timeInForce, err := getTimeInForce(s) - if err != nil { - return nil, err - } - resp, err := g.WebsocketFuturesSubmitOrder(ctx, s.AssetType, &ContractOrderCreateParams{ Contract: s.Pair, Size: amountWithDirection, Price: strconv.FormatFloat(s.Price, 'f', -1, 64), ReduceOnly: s.ReduceOnly, - TimeInForce: timeInForce, + TimeInForce: timeInForceString(s.TimeInForce), Text: s.ClientOrderID, }) if err != nil { @@ -2384,6 +2360,24 @@ func (g *Gateio) WebsocketSubmitOrder(ctx context.Context, s *order.Submit) (*or } } +// timeInForceString returns the most relevant time-in-force exchange string for a TimeInForce +// Any TIF value that is combined with POC, IOC or FOK will just return that +// Otherwise the lowercase representation is returned +func timeInForceString(tif order.TimeInForce) string { + switch { + case tif.Is(order.PostOnly): + return "poc" + case tif.Is(order.ImmediateOrCancel): + return iocTIF + case tif.Is(order.FillOrKill): + return fokTIF + case tif.Is(order.GoodTillCancel): + return gtcTIF + default: + return tif.Lower() + } +} + func (g *Gateio) deriveSpotWebsocketOrderResponse(responses *WebsocketOrderResponse) (*order.SubmitResponse, error) { resp, err := g.deriveSpotWebsocketOrderResponses([]*WebsocketOrderResponse{responses}) if err != nil { @@ -2427,7 +2421,10 @@ func (g *Gateio) deriveSpotWebsocketOrderResponses(responses []*WebsocketOrderRe purchased = resp.FilledTotal.Float64() } } - + tif, err := order.StringToTimeInForce(resp.TimeInForce) + if err != nil { + return nil, err + } out = append(out, &order.SubmitResponse{ Exchange: g.Name, OrderID: resp.ID, @@ -2443,9 +2440,7 @@ func (g *Gateio) deriveSpotWebsocketOrderResponses(responses []*WebsocketOrderRe Type: oType, Side: side, Status: status, - ImmediateOrCancel: resp.TimeInForce == iocTIF, - FillOrKill: resp.TimeInForce == fokTIF, - PostOnly: resp.TimeInForce == pocTIF, + TimeInForce: tif, Cost: cost, Purchased: purchased, Fee: resp.Fee.Float64(), @@ -2494,7 +2489,10 @@ func (g *Gateio) deriveFuturesWebsocketOrderResponses(responses []*WebsocketFutu if resp.Text != "" && strings.HasPrefix(resp.Text, "t-") { clientOrderID = resp.Text } - + tif, err := order.StringToTimeInForce(resp.TimeInForce) + if err != nil { + return nil, err + } out = append(out, &order.SubmitResponse{ Exchange: g.Name, OrderID: strconv.FormatInt(resp.ID, 10), @@ -2510,9 +2508,7 @@ func (g *Gateio) deriveFuturesWebsocketOrderResponses(responses []*WebsocketFutu Type: oType, Side: side, Status: status, - ImmediateOrCancel: resp.TimeInForce == iocTIF, - FillOrKill: resp.TimeInForce == fokTIF, - PostOnly: resp.TimeInForce == pocTIF, + TimeInForce: tif, ReduceOnly: resp.IsReduceOnly, }) } @@ -2529,9 +2525,12 @@ func (g *Gateio) getSpotOrderRequest(s *order.Submit) (*CreateOrderRequest, erro return nil, order.ErrSideIsInvalid } - timeInForce, err := getTimeInForce(s) - if err != nil { - return nil, err + var timeInForce string + switch s.TimeInForce { + case order.ImmediateOrCancel, order.FillOrKill, order.GoodTillCancel: + timeInForce = s.TimeInForce.Lower() + case order.PostOnly: + timeInForce = "poc" } return &CreateOrderRequest{ diff --git a/exchanges/huobi/huobi_types.go b/exchanges/huobi/huobi_types.go index 7ea1298b..f51a4102 100644 --- a/exchanges/huobi/huobi_types.go +++ b/exchanges/huobi/huobi_types.go @@ -980,10 +980,11 @@ type WsTradeUpdate struct { // OrderVars stores side, status and type for any order/trade type OrderVars struct { - Side order.Side - Status order.Status - OrderType order.Type - Fee float64 + Side order.Side + Status order.Status + OrderType order.Type + TimeInForce order.TimeInForce + Fee float64 } // Variables below are used to check api requests being sent out diff --git a/exchanges/huobi/huobi_wrapper.go b/exchanges/huobi/huobi_wrapper.go index 6fdaf9db..521ec2f2 100644 --- a/exchanges/huobi/huobi_wrapper.go +++ b/exchanges/huobi/huobi_wrapper.go @@ -1000,10 +1000,29 @@ func (h *HUOBI) SubmitOrder(ctx context.Context, s *order.Submit) (*order.Submit } var oType string switch s.Type { + case order.Market: + // https://huobiapi.github.io/docs/dm/v1/en/#order-and-trade + // At present, Huobi Futures does not support unlimited slippage market price when placing an order. + // To increase the probability of a transaction, users can choose to place an order based on BBO price (opponent), + // optimal 5 (optimal_5), optimal 10 (optimal_10), optimal 20 (optimal_20), among which the success probability of + // optimal 20 is the largest, while the slippage always is the largest as well. + // + // It is important to note that the above methods will not guarantee the order to be fully-filled + // The exchange will obtain the optimal N price when the order is placed + oType = "optimal_20" + switch { + case s.TimeInForce.Is(order.ImmediateOrCancel): + oType = "optimal_20_ioc" + case s.TimeInForce.Is(order.FillOrKill): + oType = "optimal_20_fok" + } case order.Limit: oType = "limit" - case order.PostOnly: - oType = "post_only" + if s.TimeInForce.Is(order.PostOnly) { + oType = "post_only" + } + default: + oType = "opponent" } offset := "open" if s.ReduceOnly { @@ -1034,21 +1053,27 @@ func (h *HUOBI) SubmitOrder(ctx context.Context, s *order.Submit) (*order.Submit switch s.Type { case order.Market: // https://huobiapi.github.io/docs/dm/v1/en/#order-and-trade - // At present, Huobi Futures does not support market price when placing an order. + // At present, Huobi Futures does not support unlimited slippage market price when placing an order. // To increase the probability of a transaction, users can choose to place an order based on BBO price (opponent), // optimal 5 (optimal_5), optimal 10 (optimal_10), optimal 20 (optimal_20), among which the success probability of // optimal 20 is the largest, while the slippage always is the largest as well. // - // It is important to note that the above methods will not guarantee the order to be filled in 100%. - // The system will obtain the optimal N price at that moment and place the order. + // It is important to note that the above methods will not guarantee the order to be fully-filled + // The exchange will obtain the optimal N price when the order is placed oType = "optimal_20" - if s.ImmediateOrCancel { + switch { + case s.TimeInForce.Is(order.ImmediateOrCancel): oType = "optimal_20_ioc" + case s.TimeInForce.Is(order.FillOrKill): + oType = "optimal_20_fok" } case order.Limit: oType = "limit" - case order.PostOnly: - oType = "post_only" + if s.TimeInForce.Is(order.PostOnly) { + oType = "post_only" + } + default: + oType = "opponent" } offset := "open" if s.ReduceOnly { @@ -1335,9 +1360,9 @@ func (h *HUOBI) GetOrderInfo(ctx context.Context, orderID string, pair currency. if err != nil { return nil, err } - maker := true - if orderVars.OrderType == order.Limit || orderVars.OrderType == order.PostOnly { - maker = false + maker := false + if orderVars.OrderType == order.Limit || orderVars.TimeInForce.Is(order.PostOnly) { + maker = true } orderDetail.Trades = append(orderDetail.Trades, order.TradeHistory{ Price: orderInfo.Data[x].Price, @@ -1374,7 +1399,7 @@ func (h *HUOBI) GetOrderInfo(ctx context.Context, orderID string, pair currency. TID: orderInfo.Data[x].OrderIDString, Type: orderVars.OrderType, Side: orderVars.Side, - IsMaker: orderVars.OrderType == order.Limit || orderVars.OrderType == order.PostOnly, + IsMaker: orderVars.OrderType == order.Limit || orderVars.TimeInForce.Is(order.PostOnly), }) } default: @@ -1507,9 +1532,8 @@ func (h *HUOBI) GetActiveOrders(ctx context.Context, req *order.MultiOrderReques return orders, err } - var orderVars OrderVars for x := range openOrders.Data.Orders { - orderVars, err = compatibleVars(openOrders.Data.Orders[x].Direction, + orderVars, err := compatibleVars(openOrders.Data.Orders[x].Direction, openOrders.Data.Orders[x].OrderPriceType, openOrders.Data.Orders[x].Status) if err != nil { @@ -1520,7 +1544,7 @@ func (h *HUOBI) GetActiveOrders(ctx context.Context, req *order.MultiOrderReques return orders, err } orders = append(orders, order.Detail{ - PostOnly: orderVars.OrderType == order.PostOnly, + TimeInForce: orderVars.TimeInForce, Leverage: openOrders.Data.Orders[x].LeverageRate, Price: openOrders.Data.Orders[x].Price, Amount: openOrders.Data.Orders[x].Volume, @@ -1562,7 +1586,7 @@ func (h *HUOBI) GetActiveOrders(ctx context.Context, req *order.MultiOrderReques return orders, err } orders = append(orders, order.Detail{ - PostOnly: orderVars.OrderType == order.PostOnly, + TimeInForce: orderVars.TimeInForce, Leverage: openOrders.Data.Orders[x].LeverageRate, Price: openOrders.Data.Orders[x].Price, Amount: openOrders.Data.Orders[x].Volume, @@ -1663,7 +1687,7 @@ func (h *HUOBI) GetOrderHistory(ctx context.Context, req *order.MultiOrderReques return orders, err } orders = append(orders, order.Detail{ - PostOnly: orderVars.OrderType == order.PostOnly, + TimeInForce: orderVars.TimeInForce, Leverage: orderHistory.Data.Orders[x].LeverageRate, Price: orderHistory.Data.Orders[x].Price, Amount: orderHistory.Data.Orders[x].Volume, @@ -1721,7 +1745,7 @@ func (h *HUOBI) GetOrderHistory(ctx context.Context, req *order.MultiOrderReques return orders, err } orders = append(orders, order.Detail{ - PostOnly: orderVars.OrderType == order.PostOnly, + TimeInForce: orderVars.TimeInForce, Leverage: openOrders.Data.Orders[x].LeverageRate, Price: openOrders.Data.Orders[x].Price, Amount: openOrders.Data.Orders[x].Volume, @@ -1964,7 +1988,8 @@ func compatibleVars(side, orderPriceType string, status int64) (OrderVars, error case "opponent": resp.OrderType = order.Market case "post_only": - resp.OrderType = order.PostOnly + resp.OrderType = order.Limit + resp.TimeInForce = order.PostOnly default: return resp, errors.New("invalid orderPriceType") } diff --git a/exchanges/kraken/futures_types.go b/exchanges/kraken/futures_types.go index b840320a..cd3a48b0 100644 --- a/exchanges/kraken/futures_types.go +++ b/exchanges/kraken/futures_types.go @@ -8,12 +8,10 @@ import ( var ( validOrderTypes = map[order.Type]string{ - order.ImmediateOrCancel: "ioc", - order.Limit: "lmt", - order.Stop: "stp", - order.PostOnly: "post", - order.TakeProfit: "take_profit", - order.Market: "mkt", + order.Limit: "lmt", + order.Stop: "stp", + order.TakeProfit: "take_profit", + order.Market: "mkt", } validSide = []string{"buy", "sell"} diff --git a/exchanges/kraken/kraken.go b/exchanges/kraken/kraken.go index 916e0c29..0a35a967 100644 --- a/exchanges/kraken/kraken.go +++ b/exchanges/kraken/kraken.go @@ -790,7 +790,7 @@ func (k *Kraken) AddOrder(ctx context.Context, symbol currency.Pair, side, order } if args.TimeInForce != "" { - params.Set("timeinforce", string(args.TimeInForce)) + params.Set("timeinforce", args.TimeInForce) } var result AddOrderResponse diff --git a/exchanges/kraken/kraken_futures.go b/exchanges/kraken/kraken_futures.go index 699cee13..b73de253 100644 --- a/exchanges/kraken/kraken_futures.go +++ b/exchanges/kraken/kraken_futures.go @@ -156,20 +156,21 @@ func (k *Kraken) FuturesEditOrder(ctx context.Context, orderID, clientOrderID st } // FuturesSendOrder sends a futures order -func (k *Kraken) FuturesSendOrder(ctx context.Context, orderType order.Type, symbol currency.Pair, side, triggerSignal, clientOrderID, reduceOnly string, - ioc bool, - size, limitPrice, stopPrice float64, -) (FuturesSendOrderData, error) { +func (k *Kraken) FuturesSendOrder(ctx context.Context, orderType order.Type, symbol currency.Pair, side, triggerSignal, clientOrderID, reduceOnly string, tif order.TimeInForce, size, limitPrice, stopPrice float64) (FuturesSendOrderData, error) { var resp FuturesSendOrderData - - if ioc && orderType != order.Market { - orderType = order.ImmediateOrCancel - } - oType, ok := validOrderTypes[orderType] if !ok { return resp, errors.New("invalid orderType") } + + if oType != "mkt" { + if tif.Is(order.PostOnly) { + oType = "post" + } else if tif.Is(order.ImmediateOrCancel) { + oType = "ioc" + } + } + params := url.Values{} params.Set("orderType", oType) symbolValue, err := k.FormatSymbol(symbol, asset.Futures) diff --git a/exchanges/kraken/kraken_test.go b/exchanges/kraken/kraken_test.go index ae8922f5..e47284d8 100644 --- a/exchanges/kraken/kraken_test.go +++ b/exchanges/kraken/kraken_test.go @@ -181,7 +181,7 @@ func TestFuturesSendOrder(t *testing.T) { t.Parallel() sharedtestvalues.SkipTestIfCredentialsUnset(t, k, canManipulateRealOrders) - _, err := k.FuturesSendOrder(t.Context(), order.Limit, futuresTestPair, "buy", "", "", "", true, 1, 1, 0.9) + _, err := k.FuturesSendOrder(t.Context(), order.Limit, futuresTestPair, "buy", "", "", "", order.ImmediateOrCancel, 1, 1, 0.9) assert.NoError(t, err, "FuturesSendOrder should not error") } diff --git a/exchanges/kraken/kraken_types.go b/exchanges/kraken/kraken_types.go index ddda5031..3cb0ca7a 100644 --- a/exchanges/kraken/kraken_types.go +++ b/exchanges/kraken/kraken_types.go @@ -426,7 +426,7 @@ type AddOrderOptions struct { ClosePrice float64 ClosePrice2 float64 Validate bool - TimeInForce RequestParamsTimeForceType + TimeInForce string } // CancelOrderResponse type @@ -629,25 +629,25 @@ type WsOpenOrderDescription struct { // WsAddOrderRequest request type for ws adding order type WsAddOrderRequest struct { - Event string `json:"event"` - Token string `json:"token"` - RequestID int64 `json:"reqid,omitempty"` // Optional, client originated ID reflected in response message. - OrderType string `json:"ordertype"` - OrderSide string `json:"type"` - Pair string `json:"pair"` - Price float64 `json:"price,string,omitempty"` // optional - Price2 float64 `json:"price2,string,omitempty"` // optional - Volume float64 `json:"volume,string,omitempty"` - Leverage float64 `json:"leverage,omitempty"` // optional - OFlags string `json:"oflags,omitempty"` // optional - StartTime string `json:"starttm,omitempty"` // optional - ExpireTime string `json:"expiretm,omitempty"` // optional - UserReferenceID string `json:"userref,omitempty"` // optional - Validate string `json:"validate,omitempty"` // optional - CloseOrderType string `json:"close[ordertype],omitempty"` // optional - ClosePrice float64 `json:"close[price],omitempty"` // optional - ClosePrice2 float64 `json:"close[price2],omitempty"` // optional - TimeInForce RequestParamsTimeForceType `json:"timeinforce,omitempty"` // optional + Event string `json:"event"` + Token string `json:"token"` + RequestID int64 `json:"reqid,omitempty"` // Optional, client originated ID reflected in response message. + OrderType string `json:"ordertype"` + OrderSide string `json:"type"` + Pair string `json:"pair"` + Price float64 `json:"price,string,omitempty"` // optional + Price2 float64 `json:"price2,string,omitempty"` // optional + Volume float64 `json:"volume,string,omitempty"` + Leverage float64 `json:"leverage,omitempty"` // optional + OFlags string `json:"oflags,omitempty"` // optional + StartTime string `json:"starttm,omitempty"` // optional + ExpireTime string `json:"expiretm,omitempty"` // optional + UserReferenceID string `json:"userref,omitempty"` // optional + Validate string `json:"validate,omitempty"` // optional + CloseOrderType string `json:"close[ordertype],omitempty"` // optional + ClosePrice float64 `json:"close[price],omitempty"` // optional + ClosePrice2 float64 `json:"close[price2],omitempty"` // optional + TimeInForce string `json:"timeinforce,omitempty"` // optional } // WsAddOrderResponse response data for ws order @@ -685,16 +685,6 @@ type OrderVars struct { Fee float64 } -// RequestParamsTimeForceType Time in force -type RequestParamsTimeForceType string - -var ( - // RequestParamsTimeGTC GTC - RequestParamsTimeGTC = RequestParamsTimeForceType("GTC") - // RequestParamsTimeIOC IOC - RequestParamsTimeIOC = RequestParamsTimeForceType("IOC") -) - type genericRESTResponse struct { Error errorResponse `json:"error"` Result any `json:"result"` diff --git a/exchanges/kraken/kraken_wrapper.go b/exchanges/kraken/kraken_wrapper.go index 6bdce41c..bb7813a6 100644 --- a/exchanges/kraken/kraken_wrapper.go +++ b/exchanges/kraken/kraken_wrapper.go @@ -697,9 +697,12 @@ func (k *Kraken) SubmitOrder(ctx context.Context, s *order.Submit) (*order.Submi status := order.New switch s.AssetType { case asset.Spot: - timeInForce := RequestParamsTimeGTC - if s.ImmediateOrCancel { - timeInForce = RequestParamsTimeIOC + var timeInForce string + switch { + case s.TimeInForce.Is(order.GoodTillDay): + timeInForce = "GTD" + case s.TimeInForce.Is(order.ImmediateOrCancel): + timeInForce = "IOC" } if k.Websocket.CanUseAuthenticatedWebsocketForWrapper() { orderID, err = k.wsAddOrder(&WsAddOrderRequest{ @@ -745,7 +748,7 @@ func (k *Kraken) SubmitOrder(ctx context.Context, s *order.Submit) (*order.Submi "", s.ClientOrderID, "", - s.ImmediateOrCancel, + s.TimeInForce, s.Amount, s.Price, 0, diff --git a/exchanges/kucoin/kucoin.go b/exchanges/kucoin/kucoin.go index 64e13624..e7b33505 100644 --- a/exchanges/kucoin/kucoin.go +++ b/exchanges/kucoin/kucoin.go @@ -1126,7 +1126,7 @@ func (ku *Kucoin) PostStopOrder(ctx context.Context, clientOID, side, symbol, or if timeInForce != "" { arg["timeInForce"] = timeInForce } - if cancelAfter > 0 && timeInForce == "GTT" { + if cancelAfter > 0 && timeInForce == order.GoodTillTime.String() { arg["cancelAfter"] = strconv.FormatFloat(cancelAfter, 'f', -1, 64) } arg["postOnly"] = postOnly diff --git a/exchanges/kucoin/kucoin_test.go b/exchanges/kucoin/kucoin_test.go index 942ec8b9..84b8cefa 100644 --- a/exchanges/kucoin/kucoin_test.go +++ b/exchanges/kucoin/kucoin_test.go @@ -4442,3 +4442,24 @@ func TestChannelName(t *testing.T) { assert.Equal(t, tt.exp, channelName(&subscription.Subscription{Channel: tt.ch}, tt.a)) } } + +func TestStringToTimeInForce(t *testing.T) { + t.Parallel() + tifMap := []struct { + String string + PostOnly bool + TimeInForce order.TimeInForce + }{ + {"GTC", false, order.GoodTillCancel}, + {"GTC", true, order.GoodTillCancel | order.PostOnly}, + {"GTT", false, order.GoodTillTime}, + {"GTT", true, order.GoodTillTime | order.PostOnly}, + {"IOC", false, order.ImmediateOrCancel}, + {"ioC", false, order.ImmediateOrCancel}, + {"Fok", false, order.FillOrKill}, + } + for a := range tifMap { + result := StringToTimeInForce(tifMap[a].String, tifMap[a].PostOnly) + assert.Equal(t, tifMap[a].TimeInForce, result) + } +} diff --git a/exchanges/kucoin/kucoin_wrapper.go b/exchanges/kucoin/kucoin_wrapper.go index f6cb1ad9..bd038387 100644 --- a/exchanges/kucoin/kucoin_wrapper.go +++ b/exchanges/kucoin/kucoin_wrapper.go @@ -666,7 +666,7 @@ func (ku *Kucoin) SubmitOrder(ctx context.Context, s *order.Submit) (*order.Subm Leverage: s.Leverage, VisibleSize: 0, ReduceOnly: s.ReduceOnly, - PostOnly: s.PostOnly, + PostOnly: s.TimeInForce.Is(order.PostOnly), Hidden: s.Hidden, Stop: stopOrderBoundary, StopPrice: s.TriggerPrice, @@ -690,13 +690,10 @@ func (ku *Kucoin) SubmitOrder(ctx context.Context, s *order.Submit) (*order.Subm var timeInForce string if oType == order.Limit { switch { - case s.FillOrKill: - timeInForce = "FOK" - case s.ImmediateOrCancel: - timeInForce = "IOC" - case s.PostOnly: + case s.TimeInForce.Is(order.FillOrKill) || s.TimeInForce.Is(order.ImmediateOrCancel): + timeInForce = s.TimeInForce.String() default: - timeInForce = "GTC" + timeInForce = order.GoodTillCancel.String() } } var stopType string @@ -719,7 +716,7 @@ func (ku *Kucoin) SubmitOrder(ctx context.Context, s *order.Submit) (*order.Subm s.Pair.String(), oType.Lower(), "", stopType, "", SpotTradeType, timeInForce, s.Amount, s.Price, stopPrice, 0, - 0, 0, s.PostOnly, s.Hidden, s.Iceberg) + 0, 0, s.TimeInForce.Is(order.PostOnly), s.Hidden, s.Iceberg) if err != nil { return nil, err } @@ -732,7 +729,7 @@ func (ku *Kucoin) SubmitOrder(ctx context.Context, s *order.Submit) (*order.Subm OrderType: s.Type.Lower(), Size: s.Amount, Price: s.Price, - PostOnly: s.PostOnly, + PostOnly: s.TimeInForce.Is(order.PostOnly), Hidden: s.Hidden, TimeInForce: timeInForce, Iceberg: s.Iceberg, @@ -792,7 +789,7 @@ func (ku *Kucoin) SubmitOrder(ctx context.Context, s *order.Submit) (*order.Subm Price: s.Price, Size: s.Amount, VisibleSize: s.Amount, - PostOnly: s.PostOnly, + PostOnly: s.TimeInForce.Is(order.PostOnly), Hidden: s.Hidden, AutoBorrow: s.AutoBorrow, AutoRepay: s.AutoBorrow, @@ -1032,7 +1029,7 @@ func (ku *Kucoin) GetOrderInfo(ctx context.Context, orderID string, pair currenc Price: orderDetail.Price, Date: orderDetail.CreatedAt.Time(), HiddenOrder: orderDetail.Hidden, - PostOnly: orderDetail.PostOnly, + TimeInForce: StringToTimeInForce(orderDetail.TimeInForce, orderDetail.PostOnly), ReduceOnly: orderDetail.ReduceOnly, Leverage: orderDetail.Leverage, AverageExecutedPrice: orderDetail.Price, @@ -1103,7 +1100,7 @@ func (ku *Kucoin) GetOrderInfo(ctx context.Context, orderID string, pair currenc Price: orderDetail.Price.Float64(), Date: orderDetail.CreatedAt.Time(), HiddenOrder: orderDetail.Hidden, - PostOnly: orderDetail.PostOnly, + TimeInForce: StringToTimeInForce(orderDetail.TimeInForce, orderDetail.PostOnly), AverageExecutedPrice: orderDetail.Price.Float64(), FeeAsset: currency.NewCode(orderDetail.FeeCurrency), ClientOrderID: orderDetail.ClientOID, @@ -1279,7 +1276,7 @@ func (ku *Kucoin) GetActiveOrders(ctx context.Context, getOrdersRequest *order.M Side: side, Type: oType, Pair: dPair, - PostOnly: futuresOrders.Items[x].PostOnly, + TimeInForce: StringToTimeInForce(futuresOrders.Items[x].TimeInForce, futuresOrders.Items[x].PostOnly), ReduceOnly: futuresOrders.Items[x].ReduceOnly, Status: status, SettlementCurrency: currency.NewCode(futuresOrders.Items[x].SettleCurrency), @@ -1375,7 +1372,7 @@ func (ku *Kucoin) GetActiveOrders(ctx context.Context, getOrdersRequest *order.M Side: side, Type: order.Stop, Pair: dPair, - PostOnly: response.Items[a].PostOnly, + TimeInForce: StringToTimeInForce(response.Items[a].TimeInForce, response.Items[a].PostOnly), Status: status, AssetType: getOrdersRequest.AssetType, HiddenOrder: response.Items[a].Hidden, @@ -1613,7 +1610,7 @@ func (ku *Kucoin) GetOrderHistory(ctx context.Context, getOrdersRequest *order.M Side: side, Type: order.Stop, Pair: dPair, - PostOnly: response.Items[a].PostOnly, + TimeInForce: StringToTimeInForce(response.Items[a].TimeInForce, response.Items[a].PostOnly), Status: status, AssetType: getOrdersRequest.AssetType, HiddenOrder: response.Items[a].Hidden, @@ -2458,3 +2455,23 @@ func (ku *Kucoin) GetCurrencyTradeURL(_ context.Context, a asset.Item, cp curren return "", fmt.Errorf("%w %v", asset.ErrNotSupported, a) } } + +// StringToTimeInForce returns an order.TimeInForce instance from string +func StringToTimeInForce(tif string, postOnly bool) order.TimeInForce { + tif = strings.ToUpper(tif) + var out order.TimeInForce + switch tif { + case "GTT": + out = order.GoodTillTime + case "IOC": + out = order.ImmediateOrCancel + case "FOK": + out = order.FillOrKill + default: + out = order.GoodTillCancel + } + if postOnly { + out |= order.PostOnly + } + return out +} diff --git a/exchanges/okx/helpers.go b/exchanges/okx/helpers.go index 18c79a19..27f6d105 100644 --- a/exchanges/okx/helpers.go +++ b/exchanges/okx/helpers.go @@ -9,46 +9,57 @@ import ( "github.com/thrasher-corp/gocryptotrader/exchanges/order" ) -// orderTypeFromString returns order.Type instance from string -func orderTypeFromString(orderType string) (order.Type, error) { +// orderTypeFromString returns the order Type and TimeInForce for okx order type strings +func orderTypeFromString(orderType string) (order.Type, order.TimeInForce, error) { orderType = strings.ToLower(orderType) switch orderType { case orderMarket: - return order.Market, nil + return order.Market, order.UnknownTIF, nil case orderLimit: - return order.Limit, nil + return order.Limit, order.UnknownTIF, nil case orderPostOnly: - return order.PostOnly, nil + return order.Limit, order.PostOnly, nil case orderFOK: - return order.FillOrKill, nil + return order.Limit, order.FillOrKill, nil case orderIOC: - return order.ImmediateOrCancel, nil + return order.Limit, order.ImmediateOrCancel, nil case orderOptimalLimitIOC: - return order.OptimalLimitIOC, nil + return order.OptimalLimitIOC, order.ImmediateOrCancel, nil case "mmp": - return order.MarketMakerProtection, nil + return order.MarketMakerProtection, order.UnknownTIF, nil case "mmp_and_post_only": - return order.MarketMakerProtectionAndPostOnly, nil + return order.MarketMakerProtectionAndPostOnly, order.PostOnly, nil case "twap": - return order.TWAP, nil + return order.TWAP, order.UnknownTIF, nil case "move_order_stop": - return order.TrailingStop, nil + return order.TrailingStop, order.UnknownTIF, nil case "chase": - return order.Chase, nil + return order.Chase, order.UnknownTIF, nil default: - return order.UnknownType, fmt.Errorf("%w %v", order.ErrTypeIsInvalid, orderType) + return order.UnknownType, order.UnknownTIF, fmt.Errorf("%w %v", order.ErrTypeIsInvalid, orderType) } } // orderTypeString returns a string representation of order.Type instance -func orderTypeString(orderType order.Type) (string, error) { - switch orderType { +func orderTypeString(orderType order.Type, tif order.TimeInForce) (string, error) { + switch tif { + case order.PostOnly: + return orderPostOnly, nil + case order.FillOrKill: + return orderFOK, nil case order.ImmediateOrCancel: - return "ioc", nil - case order.Market, order.Limit, order.Trigger, - order.PostOnly, order.FillOrKill, order.OptimalLimitIOC, - order.MarketMakerProtection, order.MarketMakerProtectionAndPostOnly, - order.Chase, order.TWAP, order.OCO: + return orderIOC, nil + } + switch orderType { + case order.Market, + order.Limit, + order.Trigger, + order.OptimalLimitIOC, + order.MarketMakerProtection, + order.MarketMakerProtectionAndPostOnly, + order.Chase, + order.TWAP, + order.OCO: return orderType.Lower(), nil case order.ConditionalStop: return "conditional", nil diff --git a/exchanges/okx/okx_test.go b/exchanges/okx/okx_test.go index b11662e5..fd1d1ce5 100644 --- a/exchanges/okx/okx_test.go +++ b/exchanges/okx/okx_test.go @@ -5823,28 +5823,32 @@ func TestGetAccountInstruments(t *testing.T) { func TestOrderTypeString(t *testing.T) { t.Parallel() - orderTypesToStringMap := map[order.Type]struct { + type OrderTypeWithTIF struct { + OrderType order.Type + TIF order.TimeInForce + } + orderTypesToStringMap := map[OrderTypeWithTIF]struct { Expected string Error error }{ - order.Market: {Expected: orderMarket}, - order.Limit: {Expected: orderLimit}, - order.PostOnly: {Expected: orderPostOnly}, - order.FillOrKill: {Expected: orderFOK}, - order.ImmediateOrCancel: {Expected: orderIOC}, - order.OptimalLimitIOC: {Expected: orderOptimalLimitIOC}, - order.MarketMakerProtection: {Expected: "mmp"}, - order.MarketMakerProtectionAndPostOnly: {Expected: "mmp_and_post_only"}, - order.Liquidation: {Error: order.ErrUnsupportedOrderType}, - order.OCO: {Expected: "oco"}, - order.TrailingStop: {Expected: "move_order_stop"}, - order.Chase: {Expected: "chase"}, - order.TWAP: {Expected: "twap"}, - order.ConditionalStop: {Expected: "conditional"}, - order.Trigger: {Expected: "trigger"}, + {OrderType: order.Market, TIF: order.UnknownTIF}: {Expected: orderMarket}, + {OrderType: order.Limit, TIF: order.UnknownTIF}: {Expected: orderLimit}, + {OrderType: order.Limit, TIF: order.PostOnly}: {Expected: orderPostOnly}, + {OrderType: order.Limit, TIF: order.FillOrKill}: {Expected: orderFOK}, + {OrderType: order.Limit, TIF: order.ImmediateOrCancel}: {Expected: orderIOC}, + {OrderType: order.OptimalLimitIOC, TIF: order.UnknownTIF}: {Expected: orderOptimalLimitIOC}, + {OrderType: order.MarketMakerProtection, TIF: order.UnknownTIF}: {Expected: "mmp"}, + {OrderType: order.MarketMakerProtectionAndPostOnly, TIF: order.UnknownTIF}: {Expected: "mmp_and_post_only"}, + {OrderType: order.Liquidation, TIF: order.UnknownTIF}: {Error: order.ErrUnsupportedOrderType}, + {OrderType: order.OCO, TIF: order.UnknownTIF}: {Expected: "oco"}, + {OrderType: order.TrailingStop, TIF: order.UnknownTIF}: {Expected: "move_order_stop"}, + {OrderType: order.Chase, TIF: order.UnknownTIF}: {Expected: "chase"}, + {OrderType: order.TWAP, TIF: order.UnknownTIF}: {Expected: "twap"}, + {OrderType: order.ConditionalStop, TIF: order.UnknownTIF}: {Expected: "conditional"}, + {OrderType: order.Trigger, TIF: order.UnknownTIF}: {Expected: "trigger"}, } - for oType, val := range orderTypesToStringMap { - orderTypeString, err := orderTypeString(oType) + for tc, val := range orderTypesToStringMap { + orderTypeString, err := orderTypeString(tc.OrderType, tc.TIF) require.ErrorIs(t, err, val.Error) assert.Equal(t, val.Expected, orderTypeString) } @@ -5895,7 +5899,7 @@ func TestGetAnnouncements(t *testing.T) { func TestGetAnnouncementTypes(t *testing.T) { t.Parallel() _, err := ok.GetAnnouncementTypes(contextGenerate()) - require.NoError(t, err) + assert.NoError(t, err) // No tests of contents of resp because currently in US based github actions announcement-types returns empty } @@ -6111,29 +6115,32 @@ func TestWsProcessSpreadTradesJSON(t *testing.T) { func TestOrderTypeFromString(t *testing.T) { t.Parallel() + orderTypeStrings := map[string]struct { OType order.Type + TIF order.TimeInForce Error error }{ "market": {OType: order.Market}, "LIMIT": {OType: order.Limit}, "limit": {OType: order.Limit}, - "post_only": {OType: order.PostOnly}, - "fok": {OType: order.FillOrKill}, - "ioc": {OType: order.ImmediateOrCancel}, - "optimal_limit_ioc": {OType: order.OptimalLimitIOC}, + "post_only": {OType: order.Limit, TIF: order.PostOnly}, + "fok": {OType: order.Limit, TIF: order.FillOrKill}, + "ioc": {OType: order.Limit, TIF: order.ImmediateOrCancel}, + "optimal_limit_ioc": {OType: order.OptimalLimitIOC, TIF: order.ImmediateOrCancel}, "mmp": {OType: order.MarketMakerProtection}, - "mmp_and_post_only": {OType: order.MarketMakerProtectionAndPostOnly}, + "mmp_and_post_only": {OType: order.MarketMakerProtectionAndPostOnly, TIF: order.PostOnly}, "trigger": {OType: order.UnknownType, Error: order.ErrTypeIsInvalid}, "chase": {OType: order.Chase}, "move_order_stop": {OType: order.TrailingStop}, "twap": {OType: order.TWAP}, "abcd": {OType: order.UnknownType, Error: order.ErrTypeIsInvalid}, } - for a := range orderTypeStrings { - oType, err := orderTypeFromString(a) - assert.ErrorIs(t, err, orderTypeStrings[a].Error) - assert.Equal(t, oType, orderTypeStrings[a].OType) + for s, exp := range orderTypeStrings { + oType, tif, err := orderTypeFromString(s) + require.ErrorIs(t, err, exp.Error) + assert.Equal(t, exp.OType, oType) + assert.Equal(t, exp.TIF.String(), tif.String(), s) } } diff --git a/exchanges/okx/okx_wrapper.go b/exchanges/okx/okx_wrapper.go index e06640d5..b1502d4a 100644 --- a/exchanges/okx/okx_wrapper.go +++ b/exchanges/okx/okx_wrapper.go @@ -930,7 +930,7 @@ func (ok *Okx) SubmitOrder(ctx context.Context, s *order.Submit) (*order.SubmitR } return s.DeriveSubmitResponse(placeSpreadOrderResponse.OrderID) } - orderTypeString, err := orderTypeString(s.Type) + orderTypeString, err := orderTypeString(s.Type, s.TimeInForce) if err != nil { return nil, err } @@ -1160,8 +1160,7 @@ func (ok *Okx) ModifyOrder(ctx context.Context, action *order.Modify) (*order.Mo return nil, currency.ErrCurrencyPairEmpty } switch action.Type { - case order.UnknownType, order.Market, order.Limit, order.PostOnly, order.FillOrKill, order.ImmediateOrCancel, - order.OptimalLimitIOC, order.MarketMakerProtection, order.MarketMakerProtectionAndPostOnly: + case order.UnknownType, order.Market, order.Limit, order.OptimalLimitIOC, order.MarketMakerProtection, order.MarketMakerProtectionAndPostOnly: amendRequest := AmendOrderRequestParams{ InstrumentID: pairFormat.Format(action.Pair), NewQuantity: action.Amount, @@ -1265,8 +1264,7 @@ func (ok *Okx) CancelOrder(ctx context.Context, ord *order.Cancel) error { } instrumentID := pairFormat.Format(ord.Pair) switch ord.Type { - case order.UnknownType, order.Market, order.Limit, order.PostOnly, order.FillOrKill, order.ImmediateOrCancel, - order.OptimalLimitIOC, order.MarketMakerProtection, order.MarketMakerProtectionAndPostOnly: + case order.UnknownType, order.Market, order.Limit, order.OptimalLimitIOC, order.MarketMakerProtection, order.MarketMakerProtectionAndPostOnly: req := CancelOrderRequestParam{ InstrumentID: instrumentID, OrderID: ord.OrderID, @@ -1319,8 +1317,7 @@ func (ok *Okx) CancelBatchOrders(ctx context.Context, o []order.Cancel) (*order. return nil, currency.ErrCurrencyPairsEmpty } switch ord.Type { - case order.UnknownType, order.Market, order.Limit, order.PostOnly, order.FillOrKill, order.ImmediateOrCancel, - order.OptimalLimitIOC, order.MarketMakerProtection, order.MarketMakerProtectionAndPostOnly: + case order.UnknownType, order.Market, order.Limit, order.OptimalLimitIOC, order.MarketMakerProtection, order.MarketMakerProtectionAndPostOnly: if o[x].ClientID == "" && o[x].OrderID == "" { return nil, fmt.Errorf("%w, order ID required for order of type %v", order.ErrOrderIDNotSet, o[x].Type) } @@ -1413,7 +1410,7 @@ func (ok *Okx) CancelAllOrders(ctx context.Context, orderCancellation *order.Can } var oType string if orderCancellation.Type != order.UnknownType && orderCancellation.Type != order.AnyType { - oType, err = orderTypeString(orderCancellation.Type) + oType, err = orderTypeString(orderCancellation.Type, orderCancellation.TimeInForce) if err != nil { return order.CancelAllResponse{}, err } @@ -1566,7 +1563,7 @@ func (ok *Okx) GetOrderInfo(ctx context.Context, orderID string, pair currency.P if err != nil { return nil, err } - orderType, err := orderTypeFromString(orderDetail.OrderType) + orderType, tif, err := orderTypeFromString(orderDetail.OrderType) if err != nil { return nil, err } @@ -1586,6 +1583,7 @@ func (ok *Okx) GetOrderInfo(ctx context.Context, orderID string, pair currency.P ExecutedAmount: orderDetail.RebateAmount.Float64(), Date: orderDetail.CreationTime.Time(), LastUpdated: orderDetail.UpdateTime.Time(), + TimeInForce: tif, }, nil } @@ -1731,7 +1729,7 @@ func (ok *Okx) GetActiveOrders(ctx context.Context, req *order.MultiOrderRequest instrumentType := GetInstrumentTypeFromAssetItem(req.AssetType) var orderType string if req.Type != order.UnknownType && req.Type != order.AnyType { - orderType, err = orderTypeString(req.Type) + orderType, err = orderTypeString(req.Type, req.TimeInForce) if err != nil { return nil, err } @@ -1775,13 +1773,11 @@ allOrders: continue } } - var orderStatus order.Status - orderStatus, err = order.StringToOrderStatus(strings.ToUpper(orderList[i].State)) + orderStatus, err := order.StringToOrderStatus(strings.ToUpper(orderList[i].State)) if err != nil { return nil, err } - var oType order.Type - oType, err = orderTypeFromString(orderList[i].OrderType) + oType, tif, err := orderTypeFromString(orderList[i].OrderType) if err != nil { return nil, err } @@ -1802,6 +1798,7 @@ allOrders: AssetType: req.AssetType, Date: orderList[i].CreationTime.Time(), LastUpdated: orderList[i].UpdateTime.Time(), + TimeInForce: tif, }) } if len(orderList) < 100 { @@ -1829,7 +1826,7 @@ func (ok *Okx) GetOrderHistory(ctx context.Context, req *order.MultiOrderRequest var resp []order.Detail // For Spread orders. if req.AssetType == asset.Spread { - oType, err := orderTypeString(req.Type) + oType, err := orderTypeString(req.Type, req.TimeInForce) if err != nil { return nil, err } @@ -1915,17 +1912,14 @@ allOrders: if !req.Pairs[j].Equal(pair) { continue } - var orderStatus order.Status - orderStatus, err = order.StringToOrderStatus(strings.ToUpper(orderList[i].State)) + orderStatus, err := order.StringToOrderStatus(strings.ToUpper(orderList[i].State)) if err != nil { log.Errorf(log.ExchangeSys, "%s %v", ok.Name, err) } if orderStatus == order.Active { continue } - orderSide := orderList[i].Side - var oType order.Type - oType, err = orderTypeFromString(orderList[i].OrderType) + oType, tif, err := orderTypeFromString(orderList[i].OrderType) if err != nil { return nil, err } @@ -1951,7 +1945,7 @@ allOrders: OrderID: orderList[i].OrderID, ClientOrderID: orderList[i].ClientOrderID, Type: oType, - Side: orderSide, + Side: orderList[i].Side, Status: orderStatus, AssetType: req.AssetType, Date: orderList[i].CreationTime.Time(), @@ -1959,6 +1953,7 @@ allOrders: Pair: pair, Cost: orderList[i].AveragePrice.Float64() * orderList[i].AccumulatedFillSize.Float64(), CostAsset: currency.NewCode(orderList[i].RebateCurrency), + TimeInForce: tif, }) } } @@ -2663,14 +2658,11 @@ func (ok *Okx) GetFuturesPositionOrders(ctx context.Context, req *futures.Positi if fPair.String() != positions[j].InstrumentID { continue } - var orderStatus order.Status - orderStatus, err = order.StringToOrderStatus(strings.ToUpper(positions[j].State)) + orderStatus, err := order.StringToOrderStatus(strings.ToUpper(positions[j].State)) if err != nil { log.Errorf(log.ExchangeSys, "%s %v", ok.Name, err) } - orderSide := positions[j].Side - var oType order.Type - oType, err = orderTypeFromString(positions[j].OrderType) + oType, tif, err := orderTypeFromString(positions[j].OrderType) if err != nil { return nil, err } @@ -2701,7 +2693,7 @@ func (ok *Okx) GetFuturesPositionOrders(ctx context.Context, req *futures.Positi OrderID: positions[j].OrderID, ClientOrderID: positions[j].ClientOrderID, Type: oType, - Side: orderSide, + Side: positions[j].Side, Status: orderStatus, AssetType: req.Asset, Date: positions[j].CreationTime.Time(), @@ -2709,6 +2701,7 @@ func (ok *Okx) GetFuturesPositionOrders(ctx context.Context, req *futures.Positi Pair: req.Pairs[i], Cost: cost, CostAsset: currency.NewCode(positions[j].RebateCurrency), + TimeInForce: tif, }) } } diff --git a/exchanges/order/limits_test.go b/exchanges/order/limits_test.go index 4dd24e02..f2f0bef8 100644 --- a/exchanges/order/limits_test.go +++ b/exchanges/order/limits_test.go @@ -1,10 +1,11 @@ package order import ( - "errors" "testing" "github.com/shopspring/decimal" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" "github.com/thrasher-corp/gocryptotrader/currency" "github.com/thrasher-corp/gocryptotrader/exchanges/asset" ) @@ -19,9 +20,7 @@ func TestLoadLimits(t *testing.T) { t.Parallel() e := ExecutionLimits{} err := e.LoadLimits(nil) - if !errors.Is(err, errCannotLoadLimit) { - t.Fatalf("expected error %v but received %v", errCannotLoadLimit, err) - } + assert.ErrorIs(t, err, errCannotLoadLimit) invalidAsset := []MinMaxLevel{ { @@ -33,11 +32,7 @@ func TestLoadLimits(t *testing.T) { }, } err = e.LoadLimits(invalidAsset) - if !errors.Is(err, asset.ErrNotSupported) { - t.Fatalf("expected error %v but received %v", - asset.ErrNotSupported, - err) - } + require.ErrorIs(t, err, asset.ErrNotSupported) invalidPairLoading := []MinMaxLevel{ { @@ -50,9 +45,7 @@ func TestLoadLimits(t *testing.T) { } err = e.LoadLimits(invalidPairLoading) - if !errors.Is(err, currency.ErrCurrencyPairEmpty) { - t.Fatalf("expected error %v but received %v", currency.ErrCurrencyPairEmpty, err) - } + assert.ErrorIs(t, err, currency.ErrCurrencyPairEmpty) newLimits := []MinMaxLevel{ { @@ -66,9 +59,7 @@ func TestLoadLimits(t *testing.T) { } err = e.LoadLimits(newLimits) - if !errors.Is(err, nil) { - t.Fatalf("expected error %v but received %v", nil, err) - } + require.NoError(t, err) badLimit := []MinMaxLevel{ { @@ -82,9 +73,7 @@ func TestLoadLimits(t *testing.T) { } err = e.LoadLimits(badLimit) - if !errors.Is(err, errInvalidPriceLevels) { - t.Fatalf("expected error %v but received %v", errInvalidPriceLevels, err) - } + require.ErrorIs(t, err, errInvalidPriceLevels) badLimit = []MinMaxLevel{ { @@ -98,9 +87,7 @@ func TestLoadLimits(t *testing.T) { } err = e.LoadLimits(badLimit) - if !errors.Is(err, errInvalidAmountLevels) { - t.Fatalf("expected error %v but received %v", errInvalidPriceLevels, err) - } + require.ErrorIs(t, err, errInvalidAmountLevels) goodLimit := []MinMaxLevel{ { @@ -110,9 +97,7 @@ func TestLoadLimits(t *testing.T) { } err = e.LoadLimits(goodLimit) - if !errors.Is(err, nil) { - t.Fatalf("expected error %v but received %v", nil, err) - } + require.NoError(t, err) noCompare := []MinMaxLevel{ { @@ -123,9 +108,7 @@ func TestLoadLimits(t *testing.T) { } err = e.LoadLimits(noCompare) - if !errors.Is(err, nil) { - t.Fatalf("expected error %v but received %v", nil, err) - } + require.NoError(t, err) noCompare = []MinMaxLevel{ { @@ -136,18 +119,14 @@ func TestLoadLimits(t *testing.T) { } err = e.LoadLimits(noCompare) - if !errors.Is(err, nil) { - t.Fatalf("expected error %v but received %v", nil, err) - } + assert.NoError(t, err) } func TestGetOrderExecutionLimits(t *testing.T) { t.Parallel() e := ExecutionLimits{} _, err := e.GetOrderExecutionLimits(asset.Spot, btcusd) - if !errors.Is(err, ErrExchangeLimitNotLoaded) { - t.Fatalf("expected error %v but received %v", ErrExchangeLimitNotLoaded, err) - } + require.ErrorIs(t, err, ErrExchangeLimitNotLoaded) newLimits := []MinMaxLevel{ { @@ -161,45 +140,30 @@ func TestGetOrderExecutionLimits(t *testing.T) { } err = e.LoadLimits(newLimits) - if !errors.Is(err, nil) { - t.Fatalf("expected error %v but received %v", errCannotLoadLimit, err) - } + require.NoError(t, err) _, err = e.GetOrderExecutionLimits(asset.Futures, ltcusd) - if !errors.Is(err, ErrCannotValidateAsset) { - t.Fatalf("expected error %v but received %v", ErrCannotValidateAsset, err) - } + require.ErrorIs(t, err, ErrCannotValidateAsset) _, err = e.GetOrderExecutionLimits(asset.Spot, ltcusd) - if !errors.Is(err, errExchangeLimitBase) { - t.Fatalf("expected error %v but received %v", errExchangeLimitBase, err) - } + require.ErrorIs(t, err, errExchangeLimitBase) _, err = e.GetOrderExecutionLimits(asset.Spot, btcltc) - if !errors.Is(err, errExchangeLimitQuote) { - t.Fatalf("expected error %v but received %v", errExchangeLimitQuote, err) - } + require.ErrorIs(t, err, errExchangeLimitQuote) tt, err := e.GetOrderExecutionLimits(asset.Spot, btcusd) - if !errors.Is(err, nil) { - t.Fatalf("expected error %v but received %v", nil, err) - } - - if tt.MaximumBaseAmount != newLimits[0].MaximumBaseAmount || - tt.MinimumBaseAmount != newLimits[0].MinimumBaseAmount || - tt.MaxPrice != newLimits[0].MaxPrice || - tt.MinPrice != newLimits[0].MinPrice { - t.Fatal("unexpected values") - } + require.NoError(t, err) + assert.Equal(t, newLimits[0].MaximumBaseAmount, tt.MaximumBaseAmount) + assert.Equal(t, newLimits[0].MinimumBaseAmount, tt.MinimumBaseAmount) + assert.Equal(t, newLimits[0].MaxPrice, tt.MaxPrice) + assert.Equal(t, newLimits[0].MinPrice, tt.MinPrice) } func TestCheckLimit(t *testing.T) { t.Parallel() e := ExecutionLimits{} err := e.CheckOrderExecutionLimits(asset.Spot, btcusd, 1337, 1337, Limit) - if !errors.Is(err, nil) { - t.Fatalf("expected error %v but received %v", nil, err) - } + require.NoError(t, err) newLimits := []MinMaxLevel{ { @@ -213,97 +177,63 @@ func TestCheckLimit(t *testing.T) { } err = e.LoadLimits(newLimits) - if !errors.Is(err, nil) { - t.Fatalf("expected error %v but received %v", errCannotLoadLimit, err) - } + require.NoError(t, err) err = e.CheckOrderExecutionLimits(asset.Futures, ltcusd, 1337, 1337, Limit) - if !errors.Is(err, ErrCannotValidateAsset) { - t.Fatalf("expected error %v but received %v", ErrCannotValidateAsset, err) - } + require.ErrorIs(t, err, ErrCannotValidateAsset) err = e.CheckOrderExecutionLimits(asset.Spot, ltcusd, 1337, 1337, Limit) - if !errors.Is(err, ErrCannotValidateBaseCurrency) { - t.Fatalf("expected error %v but received %v", ErrCannotValidateBaseCurrency, err) - } + require.ErrorIs(t, err, ErrCannotValidateBaseCurrency) err = e.CheckOrderExecutionLimits(asset.Spot, btcltc, 1337, 1337, Limit) - if !errors.Is(err, ErrCannotValidateQuoteCurrency) { - t.Fatalf("expected error %v but received %v", ErrCannotValidateQuoteCurrency, err) - } + require.ErrorIs(t, err, ErrCannotValidateQuoteCurrency) err = e.CheckOrderExecutionLimits(asset.Spot, btcusd, 1337, 9, Limit) - if !errors.Is(err, ErrPriceBelowMin) { - t.Fatalf("expected error %v but received %v", ErrPriceBelowMin, err) - } + require.ErrorIs(t, err, ErrPriceBelowMin) err = e.CheckOrderExecutionLimits(asset.Spot, btcusd, 1000001, 9, Limit) - if !errors.Is(err, ErrPriceExceedsMax) { - t.Fatalf("expected error %v but received %v", ErrPriceExceedsMax, err) - } + require.ErrorIs(t, err, ErrPriceExceedsMax) err = e.CheckOrderExecutionLimits(asset.Spot, btcusd, 999999, .5, Limit) - if !errors.Is(err, ErrAmountBelowMin) { - t.Fatalf("expected error %v but received %v", ErrAmountBelowMin, err) - } + require.ErrorIs(t, err, ErrAmountBelowMin) err = e.CheckOrderExecutionLimits(asset.Spot, btcusd, 999999, 11, Limit) - if !errors.Is(err, ErrAmountExceedsMax) { - t.Fatalf("expected error %v but received %v", ErrAmountExceedsMax, err) - } + require.ErrorIs(t, err, ErrAmountExceedsMax) err = e.CheckOrderExecutionLimits(asset.Spot, btcusd, 999999, 7, Limit) - if !errors.Is(err, nil) { - t.Fatalf("expected error %v but received %v", nil, err) - } + require.NoError(t, err) err = e.CheckOrderExecutionLimits(asset.Spot, btcusd, 999999, 7, Market) - if !errors.Is(err, nil) { - t.Fatalf("expected error %v but received %v", nil, err) - } + assert.NoError(t, err) } func TestConforms(t *testing.T) { t.Parallel() var tt MinMaxLevel err := tt.Conforms(0, 0, Limit) - if err != nil { - t.Fatal(err) - } + require.NoError(t, err) tt = MinMaxLevel{ MinNotional: 100, } err = tt.Conforms(1, 1, Limit) - if !errors.Is(err, ErrNotionalValue) { - t.Fatalf("expected error %v but received %v", ErrNotionalValue, err) - } + require.ErrorIs(t, err, ErrNotionalValue) err = tt.Conforms(200, .5, Limit) - if !errors.Is(err, nil) { - t.Fatalf("expected error %v but received %v", nil, err) - } + require.NoError(t, err) tt.PriceStepIncrementSize = 0.001 err = tt.Conforms(200.0001, .5, Limit) - if !errors.Is(err, ErrPriceExceedsStep) { - t.Fatalf("expected error %v but received %v", ErrPriceExceedsStep, err) - } + require.ErrorIs(t, err, ErrPriceExceedsStep) err = tt.Conforms(200.004, .5, Limit) - if !errors.Is(err, nil) { - t.Fatalf("expected error %v but received %v", nil, err) - } + require.NoError(t, err) tt.AmountStepIncrementSize = 0.001 err = tt.Conforms(200, .0002, Limit) - if !errors.Is(err, ErrAmountExceedsStep) { - t.Fatalf("expected error %v but received %v", ErrAmountExceedsStep, err) - } + require.ErrorIs(t, err, ErrAmountExceedsStep) err = tt.Conforms(200000, .003, Limit) - if !errors.Is(err, nil) { - t.Fatalf("expected error %v but received %v", nil, err) - } + require.NoError(t, err) tt.MinimumBaseAmount = 1 tt.MaximumBaseAmount = 10 @@ -311,115 +241,73 @@ func TestConforms(t *testing.T) { tt.MarketMaxQty = 9.9 err = tt.Conforms(200000, 1, Market) - if !errors.Is(err, ErrMarketAmountBelowMin) { - t.Fatalf("expected error %v but received: %v", ErrMarketAmountBelowMin, err) - } + require.ErrorIs(t, err, ErrMarketAmountBelowMin) err = tt.Conforms(200000, 10, Market) - if !errors.Is(err, ErrMarketAmountExceedsMax) { - t.Fatalf("expected error %v but received: %v", ErrMarketAmountExceedsMax, err) - } + require.ErrorIs(t, err, ErrMarketAmountExceedsMax) tt.MarketStepIncrementSize = 10 err = tt.Conforms(200000, 9.1, Market) - if !errors.Is(err, ErrMarketAmountExceedsStep) { - t.Fatalf("expected error %v but received: %v", ErrMarketAmountExceedsStep, err) - } + require.ErrorIs(t, err, ErrMarketAmountExceedsStep) tt.MarketStepIncrementSize = 1 err = tt.Conforms(200000, 9.1, Market) - if !errors.Is(err, nil) { - t.Fatalf("expected error %v but received: %v", nil, err) - } + assert.NoError(t, err) } func TestConformToDecimalAmount(t *testing.T) { t.Parallel() var tt MinMaxLevel - if !tt.ConformToDecimalAmount(decimal.NewFromFloat(1.001)).Equal(decimal.NewFromFloat(1.001)) { - t.Fatal("value should not be changed") - } + require.True(t, tt.ConformToDecimalAmount(decimal.NewFromFloat(1.001)).Equal(decimal.NewFromFloat(1.001))) tt = MinMaxLevel{} val := tt.ConformToDecimalAmount(decimal.NewFromInt(1)) - if !val.Equal(decimal.NewFromInt(1)) { // If there is no step amount set this should not change - // the inputted amount - t.Fatal("unexpected amount") - } + assert.True(t, val.Equal(decimal.NewFromInt(1))) // If there is no step amount set, this should not change the inputted amount tt.AmountStepIncrementSize = 0.001 val = tt.ConformToDecimalAmount(decimal.NewFromFloat(1.001)) - if !val.Equal(decimal.NewFromFloat(1.001)) { - t.Error("unexpected amount", val) - } + assert.True(t, val.Equal(decimal.NewFromFloat(1.001))) val = tt.ConformToDecimalAmount(decimal.NewFromFloat(0.0001)) - if !val.IsZero() { - t.Error("unexpected amount", val) - } + assert.True(t, val.IsZero()) val = tt.ConformToDecimalAmount(decimal.NewFromFloat(0.7777)) - if !val.Equal(decimal.NewFromFloat(0.777)) { - t.Error("unexpected amount", val) - } + assert.True(t, val.Equal(decimal.NewFromFloat(0.777))) tt.AmountStepIncrementSize = 100 val = tt.ConformToDecimalAmount(decimal.NewFromInt(100)) - if !val.Equal(decimal.NewFromInt(100)) { - t.Fatal("unexpected amount", val) - } + assert.True(t, val.Equal(decimal.NewFromInt(100))) val = tt.ConformToDecimalAmount(decimal.NewFromInt(200)) - if !val.Equal(decimal.NewFromInt(200)) { - t.Fatal("unexpected amount", val) - } + assert.True(t, val.Equal(decimal.NewFromInt(200))) val = tt.ConformToDecimalAmount(decimal.NewFromInt(150)) - if !val.Equal(decimal.NewFromInt(100)) { - t.Fatal("unexpected amount", val) - } + assert.True(t, val.Equal(decimal.NewFromInt(100))) } func TestConformToAmount(t *testing.T) { t.Parallel() var tt MinMaxLevel - if tt.ConformToAmount(1.001) != 1.001 { - t.Fatal("value should not be changed") - } + require.Equal(t, 1.001, tt.ConformToAmount(1.001)) tt = MinMaxLevel{} val := tt.ConformToAmount(1) - if val != 1 { // If there is no step amount set this should not change - // the inputted amount - t.Fatal("unexpected amount") - } + assert.Equal(t, 1.0, val, "ConformToAmount should return the same value with no step amount set") tt.AmountStepIncrementSize = 0.001 val = tt.ConformToAmount(1.001) - if val != 1.001 { - t.Error("unexpected amount", val) - } + assert.Equal(t, 1.001, val) val = tt.ConformToAmount(0.0001) - if val != 0 { - t.Error("unexpected amount", val) - } + assert.Zero(t, val) val = tt.ConformToAmount(0.7777) - if val != 0.777 { - t.Error("unexpected amount", val) - } + assert.Equal(t, 0.777, val) tt.AmountStepIncrementSize = 100 val = tt.ConformToAmount(100) - if val != 100 { - t.Fatal("unexpected amount", val) - } + assert.Equal(t, 100.0, val) val = tt.ConformToAmount(200) - if val != 200 { - t.Fatal("unexpected amount", val) - } + require.Equal(t, 200.0, val) val = tt.ConformToAmount(150) - if val != 100 { - t.Fatal("unexpected amount", val) - } + assert.Equal(t, 100.0, val) } diff --git a/exchanges/order/order_test.go b/exchanges/order/order_test.go index 4a69a0e6..f4f33608 100644 --- a/exchanges/order/order_test.go +++ b/exchanges/order/order_test.go @@ -5,7 +5,6 @@ import ( "fmt" "reflect" "strconv" - "strings" "testing" "time" @@ -67,15 +66,14 @@ func TestSubmitValidate(t *testing.T) { }, }, // valid pair but invalid order side { - ExpectedErr: errTimeInForceConflict, + ExpectedErr: ErrInvalidTimeInForce, Submit: &Submit{ - Exchange: "test", - Pair: testPair, - AssetType: asset.Spot, - Side: Ask, - Type: Market, - ImmediateOrCancel: true, - FillOrKill: true, + Exchange: "test", + Pair: testPair, + AssetType: asset.Spot, + Side: Ask, + Type: Market, + TimeInForce: TimeInForce(89), }, }, { @@ -256,195 +254,104 @@ func TestSubmit_DeriveSubmitResponse(t *testing.T) { t.Parallel() var s *Submit _, err := s.DeriveSubmitResponse("") - if !errors.Is(err, errOrderSubmitIsNil) { - t.Fatalf("received: '%v' but expected: '%v'", err, errOrderSubmitIsNil) - } + require.ErrorIs(t, err, errOrderSubmitIsNil) s = &Submit{} _, err = s.DeriveSubmitResponse("") - if !errors.Is(err, ErrOrderIDNotSet) { - t.Fatalf("received: '%v' but expected: '%v'", err, ErrOrderIDNotSet) - } + require.ErrorIs(t, err, ErrOrderIDNotSet) resp, err := s.DeriveSubmitResponse("1337") - if !errors.Is(err, nil) { - t.Fatalf("received: '%v' but expected: '%v'", err, nil) - } - - if resp.OrderID != "1337" { - t.Fatal("unexpected value") - } - - if resp.Status != New { - t.Fatal("unexpected value") - } - - if resp.Date.IsZero() { - t.Fatal("unexpected value") - } - - if resp.LastUpdated.IsZero() { - t.Fatal("unexpected value") - } + require.NoError(t, err) + require.Equal(t, "1337", resp.OrderID) + require.Equal(t, New, resp.Status) + require.False(t, resp.Date.IsZero()) + assert.False(t, resp.LastUpdated.IsZero()) } func TestSubmitResponse_DeriveDetail(t *testing.T) { t.Parallel() var s *SubmitResponse _, err := s.DeriveDetail(uuid.Nil) - if !errors.Is(err, errOrderSubmitResponseIsNil) { - t.Fatalf("received: '%v' but expected: '%v'", err, errOrderSubmitResponseIsNil) - } + require.ErrorIs(t, err, errOrderSubmitResponseIsNil) id, err := uuid.NewV4() - if err != nil { - t.Fatal(err) - } + require.NoError(t, err) s = &SubmitResponse{} deets, err := s.DeriveDetail(id) - if !errors.Is(err, nil) { - t.Fatalf("received: '%v' but expected: '%v'", err, nil) - } - - if deets.InternalOrderID != id { - t.Fatal("unexpected value") - } + require.NoError(t, err) + assert.Equal(t, id, deets.InternalOrderID) } func TestOrderSides(t *testing.T) { t.Parallel() - os := Buy - if os.String() != "BUY" { - t.Errorf("unexpected string %s", os.String()) - } - - if os.Lower() != "buy" { - t.Errorf("unexpected string %s", os.Lower()) - } - - if os.Title() != "Buy" { - t.Errorf("unexpected string %s", os.Title()) - } + assert.Equal(t, "BUY", os.String()) + assert.Equal(t, "buy", os.Lower()) + assert.Equal(t, "Buy", os.Title()) } func TestTitle(t *testing.T) { t.Parallel() orderType := Limit - if orderType.Title() != "Limit" { - t.Errorf("received '%v' expected 'Limit'", orderType.Title()) - } + require.Equal(t, "Limit", orderType.Title()) } func TestOrderTypes(t *testing.T) { t.Parallel() - var orderType Type - if orderType.String() != "UNKNOWN" { - t.Errorf("unexpected string %s", orderType.String()) - } - - if orderType.Lower() != "unknown" { - t.Errorf("unexpected string %s", orderType.Lower()) - } - - if orderType.Title() != "Unknown" { - t.Errorf("unexpected string %s", orderType.Title()) - } + assert.Equal(t, "UNKNOWN", orderType.String()) + assert.Equal(t, "unknown", orderType.Lower()) + assert.Equal(t, "Unknown", orderType.Title()) } func TestInferCostsAndTimes(t *testing.T) { t.Parallel() - var detail Detail detail.InferCostsAndTimes() - if detail.Amount != detail.ExecutedAmount+detail.RemainingAmount { - t.Errorf( - "Order detail amounts not equals. Expected 0, received %f", - detail.Amount-(detail.ExecutedAmount+detail.RemainingAmount), - ) - } + assert.Zero(t, detail.Amount, "InferCostsAndTimes on empty details should set correct Amount") detail.CloseTime = time.Now() detail.InferCostsAndTimes() - if detail.LastUpdated != detail.CloseTime { - t.Errorf( - "Order last updated not equals close time. Expected %s, received %s", - detail.CloseTime, - detail.LastUpdated, - ) - } + assert.Equal(t, detail.CloseTime, detail.LastUpdated, "Order last updated not equals close time") detail.Amount = 1 detail.ExecutedAmount = 1 detail.InferCostsAndTimes() - if detail.AverageExecutedPrice != 0 { - t.Errorf( - "Unexpected AverageExecutedPrice. Expected 0, received %f", - detail.AverageExecutedPrice, - ) - } + assert.Zero(t, detail.AverageExecutedPrice, "InferCostsAndTimes should set AverageExecutedPrice correctly") detail.Amount = 1 detail.ExecutedAmount = 1 detail.InferCostsAndTimes() - if detail.Cost != 0 { - t.Errorf( - "Unexpected Cost. Expected 0, received %f", - detail.Cost, - ) - } + assert.Zero(t, detail.Cost, "InferCostsAndTimes should set Cost correctly") + detail.ExecutedAmount = 0 detail.Amount = 1 detail.RemainingAmount = 1 detail.InferCostsAndTimes() - if detail.Amount != detail.ExecutedAmount+detail.RemainingAmount { - t.Errorf( - "Order detail amounts not equals. Expected 0, received %f", - detail.Amount-(detail.ExecutedAmount+detail.RemainingAmount), - ) - } + assert.Equal(t, detail.ExecutedAmount+detail.RemainingAmount, detail.Amount) detail.RemainingAmount = 0 detail.Amount = 1 detail.ExecutedAmount = 1 detail.Price = 2 detail.InferCostsAndTimes() - if detail.AverageExecutedPrice != 2 { - t.Errorf( - "Unexpected AverageExecutedPrice. Expected 2, received %f", - detail.AverageExecutedPrice, - ) - } + assert.Equal(t, 2.0, detail.AverageExecutedPrice) detail = Detail{Amount: 1, ExecutedAmount: 2, Cost: 3, Price: 0} detail.InferCostsAndTimes() - if detail.AverageExecutedPrice != 1.5 { - t.Errorf( - "Unexpected AverageExecutedPrice. Expected 1.5, received %f", - detail.AverageExecutedPrice, - ) - } + assert.Equal(t, 1.5, detail.AverageExecutedPrice) detail = Detail{Amount: 1, ExecutedAmount: 2, AverageExecutedPrice: 3} detail.InferCostsAndTimes() - if detail.Cost != 6 { - t.Errorf( - "Unexpected Cost. Expected 6, received %f", - detail.Cost, - ) - } + assert.Equal(t, 6.0, detail.Cost) } func TestFilterOrdersByType(t *testing.T) { t.Parallel() orders := []Detail{ - { - Type: ImmediateOrCancel, - }, { Type: Limit, }, @@ -452,19 +359,13 @@ func TestFilterOrdersByType(t *testing.T) { } FilterOrdersByType(&orders, AnyType) - if len(orders) != 3 { - t.Errorf("Orders failed to be filtered. Expected %v, received %v", 2, len(orders)) - } + assert.Len(t, orders, 2, "Orders should be filtered correctly") FilterOrdersByType(&orders, Limit) - if len(orders) != 2 { - t.Errorf("Orders failed to be filtered. Expected %v, received %v", 1, len(orders)) - } + assert.Len(t, orders, 2, "Orders should be filtered correctly") FilterOrdersByType(&orders, Stop) - if len(orders) != 1 { - t.Errorf("Orders failed to be filtered. Expected %v, received %v", 0, len(orders)) - } + assert.Len(t, orders, 1, "Orders should be filtered correctly") } var filterOrdersByTypeBenchmark = &[]Detail{ @@ -504,19 +405,13 @@ func TestFilterOrdersBySide(t *testing.T) { } FilterOrdersBySide(&orders, AnySide) - if len(orders) != 3 { - t.Errorf("Orders failed to be filtered. Expected %v, received %v", 3, len(orders)) - } + assert.Len(t, orders, 3, "Orders should be filtered correctly") FilterOrdersBySide(&orders, Buy) - if len(orders) != 2 { - t.Errorf("Orders failed to be filtered. Expected %v, received %v", 1, len(orders)) - } + assert.Len(t, orders, 2, "Orders should be filtered correctly") FilterOrdersBySide(&orders, Sell) - if len(orders) != 1 { - t.Errorf("Orders failed to be filtered. Expected %v, received %v", 0, len(orders)) - } + assert.Len(t, orders, 1, "Orders should be filtered correctly") } var filterOrdersBySideBenchmark = &[]Detail{ @@ -558,50 +453,29 @@ func TestFilterOrdersByTimeRange(t *testing.T) { } err := FilterOrdersByTimeRange(&orders, time.Unix(0, 0), time.Unix(0, 0)) - if err != nil { - t.Fatal(err) - } - if len(orders) != 3 { - t.Errorf("Orders failed to be filtered. Expected %v, received %v", 3, len(orders)) - } + require.NoError(t, err) + assert.Len(t, orders, 3, "Orders should be filtered correctly") err = FilterOrdersByTimeRange(&orders, time.Unix(100, 0), time.Unix(111, 0)) - if err != nil { - t.Fatal(err) - } - if len(orders) != 3 { - t.Errorf("Orders failed to be filtered. Expected %v, received %v", 3, len(orders)) - } + require.NoError(t, err) + assert.Len(t, orders, 3, "Orders should be filtered correctly") err = FilterOrdersByTimeRange(&orders, time.Unix(101, 0), time.Unix(111, 0)) - if err != nil { - t.Fatal(err) - } - if len(orders) != 2 { - t.Errorf("Orders failed to be filtered. Expected %v, received %v", 2, len(orders)) - } + require.NoError(t, err) + assert.Len(t, orders, 2, "Orders should be filtered correctly") err = FilterOrdersByTimeRange(&orders, time.Unix(200, 0), time.Unix(300, 0)) - if err != nil { - t.Fatal(err) - } - if len(orders) != 0 { - t.Errorf("Orders failed to be filtered. Expected %v, received %v", 0, len(orders)) - } + require.NoError(t, err) + assert.Empty(t, orders, "Orders should be filtered correctly") + orders = append(orders, Detail{}) // test for event no timestamp is set on an order, best to include it err = FilterOrdersByTimeRange(&orders, time.Unix(200, 0), time.Unix(300, 0)) - if err != nil { - t.Fatal(err) - } - if len(orders) != 1 { - t.Errorf("Orders failed to be filtered. Expected %v, received %v", 1, len(orders)) - } + require.NoError(t, err) + assert.Len(t, orders, 1, "Orders should be filtered correctly") err = FilterOrdersByTimeRange(&orders, time.Unix(300, 0), time.Unix(50, 0)) - if !errors.Is(err, common.ErrStartAfterEnd) { - t.Fatalf("received: '%v' but expected: '%v'", err, common.ErrStartAfterEnd) - } + require.ErrorIs(t, err, common.ErrStartAfterEnd) } var filterOrdersByTimeRangeBenchmark = &[]Detail{ @@ -624,9 +498,7 @@ var filterOrdersByTimeRangeBenchmark = &[]Detail{ func BenchmarkFilterOrdersByTimeRange(b *testing.B) { for b.Loop() { err := FilterOrdersByTimeRange(filterOrdersByTimeRangeBenchmark, time.Unix(50, 0), time.Unix(150, 0)) - if err != nil { - b.Fatal(err) - } + require.NoError(b, err) } } @@ -652,41 +524,30 @@ func TestFilterOrdersByPairs(t *testing.T) { currency.NewPair(currency.DOGE, currency.RUB), } FilterOrdersByPairs(&orders, currencies) - if len(orders) != 4 { - t.Errorf("Orders failed to be filtered. Expected %v, received %v", 3, len(orders)) - } + assert.Len(t, orders, 4, "Orders should be filtered correctly") currencies = []currency.Pair{ currency.NewBTCUSD(), currency.NewPair(currency.LTC, currency.EUR), } FilterOrdersByPairs(&orders, currencies) - if len(orders) != 3 { - t.Errorf("Orders failed to be filtered. Expected %v, received %v", 2, len(orders)) - } + assert.Len(t, orders, 3, "Orders should be filtered correctly") currencies = []currency.Pair{currency.NewBTCUSD()} FilterOrdersByPairs(&orders, currencies) - if len(orders) != 2 { - t.Errorf("Orders failed to be filtered. Expected %v, received %v", 1, len(orders)) - } + assert.Len(t, orders, 2, "Orders should be filtered correctly") currencies = []currency.Pair{currency.NewPair(currency.USD, currency.BTC)} FilterOrdersByPairs(&orders, currencies) - if len(orders) != 2 { - t.Errorf("Reverse Orders failed to be filtered. Expected %v, received %v", 1, len(orders)) - } + assert.Len(t, orders, 2, "Reverse Orders should be filtered correctly") currencies = []currency.Pair{} FilterOrdersByPairs(&orders, currencies) - if len(orders) != 2 { - t.Errorf("Orders failed to be filtered. Expected %v, received %v", 1, len(orders)) - } + assert.Len(t, orders, 2, "Orders should be filtered correctly") + currencies = append(currencies, currency.EMPTYPAIR) FilterOrdersByPairs(&orders, currencies) - if len(orders) != 2 { - t.Errorf("Orders failed to be filtered. Expected %v, received %v", 1, len(orders)) - } + assert.Len(t, orders, 2, "Orders should be filtered correctly") } var filterOrdersByPairsBenchmark = &[]Detail{ @@ -727,14 +588,10 @@ func TestSortOrdersByPrice(t *testing.T) { } SortOrdersByPrice(&orders, false) - if orders[0].Price != 0 { - t.Errorf("Expected: '%v', received: '%v'", 0, orders[0].Price) - } + assert.Zero(t, orders[0].Price, "Price should be correct") SortOrdersByPrice(&orders, true) - if orders[0].Price != 100 { - t.Errorf("Expected: '%v', received: '%v'", 100, orders[0].Price) - } + assert.Equal(t, 100.0, orders[0].Price, "Price should be correct") } func TestSortOrdersByDate(t *testing.T) { @@ -751,18 +608,10 @@ func TestSortOrdersByDate(t *testing.T) { } SortOrdersByDate(&orders, false) - if orders[0].Date.Unix() != time.Unix(0, 0).Unix() { - t.Errorf("Expected: '%v', received: '%v'", - time.Unix(0, 0).Unix(), - orders[0].Date.Unix()) - } + assert.Equal(t, orders[0].Date.Unix(), time.Unix(0, 0).Unix()) SortOrdersByDate(&orders, true) - if orders[0].Date.Unix() != time.Unix(2, 0).Unix() { - t.Errorf("Expected: '%v', received: '%v'", - time.Unix(2, 0).Unix(), - orders[0].Date.Unix()) - } + assert.Equal(t, orders[0].Date, time.Unix(2, 0)) } func TestSortOrdersByCurrency(t *testing.T) { @@ -793,18 +642,10 @@ func TestSortOrdersByCurrency(t *testing.T) { } SortOrdersByCurrency(&orders, false) - if orders[0].Pair.String() != currency.BTC.String()+"-"+currency.RUB.String() { - t.Errorf("Expected: '%v', received: '%v'", - currency.BTC.String()+"-"+currency.RUB.String(), - orders[0].Pair.String()) - } + assert.Equal(t, currency.BTC.String()+"-"+currency.RUB.String(), orders[0].Pair.String()) SortOrdersByCurrency(&orders, true) - if orders[0].Pair.String() != currency.LTC.String()+"-"+currency.EUR.String() { - t.Errorf("Expected: '%v', received: '%v'", - currency.LTC.String()+"-"+currency.EUR.String(), - orders[0].Pair.String()) - } + assert.Equal(t, currency.LTC.String()+"-"+currency.EUR.String(), orders[0].Pair.String()) } func TestSortOrdersByOrderSide(t *testing.T) { @@ -823,18 +664,10 @@ func TestSortOrdersByOrderSide(t *testing.T) { } SortOrdersBySide(&orders, false) - if !strings.EqualFold(orders[0].Side.String(), Buy.String()) { - t.Errorf("Expected: '%v', received: '%v'", - Buy, - orders[0].Side) - } + assert.Equal(t, Buy.String(), orders[0].Side.String()) SortOrdersBySide(&orders, true) - if !strings.EqualFold(orders[0].Side.String(), Sell.String()) { - t.Errorf("Expected: '%v', received: '%v'", - Sell, - orders[0].Side) - } + assert.Equal(t, Sell.String(), orders[0].Side.String()) } func TestSortOrdersByOrderType(t *testing.T) { @@ -845,26 +678,16 @@ func TestSortOrdersByOrderType(t *testing.T) { Type: Market, }, { Type: Limit, - }, { - Type: ImmediateOrCancel, }, { Type: TrailingStop, }, } SortOrdersByType(&orders, false) - if !strings.EqualFold(orders[0].Type.String(), ImmediateOrCancel.String()) { - t.Errorf("Expected: '%v', received: '%v'", - ImmediateOrCancel, - orders[0].Type) - } + assert.Equal(t, Limit.String(), orders[0].Type.String()) SortOrdersByType(&orders, true) - if !strings.EqualFold(orders[0].Type.String(), TrailingStop.String()) { - t.Errorf("Expected: '%v', received: '%v'", - TrailingStop, - orders[0].Type) - } + assert.Equal(t, TrailingStop.String(), orders[0].Type.String()) } func TestStringToOrderSide(t *testing.T) { @@ -896,12 +719,8 @@ func TestStringToOrderSide(t *testing.T) { testData := &cases[i] t.Run(testData.in, func(t *testing.T) { out, err := StringToOrderSide(testData.in) - if !errors.Is(err, testData.err) { - t.Fatalf("received: '%v' but expected: '%v'", err, testData.err) - } - if out != testData.out { - t.Errorf("Unexpected output %v. Expected %v", out, testData.out) - } + require.ErrorIs(t, err, testData.err) + require.Equal(t, out, testData.out) }) } } @@ -928,10 +747,6 @@ func TestStringToOrderType(t *testing.T) { {"market", Market, nil}, {"MARKET", Market, nil}, {"mArKeT", Market, nil}, - {"immediate_or_cancel", ImmediateOrCancel, nil}, - {"IMMEDIATE_OR_CANCEL", ImmediateOrCancel, nil}, - {"iMmEdIaTe_Or_CaNcEl", ImmediateOrCancel, nil}, - {"iMmEdIaTe Or CaNcEl", ImmediateOrCancel, nil}, {"stop", Stop, nil}, {"STOP", Stop, nil}, {"sToP", Stop, nil}, @@ -941,10 +756,7 @@ func TestStringToOrderType(t *testing.T) { {"TRAILING_STOP", TrailingStop, nil}, {"tRaIlInG_sToP", TrailingStop, nil}, {"tRaIlInG sToP", TrailingStop, nil}, - {"fOk", FillOrKill, nil}, - {"exchange fOk", FillOrKill, nil}, {"ios", IOS, nil}, - {"post_ONly", PostOnly, nil}, {"any", AnyType, nil}, {"ANY", AnyType, nil}, {"aNy", AnyType, nil}, @@ -974,12 +786,8 @@ func TestStringToOrderType(t *testing.T) { testData := &cases[i] t.Run(testData.in, func(t *testing.T) { out, err := StringToOrderType(testData.in) - if !errors.Is(err, testData.err) { - t.Fatalf("received: '%v' but expected: '%v'", err, testData.err) - } - if out != testData.out { - t.Errorf("Unexpected output %v. Expected %v", out, testData.out) - } + require.ErrorIs(t, err, testData.err) + assert.Equal(t, testData.out, out) }) } } @@ -1051,12 +859,8 @@ func TestStringToOrderStatus(t *testing.T) { testData := &stringsToOrderStatus[i] t.Run(testData.in, func(t *testing.T) { out, err := StringToOrderStatus(testData.in) - if !errors.Is(err, testData.err) { - t.Fatalf("received: '%v' but expected: '%v'", err, testData.err) - } - if out != testData.out { - t.Errorf("Unexpected output %v. Expected %v", out, testData.out) - } + require.ErrorIs(t, err, testData.err) + assert.Equal(t, testData.out, out) }) } } @@ -1076,73 +880,40 @@ func TestUpdateOrderFromModifyResponse(t *testing.T) { updated := time.Now() pair, err := currency.NewPairFromString("BTCUSD") - if err != nil { - t.Fatal(err) - } + require.NoError(t, err) om := ModifyResponse{ - ImmediateOrCancel: true, - PostOnly: true, - Price: 1, - Amount: 1, - TriggerPrice: 1, - RemainingAmount: 1, - Exchange: "1", - Type: 1, - Side: 1, - Status: 1, - AssetType: 1, - LastUpdated: updated, - Pair: pair, + TimeInForce: PostOnly | GoodTillTime, + Price: 1, + Amount: 1, + TriggerPrice: 1, + RemainingAmount: 1, + Exchange: "1", + Type: 1, + Side: 1, + Status: 1, + AssetType: 1, + LastUpdated: updated, + Pair: pair, } od.UpdateOrderFromModifyResponse(&om) - - if !od.ImmediateOrCancel { - t.Error("Failed to update") - } - if !od.PostOnly { - t.Error("Failed to update") - } - if od.Price != 1 { - t.Error("Failed to update") - } - if od.Amount != 1 { - t.Error("Failed to update") - } - if od.TriggerPrice != 1 { - t.Error("Failed to update") - } - if od.RemainingAmount != 1 { - t.Error("Failed to update") - } - if od.Exchange != "" { - t.Error("Should not be able to update exchange via modify") - } - if od.OrderID != "1" { - t.Error("Failed to update") - } - if od.Type != 1 { - t.Error("Failed to update") - } - if od.Side != 1 { - t.Error("Failed to update") - } - if od.Status != 1 { - t.Error("Failed to update") - } - if od.AssetType != 1 { - t.Error("Failed to update") - } - if od.LastUpdated != updated { - t.Error("Failed to update") - } - if od.Pair.String() != "BTCUSD" { - t.Error("Failed to update") - } - if od.Trades != nil { - t.Error("Failed to update") - } + require.NotEqual(t, UnknownTIF, od.TimeInForce) + assert.True(t, od.TimeInForce.Is(GoodTillTime)) + assert.True(t, od.TimeInForce.Is(PostOnly)) + assert.Equal(t, 1.0, od.Price) + assert.Equal(t, 1.0, od.Amount) + assert.Equal(t, 1.0, od.TriggerPrice) + assert.Equal(t, 1.0, od.RemainingAmount) + assert.Empty(t, od.Exchange, "Should not be able to update exchange via modify") + assert.Equal(t, "1", od.OrderID) + assert.Equal(t, Type(1), od.Type) + assert.Equal(t, Side(1), od.Side) + assert.Equal(t, Status(1), od.Status) + assert.Equal(t, asset.Item(1), od.AssetType) + assert.Equal(t, od.LastUpdated, updated) + assert.Equal(t, "BTCUSD", od.Pair.String()) + assert.Nil(t, od.Trades) } func TestUpdateOrderFromDetail(t *testing.T) { @@ -1151,149 +922,81 @@ func TestUpdateOrderFromDetail(t *testing.T) { updated := time.Now() pair, err := currency.NewPairFromString("BTCUSD") - if err != nil { - t.Fatal(err) - } + require.NoError(t, err) id, err := uuid.NewV4() - if err != nil { - t.Fatal(err) - } + require.NoError(t, err) var od *Detail err = od.UpdateOrderFromDetail(nil) - if !errors.Is(err, ErrOrderDetailIsNil) { - t.Fatalf("received: '%v' but expected: '%v'", err, ErrOrderDetailIsNil) - } + require.ErrorIs(t, err, ErrOrderDetailIsNil) om := &Detail{ - ImmediateOrCancel: true, - HiddenOrder: true, - FillOrKill: true, - PostOnly: true, - Leverage: 1, - Price: 1, - Amount: 1, - LimitPriceUpper: 1, - LimitPriceLower: 1, - TriggerPrice: 1, - QuoteAmount: 1, - ExecutedAmount: 1, - RemainingAmount: 1, - Fee: 1, - Exchange: "1", - InternalOrderID: id, - OrderID: "1", - AccountID: "1", - ClientID: "1", - ClientOrderID: "DukeOfWombleton", - Type: 1, - Side: 1, - Status: 1, - AssetType: 1, - LastUpdated: updated, - Pair: pair, - Trades: []TradeHistory{}, + TimeInForce: GoodTillCancel | PostOnly, + HiddenOrder: true, + Leverage: 1, + Price: 1, + Amount: 1, + LimitPriceUpper: 1, + LimitPriceLower: 1, + TriggerPrice: 1, + QuoteAmount: 1, + ExecutedAmount: 1, + RemainingAmount: 1, + Fee: 1, + Exchange: "1", + InternalOrderID: id, + OrderID: "1", + AccountID: "1", + ClientID: "1", + ClientOrderID: "DukeOfWombleton", + Type: 1, + Side: 1, + Status: 1, + AssetType: 1, + LastUpdated: updated, + Pair: pair, + Trades: []TradeHistory{}, } od = &Detail{Exchange: "test"} err = od.UpdateOrderFromDetail(nil) - if !errors.Is(err, ErrOrderDetailIsNil) { - t.Fatalf("received: '%v' but expected: '%v'", err, ErrOrderDetailIsNil) - } + require.ErrorIs(t, err, ErrOrderDetailIsNil) err = od.UpdateOrderFromDetail(om) - if !errors.Is(err, nil) { - t.Fatalf("received: '%v' but expected: '%v'", err, nil) - } - if od.InternalOrderID != id { - t.Error("Failed to initialize the internal order ID") - } - if !od.ImmediateOrCancel { - t.Error("Failed to update") - } - if !od.HiddenOrder { - t.Error("Failed to update") - } - if !od.FillOrKill { - t.Error("Failed to update") - } - if !od.PostOnly { - t.Error("Failed to update") - } - if od.Leverage != 1 { - t.Error("Failed to update") - } - if od.Price != 1 { - t.Error("Failed to update") - } - if od.Amount != 1 { - t.Error("Failed to update") - } - if od.LimitPriceLower != 1 { - t.Error("Failed to update") - } - if od.LimitPriceUpper != 1 { - t.Error("Failed to update") - } - if od.TriggerPrice != 1 { - t.Error("Failed to update") - } - if od.QuoteAmount != 1 { - t.Error("Failed to update") - } - if od.ExecutedAmount != 1 { - t.Error("Failed to update") - } - if od.RemainingAmount != 1 { - t.Error("Failed to update") - } - if od.Fee != 1 { - t.Error("Failed to update") - } - if od.Exchange != "test" { - t.Error("Should not be able to update exchange via modify") - } - if od.OrderID != "1" { - t.Error("Failed to update") - } - if od.ClientID != "1" { - t.Error("Failed to update") - } - if od.ClientOrderID != "DukeOfWombleton" { - t.Error("Failed to update") - } - if od.Type != 1 { - t.Error("Failed to update") - } - if od.Side != 1 { - t.Error("Failed to update") - } - if od.Status != 1 { - t.Error("Failed to update") - } - if od.AssetType != 1 { - t.Error("Failed to update") - } - if od.LastUpdated != updated { - t.Error("Failed to update") - } - if od.Pair.String() != "BTCUSD" { - t.Error("Failed to update") - } - if od.Trades != nil { - t.Error("Failed to update") - } + require.NoError(t, err) + + assert.Equal(t, od.InternalOrderID, id) + assert.True(t, od.TimeInForce.Is(GoodTillCancel)) + assert.True(t, od.TimeInForce.Is(PostOnly)) + require.True(t, od.HiddenOrder) + assert.Equal(t, 1.0, od.Leverage) + assert.Equal(t, 1.0, od.Price) + assert.Equal(t, 1.0, od.Amount) + assert.Equal(t, 1.0, od.LimitPriceLower) + assert.Equal(t, 1.0, od.LimitPriceUpper) + assert.Equal(t, 1.0, od.TriggerPrice) + assert.Equal(t, 1.0, od.QuoteAmount) + assert.Equal(t, 1.0, od.ExecutedAmount) + assert.Equal(t, 1.0, od.RemainingAmount) + assert.Equal(t, 1.0, od.Fee) + assert.Equal(t, "test", od.Exchange, "Should not be able to update exchange via modify") + assert.Equal(t, "1", od.OrderID) + assert.Equal(t, "1", od.ClientID) + assert.Equal(t, "DukeOfWombleton", od.ClientOrderID) + assert.Equal(t, Type(1), od.Type) + assert.Equal(t, Side(1), od.Side) + assert.Equal(t, Status(1), od.Status) + assert.Equal(t, asset.Item(1), od.AssetType) + assert.Equal(t, updated, od.LastUpdated) + assert.Equal(t, "BTCUSD", od.Pair.String()) + assert.Nil(t, od.Trades) om.Trades = append(om.Trades, TradeHistory{TID: "1"}, TradeHistory{TID: "2"}) err = od.UpdateOrderFromDetail(om) - if err != nil { - t.Fatal(err) - } - if len(od.Trades) != 2 { - t.Error("Failed to add trades") - } + require.NoError(t, err) + assert.Len(t, od.Trades, 2) om.Trades[0].Exchange = leet om.Trades[0].Price = 1337 om.Trades[0].Fee = 1337 @@ -1304,190 +1007,109 @@ func TestUpdateOrderFromDetail(t *testing.T) { om.Trades[0].Type = UnknownType om.Trades[0].Amount = 1337 err = od.UpdateOrderFromDetail(om) - if err != nil { - t.Fatal(err) - } - if od.Trades[0].Exchange == leet { - t.Error("Should not be able to update exchange from update") - } - if od.Trades[0].Price != 1337 { - t.Error("Failed to update trades") - } - if od.Trades[0].Fee != 1337 { - t.Error("Failed to update trades") - } - if !od.Trades[0].IsMaker { - t.Error("Failed to update trades") - } - if od.Trades[0].Timestamp != updated { - t.Error("Failed to update trades") - } - if od.Trades[0].Description != leet { - t.Error("Failed to update trades") - } - if od.Trades[0].Side != UnknownSide { - t.Error("Failed to update trades") - } - if od.Trades[0].Type != UnknownType { - t.Error("Failed to update trades") - } - if od.Trades[0].Amount != 1337 { - t.Error("Failed to update trades") - } + require.NoError(t, err) + assert.NotEqual(t, leet, od.Trades[0].Exchange, "Should not be able to update exchange from update") + assert.Equal(t, 1337.0, od.Trades[0].Price) + assert.Equal(t, 1337.0, od.Trades[0].Fee) + assert.True(t, od.Trades[0].IsMaker) + assert.Equal(t, updated, od.Trades[0].Timestamp) + assert.Equal(t, leet, od.Trades[0].Description) + assert.Equal(t, UnknownSide, od.Trades[0].Side) + assert.Equal(t, UnknownType, od.Trades[0].Type) + assert.Equal(t, 1337.0, od.Trades[0].Amount) id, err = uuid.NewV4() - if err != nil { - t.Fatal(err) - } + require.NoError(t, err) om = &Detail{ InternalOrderID: id, } err = od.UpdateOrderFromDetail(om) - if err != nil { - t.Fatal(err) - } - if od.InternalOrderID == id { - t.Error("Should not be able to update the internal order ID after initialization") - } + require.NoError(t, err) + assert.NotEqual(t, id, od.InternalOrderID, "Should not be able to update the internal order ID after initialization") } func TestClassificationError_Error(t *testing.T) { class := ClassificationError{OrderID: "1337", Exchange: "test", Err: errors.New("test error")} - if class.Error() != "Exchange test: OrderID: 1337 classification error: test error" { - t.Fatal("unexpected output") - } + require.Equal(t, "Exchange test: OrderID: 1337 classification error: test error", class.Error()) class.OrderID = "" - if class.Error() != "Exchange test: classification error: test error" { - t.Fatal("unexpected output") - } + assert.Equal(t, "Exchange test: classification error: test error", class.Error()) } func TestValidationOnOrderTypes(t *testing.T) { var cancelMe *Cancel - if cancelMe.Validate() != ErrCancelOrderIsNil { - t.Fatal("unexpected error") - } + require.ErrorIs(t, cancelMe.Validate(), ErrCancelOrderIsNil) cancelMe = new(Cancel) err := cancelMe.Validate() - if !errors.Is(err, nil) { - t.Errorf("received '%v' expected '%v'", err, nil) - } + assert.NoError(t, err) err = cancelMe.Validate(cancelMe.PairAssetRequired()) - if err == nil || err.Error() != ErrPairIsEmpty.Error() { - t.Errorf("received '%v' expected '%v'", err, ErrPairIsEmpty) - } + assert.ErrorIs(t, err, ErrPairIsEmpty) cancelMe.Pair = currency.NewBTCUSDT() err = cancelMe.Validate(cancelMe.PairAssetRequired()) - if err == nil || err.Error() != ErrAssetNotSet.Error() { - t.Errorf("received '%v' expected '%v'", err, ErrAssetNotSet) - } + assert.ErrorIs(t, err, ErrAssetNotSet) cancelMe.AssetType = asset.Spot err = cancelMe.Validate(cancelMe.PairAssetRequired()) - if !errors.Is(err, nil) { - t.Errorf("received '%v' expected '%v'", err, nil) - } + assert.NoError(t, err) + require.Error(t, cancelMe.Validate(cancelMe.StandardCancel())) - if cancelMe.Validate(cancelMe.StandardCancel()) == nil { - t.Fatal("expected error") - } - - if cancelMe.Validate(validate.Check(func() error { + require.NoError(t, cancelMe.Validate(validate.Check(func() error { return nil - })) != nil { - t.Fatal("should return nil") - } + }))) cancelMe.OrderID = "1337" - if cancelMe.Validate(cancelMe.StandardCancel()) != nil { - t.Fatal("should return nil") - } + require.NoError(t, cancelMe.Validate(cancelMe.StandardCancel())) var getOrders *MultiOrderRequest err = getOrders.Validate() - if !errors.Is(err, ErrGetOrdersRequestIsNil) { - t.Fatalf("received: '%v' but expected: '%v'", err, ErrGetOrdersRequestIsNil) - } + require.ErrorIs(t, err, ErrGetOrdersRequestIsNil) getOrders = new(MultiOrderRequest) err = getOrders.Validate() - if !errors.Is(err, asset.ErrNotSupported) { - t.Fatalf("received: '%v' but expected: '%v'", err, asset.ErrNotSupported) - } + require.ErrorIs(t, err, asset.ErrNotSupported) getOrders.AssetType = asset.Spot err = getOrders.Validate() - if !errors.Is(err, ErrSideIsInvalid) { - t.Fatalf("received: '%v' but expected: '%v'", err, ErrSideIsInvalid) - } + require.ErrorIs(t, err, ErrSideIsInvalid) getOrders.Side = AnySide err = getOrders.Validate() - if !errors.Is(err, errUnrecognisedOrderType) { - t.Fatalf("received: '%v' but expected: '%v'", err, errUnrecognisedOrderType) - } + require.ErrorIs(t, err, errUnrecognisedOrderType) errTestError := errors.New("test error") getOrders.Type = AnyType err = getOrders.Validate(validate.Check(func() error { return errTestError })) - if !errors.Is(err, errTestError) { - t.Fatalf("received: '%v' but expected: '%v'", err, errTestError) - } + require.ErrorIs(t, err, errTestError) err = getOrders.Validate(validate.Check(func() error { return nil })) - if !errors.Is(err, nil) { - t.Fatalf("received: '%v' but expected: '%v'", err, nil) - } + require.NoError(t, err) var modifyOrder *Modify - if modifyOrder.Validate() != ErrModifyOrderIsNil { - t.Fatal("unexpected error") - } + require.ErrorIs(t, modifyOrder.Validate(), ErrModifyOrderIsNil) modifyOrder = new(Modify) - if modifyOrder.Validate() != ErrPairIsEmpty { - t.Fatal("unexpected error") - } + require.ErrorIs(t, modifyOrder.Validate(), ErrPairIsEmpty) p, err := currency.NewPairFromString("BTC-USD") - if err != nil { - t.Fatal(err) - } + require.NoError(t, err) modifyOrder.Pair = p - if modifyOrder.Validate() != ErrAssetNotSet { - t.Fatal("unexpected error") - } + require.ErrorIs(t, modifyOrder.Validate(), ErrAssetNotSet) modifyOrder.AssetType = asset.Spot - if modifyOrder.Validate() != ErrOrderIDNotSet { - t.Fatal("unexpected error") - } + require.ErrorIs(t, modifyOrder.Validate(), ErrOrderIDNotSet) modifyOrder.ClientOrderID = "1337" - if modifyOrder.Validate() != nil { - t.Fatal("should not error") - } - - if modifyOrder.Validate(validate.Check(func() error { - return errors.New("this should error") - })) == nil { - t.Fatal("expected error") - } - - if modifyOrder.Validate(validate.Check(func() error { - return nil - })) != nil { - t.Fatal("unexpected error") - } + require.NoError(t, modifyOrder.Validate()) + require.Error(t, modifyOrder.Validate(validate.Check(func() error { return errors.New("this should error") }))) + require.NoError(t, modifyOrder.Validate(validate.Check(func() error { return nil }))) } func TestMatchFilter(t *testing.T) { @@ -1564,9 +1186,7 @@ func TestIsActive(t *testing.T) { } // specific tests for num, tt := range amountTests { - if tt.o.IsActive() != tt.expectedResult { - t.Errorf("amountTests[%v] failed", num) - } + assert.Equalf(t, tt.expectedResult, tt.o.IsActive(), "amountTests[%v] failed", num) } statusTests := map[int]struct { @@ -1596,9 +1216,7 @@ func TestIsActive(t *testing.T) { } // specific tests for num, tt := range statusTests { - if tt.o.IsActive() != tt.expectedResult { - t.Fatalf("statusTests[%v] failed", num) - } + require.Equalf(t, tt.expectedResult, tt.o.IsActive(), "statusTests[%v] failed", num) } } @@ -1633,9 +1251,7 @@ func TestIsInactive(t *testing.T) { } // specific tests for num, tt := range amountTests { - if tt.o.IsInactive() != tt.expectedResult { - t.Errorf("amountTests[%v] failed", num) - } + assert.Equalf(t, tt.expectedResult, tt.o.IsInactive(), "amountTests[%v] failed", num) } statusTests := map[int]struct { @@ -1665,9 +1281,7 @@ func TestIsInactive(t *testing.T) { } // specific tests for num, tt := range statusTests { - if tt.o.IsInactive() != tt.expectedResult { - t.Errorf("statusTests[%v] failed", num) - } + assert.Equalf(t, tt.expectedResult, tt.o.IsInactive(), "statusTests[%v] failed", num) } } @@ -1676,9 +1290,7 @@ var inactiveBenchmark = Detail{Status: Closed, Amount: 1} // 1000000000 1.043 ns/op 0 B/op 0 allocs/op // CURRENT func BenchmarkIsInactive(b *testing.B) { for b.Loop() { - if !inactiveBenchmark.IsInactive() { - b.Fatal("expected true") - } + require.True(b, inactiveBenchmark.IsInactive()) } } @@ -1711,31 +1323,23 @@ func TestIsOrderPlaced(t *testing.T) { for num, tt := range statusTests { t.Run(fmt.Sprintf("TEST CASE: %d", num), func(t *testing.T) { t.Parallel() - if tt.o.WasOrderPlaced() != tt.expectedResult { - t.Errorf("statusTests[%v] failed", num) - } + assert.Equalf(t, tt.expectedResult, tt.o.WasOrderPlaced(), "statusTests[%v] failed", num) }) } } func TestGenerateInternalOrderID(t *testing.T) { id, err := uuid.NewV4() - if err != nil { - t.Errorf("unable to create uuid: %s", err) - } + assert.NoError(t, err) od := Detail{ InternalOrderID: id, } od.GenerateInternalOrderID() - if od.InternalOrderID != id { - t.Error("Should not be able to generate a new internal order ID") - } + assert.Equal(t, id, od.InternalOrderID, "Should not be able to generate a new internal order ID") od = Detail{} od.GenerateInternalOrderID() - if od.InternalOrderID.IsNil() { - t.Error("unable to generate internal order ID") - } + assert.False(t, od.InternalOrderID.IsNil(), "unable to generate internal order ID") } func TestDetail_Copy(t *testing.T) { @@ -1753,13 +1357,9 @@ func TestDetail_Copy(t *testing.T) { } for i := range d { r := d[i].Copy() - if !reflect.DeepEqual(d[i], r) { - t.Errorf("[%d] Copy does not contain same elements, expected: %v\ngot:%v", i, d[i], r) - } + assert.True(t, reflect.DeepEqual(d[i], r), "[%d] Copy does not contain same elements, expected: %v\ngot:%v", i, d[i], r) if len(d[i].Trades) > 0 { - if &d[i].Trades[0] == &r.Trades[0] { - t.Errorf("[%d]Trades point to the same data elements", i) - } + assert.NotSamef(t, &d[i].Trades[0], &r.Trades[0], "[%d]Trades point to the same data elements", i) } } } @@ -1779,13 +1379,9 @@ func TestDetail_CopyToPointer(t *testing.T) { } for i := range d { r := d[i].CopyToPointer() - if !reflect.DeepEqual(d[i], *r) { - t.Errorf("[%d] Copy does not contain same elements, expected: %v\ngot:%v", i, d[i], r) - } + assert.Truef(t, reflect.DeepEqual(d[i], *r), "[%d] Copy does not contain same elements, expected: %v\ngot:%v", i, d[i], r) if len(d[i].Trades) > 0 { - if &d[i].Trades[0] == &r.Trades[0] { - t.Errorf("[%d]Trades point to the same data elements", i) - } + assert.NotSamef(t, &d[i].Trades[0], &r.Trades[0], "[%d]Trades point to the same data elements", i) } } } @@ -1806,13 +1402,9 @@ func TestDetail_CopyPointerOrderSlice(t *testing.T) { sliceCopy := CopyPointerOrderSlice(d) for i := range sliceCopy { - if !reflect.DeepEqual(*sliceCopy[i], *d[i]) { - t.Errorf("[%d] Copy does not contain same elements, expected: %v\ngot:%v", i, sliceCopy[i], d[i]) - } + assert.Truef(t, reflect.DeepEqual(*sliceCopy[i], *d[i]), "[%d] Copy does not contain same elements, expected: %v\ngot:%v", i, sliceCopy[i], d[i]) if len(sliceCopy[i].Trades) > 0 { - if &sliceCopy[i].Trades[0] == &d[i].Trades[0] { - t.Errorf("[%d]Trades point to the same data elements", i) - } + assert.NotSamef(t, &sliceCopy[i].Trades[0], &d[i].Trades[0], "[%d]Trades point to the same data elements", i) } } } @@ -1888,9 +1480,8 @@ func TestDeriveModifyResponse(t *testing.T) { func TestDeriveCancel(t *testing.T) { t.Parallel() var o *Detail - if _, err := o.DeriveCancel(); !errors.Is(err, errOrderDetailIsNil) { - t.Fatalf("received: '%v' but expected: '%v'", err, errOrderDetailIsNil) - } + _, err := o.DeriveCancel() + require.ErrorIs(t, err, errOrderDetailIsNil) pair := currency.NewPair(currency.BTC, currency.AUD) @@ -1906,20 +1497,16 @@ func TestDeriveCancel(t *testing.T) { AssetType: asset.Futures, } cancel, err := o.DeriveCancel() - if !errors.Is(err, nil) { - t.Fatalf("received: '%v' but expected: '%v'", err, nil) - } - if cancel.Exchange != "wow" || - cancel.OrderID != "wow1" || - cancel.AccountID != "wow2" || - cancel.ClientID != "wow3" || - cancel.ClientOrderID != "wow4" || - cancel.Type != Market || - cancel.Side != Long || - !cancel.Pair.Equal(pair) || - cancel.AssetType != asset.Futures { - t.Fatalf("unexpected values %+v", cancel) - } + require.NoError(t, err) + assert.Equal(t, "wow", cancel.Exchange, "DeriveCancel should set Exchange correctly") + assert.Equal(t, "wow1", cancel.OrderID, "DeriveCancel should set OrderID correctly") + assert.Equal(t, "wow2", cancel.AccountID, "DeriveCancel should set AccountID correctly") + assert.Equal(t, "wow3", cancel.ClientID, "DeriveCancel should set ClientID correctly") + assert.Equal(t, "wow4", cancel.ClientOrderID, "DeriveCancel should set ClientOrderID correctly") + assert.Equal(t, Market, cancel.Type, "DeriveCancel should set Type correctly") + assert.Equal(t, Long, cancel.Side, "DeriveCancel should set Side correctly") + assert.True(t, pair.Equal(cancel.Pair), "DeriveCancel should set Pair correctly") + assert.Equal(t, asset.Futures, cancel.AssetType, "DeriveCancel should set AssetType correctly") } func TestGetOrdersRequest_Filter(t *testing.T) { @@ -1948,14 +1535,10 @@ func TestGetOrdersRequest_Filter(t *testing.T) { } shinyAndClean := request.Filter("test", orders) - if len(shinyAndClean) != 16 { - t.Fatalf("received: '%v' but expected: '%v'", len(shinyAndClean), 16) - } + require.Len(t, shinyAndClean, 16) for x := range shinyAndClean { - if strconv.FormatInt(int64(x), 10) != shinyAndClean[x].OrderID { - t.Fatalf("received: '%v' but expected: '%v'", shinyAndClean[x].OrderID, int64(x)) - } + require.Equal(t, strconv.FormatInt(int64(x), 10), shinyAndClean[x].OrderID) } request.Pairs = []currency.Pair{btcltc} @@ -1965,29 +1548,18 @@ func TestGetOrdersRequest_Filter(t *testing.T) { request.StartTime = time.Unix(1337, 0) shinyAndClean = request.Filter("test", orders) - - if len(shinyAndClean) != 8 { - t.Fatalf("received: '%v' but expected: '%v'", len(shinyAndClean), 8) - } + require.Len(t, shinyAndClean, 8) for x := range shinyAndClean { - if strconv.FormatInt(int64(x)+8, 10) != shinyAndClean[x].OrderID { - t.Fatalf("received: '%v' but expected: '%v'", shinyAndClean[x].OrderID, int64(x)+8) - } + require.Equal(t, strconv.FormatInt(int64(x)+8, 10), shinyAndClean[x].OrderID) } } func TestIsValidOrderSubmissionSide(t *testing.T) { t.Parallel() - if IsValidOrderSubmissionSide(UnknownSide) { - t.Error("expected false") - } - if !IsValidOrderSubmissionSide(Buy) { - t.Error("expected true") - } - if IsValidOrderSubmissionSide(CouldNotBuy) { - t.Error("expected false") - } + assert.False(t, IsValidOrderSubmissionSide(UnknownSide)) + assert.True(t, IsValidOrderSubmissionSide(Buy)) + assert.False(t, IsValidOrderSubmissionSide(CouldNotBuy)) } func TestAdjustBaseAmount(t *testing.T) { @@ -1995,35 +1567,21 @@ func TestAdjustBaseAmount(t *testing.T) { var s *SubmitResponse err := s.AdjustBaseAmount(0) - if !errors.Is(err, errOrderSubmitResponseIsNil) { - t.Fatalf("received: '%v' but expected: '%v'", err, errOrderSubmitResponseIsNil) - } + require.ErrorIs(t, err, errOrderSubmitResponseIsNil) s = &SubmitResponse{} err = s.AdjustBaseAmount(0) - if !errors.Is(err, errAmountIsZero) { - t.Fatalf("received: '%v' but expected: '%v'", err, errAmountIsZero) - } + require.ErrorIs(t, err, errAmountIsZero) s.Amount = 1.7777777777 err = s.AdjustBaseAmount(1.7777777777) - if !errors.Is(err, nil) { - t.Fatalf("received: '%v' but expected: '%v'", err, nil) - } - - if s.Amount != 1.7777777777 { - t.Fatalf("received: '%v' but expected: '%v'", s.Amount, 1.7777777777) - } + require.NoError(t, err) + require.Equal(t, 1.7777777777, s.Amount) s.Amount = 1.7777777777 err = s.AdjustBaseAmount(1.777) - if !errors.Is(err, nil) { - t.Fatalf("received: '%v' but expected: '%v'", err, nil) - } - - if s.Amount != 1.777 { - t.Fatalf("received: '%v' but expected: '%v'", s.Amount, 1.777) - } + require.NoError(t, err) + assert.Equal(t, 1.777, s.Amount) } func TestAdjustQuoteAmount(t *testing.T) { @@ -2031,35 +1589,21 @@ func TestAdjustQuoteAmount(t *testing.T) { var s *SubmitResponse err := s.AdjustQuoteAmount(0) - if !errors.Is(err, errOrderSubmitResponseIsNil) { - t.Fatalf("received: '%v' but expected: '%v'", err, errOrderSubmitResponseIsNil) - } + require.ErrorIs(t, err, errOrderSubmitResponseIsNil) s = &SubmitResponse{} err = s.AdjustQuoteAmount(0) - if !errors.Is(err, errAmountIsZero) { - t.Fatalf("received: '%v' but expected: '%v'", err, errAmountIsZero) - } + require.ErrorIs(t, err, errAmountIsZero) s.QuoteAmount = 5.222222222222 err = s.AdjustQuoteAmount(5.222222222222) - if !errors.Is(err, nil) { - t.Fatalf("received: '%v' but expected: '%v'", err, nil) - } - - if s.QuoteAmount != 5.222222222222 { - t.Fatalf("received: '%v' but expected: '%v'", s.Amount, 5.222222222222) - } + require.NoError(t, err) + require.Equal(t, 5.222222222222, s.QuoteAmount) s.QuoteAmount = 5.222222222222 err = s.AdjustQuoteAmount(5.22222222) - if !errors.Is(err, nil) { - t.Fatalf("received: '%v' but expected: '%v'", err, nil) - } - - if s.QuoteAmount != 5.22222222 { - t.Fatalf("received: '%v' but expected: '%v'", s.Amount, 5.22222222) - } + require.NoError(t, err) + assert.Equal(t, 5.22222222, s.QuoteAmount) } func TestSideUnmarshal(t *testing.T) { @@ -2143,6 +1687,6 @@ func TestMarshalOrder(t *testing.T) { } j, err := json.Marshal(orderSubmit) require.NoError(t, err, "json.Marshal must not error") - exp := []byte(`{"Exchange":"test","Type":4,"Side":"BUY","Pair":"BTC-USDT","AssetType":"spot","ImmediateOrCancel":false,"FillOrKill":false,"PostOnly":false,"ReduceOnly":false,"Leverage":0,"Price":1000,"Amount":1,"QuoteAmount":0,"TriggerPrice":0,"TriggerPriceType":0,"ClientID":"","ClientOrderID":"","AutoBorrow":false,"MarginType":"multi","RetrieveFees":false,"RetrieveFeeDelay":0,"RiskManagementModes":{"Mode":"","TakeProfit":{"Enabled":false,"TriggerPriceType":0,"Price":0,"LimitPrice":0,"OrderType":0},"StopLoss":{"Enabled":false,"TriggerPriceType":0,"Price":0,"LimitPrice":0,"OrderType":0},"StopEntry":{"Enabled":false,"TriggerPriceType":0,"Price":0,"LimitPrice":0,"OrderType":0}},"Hidden":false,"Iceberg":false,"TrackingMode":0,"TrackingValue":0}`) + exp := []byte(`{"Exchange":"test","Type":4,"Side":"BUY","Pair":"BTC-USDT","AssetType":"spot","TimeInForce":"","ReduceOnly":false,"Leverage":0,"Price":1000,"Amount":1,"QuoteAmount":0,"TriggerPrice":0,"TriggerPriceType":0,"ClientID":"","ClientOrderID":"","AutoBorrow":false,"MarginType":"multi","RetrieveFees":false,"RetrieveFeeDelay":0,"RiskManagementModes":{"Mode":"","TakeProfit":{"Enabled":false,"TriggerPriceType":0,"Price":0,"LimitPrice":0,"OrderType":0},"StopLoss":{"Enabled":false,"TriggerPriceType":0,"Price":0,"LimitPrice":0,"OrderType":0},"StopEntry":{"Enabled":false,"TriggerPriceType":0,"Price":0,"LimitPrice":0,"OrderType":0}},"Hidden":false,"Iceberg":false,"TrackingMode":0,"TrackingValue":0}`) assert.Equal(t, exp, j) } diff --git a/exchanges/order/order_types.go b/exchanges/order/order_types.go index b708e4f8..a3ce3943 100644 --- a/exchanges/order/order_types.go +++ b/exchanges/order/order_types.go @@ -46,11 +46,9 @@ type Submit struct { Pair currency.Pair AssetType asset.Item - // Time in force values ------ TODO: Time In Force uint8 - ImmediateOrCancel bool - FillOrKill bool + // TimeInForce holds time in force values + TimeInForce TimeInForce - PostOnly bool // ReduceOnly reduces a position instead of opening an opposing // position; this also equates to closing the position in huobi_wrapper.go // swaps. @@ -109,19 +107,17 @@ type SubmitResponse struct { Pair currency.Pair AssetType asset.Item - ImmediateOrCancel bool - FillOrKill bool - PostOnly bool + TimeInForce TimeInForce ReduceOnly bool Leverage float64 Price float64 - AverageExecutedPrice float64 Amount float64 QuoteAmount float64 RemainingAmount float64 TriggerPrice float64 ClientID string ClientOrderID string + AverageExecutedPrice float64 LastUpdated time.Time Date time.Time @@ -164,11 +160,10 @@ type Modify struct { Pair currency.Pair // Change fields - ImmediateOrCancel bool - PostOnly bool - Price float64 - Amount float64 - TriggerPrice float64 + TimeInForce TimeInForce + Price float64 + Amount float64 + TriggerPrice float64 // added to represent a unified trigger price type information such as LastPrice, MarkPrice, and IndexPrice // https://bybit-exchange.github.io/docs/v5/order/create-order @@ -190,11 +185,10 @@ type ModifyResponse struct { AssetType asset.Item // Fields that will be copied over from Modify - ImmediateOrCancel bool - PostOnly bool - Price float64 - Amount float64 - TriggerPrice float64 + TimeInForce TimeInForce + Price float64 + Amount float64 + TriggerPrice float64 // Fields that need to be handled in scope after DeriveModifyResponse() // if applicable @@ -206,10 +200,8 @@ type ModifyResponse struct { // Detail contains all properties of an order // Each exchange has their own requirements, so not all fields are required to be populated type Detail struct { - ImmediateOrCancel bool HiddenOrder bool - FillOrKill bool - PostOnly bool + TimeInForce TimeInForce ReduceOnly bool Leverage float64 Price float64 @@ -276,6 +268,7 @@ type Cancel struct { AssetType asset.Item Pair currency.Pair MarginType margin.Type + TimeInForce TimeInForce } // CancelAllResponse returns the status from attempting to @@ -311,12 +304,13 @@ type TradeHistory struct { type MultiOrderRequest struct { // Currencies Empty array = all currencies. Some endpoints only support // singular currency enquiries - Pairs currency.Pairs - AssetType asset.Item - Type Type - Side Side - StartTime time.Time - EndTime time.Time + Pairs currency.Pairs + AssetType asset.Item + Type Type + Side Side + TimeInForce TimeInForce + StartTime time.Time + EndTime time.Time // FromOrderID for some APIs require order history searching // from a specific orderID rather than via timestamps FromOrderID string @@ -361,15 +355,12 @@ const ( UnknownType Type = 0 Limit Type = 1 << iota Market - PostOnly - ImmediateOrCancel Stop StopLimit StopMarket TakeProfit TakeProfitMarket TrailingStop - FillOrKill IOS AnyType Liquidation diff --git a/exchanges/order/orders.go b/exchanges/order/orders.go index 662b03b6..a3014ad8 100644 --- a/exchanges/order/orders.go +++ b/exchanges/order/orders.go @@ -43,7 +43,6 @@ var ( ) var ( - errTimeInForceConflict = errors.New("multiple time in force options applied") errUnrecognisedOrderType = errors.New("unrecognised order type") errUnrecognisedOrderStatus = errors.New("unrecognised order status") errExchangeNameUnset = errors.New("exchange name unset") @@ -88,8 +87,8 @@ func (s *Submit) Validate(requirements protocol.TradingRequirements, opt ...vali return ErrTypeIsInvalid } - if s.ImmediateOrCancel && s.FillOrKill { - return errTimeInForceConflict + if !s.TimeInForce.IsValid() { + return ErrInvalidTimeInForce } if s.Amount == 0 && s.QuoteAmount == 0 { @@ -159,18 +158,14 @@ func (d *Detail) UpdateOrderFromDetail(m *Detail) error { } var updated bool - if d.ImmediateOrCancel != m.ImmediateOrCancel { - d.ImmediateOrCancel = m.ImmediateOrCancel + if m.TimeInForce != UnknownTIF && d.TimeInForce != m.TimeInForce { + d.TimeInForce = m.TimeInForce updated = true } if d.HiddenOrder != m.HiddenOrder { d.HiddenOrder = m.HiddenOrder updated = true } - if d.FillOrKill != m.FillOrKill { - d.FillOrKill = m.FillOrKill - updated = true - } if m.Price > 0 && m.Price != d.Price { d.Price = m.Price updated = true @@ -207,10 +202,6 @@ func (d *Detail) UpdateOrderFromDetail(m *Detail) error { d.AccountID = m.AccountID updated = true } - if m.PostOnly != d.PostOnly { - d.PostOnly = m.PostOnly - updated = true - } if !m.Pair.IsEmpty() && !m.Pair.Equal(d.Pair) { // TODO: Add a check to see if the original pair is empty as well, but // error if it is changing from BTC-USD -> LTC-USD. @@ -322,8 +313,8 @@ func (d *Detail) UpdateOrderFromModifyResponse(m *ModifyResponse) { d.OrderID = m.OrderID updated = true } - if d.ImmediateOrCancel != m.ImmediateOrCancel { - d.ImmediateOrCancel = m.ImmediateOrCancel + if d.TimeInForce != m.TimeInForce && m.TimeInForce != UnknownTIF { + d.TimeInForce = m.TimeInForce updated = true } if m.Price > 0 && m.Price != d.Price { @@ -338,10 +329,6 @@ func (d *Detail) UpdateOrderFromModifyResponse(m *ModifyResponse) { d.TriggerPrice = m.TriggerPrice updated = true } - if m.PostOnly != d.PostOnly { - d.PostOnly = m.PostOnly - updated = true - } if !m.Pair.IsEmpty() && !m.Pair.Equal(d.Pair) { // TODO: Add a check to see if the original pair is empty as well, but // error if it is changing from BTC-USD -> LTC-USD. @@ -496,18 +483,16 @@ func (s *Submit) DeriveSubmitResponse(orderID string) (*SubmitResponse, error) { Pair: s.Pair, AssetType: s.AssetType, - ImmediateOrCancel: s.ImmediateOrCancel, - FillOrKill: s.FillOrKill, - PostOnly: s.PostOnly, - ReduceOnly: s.ReduceOnly, - Leverage: s.Leverage, - Price: s.Price, - Amount: s.Amount, - QuoteAmount: s.QuoteAmount, - TriggerPrice: s.TriggerPrice, - ClientID: s.ClientID, - ClientOrderID: s.ClientOrderID, - MarginType: s.MarginType, + TimeInForce: s.TimeInForce, + ReduceOnly: s.ReduceOnly, + Leverage: s.Leverage, + Price: s.Price, + Amount: s.Amount, + QuoteAmount: s.QuoteAmount, + TriggerPrice: s.TriggerPrice, + ClientID: s.ClientID, + ClientOrderID: s.ClientOrderID, + MarginType: s.MarginType, LastUpdated: time.Now(), Date: time.Now(), @@ -589,17 +574,15 @@ func (s *SubmitResponse) DeriveDetail(internal uuid.UUID) (*Detail, error) { Pair: s.Pair, AssetType: s.AssetType, - ImmediateOrCancel: s.ImmediateOrCancel, - FillOrKill: s.FillOrKill, - PostOnly: s.PostOnly, - ReduceOnly: s.ReduceOnly, - Leverage: s.Leverage, - Price: s.Price, - Amount: s.Amount, - QuoteAmount: s.QuoteAmount, - TriggerPrice: s.TriggerPrice, - ClientID: s.ClientID, - ClientOrderID: s.ClientOrderID, + TimeInForce: s.TimeInForce, + ReduceOnly: s.ReduceOnly, + Leverage: s.Leverage, + Price: s.Price, + Amount: s.Amount, + QuoteAmount: s.QuoteAmount, + TriggerPrice: s.TriggerPrice, + ClientID: s.ClientID, + ClientOrderID: s.ClientOrderID, InternalOrderID: internal, @@ -649,18 +632,17 @@ func (m *Modify) DeriveModifyResponse() (*ModifyResponse, error) { return nil, errOrderDetailIsNil } return &ModifyResponse{ - Exchange: m.Exchange, - OrderID: m.OrderID, - ClientOrderID: m.ClientOrderID, - Type: m.Type, - Side: m.Side, - AssetType: m.AssetType, - Pair: m.Pair, - ImmediateOrCancel: m.ImmediateOrCancel, - PostOnly: m.PostOnly, - Price: m.Price, - Amount: m.Amount, - TriggerPrice: m.TriggerPrice, + Exchange: m.Exchange, + OrderID: m.OrderID, + ClientOrderID: m.ClientOrderID, + Type: m.Type, + Side: m.Side, + AssetType: m.AssetType, + Pair: m.Pair, + TimeInForce: m.TimeInForce, + Price: m.Price, + Amount: m.Amount, + TriggerPrice: m.TriggerPrice, }, nil } @@ -691,10 +673,6 @@ func (t Type) String() string { return "LIMIT" case Market: return "MARKET" - case PostOnly: - return "POST_ONLY" - case ImmediateOrCancel: - return "IMMEDIATE_OR_CANCEL" case Stop: return "STOP" case ConditionalStop: @@ -717,8 +695,6 @@ func (t Type) String() string { return "TAKE PROFIT MARKET" case TrailingStop: return "TRAILING_STOP" - case FillOrKill: - return "FOK" case IOS: return "IOS" case Liquidation: @@ -1130,8 +1106,6 @@ func StringToOrderType(oType string) (Type, error) { return Limit, nil case Market.String(), "EXCHANGE MARKET": return Market, nil - case ImmediateOrCancel.String(), "IMMEDIATE OR CANCEL", "IOC", "EXCHANGE IOC": - return ImmediateOrCancel, nil case Stop.String(), "STOP LOSS", "STOP_LOSS", "EXCHANGE STOP": return Stop, nil case StopLimit.String(), "EXCHANGE STOP LIMIT", "STOP_LIMIT": @@ -1140,12 +1114,8 @@ func StringToOrderType(oType string) (Type, error) { return StopMarket, nil case TrailingStop.String(), "TRAILING STOP", "EXCHANGE TRAILING STOP", "MOVE_ORDER_STOP": return TrailingStop, nil - case FillOrKill.String(), "EXCHANGE FOK": - return FillOrKill, nil case IOS.String(): return IOS, nil - case PostOnly.String(): - return PostOnly, nil case AnyType.String(): return AnyType, nil case Trigger.String(): diff --git a/exchanges/order/timeinforce.go b/exchanges/order/timeinforce.go new file mode 100644 index 00000000..7ceda728 --- /dev/null +++ b/exchanges/order/timeinforce.go @@ -0,0 +1,140 @@ +package order + +import ( + "errors" + "fmt" + "strings" +) + +// var error definitions +var ( + ErrInvalidTimeInForce = errors.New("invalid time in force value provided") + ErrUnsupportedTimeInForce = errors.New("unsupported time in force value") +) + +// TimeInForce enforces a standard for time-in-force values across the code base. +type TimeInForce uint8 + +// TimeInForce types +const ( + UnknownTIF TimeInForce = 0 + GoodTillCancel TimeInForce = 1 << iota + GoodTillDay + GoodTillTime + GoodTillCrossing + FillOrKill + ImmediateOrCancel + PostOnly + + supportedTimeInForceFlag = GoodTillCancel | GoodTillDay | GoodTillTime | GoodTillCrossing | FillOrKill | ImmediateOrCancel | PostOnly +) + +// time-in-force string representations +const ( + gtcStr = "GTC" + gtdStr = "GTD" + gttStr = "GTT" + gtxStr = "GTX" + fokStr = "FOK" + iocStr = "IOC" + postonlyStr = "POSTONLY" +) + +// Is checks to see if the enum contains the flag +func (t TimeInForce) Is(in TimeInForce) bool { + return in != 0 && t&in == in +} + +// StringToTimeInForce converts time in force string value to TimeInForce instance. +func StringToTimeInForce(timeInForce string) (TimeInForce, error) { + var result TimeInForce + timeInForce = strings.ToUpper(timeInForce) + switch timeInForce { + case "IMMEDIATEORCANCEL", "IMMEDIATE_OR_CANCEL", iocStr: + result = ImmediateOrCancel + case "GOODTILLCANCEL", "GOODTILCANCEL", "GOOD_TIL_CANCELLED", "GOOD_TILL_CANCELLED", "GOOD_TILL_CANCELED", gtcStr: + result = GoodTillCancel + case "GOODTILLDAY", "GOOD_TIL_DAY", "GOOD_TILL_DAY", gtdStr: + result = GoodTillDay + case "GOODTILLTIME", "GOOD_TIL_TIME", gttStr: + result = GoodTillTime + case "GOODTILLCROSSING", "GOOD_TIL_CROSSING", "GOOD TIL CROSSING", "GOOD_TILL_CROSSING", gtxStr: + result = GoodTillCrossing + case "FILLORKILL", "FILL_OR_KILL", fokStr: + result = FillOrKill + case "POC", "POST_ONLY", "PENDINGORCANCEL", postonlyStr: + result = PostOnly + } + if result == UnknownTIF && timeInForce != "" { + return UnknownTIF, fmt.Errorf("%w: tif=%s", ErrInvalidTimeInForce, timeInForce) + } + return result, nil +} + +// IsValid returns whether or not the supplied time in force value is valid or +// not +func (t TimeInForce) IsValid() bool { + // Neither ImmediateOrCancel nor FillOrKill can coexist with anything else + // If either bit is set then it must be the only bit set + isIOCorFOK := t&(ImmediateOrCancel|FillOrKill) != 0 + hasTwoBitsSet := t&(t-1) != 0 + if isIOCorFOK && hasTwoBitsSet { + return false + } + return t == UnknownTIF || supportedTimeInForceFlag&t == t +} + +// String implements the stringer interface. +func (t TimeInForce) String() string { + if t == UnknownTIF { + return "" + } + var tifStrings []string + if t.Is(ImmediateOrCancel) { + tifStrings = append(tifStrings, iocStr) + } + if t.Is(GoodTillCancel) { + tifStrings = append(tifStrings, gtcStr) + } + if t.Is(GoodTillDay) { + tifStrings = append(tifStrings, gtdStr) + } + if t.Is(GoodTillTime) { + tifStrings = append(tifStrings, gttStr) + } + if t.Is(GoodTillCrossing) { + tifStrings = append(tifStrings, gtxStr) + } + if t.Is(FillOrKill) { + tifStrings = append(tifStrings, fokStr) + } + if t.Is(PostOnly) { + tifStrings = append(tifStrings, postonlyStr) + } + if len(tifStrings) == 0 { + return "UNKNOWN" + } + return strings.Join(tifStrings, ",") +} + +// Lower returns a lower case string representation of time-in-force +func (t TimeInForce) Lower() string { + return strings.ToLower(t.String()) +} + +// UnmarshalJSON deserializes a string data into TimeInForce instance. +func (t *TimeInForce) UnmarshalJSON(data []byte) error { + for val := range strings.SplitSeq(strings.Trim(string(data), `"`), ",") { + tif, err := StringToTimeInForce(val) + if err != nil { + return err + } + *t |= tif + } + return nil +} + +// MarshalJSON returns the JSON-encoded order time-in-force value +func (t TimeInForce) MarshalJSON() ([]byte, error) { + return []byte(`"` + t.String() + `"`), nil +} diff --git a/exchanges/order/timeinforce_test.go b/exchanges/order/timeinforce_test.go new file mode 100644 index 00000000..dc6e13b7 --- /dev/null +++ b/exchanges/order/timeinforce_test.go @@ -0,0 +1,168 @@ +package order + +import ( + "strings" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "github.com/thrasher-corp/gocryptotrader/encoding/json" +) + +func TestTimeInForceIs(t *testing.T) { + t.Parallel() + tifValuesMap := map[TimeInForce][]TimeInForce{ + GoodTillCancel | PostOnly: {GoodTillCancel, PostOnly}, + GoodTillCancel: {GoodTillCancel}, + GoodTillCrossing | PostOnly: {GoodTillCrossing, PostOnly}, + GoodTillDay: {GoodTillDay}, + GoodTillTime: {GoodTillTime}, + GoodTillTime | PostOnly: {GoodTillTime, PostOnly}, + ImmediateOrCancel: {ImmediateOrCancel}, + FillOrKill: {FillOrKill}, + PostOnly: {PostOnly}, + GoodTillCrossing: {GoodTillCrossing}, + } + for tif, exps := range tifValuesMap { + for _, v := range exps { + require.Truef(t, tif.Is(v), "%s should be %s", tif, v) + } + } +} + +func TestIsValid(t *testing.T) { + t.Parallel() + timeInForceValidityMap := map[TimeInForce]bool{ + TimeInForce(1): false, + ImmediateOrCancel: true, + GoodTillTime: true, + GoodTillCancel: true, + GoodTillDay: true, + FillOrKill: true, + PostOnly: true, + FillOrKill | ImmediateOrCancel: false, + FillOrKill | GoodTillCancel: false, + FillOrKill | PostOnly: false, + ImmediateOrCancel | GoodTillCancel: false, + ImmediateOrCancel | PostOnly: false, + GoodTillTime | PostOnly: true, + GoodTillDay | PostOnly: true, + GoodTillCrossing | PostOnly: true, + GoodTillCancel | PostOnly: true, + UnknownTIF: true, + } + for tif, value := range timeInForceValidityMap { + assert.Equal(t, value, tif.IsValid()) + } +} + +var timeInForceStringToValueMap = map[string]struct { + TIF TimeInForce + Error error +}{ + "GoodTillCancel": {TIF: GoodTillCancel}, + "GOOD_TILL_CANCELED": {TIF: GoodTillCancel}, + "GTT": {TIF: GoodTillTime}, + "GOOD_TIL_TIME": {TIF: GoodTillTime}, + "FILLORKILL": {TIF: FillOrKill}, + "immedIate_Or_Cancel": {TIF: ImmediateOrCancel}, + "IOC": {TIF: ImmediateOrCancel}, + "immediate_or_cancel": {TIF: ImmediateOrCancel}, + "IMMEDIATE_OR_CANCEL": {TIF: ImmediateOrCancel}, + "IMMEDIATEORCANCEL": {TIF: ImmediateOrCancel}, + "GOOD_TILL_CANCELLED": {TIF: GoodTillCancel}, + "good_till_day": {TIF: GoodTillDay}, + "GOOD_TILL_DAY": {TIF: GoodTillDay}, + "GTD": {TIF: GoodTillDay}, + "GOODtillday": {TIF: GoodTillDay}, + "PoC": {TIF: PostOnly}, + "PendingORCANCEL": {TIF: PostOnly}, + "GTX": {TIF: GoodTillCrossing}, + "GOOD_TILL_CROSSING": {TIF: GoodTillCrossing}, + "Good Til crossing": {TIF: GoodTillCrossing}, + "abcdfeg": {TIF: UnknownTIF, Error: ErrInvalidTimeInForce}, +} + +func TestStringToTimeInForce(t *testing.T) { + t.Parallel() + for tk, exp := range timeInForceStringToValueMap { + t.Run(tk, func(t *testing.T) { + t.Parallel() + result, err := StringToTimeInForce(tk) + if exp.Error != nil { + require.ErrorIs(t, err, exp.Error) + } else { + require.NoError(t, err) + } + assert.Equal(t, exp.TIF, result) + }) + } +} + +func TestString(t *testing.T) { + t.Parallel() + valMap := map[TimeInForce]string{ + ImmediateOrCancel: "IOC", + GoodTillCancel: "GTC", + GoodTillTime: "GTT", + GoodTillDay: "GTD", + FillOrKill: "FOK", + UnknownTIF: "", + PostOnly: "POSTONLY", + GoodTillCancel | PostOnly: "GTC,POSTONLY", + GoodTillTime | PostOnly: "GTT,POSTONLY", + GoodTillDay | PostOnly: "GTD,POSTONLY", + FillOrKill | ImmediateOrCancel: "IOC,FOK", + TimeInForce(1): "UNKNOWN", + } + for x := range valMap { + assert.Equal(t, valMap[x], x.String()) + assert.Equal(t, strings.ToLower(valMap[x]), x.Lower()) + } +} + +func TestUnmarshalJSON(t *testing.T) { + t.Parallel() + targets := []TimeInForce{ + GoodTillCancel | PostOnly | ImmediateOrCancel, GoodTillCancel | PostOnly, GoodTillCancel, UnknownTIF, PostOnly | ImmediateOrCancel, + GoodTillCancel, GoodTillCancel, PostOnly, PostOnly, ImmediateOrCancel, GoodTillDay, GoodTillDay, GoodTillTime, FillOrKill, FillOrKill, + } + data := `{"tifs": ["GTC,POSTONLY,IOC", "GTC,POSTONLY", "GTC", "", "POSTONLY,IOC", "GoodTilCancel", "GoodTILLCANCEL", "POST_ONLY", "POC","IOC", "GTD", "gtd","gtt", "fok", "fillOrKill"]}` + target := &struct { + TIFs []TimeInForce `json:"tifs"` + }{} + err := json.Unmarshal([]byte(data), &target) + require.NoError(t, err) + require.Equal(t, targets, target.TIFs) + + data = `{"tifs": ["abcd,POSTONLY,IOC", "GTC,POSTONLY", "GTC", "", "POSTONLY,IOC", "GoodTilCancel", "GoodTILLCANCEL", "POST_ONLY", "POC","IOC", "GTD", "gtd","gtt", "fok", "fillOrKill"]}` + target = &struct { + TIFs []TimeInForce `json:"tifs"` + }{} + err = json.Unmarshal([]byte(data), &target) + require.ErrorIs(t, err, ErrInvalidTimeInForce) +} + +func TestMarshalJSON(t *testing.T) { + t.Parallel() + data, err := json.Marshal(GoodTillCrossing) + require.NoError(t, err) + assert.Equal(t, []byte(`"GTX"`), data) + + data = []byte(`{"tif":"IOC"}`) + target := &struct { + TimeInForce TimeInForce `json:"tif"` + }{} + err = json.Unmarshal(data, &target) + require.NoError(t, err) + assert.Equal(t, "IOC", target.TimeInForce.String()) +} + +// BenchmarkStringToTimeInForce-8 416595 2834 ns/op 1368 B/op 81 allocs/op +func BenchmarkStringToTimeInForce(b *testing.B) { + for b.Loop() { + for k := range timeInForceStringToValueMap { + _, _ = StringToTimeInForce(k) + } + } +} diff --git a/exchanges/poloniex/poloniex_wrapper.go b/exchanges/poloniex/poloniex_wrapper.go index ae427f26..c84aa24f 100644 --- a/exchanges/poloniex/poloniex_wrapper.go +++ b/exchanges/poloniex/poloniex_wrapper.go @@ -549,8 +549,8 @@ func (p *Poloniex) ModifyOrder(ctx context.Context, action *order.Modify) (*orde oID, action.Price, action.Amount, - action.PostOnly, - action.ImmediateOrCancel) + action.TimeInForce.Is(order.PostOnly), + action.TimeInForce.Is(order.ImmediateOrCancel)) if err != nil { return nil, err }