From 694f2f7944c503c3e0af4cb539b3878f7dfe6126 Mon Sep 17 00:00:00 2001 From: Ryan O'Hara-Reid Date: Mon, 29 Apr 2024 13:54:06 +1000 Subject: [PATCH] gateio: fix spot/futures order issues (#1524) * gateio: fix spot deployment issue * fix status bug add test * to actual return type * fix linter * ch type * glorious: nits * rm space * glorious: nits --------- Co-authored-by: Ryan O'Hara-Reid --- exchanges/gateio/gateio.go | 6 +---- exchanges/gateio/gateio_test.go | 31 ++++++++++++++++++++++ exchanges/gateio/gateio_wrapper.go | 8 +++--- exchanges/gateio/gateio_ws_futures.go | 38 ++++++++++++++++++--------- exchanges/order/order_types.go | 1 + exchanges/order/orders.go | 12 ++++++++- 6 files changed, 75 insertions(+), 21 deletions(-) diff --git a/exchanges/gateio/gateio.go b/exchanges/gateio/gateio.go index 2b68b35c..8360ee10 100644 --- a/exchanges/gateio/gateio.go +++ b/exchanges/gateio/gateio.go @@ -156,7 +156,6 @@ var ( 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") errNoTickerData = errors.New("no ticker data available") - errOnlyLimitOrderType = errors.New("only order type 'limit' is allowed") errNilArgument = errors.New("null argument") errInvalidTimezone = errors.New("invalid timezone") errMultipleOrders = errors.New("multiple orders passed") @@ -632,9 +631,6 @@ func (g *Gateio) PlaceSpotOrder(ctx context.Context, arg *CreateOrderRequestData if arg.CurrencyPair.IsInvalid() { return nil, currency.ErrCurrencyPairEmpty } - if arg.Type != "limit" { - return nil, errOnlyLimitOrderType - } arg.Side = strings.ToLower(arg.Side) if arg.Side != "buy" && arg.Side != "sell" { return nil, errInvalidOrderSide @@ -647,7 +643,7 @@ func (g *Gateio) PlaceSpotOrder(ctx context.Context, arg *CreateOrderRequestData if arg.Amount <= 0 { return nil, errInvalidAmount } - if arg.Price <= 0 { + if arg.Price < 0 { return nil, errInvalidPrice } var response *SpotOrder diff --git a/exchanges/gateio/gateio_test.go b/exchanges/gateio/gateio_test.go index 74c33b02..66945418 100644 --- a/exchanges/gateio/gateio_test.go +++ b/exchanges/gateio/gateio_test.go @@ -3550,3 +3550,34 @@ func TestGetTimeInForce(t *testing.T) { require.NoError(t, err) assert.Equal(t, "fok", ret) } + +func TestProcessFuturesOrdersPushData(t *testing.T) { + t.Parallel() + testCases := []struct { + incoming string + status order.Status + }{ + {`{"channel":"futures.orders","event":"update","time":1541505434,"time_ms":1541505434123,"result":[{"contract":"BTC_USD","create_time":1628736847,"create_time_ms":1628736847325,"fill_price":40000.4,"finish_as":"","finish_time":1628736848,"finish_time_ms":1628736848321,"iceberg":0,"id":4872460,"is_close":false,"is_liq":false,"is_reduce_only":false,"left":0,"mkfr":-0.00025,"price":40000.4,"refr":0,"refu":0,"size":1,"status":"open","text":"-","tif":"gtc","tkfr":0.0005,"user":"110xxxxx"}]}`, order.Open}, + {`{"channel":"futures.orders","event":"update","time":1541505434,"time_ms":1541505434123,"result":[{"contract":"BTC_USD","create_time":1628736847,"create_time_ms":1628736847325,"fill_price":40000.4,"finish_as":"filled","finish_time":1628736848,"finish_time_ms":1628736848321,"iceberg":0,"id":4872460,"is_close":false,"is_liq":false,"is_reduce_only":false,"left":0,"mkfr":-0.00025,"price":40000.4,"refr":0,"refu":0,"size":1,"status":"finished","text":"-","tif":"gtc","tkfr":0.0005,"user":"110xxxxx"}]}`, order.Filled}, + {`{"channel":"futures.orders","event":"update","time":1541505434,"time_ms":1541505434123,"result":[{"contract":"BTC_USD","create_time":1628736847,"create_time_ms":1628736847325,"fill_price":40000.4,"finish_as":"cancelled","finish_time":1628736848,"finish_time_ms":1628736848321,"iceberg":0,"id":4872460,"is_close":false,"is_liq":false,"is_reduce_only":false,"left":0,"mkfr":-0.00025,"price":40000.4,"refr":0,"refu":0,"size":1,"status":"finished","text":"-","tif":"gtc","tkfr":0.0005,"user":"110xxxxx"}]}`, order.Cancelled}, + {`{"channel":"futures.orders","event":"update","time":1541505434,"time_ms":1541505434123,"result":[{"contract":"BTC_USD","create_time":1628736847,"create_time_ms":1628736847325,"fill_price":40000.4,"finish_as":"liquidated","finish_time":1628736848,"finish_time_ms":1628736848321,"iceberg":0,"id":4872460,"is_close":false,"is_liq":false,"is_reduce_only":false,"left":0,"mkfr":-0.00025,"price":40000.4,"refr":0,"refu":0,"size":1,"status":"finished","text":"-","tif":"gtc","tkfr":0.0005,"user":"110xxxxx"}]}`, order.Liquidated}, + {`{"channel":"futures.orders","event":"update","time":1541505434,"time_ms":1541505434123,"result":[{"contract":"BTC_USD","create_time":1628736847,"create_time_ms":1628736847325,"fill_price":40000.4,"finish_as":"ioc","finish_time":1628736848,"finish_time_ms":1628736848321,"iceberg":0,"id":4872460,"is_close":false,"is_liq":false,"is_reduce_only":false,"left":0,"mkfr":-0.00025,"price":40000.4,"refr":0,"refu":0,"size":1,"status":"finished","text":"-","tif":"gtc","tkfr":0.0005,"user":"110xxxxx"}]}`, order.Cancelled}, + {`{"channel":"futures.orders","event":"update","time":1541505434,"time_ms":1541505434123,"result":[{"contract":"BTC_USD","create_time":1628736847,"create_time_ms":1628736847325,"fill_price":40000.4,"finish_as":"auto_deleveraged","finish_time":1628736848,"finish_time_ms":1628736848321,"iceberg":0,"id":4872460,"is_close":false,"is_liq":false,"is_reduce_only":false,"left":0,"mkfr":-0.00025,"price":40000.4,"refr":0,"refu":0,"size":1,"status":"finished","text":"-","tif":"gtc","tkfr":0.0005,"user":"110xxxxx"}]}`, order.AutoDeleverage}, + {`{"channel":"futures.orders","event":"update","time":1541505434,"time_ms":1541505434123,"result":[{"contract":"BTC_USD","create_time":1628736847,"create_time_ms":1628736847325,"fill_price":40000.4,"finish_as":"reduce_only","finish_time":1628736848,"finish_time_ms":1628736848321,"iceberg":0,"id":4872460,"is_close":false,"is_liq":false,"is_reduce_only":false,"left":0,"mkfr":-0.00025,"price":40000.4,"refr":0,"refu":0,"size":1,"status":"finished","text":"-","tif":"gtc","tkfr":0.0005,"user":"110xxxxx"}]}`, order.Cancelled}, + {`{"channel":"futures.orders","event":"update","time":1541505434,"time_ms":1541505434123,"result":[{"contract":"BTC_USD","create_time":1628736847,"create_time_ms":1628736847325,"fill_price":40000.4,"finish_as":"position_closed","finish_time":1628736848,"finish_time_ms":1628736848321,"iceberg":0,"id":4872460,"is_close":false,"is_liq":false,"is_reduce_only":false,"left":0,"mkfr":-0.00025,"price":40000.4,"refr":0,"refu":0,"size":1,"status":"finished","text":"-","tif":"gtc","tkfr":0.0005,"user":"110xxxxx"}]}`, order.Closed}, + {`{"channel":"futures.orders","event":"update","time":1541505434,"time_ms":1541505434123,"result":[{"contract":"BTC_USD","create_time":1628736847,"create_time_ms":1628736847325,"fill_price":40000.4,"finish_as":"stp","finish_time":1628736848,"finish_time_ms":1628736848321,"iceberg":0,"id":4872460,"is_close":false,"is_liq":false,"is_reduce_only":false,"left":0,"mkfr":-0.00025,"price":40000.4,"refr":0,"refu":0,"size":1,"status":"finished","text":"-","tif":"gtc","tkfr":0.0005,"user":"110xxxxx"}]}`, order.STP}, + } + + for _, tc := range testCases { + tc := tc + t.Run("", func(t *testing.T) { + t.Parallel() + processed, err := g.processFuturesOrdersPushData([]byte(tc.incoming), asset.Futures) + require.NoError(t, err) + require.NotNil(t, processed) + for i := range processed { + assert.Equal(t, tc.status.String(), processed[i].Status.String()) + } + }) + } +} diff --git a/exchanges/gateio/gateio_wrapper.go b/exchanges/gateio/gateio_wrapper.go index d4c052b3..0b639215 100644 --- a/exchanges/gateio/gateio_wrapper.go +++ b/exchanges/gateio/gateio_wrapper.go @@ -996,9 +996,6 @@ func (g *Gateio) SubmitOrder(ctx context.Context, s *order.Submit) (*order.Submi s.Pair = s.Pair.Upper() switch s.AssetType { case asset.Spot, asset.Margin, asset.CrossMargin: - if s.Type != order.Limit { - return nil, errOnlyLimitOrderType - } switch { case s.Side.IsLong(): s.Side = order.Buy @@ -1007,6 +1004,10 @@ func (g *Gateio) SubmitOrder(ctx context.Context, s *order.Submit) (*order.Submi default: return nil, errInvalidOrderSide } + timeInForce, err := getTimeInForce(s) + if err != nil { + return nil, err + } sOrder, err := g.PlaceSpotOrder(ctx, &CreateOrderRequestData{ Side: s.Side.Lower(), Type: s.Type.Lower(), @@ -1015,6 +1016,7 @@ func (g *Gateio) SubmitOrder(ctx context.Context, s *order.Submit) (*order.Submi Price: types.Number(s.Price), CurrencyPair: s.Pair, Text: s.ClientOrderID, + TimeInForce: timeInForce, }) if err != nil { return nil, err diff --git a/exchanges/gateio/gateio_ws_futures.go b/exchanges/gateio/gateio_ws_futures.go index 20e293b9..e1235711 100644 --- a/exchanges/gateio/gateio_ws_futures.go +++ b/exchanges/gateio/gateio_ws_futures.go @@ -244,7 +244,13 @@ func (g *Gateio) wsHandleFuturesData(respRaw []byte, assetType asset.Item) error case futuresCandlesticksChannel: return g.processFuturesCandlesticks(respRaw, assetType) case futuresOrdersChannel: - return g.processFuturesOrdersPushData(respRaw, assetType) + var processed []order.Detail + processed, err = g.processFuturesOrdersPushData(respRaw, assetType) + if err != nil { + return err + } + g.Websocket.DataHandler <- processed + return nil case futuresUserTradesChannel: return g.procesFuturesUserTrades(respRaw, assetType) case futuresLiquidatesChannel: @@ -642,7 +648,7 @@ func (g *Gateio) processFuturesOrderbookSnapshot(event string, incoming []byte, return nil } -func (g *Gateio) processFuturesOrdersPushData(data []byte, assetType asset.Item) error { +func (g *Gateio) processFuturesOrdersPushData(data []byte, assetType asset.Item) ([]order.Detail, error) { resp := struct { Time int64 `json:"time"` Channel string `json:"channel"` @@ -651,19 +657,28 @@ func (g *Gateio) processFuturesOrdersPushData(data []byte, assetType asset.Item) }{} err := json.Unmarshal(data, &resp) if err != nil { - return err + return nil, err } orderDetails := make([]order.Detail, len(resp.Result)) for x := range resp.Result { - status, err := order.StringToOrderStatus(func() string { - if resp.Result[x].Status == "finished" { - return "cancelled" + var status order.Status + if resp.Result[x].Status == "finished" { + if resp.Result[x].FinishAs == "ioc" || resp.Result[x].FinishAs == "reduce_only" { + status = order.Cancelled + } else { + status, err = order.StringToOrderStatus(resp.Result[x].FinishAs) } - return resp.Result[x].Status - }()) - if err != nil { - return err + } else { + status, err = order.StringToOrderStatus(resp.Result[x].Status) } + if err != nil { + g.Websocket.DataHandler <- order.ClassificationError{ + Exchange: g.Name, + OrderID: strconv.FormatInt(resp.Result[x].ID, 10), + Err: err, + } + } + orderDetails[x] = order.Detail{ Amount: resp.Result[x].Size, Exchange: g.Name, @@ -679,8 +694,7 @@ func (g *Gateio) processFuturesOrdersPushData(data []byte, assetType asset.Item) CloseTime: resp.Result[x].FinishTimeMs.Time(), } } - g.Websocket.DataHandler <- orderDetails - return nil + return orderDetails, nil } func (g *Gateio) procesFuturesUserTrades(data []byte, assetType asset.Item) error { diff --git a/exchanges/order/order_types.go b/exchanges/order/order_types.go index 9e47c2fa..fd9e12a0 100644 --- a/exchanges/order/order_types.go +++ b/exchanges/order/order_types.go @@ -331,6 +331,7 @@ const ( Pending Cancelling Liquidated + STP ) // Type enforces a standard for order types across the code base diff --git a/exchanges/order/orders.go b/exchanges/order/orders.go index 3a4dfdd9..ad23b387 100644 --- a/exchanges/order/orders.go +++ b/exchanges/order/orders.go @@ -815,6 +815,10 @@ func (s Status) String() string { return "PENDING" case Cancelling: return "CANCELLING" + case Liquidated: + return "LIQUIDATED" + case STP: + return "SELF_TRADE_PREVENTION" default: return "UNKNOWN" } @@ -1141,7 +1145,7 @@ func StringToOrderStatus(status string) (Status, error) { return PartiallyFilledCancelled, nil case Open.String(): return Open, nil - case Closed.String(): + case Closed.String(), "POSITION_CLOSED": return Closed, nil case Cancelled.String(), "CANCELED", "ORDER_CANCELLED": return Cancelled, nil @@ -1161,6 +1165,12 @@ func StringToOrderStatus(status string) (Status, error) { return MarketUnavailable, nil case Cancelling.String(): return Cancelling, nil + case Liquidated.String(): + return Liquidated, nil + case AutoDeleverage.String(), "AUTO_DELEVERAGED": + return AutoDeleverage, nil + case STP.String(), "STP": + return STP, nil default: return UnknownStatus, fmt.Errorf("'%s' %w", status, errUnrecognisedOrderStatus) }