Skip to content

Commit

Permalink
fixup! Huobi: Add V2 websocket support
Browse files Browse the repository at this point in the history
  • Loading branch information
gbjk committed Nov 2, 2024
1 parent 97fb2fc commit 8e1854f
Show file tree
Hide file tree
Showing 6 changed files with 118 additions and 155 deletions.
93 changes: 26 additions & 67 deletions exchanges/huobi/huobi_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -1412,74 +1412,33 @@ func TestWsAccountUpdate(t *testing.T) {
}

func TestWsOrderUpdate(t *testing.T) {
pressXToJSON := []byte(`{
"op": "notify",
"topic": "orders.htusdt",
"ts": 1522856623232,
"data": {
"seq-id": 94984,
"order-id": 2039498445,
"symbol": "btcusdt",
"account-id": 100077,
"order-amount": "5000.000000000000000000",
"order-price": "1.662100000000000000",
"created-at": 1522858623622,
"order-type": "buy-limit",
"order-source": "api",
"order-state": "filled",
"role": "taker",
"price": "1.662100000000000000",
"filled-amount": "5000.000000000000000000",
"unfilled-amount": "0.000000000000000000",
"filled-cash-amount": "8301.357280000000000000",
"filled-fees": "8.000000000000000000"
}
}`)
err := h.wsHandleData(pressXToJSON)
require.NoError(t, err)
}

func TestWsMarketByPrice(t *testing.T) {
err := h.Websocket.AddSubscriptions(h.Websocket.Conn, &subscription.Subscription{Key: "market.btcusdt.mbp.150", Asset: asset.Spot, Pairs: currency.Pairs{btcusdtPair}, Channel: subscription.OrderbookChannel})
h := new(HUOBI) //nolint:govet // Intentional shadow to avoid future copy/paste mistakes
require.NoError(t, testexch.Setup(h), "Setup Instance must not error")
err := h.Websocket.AddSubscriptions(h.Websocket.Conn, &subscription.Subscription{Key: "orders#btcusdt", Asset: asset.Spot, Pairs: currency.Pairs{btcusdtPair}, Channel: subscription.MyOrdersChannel})
require.NoError(t, err, "AddSubscriptions must not error")
pressXToJSON := []byte(`{
"ch": "market.btcusdt.mbp.150",
"ts": 1573199608679,
"tick": {
"seqNum": 100020146795,
"prevSeqNum": 100020146794,
"bids": [],
"asks": [
[645.140000000000000000, 26.755973959140651643]
]
}
}`)
err = h.wsHandleData(pressXToJSON)
require.NoError(t, err)
pressXToJSON = []byte(`{
"id": "id2",
"rep": "market.btcusdt.mbp.150",
"status": "ok",
"data": {
"seqNum": 100020142010,
"bids": [
[618.37, 71.594],
[423.33, 77.726],
[223.18, 47.997],
[219.34, 24.82],
[210.34, 94.463]
],
"asks": [
[650.59, 14.909733438479636],
[650.63, 97.996],
[650.77, 97.465],
[651.23, 83.973],
[651.42, 34.465]
]
}
}`)
err = h.wsHandleData(pressXToJSON)
require.NoError(t, err)
h.SetSaveTradeDataStatus(true)
testexch.FixtureToDataHandler(t, "testdata/wsMyOrders.json", h.wsHandleData)
close(h.Websocket.DataHandler)
require.Len(t, h.Websocket.DataHandler, 3, "Should see correct number of records")
exp := []*order.Detail{
{
Exchange: h.Name,
Pair: btcusdtPair,
Side: order.Buy,
Status: order.Rejected,
ClientOrderID: "abc123",
Asset

Check failure on line 1430 in exchanges/huobi/huobi_test.go

View workflow job for this annotation

GitHub Actions / lint

syntax error: unexpected newline in composite literal; possibly missing comma or } (typecheck)

Check failure on line 1430 in exchanges/huobi/huobi_test.go

View workflow job for this annotation

GitHub Actions / lint

missing ',' before newline in composite literal (typecheck)

Check failure on line 1430 in exchanges/huobi/huobi_test.go

View workflow job for this annotation

GitHub Actions / GoCryptoTrader back-end (ubuntu-latest, amd64, true, false)

missing ',' before newline in composite literal

Check failure on line 1430 in exchanges/huobi/huobi_test.go

View workflow job for this annotation

GitHub Actions / GoCryptoTrader back-end (ubuntu-latest, 386, true, true)

missing ',' before newline in composite literal

Check failure on line 1430 in exchanges/huobi/huobi_test.go

View workflow job for this annotation

GitHub Actions / GoCryptoTrader back-end (macos-latest, amd64, true, true)

missing ',' before newline in composite literal

Check failure on line 1430 in exchanges/huobi/huobi_test.go

View workflow job for this annotation

GitHub Actions / GoCryptoTrader back-end (macos-13, amd64, true, true)

missing ',' before newline in composite literal

Check failure on line 1430 in exchanges/huobi/huobi_test.go

View workflow job for this annotation

GitHub Actions / GoCryptoTrader back-end (windows-latest, amd64, true, true)

missing ',' before newline in composite literal
},
{},
{},
}
for _, e := range exp {
m := <-h.Websocket.DataHandler
require.IsType(t, &order.Detail{}, m, "Must get the correct type from DataHandler")
d, _ := m.(*order.Detail)
require.NotNil(t, d)
assert.Equal(t, e, d)
}
}

