From 843050980744f4b7f572f5abcd412c921fe783da Mon Sep 17 00:00:00 2001 From: Gareth Kirwan Date: Fri, 13 Oct 2023 06:18:55 +0200 Subject: [PATCH] Okx: Websocket order channel fixes (#1346) * Okx: Fix WS order fields * Fixes float64 with string annotation erroring on empty strings: Okx Order Push Data error json: invalid use of ,string struct tag, trying to unmarshal "" into float64 Specifically this came from px field from a market order * Switch to convert.StringToFloat64 instead of okxNumericalValue * Fix typo in Notional* field names; Ironically prevented them from erroring * Okx: Add tests for first order fields * Okx: CID and maybe set WS order Filled time * Tests: Set TestFixtureToDataHandler to t.Helper * Orders: Add UnmarshalJSON to order.Side * Okx: Fix FillTime not parsed for PendingOrder * Okx: Switch to order.Side Unmarshal throughout * Okx: Add Fee and FeeAsset to order processing * Okx: Fix WS order.Detail amounts and Test This fixes Amount vs QuoteAmount for market sells where tgtCcy is quote_ccy * Add comment to order.Side.UnmarshalJSON * Okx: Replace PendingOrderItem Unmarshal with local types * Okx: string type for WS order reduceOnly Note: Not yet in unit tests, since it's not part of the spot tests I was originally fixing. I'll circle back to adding full test support for Reduce only and deleveraging positions. * Okx: Fix TestOrderPushData Amount We were expecting 0 when we're given a quoteAmount In reality, we'll calculate the size from the price * Okx: Fix order and remAmount in wsOrders Improved handling for Float64 issues and boundaries when the order is fully executed but not yet marked as Filled * Fix ErrSideIsInvalid in tests --- exchanges/okx/okx_test.go | 65 ++++++++- exchanges/okx/okx_type_convert.go | 78 ----------- exchanges/okx/okx_types.go | 128 +++++++++--------- exchanges/okx/okx_websocket.go | 67 +++++---- exchanges/okx/okx_wrapper.go | 14 +- exchanges/okx/testdata/wsOrders.json | 4 + exchanges/order/order_test.go | 12 ++ exchanges/order/orders.go | 15 ++ .../sharedtestvalues/sharedtestvalues.go | 1 + 9 files changed, 201 insertions(+), 183 deletions(-) create mode 100644 exchanges/okx/testdata/wsOrders.json diff --git a/exchanges/okx/okx_test.go b/exchanges/okx/okx_test.go index 083bed36..e2eb5d24 100644 --- a/exchanges/okx/okx_test.go +++ b/exchanges/okx/okx_test.go @@ -2490,13 +2490,70 @@ func TestBalanceAndPosition(t *testing.T) { } } -const orderPushDataJSON = `{"arg": { "channel": "orders", "instType": "SPOT", "instId": "BTC-USDT", "uid": "614488474791936"},"data": [ { "accFillSz": "0.001", "amendResult": "", "avgPx": "31527.1", "cTime": "1654084334977", "category": "normal", "ccy": "", "clOrdId": "", "code": "0", "execType": "M", "fee": "-0.02522168", "feeCcy": "USDT", "fillFee": "-0.02522168", "fillFeeCcy": "USDT", "fillNotionalUsd": "31.50818374", "fillPx": "31527.1", "fillSz": "0.001", "fillTime": "1654084353263", "instId": "BTC-USDT", "instType": "SPOT", "lever": "0", "msg": "", "notionalUsd": "31.50818374", "ordId": "452197707845865472", "ordType": "limit", "pnl": "0", "posSide": "", "px": "31527.1", "rebate": "0", "rebateCcy": "BTC", "reduceOnly": "false", "reqId": "", "side": "sell", "slOrdPx": "", "slTriggerPx": "", "slTriggerPxType": "last", "source": "", "state": "filled", "sz": "0.001", "tag": "", "tdMode": "cash", "tgtCcy": "", "tpOrdPx": "", "tpTriggerPx": "", "tpTriggerPxType": "last", "tradeId": "242589207", "uTime": "1654084353264" }]}` - func TestOrderPushData(t *testing.T) { t.Parallel() - if err := ok.WsHandleData([]byte(orderPushDataJSON)); err != nil { - t.Error("Okx Order Push Data error", err) + n := new(Okx) + sharedtestvalues.TestFixtureToDataHandler(t, ok, n, "testdata/wsOrders.json", n.WsHandleData) + seen := 0 + for reading := true; reading; { + select { + default: + reading = false + case resp := <-n.GetBase().Websocket.DataHandler: + seen++ + switch v := resp.(type) { + case *order.Detail: + switch seen { + case 1: + assert.Equal(t, "452197707845865472", v.OrderID, "OrderID") + assert.Equal(t, "HamsterParty14", v.ClientOrderID, "ClientOrderID") + assert.Equal(t, asset.Spot, v.AssetType, "AssetType") + assert.Equal(t, order.Sell, v.Side, "Side") + assert.Equal(t, order.Filled, v.Status, "Status") + assert.Equal(t, order.Limit, v.Type, "Type") + assert.Equal(t, currency.NewPairWithDelimiter("BTC", "USDT", "-"), v.Pair, "Pair") + assert.Equal(t, 31527.1, v.AverageExecutedPrice, "AverageExecutedPrice") + assert.Equal(t, time.UnixMilli(1654084334977), v.Date, "Date") + assert.Equal(t, time.UnixMilli(1654084353263), v.CloseTime, "CloseTime") + assert.Equal(t, 0.001, v.Amount, "Amount") + assert.Equal(t, 0.001, v.ExecutedAmount, "ExecutedAmount") + assert.Equal(t, 0.000, v.RemainingAmount, "RemainingAmount") + assert.Equal(t, 31527.1, v.Price, "Price") + assert.Equal(t, 0.02522168, v.Fee, "Fee") + assert.Equal(t, currency.USDT, v.FeeAsset, "FeeAsset") + case 2: + assert.Equal(t, "620258920632008725", v.OrderID, "OrderID") + assert.Equal(t, asset.Spot, v.AssetType, "AssetType") + assert.Equal(t, order.Market, v.Type, "Type") + assert.Equal(t, order.Sell, v.Side, "Side") + assert.Equal(t, order.Active, v.Status, "Status") + assert.Equal(t, 0.0, v.Amount, "Amount should be 0 for a market sell") + assert.Equal(t, 10.0, v.QuoteAmount, "QuoteAmount") + case 3: + assert.Equal(t, "620258920632008725", v.OrderID, "OrderID") + assert.Equal(t, 10.0, v.QuoteAmount, "QuoteAmount") + assert.Equal(t, 0.00038127046945832905, v.Amount, "Amount") + assert.Equal(t, 0.010000249968, v.Fee, "Fee") + assert.Equal(t, 0.0, v.RemainingAmount, "RemainingAmount") + assert.Equal(t, 0.00038128, v.ExecutedAmount, "ExecutedAmount") + assert.Equal(t, order.PartiallyFilled, v.Status, "Status") + case 4: + assert.Equal(t, "620258920632008725", v.OrderID, "OrderID") + assert.Equal(t, 10.0, v.QuoteAmount, "QuoteAmount") + assert.Equal(t, 0.010000249968, v.Fee, "Fee") + assert.Equal(t, 0.0, v.RemainingAmount, "RemainingAmount") + assert.Equal(t, 0.00038128, v.ExecutedAmount, "ExecutedAmount") + assert.Equal(t, 0.00038128, v.Amount, "Amount should be derived because order filled") + assert.Equal(t, order.Filled, v.Status, "Status") + } + case error: + t.Error(v) + default: + t.Errorf("Got unexpected data: %T %v", v, v) + } + } } + assert.Equal(t, 4, seen, "Saw 4 records") } const algoOrdersPushDataJSON = `{"arg": {"channel": "orders-algo","uid": "77982378738415879","instType": "FUTURES","instId": "BTC-USD-200329"},"data": [{"instType": "FUTURES","instId": "BTC-USD-200329","ordId": "312269865356374016","ccy": "BTC","algoId": "1234","px": "999","sz": "3","tdMode": "cross","tgtCcy": "","notionalUsd": "","ordType": "trigger","side": "buy","posSide": "long","state": "live","lever": "20","tpTriggerPx": "","tpTriggerPxType": "","tpOrdPx": "","slTriggerPx": "","slTriggerPxType": "","triggerPx": "99","triggerPxType": "last","ordPx": "12","actualSz": "","actualPx": "","tag": "adadadadad","actualSide": "","triggerTime": "1597026383085","cTime": "1597026383000"}]}` diff --git a/exchanges/okx/okx_type_convert.go b/exchanges/okx/okx_type_convert.go index 42530fa3..4d911327 100644 --- a/exchanges/okx/okx_type_convert.go +++ b/exchanges/okx/okx_type_convert.go @@ -139,7 +139,6 @@ func (a *OrderDetail) UnmarshalJSON(data []byte) error { type Alias OrderDetail chil := &struct { *Alias - Side string `json:"side"` UpdateTime int64 `json:"uTime,string"` CreationTime int64 `json:"cTime,string"` FillTime string `json:"fillTime"` @@ -152,7 +151,6 @@ func (a *OrderDetail) UnmarshalJSON(data []byte) error { var err error a.UpdateTime = time.UnixMilli(chil.UpdateTime) a.CreationTime = time.UnixMilli(chil.CreationTime) - a.Side, err = order.StringToOrderSide(chil.Side) if chil.FillTime == "" { a.FillTime = time.Time{} } else { @@ -169,38 +167,6 @@ func (a *OrderDetail) UnmarshalJSON(data []byte) error { return nil } -// UnmarshalJSON deserializes JSON, and timestamp information. -func (a *PendingOrderItem) UnmarshalJSON(data []byte) error { - type Alias PendingOrderItem - chil := &struct { - *Alias - Side string `json:"side"` - UpdateTime string `json:"uTime"` - CreationTime string `json:"cTime"` - }{ - Alias: (*Alias)(a), - } - err := json.Unmarshal(data, chil) - if err != nil { - return err - } - uTime, err := strconv.ParseInt(chil.UpdateTime, 10, 64) - if err != nil { - return err - } - cTime, err := strconv.ParseInt(chil.CreationTime, 10, 64) - if err != nil { - return err - } - a.Side, err = order.StringToOrderSide(chil.Side) - if err != nil { - return err - } - a.CreationTime = time.UnixMilli(cTime) - a.UpdateTime = time.UnixMilli(uTime) - return nil -} - // UnmarshalJSON deserializes JSON, and timestamp information. func (a *RfqTradeResponse) UnmarshalJSON(data []byte) error { type Alias RfqTradeResponse @@ -233,29 +199,6 @@ func (a *BlockTicker) UnmarshalJSON(data []byte) error { return nil } -// UnmarshalJSON deserializes JSON, and timestamp information. -func (a *BlockTrade) UnmarshalJSON(data []byte) error { - type Alias BlockTrade - chil := &struct { - *Alias - Side string `json:"side"` - }{ - Alias: (*Alias)(a), - } - if err := json.Unmarshal(data, chil); err != nil { - return err - } - switch { - case strings.EqualFold(chil.Side, "buy"): - a.Side = order.Buy - case strings.EqualFold(chil.Side, "sell"): - a.Side = order.Sell - default: - a.Side = order.UnknownSide - } - return nil -} - // UnmarshalJSON deserializes JSON, and timestamp information. func (a *UnitConvertResponse) UnmarshalJSON(data []byte) error { type Alias UnitConvertResponse @@ -277,27 +220,6 @@ func (a *UnitConvertResponse) UnmarshalJSON(data []byte) error { return nil } -// UnmarshalJSON deserializes JSON, and timestamp information. -func (a *QuoteLeg) UnmarshalJSON(data []byte) error { - type Alias QuoteLeg - chil := &struct { - *Alias - Side string `json:"side"` - }{ - Alias: (*Alias)(a), - } - if err := json.Unmarshal(data, chil); err != nil { - return err - } - chil.Side = strings.ToLower(chil.Side) - if chil.Side == "buy" { - a.Side = order.Buy - } else { - a.Side = order.Sell - } - return nil -} - // MarshalJSON serialized QuoteLeg instance into bytes func (a *QuoteLeg) MarshalJSON() ([]byte, error) { type Alias QuoteLeg diff --git a/exchanges/okx/okx_types.go b/exchanges/okx/okx_types.go index d0b803c0..54b4aa2c 100644 --- a/exchanges/okx/okx_types.go +++ b/exchanges/okx/okx_types.go @@ -230,7 +230,7 @@ type TradeResponse struct { TradeID string `json:"tradeId"` Price float64 `json:"px,string"` Quantity float64 `json:"sz,string"` - Side string `json:"side"` + Side order.Side `json:"side"` Timestamp okxUnixMilliTime `json:"ts"` } @@ -433,7 +433,7 @@ type LiquidationOrderDetailItem struct { BankruptcyPx string `json:"bkPx"` Currency string `json:"ccy"` PosSide string `json:"posSide"` - Side string `json:"side"` + Side string `json:"side"` // May be empty QuantityOfLiquidation float64 `json:"sz,string"` Timestamp okxUnixMilliTime `json:"ts"` } @@ -717,42 +717,42 @@ type OrderHistoryRequestParams struct { // PendingOrderItem represents a pending order Item in pending orders list. type PendingOrderItem struct { - AccumulatedFillSize okxNumericalValue `json:"accFillSz"` - AveragePrice okxNumericalValue `json:"avgPx"` - CreationTime time.Time `json:"cTime"` - Category string `json:"category"` - Currency string `json:"ccy"` - ClientOrderID string `json:"clOrdId"` - TransactionFee string `json:"fee"` - FeeCurrency string `json:"feeCcy"` - LastFilledPrice string `json:"fillPx"` - LastFilledSize okxNumericalValue `json:"fillSz"` - FillTime string `json:"fillTime"` - InstrumentID string `json:"instId"` - InstrumentType string `json:"instType"` - Leverage okxNumericalValue `json:"lever"` - OrderID string `json:"ordId"` - OrderType string `json:"ordType"` - ProfitAndLose string `json:"pnl"` - PositionSide string `json:"posSide"` - RebateAmount string `json:"rebate"` - RebateCurrency string `json:"rebateCcy"` - Side order.Side `json:"side"` - StopLossOrdPrice string `json:"slOrdPx"` - StopLossTriggerPrice string `json:"slTriggerPx"` - StopLossTriggerPriceType string `json:"slTriggerPxType"` - State string `json:"state"` - Price float64 `json:"px,string"` - Size float64 `json:"sz,string"` - Tag string `json:"tag"` - QuantityType string `json:"tgtCcy"` - TradeMode string `json:"tdMode"` - Source string `json:"source"` - TakeProfitOrdPrice string `json:"tpOrdPx"` - TakeProfitTriggerPrice string `json:"tpTriggerPx"` - TakeProfitTriggerPriceType string `json:"tpTriggerPxType"` - TradeID string `json:"tradeId"` - UpdateTime time.Time `json:"uTime"` + AccumulatedFillSize convert.StringToFloat64 `json:"accFillSz"` + AveragePrice convert.StringToFloat64 `json:"avgPx"` + CreationTime okxUnixMilliTime `json:"cTime"` + Category string `json:"category"` + Currency string `json:"ccy"` + ClientOrderID string `json:"clOrdId"` + Fee convert.StringToFloat64 `json:"fee"` + FeeCurrency currency.Code `json:"feeCcy"` + LastFilledPrice convert.StringToFloat64 `json:"fillPx"` + LastFilledSize convert.StringToFloat64 `json:"fillSz"` + FillTime okxUnixMilliTime `json:"fillTime"` + InstrumentID string `json:"instId"` + InstrumentType string `json:"instType"` + Leverage convert.StringToFloat64 `json:"lever"` + OrderID string `json:"ordId"` + OrderType string `json:"ordType"` + ProfitAndLoss string `json:"pnl"` + PositionSide string `json:"posSide"` + RebateAmount convert.StringToFloat64 `json:"rebate"` + RebateCurrency string `json:"rebateCcy"` + Side order.Side `json:"side"` + StopLossOrdPrice convert.StringToFloat64 `json:"slOrdPx"` + StopLossTriggerPrice convert.StringToFloat64 `json:"slTriggerPx"` + StopLossTriggerPriceType string `json:"slTriggerPxType"` + State string `json:"state"` + Price convert.StringToFloat64 `json:"px"` + Size convert.StringToFloat64 `json:"sz"` + Tag string `json:"tag"` + SizeType string `json:"tgtCcy"` + TradeMode string `json:"tdMode"` + Source string `json:"source"` + TakeProfitOrdPrice convert.StringToFloat64 `json:"tpOrdPx"` + TakeProfitTriggerPrice convert.StringToFloat64 `json:"tpTriggerPx"` + TakeProfitTriggerPriceType string `json:"tpTriggerPxType"` + TradeID string `json:"tradeId"` + UpdateTime okxUnixMilliTime `json:"uTime"` } // TransactionDetailRequestParams retrieve recently-filled transaction details in the last 3 day. @@ -780,7 +780,7 @@ type TransactionDetail struct { Tag string `json:"tag"` FillPrice float64 `json:"fillPx,string"` FillSize float64 `json:"fillSz,string"` - Side string `json:"side"` + Side order.Side `json:"side"` PositionSide string `json:"posSide"` ExecType string `json:"execType"` FeeCurrency string `json:"feeCcy"` @@ -862,7 +862,7 @@ type AlgoOrderResponse struct { AlgoOrderID string `json:"algoId"` Quantity string `json:"sz"` OrderType string `json:"ordType"` - Side string `json:"side"` + Side order.Side `json:"side"` PositionSide string `json:"posSide"` TradeMode string `json:"tdMode"` QuantityType string `json:"tgtCcy"` @@ -1175,7 +1175,7 @@ type EstimateQuoteResponse struct { QuoteTime okxUnixMilliTime `json:"quoteTime"` RfqSize string `json:"rfqSz"` RfqSizeCurrency string `json:"rfqSzCcy"` - Side string `json:"side"` + Side order.Side `json:"side"` TTLMs string `json:"ttlMs"` // Validity period of quotation in milliseconds } @@ -1201,7 +1201,7 @@ type ConvertTradeResponse struct { InstrumentID string `json:"instId"` QuoteCurrency string `json:"quoteCcy"` QuoteID string `json:"quoteId"` - Side string `json:"side"` + Side order.Side `json:"side"` State string `json:"state"` TradeID string `json:"tradeId"` Timestamp okxUnixMilliTime `json:"ts"` @@ -1210,7 +1210,7 @@ type ConvertTradeResponse struct { // ConvertHistory holds convert trade history response type ConvertHistory struct { InstrumentID string `json:"instId"` - Side string `json:"side"` + Side order.Side `json:"side"` FillPrice float64 `json:"fillPx,string"` BaseCurrency string `json:"baseCcy"` QuoteCurrency string `json:"quoteCcy"` @@ -1482,12 +1482,12 @@ type LeverageResponse struct { // MaximumLoanInstrument represents maximum loan of an instrument id. type MaximumLoanInstrument struct { - InstrumentID string `json:"instId"` - MgnMode string `json:"mgnMode"` - MgnCcy string `json:"mgnCcy"` - MaxLoan string `json:"maxLoan"` - Ccy string `json:"ccy"` - Side string `json:"side"` + InstrumentID string `json:"instId"` + MgnMode string `json:"mgnMode"` + MgnCcy string `json:"mgnCcy"` + MaxLoan string `json:"maxLoan"` + Ccy string `json:"ccy"` + Side order.Side `json:"side"` } // TradeFeeRate holds trade fee rate information for a given instrument type. @@ -1564,7 +1564,7 @@ type LoanBorrowAndReplay struct { Currency string `json:"ccy"` LoanQuota string `json:"loanQuota"` PosLoan string `json:"posLoan"` - Side string `json:"side"` + Side string `json:"side"` // borrow or repay UsedLoan string `json:"usedLoan"` } @@ -2374,7 +2374,7 @@ type WSTradeData struct { TradeID string `json:"tradeId"` Price float64 `json:"px,string"` Size float64 `json:"sz,string"` - Side string `json:"side"` + Side order.Side `json:"side"` Timestamp okxUnixMilliTime `json:"ts"` } @@ -2500,16 +2500,16 @@ type WsBalanceAndPosition struct { // WsOrder represents a websocket order. type WsOrder struct { PendingOrderItem - AmendResult string `json:"amendResult"` - Code string `json:"code"` - ExecType string `json:"execType"` - FillFee string `json:"fillFee"` - FillFeeCurrency string `json:"fillFeeCcy"` - FillNationalUsd float64 `json:"fillNationalUsd,string"` - Msg string `json:"msg"` - NationalUSD string `json:"nationalUsd"` - ReduceOnly bool `json:"reduceOnly"` - RequestID string `json:"reqId"` + AmendResult string `json:"amendResult"` + Code string `json:"code"` + ExecType string `json:"execType"` + FillFee convert.StringToFloat64 `json:"fillFee"` + FillFeeCurrency string `json:"fillFeeCcy"` + FillNotionalUsd convert.StringToFloat64 `json:"fillNotionalUsd"` + Msg string `json:"msg"` + NotionalUSD convert.StringToFloat64 `json:"notionalUsd"` + ReduceOnly bool `json:"reduceOnly,string"` + RequestID string `json:"reqId"` } // WsOrderResponse holds order list push data through the websocket connection @@ -2537,7 +2537,7 @@ type WsAlgoOrderDetail struct { TargetCurrency string `json:"tgtCcy"` NotionalUsd string `json:"notionalUsd"` OrderType string `json:"ordType"` - Side string `json:"side"` + Side order.Side `json:"side"` PositionSide string `json:"posSide"` State string `json:"state"` Leverage string `json:"lever"` @@ -2581,7 +2581,7 @@ type WsAdvancedAlgoOrderDetail struct { PriceLimit string `json:"pxLimit"` PriceSpread string `json:"pxSpread"` PriceVariation string `json:"pxVar"` - Side string `json:"side"` + Side order.Side `json:"side"` StopLossOrderPrice string `json:"slOrdPx"` StopLossTriggerPrice string `json:"slTriggerPx"` State string `json:"state"` @@ -2839,7 +2839,7 @@ type GridSubOrderData struct { ProfitAdLoss string `json:"pnl"` PositionSide string `json:"posSide"` Price string `json:"px"` - Side string `json:"side"` + Side order.Side `json:"side"` State string `json:"state"` Size string `json:"sz"` Tag string `json:"tag"` diff --git a/exchanges/okx/okx_websocket.go b/exchanges/okx/okx_websocket.go index d59755f9..4a1eb77c 100644 --- a/exchanges/okx/okx_websocket.go +++ b/exchanges/okx/okx_websocket.go @@ -1009,18 +1009,13 @@ func (ok *Okx) wsProcessTrades(data []byte) error { if err != nil { return err } - var side order.Side - side, err = order.StringToOrderSide(response.Data[i].Side) - if err != nil { - return err - } for j := range assets { trades = append(trades, trade.Data{ Amount: response.Data[i].Quantity, AssetType: assets[j], CurrencyPair: pair, Exchange: ok.Name, - Side: side, + Side: response.Data[i].Side, Timestamp: response.Data[i].Timestamp.Time(), TID: response.Data[i].TradeID, Price: response.Data[i].Price, @@ -1060,40 +1055,62 @@ func (ok *Okx) wsProcessOrders(respRaw []byte) error { if err != nil { return err } + avgPrice := response.Data[x].AveragePrice.Float64() - orderAmount := response.Data[x].Size + orderAmount := response.Data[x].Size.Float64() + execAmount := response.Data[x].AccumulatedFillSize.Float64() + var quoteAmount float64 - if response.Data[x].QuantityType == "quote_ccy" { + if response.Data[x].SizeType == "quote_ccy" { // Size is quote amount. quoteAmount = orderAmount - if avgPrice > 0 { - orderAmount /= avgPrice + if orderStatus == order.Filled { + // We prefer to take execAmount over calculating from quoteAmount / avgPrice + // because it avoids rounding issues + orderAmount = execAmount } else { - // Size not in Base, and we can't derive a sane value for it - orderAmount = 0 + if avgPrice > 0 { + orderAmount /= avgPrice + } else { + // Size not in Base, and we can't derive a sane value for it + orderAmount = 0 + } } } + var remainingAmount float64 - if orderStatus != order.Filled { - remainingAmount = orderAmount - response.Data[x].AccumulatedFillSize.Float64() + // Float64 rounding may lead to execAmount > orderAmount by a tiny fraction + // noting that the order can be fully executed before it's marked as status Filled + if orderStatus != order.Filled && orderAmount > execAmount { + remainingAmount = orderAmount - execAmount } - ok.Websocket.DataHandler <- &order.Detail{ - Price: response.Data[x].Price, + + d := &order.Detail{ Amount: orderAmount, - QuoteAmount: quoteAmount, - ExecutedAmount: response.Data[x].AccumulatedFillSize.Float64(), - RemainingAmount: remainingAmount, + AssetType: a, AverageExecutedPrice: avgPrice, - Exchange: ok.Name, - OrderID: response.Data[x].OrderID, ClientOrderID: response.Data[x].ClientOrderID, - Type: orderType, + Date: response.Data[x].CreationTime.Time(), + Exchange: ok.Name, + ExecutedAmount: execAmount, + Fee: 0.0 - response.Data[x].Fee.Float64(), + FeeAsset: response.Data[x].FeeCurrency, + OrderID: response.Data[x].OrderID, + Pair: pair, + Price: response.Data[x].Price.Float64(), + QuoteAmount: quoteAmount, + RemainingAmount: remainingAmount, Side: response.Data[x].Side, Status: orderStatus, - AssetType: a, - Date: response.Data[x].CreationTime, - Pair: pair, + Type: orderType, } + if orderStatus == order.Filled { + d.CloseTime = response.Data[x].FillTime.Time() + if d.Amount == 0 { + d.Amount = d.ExecutedAmount + } + } + ok.Websocket.DataHandler <- d } return nil } diff --git a/exchanges/okx/okx_wrapper.go b/exchanges/okx/okx_wrapper.go index e06df5b4..034d611e 100644 --- a/exchanges/okx/okx_wrapper.go +++ b/exchanges/okx/okx_wrapper.go @@ -684,18 +684,13 @@ func (ok *Okx) GetRecentTrades(ctx context.Context, p currency.Pair, assetType a } resp := make([]trade.Data, len(tradeData)) - var side order.Side for x := range tradeData { - side, err = order.StringToOrderSide(tradeData[x].Side) - if err != nil { - return nil, err - } resp[x] = trade.Data{ TID: tradeData[x].TradeID, Exchange: ok.Name, CurrencyPair: p, AssetType: assetType, - Side: side, + Side: tradeData[x].Side, Price: tradeData[x].Price, Amount: tradeData[x].Quantity, Timestamp: tradeData[x].Timestamp.Time(), @@ -744,11 +739,6 @@ allTrades: // reached end of trades to crawl break allTrades } - var tradeSide order.Side - tradeSide, err = order.StringToOrderSide(trades[i].Side) - if err != nil { - return nil, err - } resp = append(resp, trade.Data{ TID: trades[i].TradeID, Exchange: ok.Name, @@ -757,7 +747,7 @@ allTrades: Price: trades[i].Price, Amount: trades[i].Quantity, Timestamp: trades[i].Timestamp.Time(), - Side: tradeSide, + Side: trades[i].Side, }) } tradeIDEnd = trades[len(trades)-1].TradeID diff --git a/exchanges/okx/testdata/wsOrders.json b/exchanges/okx/testdata/wsOrders.json new file mode 100644 index 00000000..c043280d --- /dev/null +++ b/exchanges/okx/testdata/wsOrders.json @@ -0,0 +1,4 @@ +{"arg":{"channel":"orders","instType":"SPOT","instId":"BTC-USDT","uid":"614488474791936"},"data":[{"accFillSz":"0.001","amendResult":"","avgPx":"31527.1","cTime":"1654084334977","category":"normal","ccy":"","clOrdId":"HamsterParty14","code":"0","execType":"M","fee":"-0.02522168","feeCcy":"USDT","fillFee":"-0.02522168","fillFeeCcy":"USDT","fillNotionalUsd":"31.50818374","fillPx":"31527.1","fillSz":"0.001","fillTime":"1654084353263","instId":"BTC-USDT","instType":"SPOT","lever":"0","msg":"","notionalUsd":"31.50818374","ordId":"452197707845865472","ordType":"limit","pnl":"0","posSide":"","px":"31527.1","rebate":"0","rebateCcy":"BTC","reduceOnly":"false","reqId":"","side":"sell","slOrdPx":"","slTriggerPx":"","slTriggerPxType":"last","source":"","state":"filled","sz":"0.001","tag":"","tdMode":"cash","tgtCcy":"","tpOrdPx":"","tpTriggerPx":"","tpTriggerPxType":"last","tradeId":"242589207","uTime":"1654084353264"}]} +{"arg":{"channel":"orders","instType":"SPOT","uid":"448743607034327908"},"data":[{"accFillSz":"0","algoClOrdId":"","algoId":"","amendResult":"","amendSource":"","attachAlgoClOrdId":"","avgPx":"0","cTime":"1694153250532","cancelSource":"","category":"normal","ccy":"","clOrdId":"","code":"0","execType":"","fee":"0","feeCcy":"USDT","fillFee":"0","fillFeeCcy":"","fillFwdPx":"","fillMarkPx":"","fillMarkVol":"","fillNotionalUsd":"","fillPnl":"0","fillPx":"","fillPxUsd":"","fillPxVol":"","fillSz":"0","fillTime":"","instId":"BTC-USDT","instType":"SPOT","lever":"0","msg":"","notionalUsd":"10.000599999999999","ordId":"620258920632008725","ordType":"market","pnl":"0","posSide":"","px":"","pxType":"","pxUsd":"","pxVol":"","quickMgnType":"","rebate":"0","rebateCcy":"BTC","reduceOnly":"false","reqId":"","side":"sell","slOrdPx":"","slTriggerPx":"","slTriggerPxType":"","source":"","state":"live","stpId":"","stpMode":"","sz":"10","tag":"","tdMode":"cash","tgtCcy":"quote_ccy","tpOrdPx":"","tpTriggerPx":"","tpTriggerPxType":"","tradeId":"","uTime":"1694153250532"}]} +{"arg":{"channel":"orders","instType":"SPOT","uid":"448743607034327908"},"data":[{"accFillSz":"0.00038128","algoClOrdId":"","algoId":"","amendResult":"","amendSource":"","attachAlgoClOrdId":"","avgPx":"26228.1","cTime":"1694153250532","cancelSource":"","category":"normal","ccy":"","clOrdId":"","code":"0","execType":"T","fee":"-0.010000249968","feeCcy":"USDT","fillFee":"-0.010000249968","fillFeeCcy":"USDT","fillFwdPx":"","fillMarkPx":"","fillMarkVol":"","fillNotionalUsd":"10.00084998299808","fillPnl":"0","fillPx":"26228.1","fillPxUsd":"","fillPxVol":"","fillSz":"0.00038128","fillTime":"1694153250535","instId":"BTC-USDT","instType":"SPOT","lever":"0","msg":"","notionalUsd":"10.000599999999999","ordId":"620258920632008725","ordType":"market","pnl":"0","posSide":"","px":"","pxType":"","pxUsd":"","pxVol":"","quickMgnType":"","rebate":"0","rebateCcy":"BTC","reduceOnly":"false","reqId":"","side":"sell","slOrdPx":"","slTriggerPx":"","slTriggerPxType":"","source":"","state":"partially_filled","stpId":"","stpMode":"","sz":"10","tag":"","tdMode":"cash","tgtCcy":"quote_ccy","tpOrdPx":"","tpTriggerPx":"","tpTriggerPxType":"","tradeId":"435550732","uTime":"1694153250535"}]} +{"arg":{"channel":"orders","instType":"SPOT","uid":"448743607034327908"},"data":[{"accFillSz":"0.00038128","algoClOrdId":"","algoId":"","amendResult":"","amendSource":"","attachAlgoClOrdId":"","avgPx":"26228.1","cTime":"1694153250532","cancelSource":"","category":"normal","ccy":"","clOrdId":"","code":"0","execType":"","fee":"-0.010000249968","feeCcy":"USDT","fillFee":"0","fillFeeCcy":"","fillFwdPx":"","fillMarkPx":"","fillMarkVol":"","fillNotionalUsd":"10.00084998299808","fillPnl":"0","fillPx":"","fillPxUsd":"","fillPxVol":"","fillSz":"0","fillTime":"","instId":"BTC-USDT","instType":"SPOT","lever":"0","msg":"","notionalUsd":"10.000599999999999","ordId":"620258920632008725","ordType":"market","pnl":"0","posSide":"","px":"","pxType":"","pxUsd":"","pxVol":"","quickMgnType":"","rebate":"0","rebateCcy":"BTC","reduceOnly":"false","reqId":"","side":"sell","slOrdPx":"","slTriggerPx":"","slTriggerPxType":"","source":"","state":"filled","stpId":"","stpMode":"","sz":"10","tag":"","tdMode":"cash","tgtCcy":"quote_ccy","tpOrdPx":"","tpTriggerPx":"","tpTriggerPxType":"","tradeId":"","uTime":"1694153250535"}]} diff --git a/exchanges/order/order_test.go b/exchanges/order/order_test.go index 8c5f1dff..c4fb0a14 100644 --- a/exchanges/order/order_test.go +++ b/exchanges/order/order_test.go @@ -1,6 +1,7 @@ package order import ( + "encoding/json" "errors" "fmt" "reflect" @@ -10,6 +11,7 @@ import ( "time" "github.com/gofrs/uuid" + "github.com/stretchr/testify/assert" "github.com/thrasher-corp/gocryptotrader/common" "github.com/thrasher-corp/gocryptotrader/currency" "github.com/thrasher-corp/gocryptotrader/exchanges/asset" @@ -2045,3 +2047,13 @@ func TestAdjustQuoteAmount(t *testing.T) { t.Fatalf("received: '%v' but expected: '%v'", s.Amount, 5.22222222) } } + +func TestSideUnmarshal(t *testing.T) { + t.Parallel() + var s Side + assert.Nil(t, s.UnmarshalJSON([]byte(`"SELL"`)), "Quoted valid side okay") + assert.Equal(t, Sell, s, "Correctly set order Side") + assert.ErrorIs(t, s.UnmarshalJSON([]byte(`"STEAL"`)), ErrSideIsInvalid, "Quoted invalid side errors") + var jErr *json.UnmarshalTypeError + assert.ErrorAs(t, s.UnmarshalJSON([]byte(`14`)), &jErr, "non-string valid json is rejected") +} diff --git a/exchanges/order/orders.go b/exchanges/order/orders.go index 077ec7ef..98400f66 100644 --- a/exchanges/order/orders.go +++ b/exchanges/order/orders.go @@ -1,8 +1,11 @@ package order import ( + "bytes" + "encoding/json" "errors" "fmt" + "reflect" "sort" "strings" "time" @@ -1060,6 +1063,18 @@ func StringToOrderSide(side string) (Side, error) { } } +// UnmarshalJSON parses the JSON-encoded order side and stores the result +// It expects a quoted string input, and uses StringToOrderSide to parse it +func (s *Side) UnmarshalJSON(data []byte) (err error) { + if !bytes.HasPrefix(data, []byte(`"`)) { + // Note that we don't need to worry about invalid JSON here, it wouldn't have made it past the deserialiser far + // TODO: Can use reflect.TypeFor[s]() when it's released, probably 1.21 + return &json.UnmarshalTypeError{Value: string(data), Type: reflect.TypeOf(s), Offset: 1} + } + *s, err = StringToOrderSide(string(data[1 : len(data)-1])) // Remove quotes + return +} + // StringToOrderType for converting case insensitive order type // and returning a real Type func StringToOrderType(oType string) (Type, error) { diff --git a/exchanges/sharedtestvalues/sharedtestvalues.go b/exchanges/sharedtestvalues/sharedtestvalues.go index 8d0cce61..a8d10519 100644 --- a/exchanges/sharedtestvalues/sharedtestvalues.go +++ b/exchanges/sharedtestvalues/sharedtestvalues.go @@ -154,6 +154,7 @@ func ForceFileStandard(t *testing.T, pattern string) error { // TestFixtureToDataHandler takes a new empty exchange and configures a new websocket handler for it, and squirts the json path contents to it // It accepts a reader function, which is probably e.wsHandleData but could be anything func TestFixtureToDataHandler(t *testing.T, seed, e exchange.IBotExchange, fixturePath string, reader func([]byte) error) { + t.Helper() b := e.GetBase() seedBase := seed.GetBase()