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
This commit is contained in:
Gareth Kirwan
2023-10-13 06:18:55 +02:00
committed by GitHub
parent 233a65a778
commit 8430509807
9 changed files with 201 additions and 183 deletions

View File

@@ -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"}]}`

View File

@@ -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

View File

@@ -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"`

View File

@@ -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
}

View File

@@ -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

4
exchanges/okx/testdata/wsOrders.json vendored Normal file
View File

@@ -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"}]}

View File

@@ -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")
}

View File

@@ -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) {

View File

@@ -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()