func TestWsOrdersUpdate(t *testing.T) {
Expand Down
73 changes: 30 additions & 43 deletions exchanges/huobi/huobi_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -899,11 +899,11 @@ type wsAuthReq struct {
Signature string `json:"signature"`
}

// wsAccountUpdateMsg contains account updates to balances
type wsAccountUpdateMsg struct {
Data WsAccountUpdate `json:"data"`
}

// WsAccountUpdate contains account updates to balances
type WsAccountUpdate struct {
Currency string `json:"currency"`
AccountID int64 `json:"accountId"`
Expand All @@ -915,48 +915,35 @@ type WsAccountUpdate struct {
SeqNum string `json:"seqNum"`
}

// WsAuthenticatedOrdersUpdateResponse response from OrdersUpdate authenticated subscription
type WsAuthenticatedOrdersUpdateResponse struct {
Data WsAuthenticatedOrdersUpdateResponseData `json:"data"`
}

// WsAuthenticatedOrdersUpdateResponseData order update data
type WsAuthenticatedOrdersUpdateResponseData struct {
UnfilledAmount float64 `json:"unfilled-amount,string"`
FilledAmount float64 `json:"filled-amount,string"`
Price float64 `json:"price,string"`
OrderID int64 `json:"order-id"`
Symbol string `json:"symbol"`
MatchID int64 `json:"match-id"`
FilledCashAmount float64 `json:"filled-cash-amount,string"`
Role string `json:"role"`
OrderState string `json:"order-state"`
OrderType string `json:"order-type"`
}

// WsAuthenticatedOrdersResponse response from Orders authenticated subscription
type WsAuthenticatedOrdersResponse struct {
Data []WsAuthenticatedOrdersResponseData `json:"data"`
}

// WsAuthenticatedOrdersResponseData order data
type WsAuthenticatedOrdersResponseData struct {
SeqID int64 `json:"seq-id"`
OrderID int64 `json:"order-id"`
Symbol string `json:"symbol"`
AccountID int64 `json:"account-id"`
OrderAmount float64 `json:"order-amount,string"`
OrderPrice float64 `json:"order-price,string"`
CreatedAt int64 `json:"created-at"`
OrderType string `json:"order-type"`
OrderSource string `json:"order-source"`
OrderState string `json:"order-state"`
Role string `json:"role"`
Price float64 `json:"price,string"`
FilledAmount float64 `json:"filled-amount,string"`
UnfilledAmount float64 `json:"unfilled-amount,string"`
FilledCashAmount float64 `json:"filled-cash-amount,string"`
FilledFees float64 `json:"filled-fees,string"`
type wsOrderUpdateMsg struct {
Data WsOrderUpdate `json:"data"`
}

type orderSide order.Side

// wsOrderUpdateMsg contains updates to orders
type WsOrderUpdate struct {
EventType string `json:"eventType"`
Symbol string `json:"symbol"`
AccountID int64 `json:"accountId"`
OrderID int64 `json:"orderId"`
TradeID int64 `json:"tradeId"`
ClientOrderID string `json:"clientOrderId"`
Source string `json:"orderSource"`
Price float64 `json:"orderPrice,string"`
Size float64 `json:"orderSize,string"`
Value float64 `json:"orderValue,string"`
OrderType string `json:"type"`
TradePrice float64 `json:"tradePrice,string"`
TradeVolume float64 `json:"tradeVolume,string"`
RemainingAmount float64 `json:"remainAmt,string"`
ExecutedAmount float64 `json:"execAmt,string"`
Taker bool `json:"aggressor"`
Side order.Side `json:"orderSide"`
OrderStatus string `json:"orderStatus"`
LastActTime int64 `json:"lastActTime"`
CreateTime int64 `json:"orderCreateTime"`
TradeTime int64 `json:"tradeTime"`
}

// OrderVars stores side, status and type for any order/trade
Expand Down
95 changes: 55 additions & 40 deletions exchanges/huobi/huobi_websocket.go
Original file line number Diff line number Diff line change
Expand Up @@ -218,8 +218,10 @@ func (h *HUOBI) wsHandleChannelMsgs(s *subscription.Subscription, respRaw []byte
return h.wsHandleAllTradesMsg(s, respRaw)
case subscription.MyAccountChannel:
return h.wsHandleMyAccountMsg(respRaw)
case subscription.MyOrdersChannel, subscription.MyTradesChannel:
case subscription.MyOrdersChannel:
return h.wsHandleMyOrdersMsg(s, respRaw)
case subscription.MyTradesChannel:
panic("Think of the fish")
}
return fmt.Errorf("%w: %s", common.ErrNotYetImplemented, s.Channel)
}
Expand Down Expand Up @@ -345,50 +347,61 @@ func (h *HUOBI) wsHandleMyOrdersMsg(s *subscription.Subscription, respRaw []byte
if len(s.Pairs) != 1 {
return subscription.ErrNotSinglePair
}
var response WsAuthenticatedOrdersUpdateResponse
if err := json.Unmarshal(respRaw, &response); err != nil {
var msg wsOrderUpdateMsg
if err := json.Unmarshal(respRaw, &msg); err != nil {
return err
}
orderID := strconv.FormatInt(response.Data.OrderID, 10)
oSide, err := stringToOrderSide(response.Data.OrderType)
if err != nil {
return &order.ClassificationError{
Exchange: h.Name,
OrderID: orderID,
Err: err,
}
o := msg.Data
d := &order.Detail{
Price: o.Price,
Amount: o.Size,
ExecutedAmount: o.ExecutedAmount,
RemainingAmount: o.RemainingAmount,
Exchange: h.Name,
Side: o.Side,
AssetType: s.Asset,
Pair: s.Pairs[0],
}
oType, err := stringToOrderType(response.Data.OrderType)
if err != nil {
if o.OrderID != 0 {
d.OrderID = strconv.FormatInt(o.OrderID, 10)
}
switch o.EventType {
case "trigger", "deletion", "cancellation":
d.LastUpdated = time.Unix(o.LastActTime*1000, 0)
case "creation":
d.LastUpdated = time.Unix(o.CreateTime*1000, 0)
case "trade":
d.LastUpdated = time.Unix(o.TradeTime*1000, 0)
}
var err error
if d.Status, err = order.StringToOrderStatus(o.OrderStatus); err != nil {
return &order.ClassificationError{
Exchange: h.Name,
OrderID: orderID,
OrderID: d.OrderID,
Err: err,
}
}
oStatus, err := stringToOrderStatus(response.Data.OrderState)
if err != nil {
return &order.ClassificationError{
Exchange: h.Name,
OrderID: orderID,
Err: err,
if o.Side == order.UnknownSide {
d.Side, err = stringToOrderSide(o.OrderType)
if err != nil {
return &order.ClassificationError{
Exchange: h.Name,
OrderID: d.OrderID,
Err: err,
}
}
}
h.Websocket.DataHandler <- &order.Detail{
Price: response.Data.Price,
Amount: response.Data.UnfilledAmount + response.Data.FilledAmount,
ExecutedAmount: response.Data.FilledAmount,
RemainingAmount: response.Data.UnfilledAmount,
Exchange: h.Name,
OrderID: orderID,
Type: oType,
Side: oSide,
Status: oStatus,
AssetType: s.Asset,
Pair: s.Pairs[0],
// TODO
//LastUpdated: time.Unix(response.Timestamp*1000, 0),
if o.OrderType != "" {
d.Type, err = stringToOrderType(o.OrderType)
if err != nil {
return &order.ClassificationError{
Exchange: h.Name,
OrderID: d.OrderID,
Err: err,
}
}
}
h.Websocket.DataHandler <- d
return nil
}

Expand Down Expand Up @@ -532,17 +545,20 @@ func (h *HUOBI) wsLogin(ctx context.Context) error {

func stringToOrderStatus(status string) (order.Status, error) {
switch status {
case "rejected":
return order.Rejected, nil
case "submitted":
return order.New, nil
case "canceled":
return order.Cancelled, nil
case "partial-filled":
return order.PartiallyFilled, nil
case "filled":
return order.Filled, nil
case "partial-canceled":
return order.PartiallyCancelled, nil
case "canceled":
return order.Cancelled, nil
default:
return order.UnknownStatus,
errors.New(status + " not recognised as order status")
return order.UnknownStatus, errors.New(status + " not recognised as order status")
}
}

Expand All @@ -554,8 +570,7 @@ func stringToOrderSide(side string) (order.Side, error) {
return order.Sell, nil
}

return order.UnknownSide,
errors.New(side + " not recognised as order side")
return order.UnknownSide, errors.New(side + " not recognised as order side")
}

func stringToOrderType(oType string) (order.Type, error) {
Expand Down
3 changes: 3 additions & 0 deletions exchanges/huobi/testdata/wsMyOrders.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{"action":"push","ch":"orders#btcusdt","data":{"orderSide":"buy","lastActTime":1583853365586,"clientOrderId":"abc123","orderStatus":"rejected","symbol":"btcusdt","eventType":"trigger","errCode":2002,"errMessage":"invalid.client.order.id (NT)"}}
{"action":"push","ch":"orders#btcusdt","data":{"orderSide":"buy","lastActTime":1583853365586,"clientOrderId":"abc123","orderStatus":"canceled","symbol":"btcusdt","eventType":"deletion"}}
{"action":"push","ch":"orders#btcusdt","data":{"orderSize":"2.000000000000000000","orderCreateTime":1583853365586,"accountld":992701,"orderPrice":"77.000000000000000000","type":"sell-limit","orderId":27163533,"clientOrderId":"abc123","orderSource":"spot-api","orderStatus":"submitted","symbol":"btcusdt","eventType":"creation"}}
3 changes: 1 addition & 2 deletions exchanges/order/order_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -184,8 +184,7 @@ type ModifyResponse struct {
}

// Detail contains all properties of an order
// Each exchange has their own requirements, so not all fields
// are required to be populated
// Each exchange has their own requirements, so not all fields are required to be populated
type Detail struct {
ImmediateOrCancel bool
HiddenOrder bool
Expand Down
6 changes: 3 additions & 3 deletions exchanges/order/orders.go
Original file line number Diff line number Diff line change
Expand Up @@ -1162,15 +1162,15 @@ func StringToOrderStatus(status string) (Status, error) {
switch status {
case AnyStatus.String():
return AnyStatus, nil
case New.String(), "PLACED", "ACCEPTED":
case New.String(), "PLACED", "ACCEPTED", "SUBMITTED":
return New, nil
case Active.String(), "STATUS_ACTIVE", "LIVE":
return Active, nil
case PartiallyFilled.String(), "PARTIALLY MATCHED", "PARTIALLY FILLED":
case PartiallyFilled.String(), "PARTIAL-FILLED", "PARTIALLY MATCHED", "PARTIALLY FILLED":
return PartiallyFilled, nil
case Filled.String(), "FULLY MATCHED", "FULLY FILLED", "ORDER_FULLY_TRANSACTED", "EFFECTIVE":
return Filled, nil
case PartiallyCancelled.String(), "PARTIALLY CANCELLED", "ORDER_PARTIALLY_TRANSACTED":
case PartiallyCancelled.String(), "PARTIAL-CANCELED", "PARTIALLY CANCELLED", "ORDER_PARTIALLY_TRANSACTED":
return PartiallyCancelled, nil
case PartiallyFilledCancelled.String(), "PARTIALLYFILLEDCANCELED":
return PartiallyFilledCancelled, nil
Expand Down

0 comments on commit 8e1854f

Please sign in to comment.