Binance/FTX: spot websocket fixes (#753)

* exchange/binance/websocket:

1. resolve order type by reading the dedicated executionReport field (X)

2. populate average price (computed as advised in docs: Z/z)

3. fee taken is now assigned to order.Detail.Fee (not to order.Detail.Cost)

4. proper order.Detail.Cost and order.Detail.CostAsset

* order.Detail: add a new field: FeeAsset

* exchange/binance/websocket: wsOrderUpdate: assign FeeAsset

* exchange/binance/websocket: wsOrderUpdate: use TransactionTime instead of EventTime (more precise) and populate order.Detail.TargetAmount

* exchange/binance/websocket: wsOrderUpdate: use full name for variables (isn't obvious otherwise), do not mix var and :=, order temporary variable by their order in order.Detail

* exchange/binance/websocket: order fields of WsOrderUpdateData by the documented/received-over-the-websocket order of fields

* exchange/binance/websocket: add explicitly "M" as WsOrderUpdateData.Ignored2, which would otherwise override small "m"

* exchange/binance/websocket: check for zero when computing averagePrice

* exchange/binance/test: update TestExecutionTypeToOrderStatus and TestWsOrderExecutionReport

* exchange/binance/websocket: wsOrderUpdate now reports cost thus far, not the total order cost as that cannot be predicted in the case of partially filled orders

* exchange/ftx/websocket: WsOrderDataStore now reports cost thus far, not the total order cost as that cannot be predicted in the case of partially filled orders

* exchange/binance/websocket: executionReport now does not set TargetAmount as it is not applicable

* exchanges: binance/websocket and ftx/websocket now properly set Date and LastUpdated time fields when reporting a trade

* exchange/binance/websocket: check if WsOrderUpdateData.CommissionAsset is populated when assigning feeAsset

* exchange/binance/test: fix TestWsOrderExecutionReport
This commit is contained in:
Yordan Miladinov
2021-08-18 11:31:03 +03:00
committed by GitHub
parent 4ba2c710b5
commit 3467914ab7
5 changed files with 112 additions and 94 deletions

View File

@@ -2266,9 +2266,11 @@ func TestExecutionTypeToOrderStatus(t *testing.T) {
}
testCases := []TestCases{
{Case: "NEW", Result: order.New},
{Case: "PARTIALLY_FILLED", Result: order.PartiallyFilled},
{Case: "FILLED", Result: order.Filled},
{Case: "CANCELED", Result: order.Cancelled},
{Case: "PENDING_CANCEL", Result: order.PendingCancel},
{Case: "REJECTED", Result: order.Rejected},
{Case: "TRADE", Result: order.PartiallyFilled},
{Case: "EXPIRED", Result: order.Expired},
{Case: "LOL", Result: order.UnknownStatus},
}
@@ -2491,20 +2493,26 @@ func TestWsOrderExecutionReport(t *testing.T) {
payload := []byte(`{"stream":"jTfvpakT2yT0hVIo5gYWVihZhdM2PrBgJUZ5PyfZ4EVpCkx4Uoxk5timcrQc","data":{"e":"executionReport","E":1616627567900,"s":"BTCUSDT","c":"c4wyKsIhoAaittTYlIVLqk","S":"BUY","o":"LIMIT","f":"GTC","q":"0.00028400","p":"52789.10000000","P":"0.00000000","F":"0.00000000","g":-1,"C":"","x":"NEW","X":"NEW","r":"NONE","i":5340845958,"l":"0.00000000","z":"0.00000000","L":"0.00000000","n":"0","N":"BTC","T":1616627567900,"t":-1,"I":11388173160,"w":true,"m":false,"M":false,"O":1616627567900,"Z":"0.00000000","Y":"0.00000000","Q":"0.00000000"}}`)
// this is a buy BTC order, normally commission is charged in BTC, vice versa.
expRes := order.Detail{
Price: 52789.1,
Amount: 0.00028400,
Exchange: "Binance",
ID: "5340845958",
ClientOrderID: "c4wyKsIhoAaittTYlIVLqk",
Side: order.Buy,
Type: order.Limit,
Status: order.New,
AssetType: asset.Spot,
Pair: currency.NewPair(currency.BTC, currency.USDT),
RemainingAmount: 0.000284,
Date: time.Unix(0, 1616627567900*int64(time.Millisecond)),
Cost: 0,
CostAsset: currency.BTC,
Price: 52789.1,
Amount: 0.00028400,
AverageExecutedPrice: 0,
TargetAmount: 0,
ExecutedAmount: 0,
RemainingAmount: 0.00028400,
Cost: 0,
CostAsset: currency.USDT,
Fee: 0,
FeeAsset: currency.BTC,
Exchange: "Binance",
ID: "5340845958",
ClientOrderID: "c4wyKsIhoAaittTYlIVLqk",
Type: order.Limit,
Side: order.Buy,
Status: order.New,
AssetType: asset.Spot,
Date: time.Unix(0, 1616627567900*int64(time.Millisecond)),
LastUpdated: time.Unix(0, 1616627567900*int64(time.Millisecond)),
Pair: currency.NewPair(currency.BTC, currency.USDT),
}
// empty the channel. otherwise mock_test will fail
for len(b.Websocket.DataHandler) > 0 {

View File

@@ -736,37 +736,38 @@ type wsOrderUpdate struct {
// WsOrderUpdateData defines websocket account order update data
type WsOrderUpdateData struct {
ClientOrderID string `json:"c"`
EventTime time.Time `json:"E"`
IcebergQuantity float64 `json:"F,string"`
LastExecutedPrice float64 `json:"L,string"`
CommissionAsset string `json:"N"`
OrderCreationTime time.Time `json:"O"`
StopPrice float64 `json:"P,string"`
QuoteOrderQuantity float64 `json:"Q,string"`
Side string `json:"S"`
TransactionTime time.Time `json:"T"`
OrderStatus string `json:"X"`
LastQuoteAssetTransactedQuantity float64 `json:"Y,string"`
CumulativeQuoteTransactedQuantity float64 `json:"Z,string"`
CancelledClientOrderID string `json:"C"`
EventType string `json:"e"`
EventTime time.Time `json:"E"`
Symbol string `json:"s"`
ClientOrderID string `json:"c"`
Side string `json:"S"`
OrderType string `json:"o"`
TimeInForce string `json:"f"`
Quantity float64 `json:"q,string"`
Price float64 `json:"p,string"`
StopPrice float64 `json:"P,string"`
IcebergQuantity float64 `json:"F,string"`
OrderListID int64 `json:"g"`
CancelledClientOrderID string `json:"C"`
CurrentExecutionType string `json:"x"`
OrderStatus string `json:"X"`
RejectionReason string `json:"r"`
OrderID int64 `json:"i"`
LastExecutedQuantity float64 `json:"l,string"`
IsMaker bool `json:"m"`
Commission float64 `json:"n,string"`
OrderType string `json:"o"`
Price float64 `json:"p,string"`
Quantity float64 `json:"q,string"`
RejectionReason string `json:"r"`
Symbol string `json:"s"`
TradeID int64 `json:"t"`
Ignored int64 `json:"I"` // must be ignored explicitly, otherwise it overwrites 'i'
IsOnOrderBook bool `json:"w"`
CurrentExecutionType string `json:"x"`
CumulativeFilledQuantity float64 `json:"z,string"`
LastExecutedPrice float64 `json:"L,string"`
Commission float64 `json:"n,string"`
CommissionAsset string `json:"N"`
TransactionTime time.Time `json:"T"`
TradeID int64 `json:"t"`
Ignored int64 `json:"I"` // Must be ignored explicitly, otherwise it overwrites 'i'.
IsOnOrderBook bool `json:"w"`
IsMaker bool `json:"m"`
Ignored2 bool `json:"M"` // See the comment for "I".
OrderCreationTime time.Time `json:"O"`
CumulativeQuoteTransactedQuantity float64 `json:"Z,string"`
LastQuoteAssetTransactedQuantity float64 `json:"Y,string"`
QuoteOrderQuantity float64 `json:"Q,string"`
}
type wsListStatus struct {

View File

@@ -207,66 +207,68 @@ func (b *Binance) wsHandleData(respRaw []byte) error {
b.Name,
err)
}
var orderID = strconv.FormatInt(data.Data.OrderID, 10)
oType, err := order.StringToOrderType(data.Data.OrderType)
if err != nil {
b.Websocket.DataHandler <- order.ClassificationError{
Exchange: b.Name,
OrderID: orderID,
Err: err,
}
averagePrice := 0.0
if data.Data.CumulativeFilledQuantity != 0 {
averagePrice = data.Data.CumulativeQuoteTransactedQuantity / data.Data.CumulativeFilledQuantity
}
var oSide order.Side
oSide, err = order.StringToOrderSide(data.Data.Side)
if err != nil {
b.Websocket.DataHandler <- order.ClassificationError{
Exchange: b.Name,
OrderID: orderID,
Err: err,
}
}
var oStatus order.Status
oStatus, err = stringToOrderStatus(data.Data.CurrentExecutionType)
if err != nil {
b.Websocket.DataHandler <- order.ClassificationError{
Exchange: b.Name,
OrderID: orderID,
Err: err,
}
}
if oStatus == order.PartiallyFilled && data.Data.CumulativeFilledQuantity == data.Data.Quantity {
oStatus = order.Filled
}
var p currency.Pair
var a asset.Item
p, a, err = b.GetRequestFormattedPairAndAssetType(data.Data.Symbol)
remainingAmount := data.Data.Quantity - data.Data.CumulativeFilledQuantity
pair, assetType, err := b.GetRequestFormattedPairAndAssetType(data.Data.Symbol)
if err != nil {
return err
}
var feeAsset currency.Code
if data.Data.CommissionAsset != "" {
feeAsset = currency.NewCode(data.Data.CommissionAsset)
}
orderID := strconv.FormatInt(data.Data.OrderID, 10)
orderStatus, err := stringToOrderStatus(data.Data.OrderStatus)
if err != nil {
b.Websocket.DataHandler <- order.ClassificationError{
Exchange: b.Name,
OrderID: orderID,
Err: err,
}
}
clientOrderID := data.Data.ClientOrderID
if oStatus == order.Cancelled {
if orderStatus == order.Cancelled {
clientOrderID = data.Data.CancelledClientOrderID
}
var costAsset currency.Code
if data.Data.CommissionAsset != "" {
costAsset = currency.NewCode(data.Data.CommissionAsset)
orderType, err := order.StringToOrderType(data.Data.OrderType)
if err != nil {
b.Websocket.DataHandler <- order.ClassificationError{
Exchange: b.Name,
OrderID: orderID,
Err: err,
}
}
orderSide, err := order.StringToOrderSide(data.Data.Side)
if err != nil {
b.Websocket.DataHandler <- order.ClassificationError{
Exchange: b.Name,
OrderID: orderID,
Err: err,
}
}
b.Websocket.DataHandler <- &order.Detail{
Price: data.Data.Price,
Amount: data.Data.Quantity,
ExecutedAmount: data.Data.CumulativeFilledQuantity,
RemainingAmount: data.Data.Quantity - data.Data.CumulativeFilledQuantity,
Exchange: b.Name,
ID: orderID,
Type: oType,
Side: oSide,
Status: oStatus,
AssetType: a,
Date: data.Data.OrderCreationTime,
Pair: p,
ClientOrderID: clientOrderID,
Cost: data.Data.Commission,
CostAsset: costAsset,
Price: data.Data.Price,
Amount: data.Data.Quantity,
AverageExecutedPrice: averagePrice,
ExecutedAmount: data.Data.CumulativeFilledQuantity,
RemainingAmount: remainingAmount,
Cost: data.Data.CumulativeQuoteTransactedQuantity,
CostAsset: pair.Quote,
Fee: data.Data.Commission,
FeeAsset: feeAsset,
Exchange: b.Name,
ID: orderID,
ClientOrderID: clientOrderID,
Type: orderType,
Side: orderSide,
Status: orderStatus,
AssetType: assetType,
Date: data.Data.OrderCreationTime,
LastUpdated: data.Data.TransactionTime,
Pair: pair,
}
return nil
case "listStatus":
@@ -435,12 +437,16 @@ func stringToOrderStatus(status string) (order.Status, error) {
switch status {
case "NEW":
return order.New, nil
case "PARTIALLY_FILLED":
return order.PartiallyFilled, nil
case "FILLED":
return order.Filled, nil
case "CANCELED":
return order.Cancelled, nil
case "PENDING_CANCEL":
return order.PendingCancel, nil
case "REJECTED":
return order.Rejected, nil
case "TRADE":
return order.PartiallyFilled, nil
case "EXPIRED":
return order.Expired, nil
default:

View File

@@ -353,7 +353,7 @@ func (f *FTX) wsHandleData(respRaw []byte) error {
resp.AverageExecutedPrice = resultData.OrderData.AvgFillPrice
resp.ExecutedAmount = resultData.OrderData.FilledSize
resp.RemainingAmount = resultData.OrderData.Size - resultData.OrderData.FilledSize
resp.Cost = resp.AverageExecutedPrice * resp.Amount
resp.Cost = resp.AverageExecutedPrice * resultData.OrderData.FilledSize
// Fee: orderVars.Fee is incorrect.
resp.Exchange = f.Name
resp.ID = strconv.FormatInt(resultData.OrderData.ID, 10)
@@ -363,6 +363,8 @@ func (f *FTX) wsHandleData(respRaw []byte) error {
resp.Status = orderVars.Status
resp.AssetType = assetType
resp.Date = resultData.OrderData.CreatedAt
// There's no current timestamp, so this is the best we can get.
resp.LastUpdated = resultData.OrderData.CreatedAt
resp.Pair = pair
f.Websocket.DataHandler <- &resp
case wsFills:

View File

@@ -135,6 +135,7 @@ type Detail struct {
Cost float64
CostAsset currency.Code
Fee float64
FeeAsset currency.Code
Exchange string
InternalOrderID string
ID string