From 5412e914b5046c60ac26debaaf451981563f9a76 Mon Sep 17 00:00:00 2001 From: Samuael A <39623015+samuael@users.noreply.github.com> Date: Sun, 29 Oct 2023 21:28:08 +0300 Subject: [PATCH 1/8] Added TimeInForce type and updated related files --- .../exchange_wrapper_standards_test.go | 40 +++--- engine/order_manager.go | 8 +- exchanges/binance/binance_test.go | 2 +- exchanges/binance/binance_types.go | 2 +- exchanges/binance/binance_wrapper.go | 6 +- exchanges/bittrex/bittrex_websocket.go | 38 ++--- exchanges/bittrex/bittrex_wrapper.go | 28 ++-- exchanges/btcmarkets/btcmarkets.go | 11 +- exchanges/btcmarkets/btcmarkets_test.go | 4 +- exchanges/coinbasepro/coinbasepro.go | 2 +- exchanges/coinbasepro/coinbasepro_wrapper.go | 6 +- exchanges/huobi/huobi_wrapper.go | 2 +- exchanges/kraken/kraken_types.go | 50 +++---- exchanges/kraken/kraken_wrapper.go | 8 +- exchanges/order/order_test.go | 127 +++++++++-------- exchanges/order/order_types.go | 66 +++++---- exchanges/order/orders.go | 130 +++++++++++------- exchanges/poloniex/poloniex_wrapper.go | 2 +- 18 files changed, 299 insertions(+), 233 deletions(-) diff --git a/cmd/exchange_wrapper_standards/exchange_wrapper_standards_test.go b/cmd/exchange_wrapper_standards/exchange_wrapper_standards_test.go index 995c40ecce4..e19588c4c3b 100644 --- a/cmd/exchange_wrapper_standards/exchange_wrapper_standards_test.go +++ b/cmd/exchange_wrapper_standards/exchange_wrapper_standards_test.go @@ -420,29 +420,29 @@ 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: 1337, - Amount: 1, - ClientID: "1337", - ClientOrderID: "13371337", - ImmediateOrCancel: true, + Exchange: exchName, + Type: order.Limit, + Side: order.Buy, + Pair: argGenerator.AssetParams.Pair, + AssetType: argGenerator.AssetParams.Asset, + Price: 1337, + Amount: 1, + ClientID: "1337", + ClientOrderID: "13371337", + TimeInForce: order.IOC, }) 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: 1337, - 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: 1337, + 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 8224a3d3178..3bd830d8bd1 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_test.go b/exchanges/binance/binance_test.go index 8cc074b0cc8..c6e92fb995c 100644 --- a/exchanges/binance/binance_test.go +++ b/exchanges/binance/binance_test.go @@ -1443,7 +1443,7 @@ func TestNewOrderTest(t *testing.T) { TradeType: BinanceRequestParamsOrderLimit, Price: 0.0025, Quantity: 100000, - TimeInForce: BinanceRequestParamsTimeGTC, + TimeInForce: order.GoodTillCancel.String(), } err := b.NewOrderTest(context.Background(), req) diff --git a/exchanges/binance/binance_types.go b/exchanges/binance/binance_types.go index 700a288d252..d3bd6f13bfa 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 dedc56f2b2e..6168fcefa9b 100644 --- a/exchanges/binance/binance_wrapper.go +++ b/exchanges/binance/binance_wrapper.go @@ -990,15 +990,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 == order.IOC { + timeInForce = order.IOC.String() } requestParamsOrderType = BinanceRequestParamsOrderLimit default: diff --git a/exchanges/bittrex/bittrex_websocket.go b/exchanges/bittrex/bittrex_websocket.go index 565b455763b..4cf480ff91f 100644 --- a/exchanges/bittrex/bittrex_websocket.go +++ b/exchanges/bittrex/bittrex_websocket.go @@ -602,23 +602,29 @@ func (b *Bittrex) WsProcessUpdateOrder(data *OrderUpdateMessage) error { Err: err, } } - + timeInForce, err := order.StringToTimeInForce(data.Delta.TimeInForce) + if err != nil { + b.Websocket.DataHandler <- order.ClassificationError{ + Exchange: b.Name, + OrderID: data.Delta.ID, + Err: err, + } + } b.Websocket.DataHandler <- &order.Detail{ - ImmediateOrCancel: data.Delta.TimeInForce == string(ImmediateOrCancel), - FillOrKill: data.Delta.TimeInForce == string(GoodTilCancelled), - PostOnly: data.Delta.TimeInForce == string(PostOnlyGoodTilCancelled), - Price: data.Delta.Limit, - Amount: data.Delta.Quantity, - RemainingAmount: data.Delta.Quantity - data.Delta.FillQuantity, - ExecutedAmount: data.Delta.FillQuantity, - Exchange: b.Name, - OrderID: data.Delta.ID, - Type: orderType, - Side: orderSide, - Status: orderStatus, - AssetType: asset.Spot, - Date: data.Delta.CreatedAt, - Pair: pair, + TimeInForce: timeInForce, + PostOnly: data.Delta.TimeInForce == string(PostOnlyGoodTilCancelled), + Price: data.Delta.Limit, + Amount: data.Delta.Quantity, + RemainingAmount: data.Delta.Quantity - data.Delta.FillQuantity, + ExecutedAmount: data.Delta.FillQuantity, + Exchange: b.Name, + OrderID: data.Delta.ID, + Type: orderType, + Side: orderSide, + Status: orderStatus, + AssetType: asset.Spot, + Date: data.Delta.CreatedAt, + Pair: pair, } return nil } diff --git a/exchanges/bittrex/bittrex_wrapper.go b/exchanges/bittrex/bittrex_wrapper.go index 3b2e65c77c3..0d5b303083e 100644 --- a/exchanges/bittrex/bittrex_wrapper.go +++ b/exchanges/bittrex/bittrex_wrapper.go @@ -698,9 +698,9 @@ func (b *Bittrex) GetOrderInfo(ctx context.Context, orderID string, _ currency.P // ConstructOrderDetail constructs an order detail item from the underlying data func (b *Bittrex) ConstructOrderDetail(orderData *OrderData) (*order.Detail, error) { - immediateOrCancel := false - if orderData.TimeInForce == string(ImmediateOrCancel) { - immediateOrCancel = true + timeInForce, err := order.StringToTimeInForce(orderData.TimeInForce) + if err != nil { + timeInForce = order.UnknownTIF } format, err := b.GetPairFormat(asset.Spot, false) @@ -743,17 +743,17 @@ func (b *Bittrex) ConstructOrderDetail(orderData *OrderData) (*order.Detail, err } return &order.Detail{ - ImmediateOrCancel: immediateOrCancel, - Amount: orderData.Quantity, - ExecutedAmount: orderData.FillQuantity, - RemainingAmount: orderData.Quantity - orderData.FillQuantity, - Price: orderData.Limit, - Date: orderData.CreatedAt, - OrderID: orderData.ID, - Exchange: b.Name, - Type: orderType, - Pair: orderPair, - Status: orderStatus, + TimeInForce: timeInForce, + Amount: orderData.Quantity, + ExecutedAmount: orderData.FillQuantity, + RemainingAmount: orderData.Quantity - orderData.FillQuantity, + Price: orderData.Limit, + Date: orderData.CreatedAt, + OrderID: orderData.ID, + Exchange: b.Name, + Type: orderType, + Pair: orderPair, + Status: orderStatus, }, nil } 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 5820acf5cd4..288a60bb659 100644 --- a/exchanges/btcmarkets/btcmarkets_test.go +++ b/exchanges/btcmarkets/btcmarkets_test.go @@ -987,12 +987,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 e7667fba793..06dabc4739a 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 string, price, amount float64, side, timeInforce string, cancelAfter, productID, stp string, postOnly bool) (string, error) { resp := GeneralizedOrderResponse{} req := make(map[string]interface{}) req["type"] = order.Limit.Lower() diff --git a/exchanges/coinbasepro/coinbasepro_wrapper.go b/exchanges/coinbasepro/coinbasepro_wrapper.go index 4e76dfa75ec..c6ca2d84ba8 100644 --- a/exchanges/coinbasepro/coinbasepro_wrapper.go +++ b/exchanges/coinbasepro/coinbasepro_wrapper.go @@ -563,9 +563,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.IOC { + timeInForce = order.IOC.String() } orderID, err = c.PlaceLimitOrder(ctx, "", diff --git a/exchanges/huobi/huobi_wrapper.go b/exchanges/huobi/huobi_wrapper.go index 3e818d0c65f..5670041bed8 100644 --- a/exchanges/huobi/huobi_wrapper.go +++ b/exchanges/huobi/huobi_wrapper.go @@ -1058,7 +1058,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_types.go b/exchanges/kraken/kraken_types.go index 2f833479ff2..1885748e33c 100644 --- a/exchanges/kraken/kraken_types.go +++ b/exchanges/kraken/kraken_types.go @@ -427,7 +427,7 @@ type AddOrderOptions struct { ClosePrice float64 ClosePrice2 float64 Validate bool - TimeInForce RequestParamsTimeForceType + TimeInForce string } // CancelOrderResponse type @@ -675,25 +675,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 @@ -730,13 +730,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 6e1fc8efa38..99f4bdfd9ba 100644 --- a/exchanges/kraken/kraken_wrapper.go +++ b/exchanges/kraken/kraken_wrapper.go @@ -819,9 +819,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.GoodTillCancel.String() + if s.TimeInForce == order.IOC { + timeInForce = s.TimeInForce.String() } if k.Websocket.CanUseAuthenticatedWebsocketForWrapper() { orderID, err = k.wsAddOrder(&WsAddOrderRequest{ @@ -867,7 +867,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/order/order_test.go b/exchanges/order/order_test.go index c533e272330..78a96745786 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: GoodTillCancel, + 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 != GoodTillCancel { 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,27 @@ func TestSideUnmarshal(t *testing.T) { var jErr *json.UnmarshalTypeError assert.ErrorAs(t, s.UnmarshalJSON([]byte(`14`)), &jErr, "non-string valid json is rejected") } + +func TestSupported(t *testing.T) { + t.Parallel() + s := Supported() + if len(supportedTIFItems) != len(s) { + t.Fatal("TestSupported mismatched lengths") + } + for i := 0; i < len(supportedTIFItems); i++ { + if s[i] != supportedTIFItems[i] { + t.Fatal("TestSupported returned an unexpected result") + } + } +} + +func TestIsValid(t *testing.T) { + t.Parallel() + if TimeInForce(50).IsValid() { + t.Fatal("TestIsValid returned an unexpected result") + } + + if !GoodTillCancel.IsValid() { + t.Fatal("TestIsValid returned an unexpected result") + } +} diff --git a/exchanges/order/order_types.go b/exchanges/order/order_types.go index b6136b5b1e1..050e1beda6c 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 @@ -96,17 +95,16 @@ type SubmitResponse struct { Pair currency.Pair AssetType asset.Item - ImmediateOrCancel bool - FillOrKill bool - PostOnly bool - ReduceOnly bool - Leverage float64 - Price float64 - Amount float64 - QuoteAmount float64 - TriggerPrice float64 - ClientID string - ClientOrderID string + TimeInForce TimeInForce + PostOnly bool + ReduceOnly bool + Leverage float64 + Price float64 + Amount float64 + QuoteAmount float64 + TriggerPrice float64 + ClientID string + ClientOrderID string LastUpdated time.Time Date time.Time @@ -137,11 +135,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 } // ModifyResponse is an order modifying return type @@ -157,11 +155,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 @@ -174,9 +172,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 @@ -371,6 +368,23 @@ const ( MissingData ) +// TimeInForce enforces a standard for time-in-force values accross the code base. +type TimeInForce uint8 + +const ( + UnknownTIF TimeInForce = 0 + GoodTillCancel TimeInForce = iota + GoodTillTime + FOK // FOK represents FillOrKill, used shorter version as the FillOrKill name is reserved for order type value. + IOC // IOC represents ImmediateOrCancel, used shorter version as the ImmediateOrCancel name is reserved for order type value. + + supportedTimeInForceFlag = UnknownTIF | GoodTillCancel | GoodTillTime | FOK | IOC +) + +var ( + supportedTIFItems = []TimeInForce{GoodTillCancel, GoodTillTime, FOK, IOC} +) + // ByPrice used for sorting orders by price type ByPrice []Detail diff --git a/exchanges/order/orders.go b/exchanges/order/orders.go index 98400f66028..be81dfaf3af 100644 --- a/exchanges/order/orders.go +++ b/exchanges/order/orders.go @@ -37,7 +37,7 @@ var ( // ErrOrderNotFound is returned when no order is found ErrOrderNotFound = errors.New("order not found") - errTimeInForceConflict = errors.New("multiple time in force options applied") + errInvalidTimeInForce = errors.New("invalid time in force value provided") errUnrecognisedOrderType = errors.New("unrecognised order type") errUnrecognisedOrderStatus = errors.New("unrecognised order status") errExchangeNameUnset = errors.New("exchange name unset") @@ -82,8 +82,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 { @@ -124,18 +124,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 @@ -291,8 +287,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 { @@ -467,18 +463,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(), @@ -560,17 +555,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, @@ -620,18 +614,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 } @@ -698,6 +692,22 @@ func (t Type) String() string { } } +// String implements the stringer interface. +func (t TimeInForce) String() string { + switch t { + case IOC: + return "IOC" + case GoodTillCancel: + return "GTC" + case GoodTillTime: + return "GTT" + case FOK: + return "FOK" + default: + return "UNKNOWN" + } +} + // Lower returns the type lower case string func (t Type) Lower() string { return strings.ToLower(t.String()) @@ -1159,6 +1169,34 @@ 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 "IOC", "IMMEDIATEORCANCEL", "IMMEDIATE_OR_CANCEL": + return IOC, nil + case "GTC", "GOODTILLCANCEL", "GOOD_TIL_CANCELLED": + return GoodTillCancel, nil + case "GTT", "GOODTILLTIME": + return GoodTillTime, nil + case "FOK", "FILLORKILL", "FILL_OR_KILL": + return FOK, nil + default: + return UnknownTIF, errInvalidTimeInForce + } +} + +// Supported returns a list of supported time in force types +func Supported() []TimeInForce { + return supportedTIFItems +} + +// IsValid returns whether or not the supplied time in force value is valid or +// not +func (a TimeInForce) IsValid() bool { + return a == UnknownTIF || supportedTimeInForceFlag&a == a +} + 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 ee0efc0c139..6d80746fd47 100644 --- a/exchanges/poloniex/poloniex_wrapper.go +++ b/exchanges/poloniex/poloniex_wrapper.go @@ -657,7 +657,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 } From 0bbd6fbc7ee051e1bcf90c2a831639f8ac8d5f1d Mon Sep 17 00:00:00 2001 From: Samuael A <39623015+samuael@users.noreply.github.com> Date: Sun, 29 Oct 2023 21:39:18 +0300 Subject: [PATCH 2/8] Linter issue fix and minor coinbasepro type update --- exchanges/binance/binance.go | 2 +- exchanges/coinbasepro/coinbasepro.go | 2 +- exchanges/coinbasepro/coinbasepro_test.go | 4 ++-- exchanges/coinbasepro/coinbasepro_types.go | 11 ----------- exchanges/coinbasepro/coinbasepro_wrapper.go | 4 ++-- exchanges/kraken/kraken.go | 2 +- exchanges/order/order_types.go | 3 ++- exchanges/order/orders.go | 4 ++-- 8 files changed, 11 insertions(+), 21 deletions(-) diff --git a/exchanges/binance/binance.go b/exchanges/binance/binance.go index d3a40cd80bf..ec65fa9abb3 100644 --- a/exchanges/binance/binance.go +++ b/exchanges/binance/binance.go @@ -629,7 +629,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/coinbasepro/coinbasepro.go b/exchanges/coinbasepro/coinbasepro.go index 06dabc4739a..6ae5e1b04b5 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, timeInforce string, 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 af5c2ab37ef..ddb17686ce1 100644 --- a/exchanges/coinbasepro/coinbasepro_test.go +++ b/exchanges/coinbasepro/coinbasepro_test.go @@ -196,8 +196,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 c6ca2d84ba8..1af57467eb7 100644 --- a/exchanges/coinbasepro/coinbasepro_wrapper.go +++ b/exchanges/coinbasepro/coinbasepro_wrapper.go @@ -569,13 +569,13 @@ func (c *CoinbasePro) SubmitOrder(ctx context.Context, s *order.Submit) (*order. } 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/kraken/kraken.go b/exchanges/kraken/kraken.go index 1fc2318aa50..54b22d69550 100644 --- a/exchanges/kraken/kraken.go +++ b/exchanges/kraken/kraken.go @@ -920,7 +920,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/order/order_types.go b/exchanges/order/order_types.go index 050e1beda6c..708db7cef6d 100644 --- a/exchanges/order/order_types.go +++ b/exchanges/order/order_types.go @@ -368,9 +368,10 @@ const ( MissingData ) -// TimeInForce enforces a standard for time-in-force values accross the code base. +// TimeInForce enforces a standard for time-in-force values across the code base. type TimeInForce uint8 +// TimeInForce types const ( UnknownTIF TimeInForce = 0 GoodTillCancel TimeInForce = iota diff --git a/exchanges/order/orders.go b/exchanges/order/orders.go index be81dfaf3af..6a02cc76ec0 100644 --- a/exchanges/order/orders.go +++ b/exchanges/order/orders.go @@ -1193,8 +1193,8 @@ func Supported() []TimeInForce { // IsValid returns whether or not the supplied time in force value is valid or // not -func (a TimeInForce) IsValid() bool { - return a == UnknownTIF || supportedTimeInForceFlag&a == a +func (t TimeInForce) IsValid() bool { + return t == UnknownTIF || supportedTimeInForceFlag&t == t } func (o *ClassificationError) Error() string { From b786e75dd7787c4fbff6c727ed66ee65b91702ab Mon Sep 17 00:00:00 2001 From: Samuael A <39623015+samuael@users.noreply.github.com> Date: Sun, 29 Oct 2023 22:04:33 +0300 Subject: [PATCH 3/8] Bitrex consts update --- exchanges/bittrex/bittrex_types.go | 10 +++++----- exchanges/bittrex/bittrex_websocket.go | 2 +- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/exchanges/bittrex/bittrex_types.go b/exchanges/bittrex/bittrex_types.go index da217efdcea..73e8349733f 100644 --- a/exchanges/bittrex/bittrex_types.go +++ b/exchanges/bittrex/bittrex_types.go @@ -19,11 +19,11 @@ type TimeInForce string // All order status types const ( - GoodTilCancelled TimeInForce = "GOOD_TIL_CANCELLED" - ImmediateOrCancel TimeInForce = "IMMEDIATE_OR_CANCEL" - FillOrKill TimeInForce = "FILL_OR_KILL" - PostOnlyGoodTilCancelled TimeInForce = "POST_ONLY_GOOD_TIL_CANCELLED" - BuyNow TimeInForce = "BUY_NOW" + GoodTilCancelled = "GOOD_TIL_CANCELLED" + ImmediateOrCancel = "IMMEDIATE_OR_CANCEL" + FillOrKill = "FILL_OR_KILL" + PostOnlyGoodTilCancelled = "POST_ONLY_GOOD_TIL_CANCELLED" + BuyNow = "BUY_NOW" ) // OrderData holds order data diff --git a/exchanges/bittrex/bittrex_websocket.go b/exchanges/bittrex/bittrex_websocket.go index 4cf480ff91f..a62cfd139d4 100644 --- a/exchanges/bittrex/bittrex_websocket.go +++ b/exchanges/bittrex/bittrex_websocket.go @@ -612,7 +612,7 @@ func (b *Bittrex) WsProcessUpdateOrder(data *OrderUpdateMessage) error { } b.Websocket.DataHandler <- &order.Detail{ TimeInForce: timeInForce, - PostOnly: data.Delta.TimeInForce == string(PostOnlyGoodTilCancelled), + PostOnly: data.Delta.TimeInForce == PostOnlyGoodTilCancelled, Price: data.Delta.Limit, Amount: data.Delta.Quantity, RemainingAmount: data.Delta.Quantity - data.Delta.FillQuantity, From afa9695340bbb12655a74bbe5a94fd502c712131 Mon Sep 17 00:00:00 2001 From: Samuael A <39623015+samuael@users.noreply.github.com> Date: Mon, 30 Oct 2023 16:28:03 +0300 Subject: [PATCH 4/8] added unit test and minor changes in bittrex --- exchanges/binance/binance_test.go | 2 +- exchanges/binance/binance_wrapper.go | 2 +- exchanges/bittrex/bittrex.go | 4 +- exchanges/bittrex/bittrex_types.go | 16 ++------ exchanges/bittrex/bittrex_websocket.go | 4 +- exchanges/bittrex/bittrex_wrapper.go | 37 +++++++++++++++--- exchanges/coinbasepro/coinbasepro_wrapper.go | 2 +- exchanges/kraken/kraken_wrapper.go | 2 +- exchanges/order/order_test.go | 41 ++++++++++++-------- exchanges/order/order_types.go | 17 ++++---- exchanges/order/orders.go | 29 +++++++------- 11 files changed, 92 insertions(+), 64 deletions(-) diff --git a/exchanges/binance/binance_test.go b/exchanges/binance/binance_test.go index c6e92fb995c..4f475f3ad82 100644 --- a/exchanges/binance/binance_test.go +++ b/exchanges/binance/binance_test.go @@ -1443,7 +1443,7 @@ func TestNewOrderTest(t *testing.T) { TradeType: BinanceRequestParamsOrderLimit, Price: 0.0025, Quantity: 100000, - TimeInForce: order.GoodTillCancel.String(), + TimeInForce: order.GTC.String(), } err := b.NewOrderTest(context.Background(), req) diff --git a/exchanges/binance/binance_wrapper.go b/exchanges/binance/binance_wrapper.go index 6168fcefa9b..30b70414474 100644 --- a/exchanges/binance/binance_wrapper.go +++ b/exchanges/binance/binance_wrapper.go @@ -990,7 +990,7 @@ func (b *Binance) SubmitOrder(ctx context.Context, s *order.Submit) (*order.Subm } else { sideType = order.Sell.String() } - timeInForce := order.GoodTillCancel.String() + timeInForce := order.GTC.String() var requestParamsOrderType RequestParamsOrderType switch s.Type { case order.Market: diff --git a/exchanges/bittrex/bittrex.go b/exchanges/bittrex/bittrex.go index 3cc0f93c6b1..12fec172259 100644 --- a/exchanges/bittrex/bittrex.go +++ b/exchanges/bittrex/bittrex.go @@ -136,7 +136,7 @@ func (b *Bittrex) GetMarketHistory(ctx context.Context, currency string) ([]Trad } // Order places an order -func (b *Bittrex) Order(ctx context.Context, marketName, side, orderType string, timeInForce TimeInForce, price, amount, ceiling float64) (OrderData, error) { +func (b *Bittrex) Order(ctx context.Context, marketName, side, orderType, timeInForce string, price, amount, ceiling float64) (OrderData, error) { req := make(map[string]interface{}) req["marketSymbol"] = marketName req["direction"] = side @@ -151,7 +151,7 @@ func (b *Bittrex) Order(ctx context.Context, marketName, side, orderType string, if timeInForce != "" { req["timeInForce"] = timeInForce } else { - req["timeInForce"] = GoodTilCancelled + req["timeInForce"] = goodTilCancelled } var resp OrderData return resp, b.SendAuthHTTPRequest(ctx, exchange.RestSpot, http.MethodPost, submitOrder, nil, req, &resp, nil) diff --git a/exchanges/bittrex/bittrex_types.go b/exchanges/bittrex/bittrex_types.go index 73e8349733f..40e3a085d60 100644 --- a/exchanges/bittrex/bittrex_types.go +++ b/exchanges/bittrex/bittrex_types.go @@ -9,23 +9,15 @@ import ( "github.com/thrasher-corp/gocryptotrader/exchanges/stream" ) +const ( + goodTilCancelled = "GOOD_TIL_CANCELLED" +) + // CancelOrderRequest holds request data for CancelOrder type CancelOrderRequest struct { OrderID int64 `json:"orderId,string"` } -// TimeInForce defines timeInForce types -type TimeInForce string - -// All order status types -const ( - GoodTilCancelled = "GOOD_TIL_CANCELLED" - ImmediateOrCancel = "IMMEDIATE_OR_CANCEL" - FillOrKill = "FILL_OR_KILL" - PostOnlyGoodTilCancelled = "POST_ONLY_GOOD_TIL_CANCELLED" - BuyNow = "BUY_NOW" -) - // OrderData holds order data type OrderData struct { ID string `json:"id"` diff --git a/exchanges/bittrex/bittrex_websocket.go b/exchanges/bittrex/bittrex_websocket.go index a62cfd139d4..b0cb9f0bcad 100644 --- a/exchanges/bittrex/bittrex_websocket.go +++ b/exchanges/bittrex/bittrex_websocket.go @@ -602,7 +602,7 @@ func (b *Bittrex) WsProcessUpdateOrder(data *OrderUpdateMessage) error { Err: err, } } - timeInForce, err := order.StringToTimeInForce(data.Delta.TimeInForce) + timeInForce, err := timeInForceFromString(data.Delta.TimeInForce) if err != nil { b.Websocket.DataHandler <- order.ClassificationError{ Exchange: b.Name, @@ -612,7 +612,7 @@ func (b *Bittrex) WsProcessUpdateOrder(data *OrderUpdateMessage) error { } b.Websocket.DataHandler <- &order.Detail{ TimeInForce: timeInForce, - PostOnly: data.Delta.TimeInForce == PostOnlyGoodTilCancelled, + PostOnly: data.Delta.TimeInForce == order.PostOnlyGTC.String(), Price: data.Delta.Limit, Amount: data.Delta.Quantity, RemainingAmount: data.Delta.Quantity - data.Delta.FillQuantity, diff --git a/exchanges/bittrex/bittrex_wrapper.go b/exchanges/bittrex/bittrex_wrapper.go index 0d5b303083e..2e62854e2cd 100644 --- a/exchanges/bittrex/bittrex_wrapper.go +++ b/exchanges/bittrex/bittrex_wrapper.go @@ -627,7 +627,7 @@ func (b *Bittrex) SubmitOrder(ctx context.Context, s *order.Submit) (*order.Subm formattedPair.String(), s.Side.String(), s.Type.String(), - GoodTilCancelled, + timeInForceToString(s.TimeInForce), s.Price, s.Amount, 0.0) @@ -698,10 +698,7 @@ func (b *Bittrex) GetOrderInfo(ctx context.Context, orderID string, _ currency.P // ConstructOrderDetail constructs an order detail item from the underlying data func (b *Bittrex) ConstructOrderDetail(orderData *OrderData) (*order.Detail, error) { - timeInForce, err := order.StringToTimeInForce(orderData.TimeInForce) - if err != nil { - timeInForce = order.UnknownTIF - } + timeInForce, _ := timeInForceFromString(orderData.TimeInForce) format, err := b.GetPairFormat(asset.Spot, false) if err != nil { @@ -1095,3 +1092,33 @@ func (b *Bittrex) GetHistoricCandlesExtended(_ context.Context, _ currency.Pair, func (b *Bittrex) GetFuturesContractDetails(context.Context, asset.Item) ([]futures.Contract, error) { return nil, common.ErrFunctionNotSupported } + +func timeInForceFromString(s string) (order.TimeInForce, error) { + switch s { + case "GOOD_TIL_CANCELLED": + return order.GTC, nil + case "IMMEDIATE_OR_CANCEL": + return order.IOC, nil + case "FILL_OR_KILL": + return order.FOK, nil + case "POST_ONLY_GOOD_TIL_CANCELLED": + return order.PostOnlyGTC, nil + default: + return order.UnknownTIF, order.ErrInvalidTimeInForce + } +} + +// timeInForceToString returns string given TimeInForce instance +func timeInForceToString(t order.TimeInForce) string { + switch t { + case order.IOC: + return "IMMEDIATE_OR_CANCEL" + case order.FOK: + return "FILL_OR_KILL" + case order.PostOnlyGTC: + return order.PostOnlyGTC.String() + default: + // The exchange Uses GTC as a default TimeInForce value + return goodTilCancelled + } +} diff --git a/exchanges/coinbasepro/coinbasepro_wrapper.go b/exchanges/coinbasepro/coinbasepro_wrapper.go index 1af57467eb7..708baa00247 100644 --- a/exchanges/coinbasepro/coinbasepro_wrapper.go +++ b/exchanges/coinbasepro/coinbasepro_wrapper.go @@ -563,7 +563,7 @@ func (c *CoinbasePro) SubmitOrder(ctx context.Context, s *order.Submit) (*order. fPair.String(), "") case order.Limit: - timeInForce := order.GoodTillCancel.String() + timeInForce := order.GTC.String() if s.TimeInForce == order.IOC { timeInForce = order.IOC.String() } diff --git a/exchanges/kraken/kraken_wrapper.go b/exchanges/kraken/kraken_wrapper.go index 99f4bdfd9ba..fa710d1566d 100644 --- a/exchanges/kraken/kraken_wrapper.go +++ b/exchanges/kraken/kraken_wrapper.go @@ -819,7 +819,7 @@ func (k *Kraken) SubmitOrder(ctx context.Context, s *order.Submit) (*order.Submi status := order.New switch s.AssetType { case asset.Spot: - timeInForce := order.GoodTillCancel.String() + timeInForce := order.GTC.String() if s.TimeInForce == order.IOC { timeInForce = s.TimeInForce.String() } diff --git a/exchanges/order/order_test.go b/exchanges/order/order_test.go index 78a96745786..e20dd3744d8 100644 --- a/exchanges/order/order_test.go +++ b/exchanges/order/order_test.go @@ -62,7 +62,7 @@ func TestSubmit_Validate(t *testing.T) { }, }, // valid pair but invalid order side { - ExpectedErr: errInvalidTimeInForce, + ExpectedErr: ErrInvalidTimeInForce, Submit: &Submit{ Exchange: "test", Pair: testPair, @@ -1083,7 +1083,7 @@ func TestUpdateOrderFromDetail(t *testing.T) { } om := &Detail{ - TimeInForce: GoodTillCancel, + TimeInForce: GTC, HiddenOrder: true, PostOnly: true, Leverage: 1, @@ -1126,7 +1126,7 @@ func TestUpdateOrderFromDetail(t *testing.T) { if od.InternalOrderID != id { t.Error("Failed to initialize the internal order ID") } - if od.TimeInForce != GoodTillCancel { + if od.TimeInForce != GTC { t.Error("Failed to update") } if !od.HiddenOrder { @@ -2058,26 +2058,33 @@ func TestSideUnmarshal(t *testing.T) { assert.ErrorAs(t, s.UnmarshalJSON([]byte(`14`)), &jErr, "non-string valid json is rejected") } -func TestSupported(t *testing.T) { - t.Parallel() - s := Supported() - if len(supportedTIFItems) != len(s) { - t.Fatal("TestSupported mismatched lengths") - } - for i := 0; i < len(supportedTIFItems); i++ { - if s[i] != supportedTIFItems[i] { - t.Fatal("TestSupported returned an unexpected result") - } - } -} - func TestIsValid(t *testing.T) { t.Parallel() if TimeInForce(50).IsValid() { t.Fatal("TestIsValid returned an unexpected result") } - if !GoodTillCancel.IsValid() { + if !GTC.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) + } + _, err = StringToTimeInForce("GoodTillCancel") + if err != nil { + t.Fatal(err) + } + _, err = StringToTimeInForce("") + if !errors.Is(err, ErrInvalidTimeInForce) { + t.Fatalf("expected %v, got %v", ErrInvalidTimeInForce, err) + } + _, err = StringToTimeInForce("IOC") + if err != nil { + t.Fatal(err) + } +} diff --git a/exchanges/order/order_types.go b/exchanges/order/order_types.go index 708db7cef6d..4c873d6c747 100644 --- a/exchanges/order/order_types.go +++ b/exchanges/order/order_types.go @@ -373,17 +373,18 @@ type TimeInForce uint8 // TimeInForce types const ( - UnknownTIF TimeInForce = 0 - GoodTillCancel TimeInForce = iota - GoodTillTime - FOK // FOK represents FillOrKill, used shorter version as the FillOrKill name is reserved for order type value. - IOC // IOC represents ImmediateOrCancel, used shorter version as the ImmediateOrCancel name is reserved for order type value. - - supportedTimeInForceFlag = UnknownTIF | GoodTillCancel | GoodTillTime | FOK | IOC + 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 ) var ( - supportedTIFItems = []TimeInForce{GoodTillCancel, GoodTillTime, FOK, IOC} + supportedTIFItems = []TimeInForce{GTC, GTT, FOK, IOC} ) // ByPrice used for sorting orders by price diff --git a/exchanges/order/orders.go b/exchanges/order/orders.go index 6a02cc76ec0..965e360dea5 100644 --- a/exchanges/order/orders.go +++ b/exchanges/order/orders.go @@ -36,8 +36,9 @@ 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") - errInvalidTimeInForce = errors.New("invalid time in force value provided") errUnrecognisedOrderType = errors.New("unrecognised order type") errUnrecognisedOrderStatus = errors.New("unrecognised order status") errExchangeNameUnset = errors.New("exchange name unset") @@ -83,7 +84,7 @@ func (s *Submit) Validate(opt ...validate.Checker) error { } if !s.TimeInForce.IsValid() { - return errInvalidTimeInForce + return ErrInvalidTimeInForce } if s.Amount == 0 && s.QuoteAmount == 0 { @@ -697,12 +698,15 @@ func (t TimeInForce) String() string { switch t { case IOC: return "IOC" - case GoodTillCancel: + case GTC: return "GTC" - case GoodTillTime: + 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" } @@ -1175,22 +1179,19 @@ func StringToTimeInForce(timeInForce string) (TimeInForce, error) { switch timeInForce { case "IOC", "IMMEDIATEORCANCEL", "IMMEDIATE_OR_CANCEL": return IOC, nil - case "GTC", "GOODTILLCANCEL", "GOOD_TIL_CANCELLED": - return GoodTillCancel, nil - case "GTT", "GOODTILLTIME": - return GoodTillTime, nil + case "GTC", "GOODTILLCANCEL", "GOOD_TIL_CANCELLED", "GOOD_TILL_CANCELLED": + return GTC, nil + case "GTT", "GOODTILLTIME", "GOOD_TIL_TIME": + return GTT, nil case "FOK", "FILLORKILL", "FILL_OR_KILL": return FOK, nil + case "POST_ONLY_GOOD_TIL_CANCELLED", "POST_ONLY_GOOD_TILL_CANCELLED": + return PostOnlyGTC, nil default: - return UnknownTIF, errInvalidTimeInForce + return UnknownTIF, fmt.Errorf("%w, tif=%s", ErrInvalidTimeInForce, timeInForce) } } -// Supported returns a list of supported time in force types -func Supported() []TimeInForce { - return supportedTIFItems -} - // IsValid returns whether or not the supplied time in force value is valid or // not func (t TimeInForce) IsValid() bool { From da4b3cb580a2baf4ffd3d056e9559ed0dabdb280 Mon Sep 17 00:00:00 2001 From: Samuael A <39623015+samuael@users.noreply.github.com> Date: Wed, 15 Nov 2023 15:08:16 +0300 Subject: [PATCH 5/8] Unit tests update --- exchanges/order/order_test.go | 66 +++++++++++++++++++++++++++++++++- exchanges/order/order_types.go | 4 +-- exchanges/order/orders.go | 10 +++--- 3 files changed, 72 insertions(+), 8 deletions(-) diff --git a/exchanges/order/order_test.go b/exchanges/order/order_test.go index 70e6fde39e3..218e46dc870 100644 --- a/exchanges/order/order_test.go +++ b/exchanges/order/order_test.go @@ -2064,9 +2064,27 @@ func TestIsValid(t *testing.T) { 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) { @@ -2075,10 +2093,39 @@ func TestStringToTimeInForce(t *testing.T) { if !errors.Is(err, ErrInvalidTimeInForce) { t.Fatalf("expected %v, got %v", ErrInvalidTimeInForce, err) } - _, err = StringToTimeInForce("GoodTillCancel") + 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("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) @@ -2088,3 +2135,20 @@ func TestStringToTimeInForce(t *testing.T) { 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 4c873d6c747..9364f35ad1e 100644 --- a/exchanges/order/order_types.go +++ b/exchanges/order/order_types.go @@ -380,11 +380,11 @@ const ( IOC // IOC represents ImmediateOrCancel PostOnlyGTC // PostOnlyGCT represents PostOnlyGoodTilCancelled - supportedTimeInForceFlag = UnknownTIF | GTC | GTT | FOK | IOC + supportedTimeInForceFlag = UnknownTIF | GTC | GTT | FOK | IOC | PostOnlyGTC ) var ( - supportedTIFItems = []TimeInForce{GTC, GTT, FOK, IOC} + supportedTIFItems = []TimeInForce{GTC, GTT, FOK, IOC, PostOnlyGTC} ) // ByPrice used for sorting orders by price diff --git a/exchanges/order/orders.go b/exchanges/order/orders.go index 965e360dea5..717d11020c8 100644 --- a/exchanges/order/orders.go +++ b/exchanges/order/orders.go @@ -1177,15 +1177,15 @@ func StringToOrderStatus(status string) (Status, error) { func StringToTimeInForce(timeInForce string) (TimeInForce, error) { timeInForce = strings.ToUpper(timeInForce) switch timeInForce { - case "IOC", "IMMEDIATEORCANCEL", "IMMEDIATE_OR_CANCEL": + case "IMMEDIATEORCANCEL", "IMMEDIATE_OR_CANCEL", IOC.String(): return IOC, nil - case "GTC", "GOODTILLCANCEL", "GOOD_TIL_CANCELLED", "GOOD_TILL_CANCELLED": + case "GOODTILLCANCEL", "GOOD_TIL_CANCELLED", "GOOD_TILL_CANCELLED", "GOOD_TILL_CANCELED", GTC.String(): return GTC, nil - case "GTT", "GOODTILLTIME", "GOOD_TIL_TIME": + case "GOODTILLTIME", "GOOD_TIL_TIME", GTT.String(): return GTT, nil - case "FOK", "FILLORKILL", "FILL_OR_KILL": + case "FILLORKILL", "FILL_OR_KILL", FOK.String(): return FOK, nil - case "POST_ONLY_GOOD_TIL_CANCELLED", "POST_ONLY_GOOD_TILL_CANCELLED": + case "POST_ONLY_GOOD_TILL_CANCELLED", PostOnlyGTC.String(): return PostOnlyGTC, nil default: return UnknownTIF, fmt.Errorf("%w, tif=%s", ErrInvalidTimeInForce, timeInForce) From e11beefc3491cd557e1d1a6bce3d065cfcfcd0ea Mon Sep 17 00:00:00 2001 From: Samuael A <39623015+samuael@users.noreply.github.com> Date: Thu, 23 Nov 2023 16:08:44 +0300 Subject: [PATCH 6/8] Fix minor linter issues --- exchanges/kucoin/kucoin_wrapper.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/exchanges/kucoin/kucoin_wrapper.go b/exchanges/kucoin/kucoin_wrapper.go index d9919ffee68..a4309fb0e19 100644 --- a/exchanges/kucoin/kucoin_wrapper.go +++ b/exchanges/kucoin/kucoin_wrapper.go @@ -754,9 +754,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: From 9add8480eaad5d6de6104a5edfb6272da1f74b9a Mon Sep 17 00:00:00 2001 From: Samuael A <39623015+samuael@users.noreply.github.com> Date: Mon, 27 Nov 2023 21:55:04 +0300 Subject: [PATCH 7/8] Update TestStringToTimeInForce unit test --- exchanges/order/order_test.go | 14 ++++++++++++++ exchanges/order/order_types.go | 4 ---- 2 files changed, 14 insertions(+), 4 deletions(-) diff --git a/exchanges/order/order_test.go b/exchanges/order/order_test.go index 218e46dc870..fea56d50735 100644 --- a/exchanges/order/order_test.go +++ b/exchanges/order/order_test.go @@ -2105,6 +2105,20 @@ func TestStringToTimeInForce(t *testing.T) { 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) diff --git a/exchanges/order/order_types.go b/exchanges/order/order_types.go index 9364f35ad1e..b2cc4d879b9 100644 --- a/exchanges/order/order_types.go +++ b/exchanges/order/order_types.go @@ -383,10 +383,6 @@ const ( supportedTimeInForceFlag = UnknownTIF | GTC | GTT | FOK | IOC | PostOnlyGTC ) -var ( - supportedTIFItems = []TimeInForce{GTC, GTT, FOK, IOC, PostOnlyGTC} -) - // ByPrice used for sorting orders by price type ByPrice []Detail From d82cc0c50bcbcebc9721902a2c3a3c3df0b81dd1 Mon Sep 17 00:00:00 2001 From: Samuael A <39623015+samuael@users.noreply.github.com> Date: Mon, 18 Mar 2024 20:44:35 +0300 Subject: [PATCH 8/8] fix conflict with gateio timeInForce --- exchanges/gateio/gateio_test.go | 2 +- exchanges/gateio/gateio_wrapper.go | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/exchanges/gateio/gateio_test.go b/exchanges/gateio/gateio_test.go index ce39ae91b55..e2a16a2bd71 100644 --- a/exchanges/gateio/gateio_test.go +++ b/exchanges/gateio/gateio_test.go @@ -3555,7 +3555,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