diff --git a/cmd/exchange_wrapper_standards/exchange_wrapper_standards_test.go b/cmd/exchange_wrapper_standards/exchange_wrapper_standards_test.go index 23717b2b7de..3c232b1a794 100644 --- a/cmd/exchange_wrapper_standards/exchange_wrapper_standards_test.go +++ b/cmd/exchange_wrapper_standards/exchange_wrapper_standards_test.go @@ -443,30 +443,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.IOC, + 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.IOC, }) case argGenerator.MethodInputType.AssignableTo(orderCancelParam): input = reflect.ValueOf(&order.Cancel{ diff --git a/engine/order_manager.go b/engine/order_manager.go index 32ed6f15fb5..252acf1ebb7 100644 --- a/engine/order_manager.go +++ b/engine/order_manager.go @@ -404,10 +404,10 @@ 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 // Used by Bithumb. + mod.Side = det.Side // Used by Bithumb. + mod.PostOnly = det.PostOnly // Used by Poloniex. + 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 c76b40af8eb..be359f23c21 100644 --- a/exchanges/binance/binance.go +++ b/exchanges/binance/binance.go @@ -603,7 +603,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_test.go b/exchanges/binance/binance_test.go index 89eca9cc784..fb1f22f3561 100644 --- a/exchanges/binance/binance_test.go +++ b/exchanges/binance/binance_test.go @@ -1419,7 +1419,7 @@ func TestNewOrderTest(t *testing.T) { TradeType: BinanceRequestParamsOrderLimit, Price: 0.0025, Quantity: 100000, - TimeInForce: BinanceRequestParamsTimeGTC, + TimeInForce: order.GTC.String(), } err := b.NewOrderTest(context.Background(), req) diff --git a/exchanges/binance/binance_types.go b/exchanges/binance/binance_types.go index 15172903b6e..766e5f19929 100644 --- a/exchanges/binance/binance_types.go +++ b/exchanges/binance/binance_types.go @@ -374,7 +374,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. diff --git a/exchanges/binance/binance_wrapper.go b/exchanges/binance/binance_wrapper.go index 5aa470c5c05..e5d75dcd410 100644 --- a/exchanges/binance/binance_wrapper.go +++ b/exchanges/binance/binance_wrapper.go @@ -922,15 +922,15 @@ func (b *Binance) SubmitOrder(ctx context.Context, s *order.Submit) (*order.Subm } else { sideType = order.Sell.String() } - timeInForce := BinanceRequestParamsTimeGTC + timeInForce := order.GTC.String() var requestParamsOrderType RequestParamsOrderType switch s.Type { case order.Market: timeInForce = "" requestParamsOrderType = BinanceRequestParamsOrderMarket case order.Limit: - if s.ImmediateOrCancel { - timeInForce = BinanceRequestParamsTimeIOC + if s.TimeInForce == order.IOC { + timeInForce = order.IOC.String() } requestParamsOrderType = BinanceRequestParamsOrderLimit default: diff --git a/exchanges/btcmarkets/btcmarkets.go b/exchanges/btcmarkets/btcmarkets.go index 7ca254cd86d..c54cf64f24f 100644 --- a/exchanges/btcmarkets/btcmarkets.go +++ b/exchanges/btcmarkets/btcmarkets.go @@ -376,13 +376,12 @@ 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 + switch s.TimeInForce { + case order.IOC, order.FOK: + return s.TimeInForce.String() + default: + return "" // GTC (good till cancelled, default value) } - return "" // GTC (good till cancelled, default value) } // NewOrder requests a new order and returns an ID diff --git a/exchanges/btcmarkets/btcmarkets_test.go b/exchanges/btcmarkets/btcmarkets_test.go index 06753f4cdcb..c8a8da40f5d 100644 --- a/exchanges/btcmarkets/btcmarkets_test.go +++ b/exchanges/btcmarkets/btcmarkets_test.go @@ -972,12 +972,12 @@ func TestGetTimeInForce(t *testing.T) { t.Fatal("unexpected value") } - f = b.getTimeInForce(&order.Submit{ImmediateOrCancel: true}) + f = b.getTimeInForce(&order.Submit{TimeInForce: order.IOC}) if f != immediateOrCancel { t.Fatalf("received: '%v' but expected: '%v'", f, immediateOrCancel) } - f = b.getTimeInForce(&order.Submit{FillOrKill: true}) + f = b.getTimeInForce(&order.Submit{TimeInForce: order.FOK}) if f != fillOrKill { t.Fatalf("received: '%v' but expected: '%v'", f, fillOrKill) } diff --git a/exchanges/coinbasepro/coinbasepro.go b/exchanges/coinbasepro/coinbasepro.go index 6398745ea20..707983beeb9 100644 --- a/exchanges/coinbasepro/coinbasepro.go +++ b/exchanges/coinbasepro/coinbasepro.go @@ -334,7 +334,7 @@ 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) { +func (c *CoinbasePro) PlaceLimitOrder(ctx context.Context, clientRef, side, timeInforce, cancelAfter, productID, stp string, price, amount float64, postOnly bool) (string, error) { resp := GeneralizedOrderResponse{} req := make(map[string]interface{}) req["type"] = order.Limit.Lower() diff --git a/exchanges/coinbasepro/coinbasepro_test.go b/exchanges/coinbasepro/coinbasepro_test.go index 0b04d4ab64b..a362345529e 100644 --- a/exchanges/coinbasepro/coinbasepro_test.go +++ b/exchanges/coinbasepro/coinbasepro_test.go @@ -182,8 +182,8 @@ func TestAuthRequests(t *testing.T) { t.Error("Expecting error") } orderResponse, err := c.PlaceLimitOrder(context.Background(), - "", 0.001, 0.001, - order.Buy.Lower(), "", "", testPair.String(), "", false) + "", + order.Buy.Lower(), "", "", testPair.String(), "", 0.001, 0.001, false) if orderResponse != "" { t.Error("Expecting no data returned") } diff --git a/exchanges/coinbasepro/coinbasepro_types.go b/exchanges/coinbasepro/coinbasepro_types.go index 2a8765edba7..cb7d44629ec 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 21a34e2ea22..1fd2820918d 100644 --- a/exchanges/coinbasepro/coinbasepro_wrapper.go +++ b/exchanges/coinbasepro/coinbasepro_wrapper.go @@ -475,19 +475,19 @@ 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.GTC.String() + if s.TimeInForce == order.IOC { + timeInForce = order.IOC.String() } orderID, err = c.PlaceLimitOrder(ctx, "", - s.Price, - s.Amount, s.Side.Lower(), timeInForce, "", fPair.String(), "", + s.Price, + s.Amount, false) default: err = fmt.Errorf("%w %v", order.ErrUnsupportedOrderType, s.Type) diff --git a/exchanges/gateio/gateio_test.go b/exchanges/gateio/gateio_test.go index 74c33b02ec3..5696a848df5 100644 --- a/exchanges/gateio/gateio_test.go +++ b/exchanges/gateio/gateio_test.go @@ -3546,7 +3546,7 @@ func TestGetTimeInForce(t *testing.T) { require.NoError(t, err) assert.Equal(t, "gtc", ret) - ret, err = getTimeInForce(&order.Submit{Type: order.Market, FillOrKill: true}) + ret, err = getTimeInForce(&order.Submit{Type: order.Market, TimeInForce: order.FOK}) require.NoError(t, err) assert.Equal(t, "fok", ret) } diff --git a/exchanges/gateio/gateio_wrapper.go b/exchanges/gateio/gateio_wrapper.go index 1e67f41fa7b..83a27415466 100644 --- a/exchanges/gateio/gateio_wrapper.go +++ b/exchanges/gateio/gateio_wrapper.go @@ -2561,7 +2561,7 @@ var errPostOnlyOrderTypeUnsupported = errors.New("post only is only supported fo // IOC func getTimeInForce(s *order.Submit) (string, error) { timeInForce := "gtc" // limit order taker/maker - if s.Type == order.Market || s.ImmediateOrCancel { + if s.Type == order.Market || s.TimeInForce == order.IOC { timeInForce = "ioc" // market taker only } if s.PostOnly { @@ -2570,7 +2570,7 @@ func getTimeInForce(s *order.Submit) (string, error) { } timeInForce = "poc" // limit order maker only } - if s.FillOrKill { + if s.TimeInForce == order.IOC { timeInForce = "fok" // market order entire fill or kill } return timeInForce, nil diff --git a/exchanges/huobi/huobi_wrapper.go b/exchanges/huobi/huobi_wrapper.go index 90d70491bd8..8361690686a 100644 --- a/exchanges/huobi/huobi_wrapper.go +++ b/exchanges/huobi/huobi_wrapper.go @@ -1109,7 +1109,7 @@ func (h *HUOBI) SubmitOrder(ctx context.Context, s *order.Submit) (*order.Submit // 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. oType = "optimal_20" - if s.ImmediateOrCancel { + if s.TimeInForce == order.IOC { oType = "optimal_20_ioc" } case order.Limit: diff --git a/exchanges/kraken/kraken.go b/exchanges/kraken/kraken.go index 730a61f34f2..52ddce419b9 100644 --- a/exchanges/kraken/kraken.go +++ b/exchanges/kraken/kraken.go @@ -924,7 +924,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) } if err := k.SendAuthenticatedHTTPRequest(ctx, exchange.RestSpot, krakenOrderPlace, params, &response); err != nil { diff --git a/exchanges/kraken/kraken_types.go b/exchanges/kraken/kraken_types.go index dcd51b155d6..9c1748a7bb5 100644 --- a/exchanges/kraken/kraken_types.go +++ b/exchanges/kraken/kraken_types.go @@ -430,7 +430,7 @@ type AddOrderOptions struct { ClosePrice float64 ClosePrice2 float64 Validate bool - TimeInForce RequestParamsTimeForceType + TimeInForce string } // CancelOrderResponse type @@ -678,25 +678,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 @@ -733,13 +733,3 @@ type OrderVars struct { OrderType order.Type Fee float64 } - -// RequestParamsTimeForceType Time in force -type RequestParamsTimeForceType string - -var ( - // RequestParamsTimeGTC GTC - RequestParamsTimeGTC = RequestParamsTimeForceType("GTC") - // RequestParamsTimeIOC IOC - RequestParamsTimeIOC = RequestParamsTimeForceType("IOC") -) diff --git a/exchanges/kraken/kraken_wrapper.go b/exchanges/kraken/kraken_wrapper.go index 5875917592c..70ef9403f16 100644 --- a/exchanges/kraken/kraken_wrapper.go +++ b/exchanges/kraken/kraken_wrapper.go @@ -748,9 +748,9 @@ 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 + timeInForce := order.GTC.String() + if s.TimeInForce == order.IOC { + timeInForce = s.TimeInForce.String() } if k.Websocket.CanUseAuthenticatedWebsocketForWrapper() { orderID, err = k.wsAddOrder(&WsAddOrderRequest{ @@ -796,7 +796,7 @@ func (k *Kraken) SubmitOrder(ctx context.Context, s *order.Submit) (*order.Submi "", s.ClientOrderID, "", - s.ImmediateOrCancel, + s.TimeInForce == order.IOC, s.Amount, s.Price, 0, diff --git a/exchanges/kucoin/kucoin_wrapper.go b/exchanges/kucoin/kucoin_wrapper.go index 18d71434275..e5b435861b6 100644 --- a/exchanges/kucoin/kucoin_wrapper.go +++ b/exchanges/kucoin/kucoin_wrapper.go @@ -732,9 +732,9 @@ func (ku *Kucoin) SubmitOrder(ctx context.Context, s *order.Submit) (*order.Subm timeInForce := "" if s.Type == order.Limit { switch { - case s.FillOrKill: + case s.TimeInForce == order.FOK: timeInForce = "FOK" - case s.ImmediateOrCancel: + case s.TimeInForce == order.IOC: timeInForce = "IOC" case s.PostOnly: default: diff --git a/exchanges/order/order_test.go b/exchanges/order/order_test.go index 31e5b8c739f..4da1993898c 100644 --- a/exchanges/order/order_test.go +++ b/exchanges/order/order_test.go @@ -62,15 +62,14 @@ func TestSubmit_Validate(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), }, }, { @@ -998,24 +997,24 @@ func TestUpdateOrderFromModifyResponse(t *testing.T) { } 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: IOC, + PostOnly: true, + 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 { + if od.TimeInForce == UnknownTIF { t.Error("Failed to update") } if !od.PostOnly { @@ -1084,34 +1083,33 @@ func TestUpdateOrderFromDetail(t *testing.T) { } 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", - WalletAddress: "1", - Type: 1, - Side: 1, - Status: 1, - AssetType: 1, - LastUpdated: updated, - Pair: pair, - Trades: []TradeHistory{}, + TimeInForce: GTC, + HiddenOrder: 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", + WalletAddress: "1", + Type: 1, + Side: 1, + Status: 1, + AssetType: 1, + LastUpdated: updated, + Pair: pair, + Trades: []TradeHistory{}, } od = &Detail{Exchange: "test"} @@ -1128,15 +1126,12 @@ func TestUpdateOrderFromDetail(t *testing.T) { if od.InternalOrderID != id { t.Error("Failed to initialize the internal order ID") } - if !od.ImmediateOrCancel { + if od.TimeInForce != GTC { 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") } @@ -2062,3 +2057,112 @@ func TestSideUnmarshal(t *testing.T) { var jErr *json.UnmarshalTypeError assert.ErrorAs(t, s.UnmarshalJSON([]byte(`14`)), &jErr, "non-string valid json is rejected") } + +func TestIsValid(t *testing.T) { + t.Parallel() + if TimeInForce(50).IsValid() { + t.Fatal("TestIsValid returned an unexpected result") + } + + if !IOC.IsValid() { + t.Fatal("TestIsValid returned an unexpected result") + } + if !GTT.IsValid() { + t.Fatal("TestIsValid returned an unexpected result") + } + if !GTC.IsValid() { + t.Fatal("TestIsValid returned an unexpected result") + } + if !FOK.IsValid() { + t.Fatal("TestIsValid returned an unexpected result") + } + if !PostOnlyGTC.IsValid() { + t.Fatal("TestIsValid returned an unexpected result") + } + if !UnknownTIF.IsValid() { + t.Fatal("TestIsValid returned an unexpected result") + } + if TimeInForce(22).IsValid() { + t.Fatal("TestIsValid returned an unexpected result") + } +} + +func TestStringToTimeInForce(t *testing.T) { + t.Parallel() + _, err := StringToTimeInForce("Unknown") + if !errors.Is(err, ErrInvalidTimeInForce) { + t.Fatalf("expected %v, got %v", ErrInvalidTimeInForce, err) + } + tif1, err := StringToTimeInForce("GoodTillCancel") + if err != nil { + t.Fatal(err) + } + + tif, err := StringToTimeInForce("GOOD_TILL_CANCELED") + if err != nil { + t.Fatal(err) + } else if tif1 != tif { + t.Fatalf("unexpected result") + } + + tif, err = StringToTimeInForce("GTT") + if err != nil { + t.Fatal(err) + } else if tif != GTT { + t.Fatalf("expected %v, got %v", GTT, tif) + } + + tif, err = StringToTimeInForce("GOOD_TIL_TIME") + if err != nil { + t.Fatal(err) + } else if tif != GTT { + t.Fatalf("expected %v, got %v", GTT, tif) + } + + tif, err = StringToTimeInForce("FILLORKILL") + if err != nil { + t.Error(err) + } else if tif != FOK { + t.Fatalf("expected %v, got %v", FOK, tif) + } + + tif, err = StringToTimeInForce("POST_ONLY_GOOD_TIL_CANCELLED") + if err != nil { + t.Fatal(err) + } else if tif != PostOnlyGTC { + t.Fatalf("expected %v, got %v", PostOnlyGTC, tif) + } + + tif, err = StringToTimeInForce("immedIate_Or_Cancel") + if err != nil { + t.Fatal(err) + } else if tif != IOC { + t.Fatalf("expected %v, got %v", IOC, tif) + } + + _, err = StringToTimeInForce("") + if !errors.Is(err, ErrInvalidTimeInForce) { + t.Fatalf("expected %v, got %v", ErrInvalidTimeInForce, err) + } + _, err = StringToTimeInForce("IOC") + if err != nil { + t.Fatal(err) + } +} + +func TestString(t *testing.T) { + t.Parallel() + valMap := map[string]TimeInForce{ + "IOC": IOC, + "GTC": GTC, + "GTT": GTT, + "FOK": FOK, + "POST_ONLY_GOOD_TIL_CANCELLED": PostOnlyGTC, + "UNKNOWN": UnknownTIF, + } + for x := range valMap { + if result := valMap[x].String(); x != result { + t.Fatalf("expected %v, got %v", x, result) + } + } +} diff --git a/exchanges/order/order_types.go b/exchanges/order/order_types.go index 9e47c2fa07d..5c851301069 100644 --- a/exchanges/order/order_types.go +++ b/exchanges/order/order_types.go @@ -44,9 +44,8 @@ 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 @@ -101,18 +100,19 @@ type SubmitResponse struct { Pair currency.Pair AssetType asset.Item - ImmediateOrCancel bool - FillOrKill bool + TimeInForce TimeInForce PostOnly bool ReduceOnly bool Leverage float64 Price float64 - AverageExecutedPrice float64 Amount float64 QuoteAmount float64 TriggerPrice float64 ClientID string ClientOrderID string + ImmediateOrCancel bool + FillOrKill bool + AverageExecutedPrice float64 LastUpdated time.Time Date time.Time @@ -143,11 +143,11 @@ type Modify struct { Pair currency.Pair // Change fields - ImmediateOrCancel bool - PostOnly bool - Price float64 - Amount float64 - TriggerPrice float64 + TimeInForce TimeInForce + PostOnly bool + 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 @@ -169,11 +169,11 @@ 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 + PostOnly bool + Price float64 + Amount float64 + TriggerPrice float64 // Fields that need to be handled in scope after DeriveModifyResponse() // if applicable @@ -186,9 +186,8 @@ type ModifyResponse struct { // Each exchange has their own requirements, so not all fields // are required to be populated type Detail struct { - ImmediateOrCancel bool HiddenOrder bool - FillOrKill bool + TimeInForce TimeInForce PostOnly bool ReduceOnly bool Leverage float64 @@ -385,6 +384,21 @@ const ( MissingData ) +// TimeInForce enforces a standard for time-in-force values across the code base. +type TimeInForce uint8 + +// TimeInForce types +const ( + UnknownTIF TimeInForce = 0 + GTC TimeInForce = iota // GTC represents GoodTillCancel + GTT // GTT represents GoodTillTime + FOK // FOK represents FillOrKill + IOC // IOC represents ImmediateOrCancel + PostOnlyGTC // PostOnlyGCT represents PostOnlyGoodTilCancelled + + supportedTimeInForceFlag = UnknownTIF | GTC | GTT | FOK | IOC | PostOnlyGTC +) + // ByPrice used for sorting orders by price type ByPrice []Detail diff --git a/exchanges/order/orders.go b/exchanges/order/orders.go index 3a4dfdd97a2..018762dfd75 100644 --- a/exchanges/order/orders.go +++ b/exchanges/order/orders.go @@ -36,6 +36,8 @@ var ( ErrUnableToPlaceOrder = errors.New("order not placed") // ErrOrderNotFound is returned when no order is found ErrOrderNotFound = errors.New("order not found") + // ErrInvalidTimeInForce is returned when an invalid time-in-force value is provided + ErrInvalidTimeInForce = errors.New("invalid time in force value provided") // ErrUnknownPriceType returned when price type is unknown ErrUnknownPriceType = errors.New("unknown price type") @@ -85,8 +87,8 @@ func (s *Submit) Validate(opt ...validate.Checker) error { return ErrTypeIsInvalid } - if s.ImmediateOrCancel && s.FillOrKill { - return errTimeInForceConflict + if !s.TimeInForce.IsValid() { + return ErrInvalidTimeInForce } if s.Amount == 0 && s.QuoteAmount == 0 { @@ -127,18 +129,14 @@ func (d *Detail) UpdateOrderFromDetail(m *Detail) error { } var updated bool - if d.ImmediateOrCancel != m.ImmediateOrCancel { - d.ImmediateOrCancel = m.ImmediateOrCancel + if 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 @@ -294,8 +292,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 { @@ -470,18 +468,17 @@ 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, + 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, LastUpdated: time.Now(), Date: time.Now(), @@ -563,17 +560,16 @@ 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, + 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, InternalOrderID: internal, @@ -623,18 +619,18 @@ 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, + PostOnly: m.PostOnly, + Price: m.Price, + Amount: m.Amount, + TriggerPrice: m.TriggerPrice, }, nil } @@ -701,6 +697,25 @@ func (t Type) String() string { } } +// String implements the stringer interface. +func (t TimeInForce) String() string { + switch t { + case IOC: + return "IOC" + case GTC: + return "GTC" + case GTT: + return "GTT" + case FOK: + return "FOK" + case PostOnlyGTC: + // Added in Bittrex exchange to represent PostOnly and GTC + return "POST_ONLY_GOOD_TIL_CANCELLED" + default: + return "UNKNOWN" + } +} + // Lower returns the type lower case string func (t Type) Lower() string { return strings.ToLower(t.String()) @@ -1166,6 +1181,31 @@ func StringToOrderStatus(status string) (Status, error) { } } +// StringToTimeInForce converts time in force string value to TimeInForce instance. +func StringToTimeInForce(timeInForce string) (TimeInForce, error) { + timeInForce = strings.ToUpper(timeInForce) + switch timeInForce { + case "IMMEDIATEORCANCEL", "IMMEDIATE_OR_CANCEL", IOC.String(): + return IOC, nil + case "GOODTILLCANCEL", "GOOD_TIL_CANCELLED", "GOOD_TILL_CANCELLED", "GOOD_TILL_CANCELED", GTC.String(): + return GTC, nil + case "GOODTILLTIME", "GOOD_TIL_TIME", GTT.String(): + return GTT, nil + case "FILLORKILL", "FILL_OR_KILL", FOK.String(): + return FOK, nil + case "POST_ONLY_GOOD_TILL_CANCELLED", PostOnlyGTC.String(): + return PostOnlyGTC, nil + default: + return UnknownTIF, fmt.Errorf("%w, tif=%s", ErrInvalidTimeInForce, timeInForce) + } +} + +// IsValid returns whether or not the supplied time in force value is valid or +// not +func (t TimeInForce) IsValid() bool { + return t == UnknownTIF || supportedTimeInForceFlag&t == t +} + func (o *ClassificationError) Error() string { if o.OrderID != "" { return fmt.Sprintf("Exchange %s: OrderID: %s classification error: %v", diff --git a/exchanges/poloniex/poloniex_wrapper.go b/exchanges/poloniex/poloniex_wrapper.go index 57f28fac98b..6bb0b646e57 100644 --- a/exchanges/poloniex/poloniex_wrapper.go +++ b/exchanges/poloniex/poloniex_wrapper.go @@ -602,7 +602,7 @@ func (p *Poloniex) ModifyOrder(ctx context.Context, action *order.Modify) (*orde action.Price, action.Amount, action.PostOnly, - action.ImmediateOrCancel) + action.TimeInForce == order.IOC) if err != nil { return nil, err }