Bitmex: Fix deprecated API endpoints and add config migration support (#1901)

* Bitmex: Fix configured WS url ignored

* Bitmex: Replace deprecated WS api endpoint

* [Bitmex deprecated the old WS multiplexing endpoint](https://blog.bitmex.com/api_announcement/api-update-remove-support-realtimemd/)
* [Bitmex deprecated the www WS endpoint in 2021](https://blog.bitmex.com/api_announcement/change-of-websocket-endpoint/). Apparently still in service though.

Fixes #1894
This commit is contained in:
Gareth Kirwan
2025-05-14 05:56:53 +02:00
committed by GitHub
parent 61fc778818
commit c2bb050eac
6 changed files with 189 additions and 102 deletions

View File

@@ -9,6 +9,7 @@ import (
v5 "github.com/thrasher-corp/gocryptotrader/config/versions/v5" v5 "github.com/thrasher-corp/gocryptotrader/config/versions/v5"
v6 "github.com/thrasher-corp/gocryptotrader/config/versions/v6" v6 "github.com/thrasher-corp/gocryptotrader/config/versions/v6"
v7 "github.com/thrasher-corp/gocryptotrader/config/versions/v7" v7 "github.com/thrasher-corp/gocryptotrader/config/versions/v7"
v8 "github.com/thrasher-corp/gocryptotrader/config/versions/v8"
) )
func init() { func init() {
@@ -20,4 +21,5 @@ func init() {
Manager.registerVersion(5, &v5.Version{}) Manager.registerVersion(5, &v5.Version{})
Manager.registerVersion(6, &v6.Version{}) Manager.registerVersion(6, &v6.Version{})
Manager.registerVersion(7, &v7.Version{}) Manager.registerVersion(7, &v7.Version{})
Manager.registerVersion(8, &v8.Version{})
} }

43
config/versions/v8/v8.go Normal file
View File

@@ -0,0 +1,43 @@
package v8
import (
"context"
"errors"
"github.com/buger/jsonparser"
)
// Version is an ExchangeVersion to remove deprecated WS endpoints from user config
// Announcements:
// * https://blog.bitmex.com/api_announcement/change-of-websocket-endpoint/
// * https://blog.bitmex.com/api_announcement/api-update-remove-support-realtimemd/
type Version struct{}
// Exchanges returns just Bitmex
func (v *Version) Exchanges() []string { return []string{"Bitmex"} }
// UpgradeExchange replaces deprecated WS endpoints
func (v *Version) UpgradeExchange(_ context.Context, e []byte) ([]byte, error) {
url, err := jsonparser.GetString(e, "api", "urlEndpoints", "WebsocketSpotURL")
switch {
case errors.Is(err, jsonparser.KeyPathNotFoundError):
return e, nil
case err != nil:
return e, err
}
switch url {
case "wss://ws.bitmex.com/realtimemd", "wss://www.bitmex.com/realtimemd", "wss://www.bitmex.com/realtime":
// Old defaults, just delete them
return jsonparser.Delete(e, "api", "urlEndpoints", "WebsocketSpotURL"), nil
case "wss://ws.testnet.bitmex.com/realtimemd", "wss://testnet.bitmex.com/realtimemd", "wss://testnet.bitmex.com/realtime":
// User wants to use testnet
return jsonparser.Set(e, []byte(`"wss://ws.testnet.bitmex.com/realtime"`), "api", "urlEndpoints", "WebsocketSpotURL")
}
return e, nil
}
// DowngradeExchange is a no-op for v8
func (v *Version) DowngradeExchange(_ context.Context, e []byte) ([]byte, error) {
return e, nil
}

View File

@@ -0,0 +1,56 @@
package v8_test
import (
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
v8 "github.com/thrasher-corp/gocryptotrader/config/versions/v8"
)
func TestExchanges(t *testing.T) {
t.Parallel()
assert.Equal(t, []string{"Bitmex"}, new(v8.Version).Exchanges())
}
func TestUpgradeExchange(t *testing.T) {
t.Parallel()
for _, tt := range []struct {
in string
exp string
}{
{"wss://private.bitmex.com/realtimemd", `"WebsocketSpotURL": "wss://private.bitmex.com/realtimemd"`},
{"wss://ws.bitmex.com/realtimemd", ""},
{"wss://www.bitmex.com/realtimemd", ""},
{"wss://www.bitmex.com/realtime", ""},
{"wss://ws.testnet.bitmex.com/realtimemd", `"WebsocketSpotURL": "wss://ws.testnet.bitmex.com/realtime"`},
{"wss://testnet.bitmex.com/realtimemd", `"WebsocketSpotURL": "wss://ws.testnet.bitmex.com/realtime"`},
{"wss://testnet.bitmex.com/realtime", `"WebsocketSpotURL": "wss://ws.testnet.bitmex.com/realtime"`},
} {
t.Run(tt.in, func(t *testing.T) {
t.Parallel()
in := []byte(`{"name":"Bitmex","api":{"urlEndpoints":{"WebsocketSpotURL": "` + tt.in + `"}}}`)
out, err := new(v8.Version).UpgradeExchange(t.Context(), in)
require.NoError(t, err)
exp := `{"name":"Bitmex","api":{"urlEndpoints":{` + tt.exp + `}}}`
assert.Equal(t, exp, string(out))
})
}
in := []byte(`{"name":"Bitmex","api":{}`)
out, err := new(v8.Version).UpgradeExchange(t.Context(), in)
require.NoError(t, err, "UpgradeExchange must not error when urlEndpoints is missing")
assert.Equal(t, string(in), string(out), "UpgradeExchange should return same input not error when urlEndpoints is missing")
_, err = new(v8.Version).UpgradeExchange(t.Context(), []byte(`{"name":"Bitmex","api":{"urlEndpoints":{"WebsocketSpotURL": 42}}}`))
require.ErrorContains(t, err, "Value is not a string", "UpgradeExchange must error correctly on string value")
}
func TestDowngradeExchange(t *testing.T) {
t.Parallel()
in := []byte(`{"name":"Bitmex","api":{"urlEndpoints":{"WebsocketSpotURL": 42}}}`)
out, err := new(v8.Version).DowngradeExchange(t.Context(), in)
require.NoError(t, err)
require.Equal(t, string(in), string(out), "DowngradeExchange must not change json")
}

View File

@@ -692,7 +692,7 @@ func TestUpdateTradablePairs(t *testing.T) {
func TestWsPositionUpdate(t *testing.T) { func TestWsPositionUpdate(t *testing.T) {
t.Parallel() t.Parallel()
pressXToJSON := []byte(`[0, "public", "public", {"table":"position", pressXToJSON := []byte(`{"table":"position",
"action":"update", "action":"update",
"data":[{ "data":[{
"account":2,"symbol":"ETHUSD","currency":"XBt", "account":2,"symbol":"ETHUSD","currency":"XBt",
@@ -700,14 +700,14 @@ func TestWsPositionUpdate(t *testing.T) {
"riskValue":87960,"homeNotional":0.0008796,"posState":"Liquidation","maintMargin":263, "riskValue":87960,"homeNotional":0.0008796,"posState":"Liquidation","maintMargin":263,
"unrealisedGrossPnl":-677,"unrealisedPnl":-677,"unrealisedPnlPcnt":-0.0078,"unrealisedRoePcnt":-0.7756, "unrealisedGrossPnl":-677,"unrealisedPnl":-677,"unrealisedPnlPcnt":-0.0078,"unrealisedRoePcnt":-0.7756,
"simpleQty":0.001,"liquidationPrice":1140.1, "timestamp":"2017-04-04T22:07:45.442Z" "simpleQty":0.001,"liquidationPrice":1140.1, "timestamp":"2017-04-04T22:07:45.442Z"
}]}]`) }]}`)
err := b.wsHandleData(pressXToJSON) err := b.wsHandleData(pressXToJSON)
require.NoError(t, err) require.NoError(t, err)
} }
func TestWsInsertExectuionUpdate(t *testing.T) { func TestWsInsertExectuionUpdate(t *testing.T) {
t.Parallel() t.Parallel()
pressXToJSON := []byte(`[0, "public", "public", {"table":"execution", pressXToJSON := []byte(`{"table":"execution",
"action":"insert", "action":"insert",
"data":[{ "data":[{
"execID":"0193e879-cb6f-2891-d099-2c4eb40fee21", "execID":"0193e879-cb6f-2891-d099-2c4eb40fee21",
@@ -722,23 +722,23 @@ func TestWsInsertExectuionUpdate(t *testing.T) {
"text":"Liquidation","trdMatchID":"7f4ab7f6-0006-3234-76f4-ae1385aad00f","execCost":88155,"execComm":66, "text":"Liquidation","trdMatchID":"7f4ab7f6-0006-3234-76f4-ae1385aad00f","execCost":88155,"execComm":66,
"homeNotional":-0.00088155,"foreignNotional":1,"transactTime":"2017-04-04T22:07:46.035Z", "homeNotional":-0.00088155,"foreignNotional":1,"transactTime":"2017-04-04T22:07:46.035Z",
"timestamp":"2017-04-04T22:07:46.035Z" "timestamp":"2017-04-04T22:07:46.035Z"
}]}]`) }]}`)
err := b.wsHandleData(pressXToJSON) err := b.wsHandleData(pressXToJSON)
require.NoError(t, err) require.NoError(t, err)
} }
func TestWSPositionUpdateHandling(t *testing.T) { func TestWSPositionUpdateHandling(t *testing.T) {
t.Parallel() t.Parallel()
pressXToJSON := []byte(`[0, "public", "public", {"table":"position", pressXToJSON := []byte(`{"table":"position",
"action":"update", "action":"update",
"data":[{ "data":[{
"account":2,"symbol":"ETHUSD","currency":"XBt","currentQty":1, "account":2,"symbol":"ETHUSD","currency":"XBt","currentQty":1,
"markPrice":1136.88,"posState":"Liquidated","simpleQty":0.001,"liquidationPrice":1140.1,"bankruptPrice":1134.37, "markPrice":1136.88,"posState":"Liquidated","simpleQty":0.001,"liquidationPrice":1140.1,"bankruptPrice":1134.37,
"timestamp":"2017-04-04T22:07:46.019Z" "timestamp":"2017-04-04T22:07:46.019Z"
}]}]`) }]}`)
err := b.wsHandleData(pressXToJSON) err := b.wsHandleData(pressXToJSON)
require.NoError(t, err) require.NoError(t, err)
pressXToJSON = []byte(`[0, "public", "public", {"table":"position", pressXToJSON = []byte(`{"table":"position",
"action":"update", "action":"update",
"data":[{ "data":[{
"account":2,"symbol":"ETHUSD","currency":"XBt", "account":2,"symbol":"ETHUSD","currency":"XBt",
@@ -751,14 +751,14 @@ func TestWSPositionUpdateHandling(t *testing.T) {
"unrealisedPnlPcnt":0,"unrealisedRoePcnt":0,"simpleQty":0,"simpleCost":0,"simpleValue":0,"avgCostPrice":null, "unrealisedPnlPcnt":0,"unrealisedRoePcnt":0,"simpleQty":0,"simpleCost":0,"simpleValue":0,"avgCostPrice":null,
"avgEntryPrice":null,"breakEvenPrice":null,"marginCallPrice":null,"liquidationPrice":null,"bankruptPrice":null, "avgEntryPrice":null,"breakEvenPrice":null,"marginCallPrice":null,"liquidationPrice":null,"bankruptPrice":null,
"timestamp":"2017-04-04T22:07:46.140Z" "timestamp":"2017-04-04T22:07:46.140Z"
}]}]`) }]}`)
err = b.wsHandleData(pressXToJSON) err = b.wsHandleData(pressXToJSON)
require.NoError(t, err) require.NoError(t, err)
} }
func TestWSOrderbookHandling(t *testing.T) { func TestWSOrderbookHandling(t *testing.T) {
t.Parallel() t.Parallel()
pressXToJSON := []byte(`[0, "public", "public", { pressXToJSON := []byte(`{
"table":"orderBookL2_25", "table":"orderBookL2_25",
"keys":["symbol","id","side"], "keys":["symbol","id","side"],
"types":{"id":"long","price":"float","side":"symbol","size":"long","symbol":"symbol"}, "types":{"id":"long","price":"float","side":"symbol","size":"long","symbol":"symbol"},
@@ -772,58 +772,58 @@ func TestWSOrderbookHandling(t *testing.T) {
{"symbol":"ETHUSD","id":17999995000,"side":"Buy","size":10,"price":50}, {"symbol":"ETHUSD","id":17999995000,"side":"Buy","size":10,"price":50},
{"symbol":"ETHUSD","id":17999996000,"side":"Buy","size":20,"price":40}, {"symbol":"ETHUSD","id":17999996000,"side":"Buy","size":20,"price":40},
{"symbol":"ETHUSD","id":17999997000,"side":"Buy","size":100,"price":30} {"symbol":"ETHUSD","id":17999997000,"side":"Buy","size":100,"price":30}
]}]`) ]}`)
err := b.wsHandleData(pressXToJSON) err := b.wsHandleData(pressXToJSON)
require.NoError(t, err) require.NoError(t, err)
pressXToJSON = []byte(`[0, "public", "public", { pressXToJSON = []byte(`{
"table":"orderBookL2_25", "table":"orderBookL2_25",
"action":"update", "action":"update",
"data":[ "data":[
{"symbol":"ETHUSD","id":17999995000,"side":"Buy","size":5,"timestamp":"2017-04-04T22:16:38.461Z"} {"symbol":"ETHUSD","id":17999995000,"side":"Buy","size":5,"timestamp":"2017-04-04T22:16:38.461Z"}
]}]`) ]}`)
err = b.wsHandleData(pressXToJSON) err = b.wsHandleData(pressXToJSON)
require.NoError(t, err) require.NoError(t, err)
pressXToJSON = []byte(`[0, "public", "public", { pressXToJSON = []byte(`{
"table":"orderBookL2_25", "table":"orderBookL2_25",
"action":"update", "action":"update",
"data":[]}]`) "data":[]}`)
err = b.wsHandleData(pressXToJSON) err = b.wsHandleData(pressXToJSON)
require.ErrorContains(t, err, "empty orderbook") require.ErrorContains(t, err, "empty orderbook")
pressXToJSON = []byte(`[0, "public", "public", { pressXToJSON = []byte(`{
"table":"orderBookL2_25", "table":"orderBookL2_25",
"action":"delete", "action":"delete",
"data":[ "data":[
{"symbol":"ETHUSD","id":17999995000,"side":"Buy","timestamp":"2017-04-04T22:16:38.461Z"} {"symbol":"ETHUSD","id":17999995000,"side":"Buy","timestamp":"2017-04-04T22:16:38.461Z"}
]}]`) ]}`)
err = b.wsHandleData(pressXToJSON) err = b.wsHandleData(pressXToJSON)
require.NoError(t, err) require.NoError(t, err)
pressXToJSON = []byte(`[0, "public", "public", { pressXToJSON = []byte(`{
"table":"orderBookL2_25", "table":"orderBookL2_25",
"action":"delete", "action":"delete",
"data":[ "data":[
{"symbol":"ETHUSD","id":17999995000,"side":"Buy","timestamp":"2017-04-04T22:16:38.461Z"} {"symbol":"ETHUSD","id":17999995000,"side":"Buy","timestamp":"2017-04-04T22:16:38.461Z"}
]}]`) ]}`)
err = b.wsHandleData(pressXToJSON) err = b.wsHandleData(pressXToJSON)
assert.ErrorIs(t, err, orderbook.ErrOrderbookInvalid) assert.ErrorIs(t, err, orderbook.ErrOrderbookInvalid)
} }
func TestWSDeleveragePositionUpdateHandling(t *testing.T) { func TestWSDeleveragePositionUpdateHandling(t *testing.T) {
t.Parallel() t.Parallel()
pressXToJSON := []byte(`[0, "public", "public", {"table":"position", pressXToJSON := []byte(`{"table":"position",
"action":"update", "action":"update",
"data":[{ "data":[{
"account":2,"symbol":"ETHUSD","currency":"XBt","currentQty":2000, "account":2,"symbol":"ETHUSD","currency":"XBt","currentQty":2000,
"markPrice":1160.72,"posState":"Deleverage","simpleQty":1.746,"liquidationPrice":1140.1, "markPrice":1160.72,"posState":"Deleverage","simpleQty":1.746,"liquidationPrice":1140.1,
"timestamp":"2017-04-04T22:16:38.460Z" "timestamp":"2017-04-04T22:16:38.460Z"
}]}]`) }]}`)
err := b.wsHandleData(pressXToJSON) err := b.wsHandleData(pressXToJSON)
require.NoError(t, err) require.NoError(t, err)
pressXToJSON = []byte(`[0, "public", "public", {"table":"position", pressXToJSON = []byte(`{"table":"position",
"action":"update", "action":"update",
"data":[{ "data":[{
"account":2,"symbol":"ETHUSD","currency":"XBt", "account":2,"symbol":"ETHUSD","currency":"XBt",
@@ -837,14 +837,14 @@ func TestWSDeleveragePositionUpdateHandling(t *testing.T) {
"simpleQty":0,"simpleCost":0,"simpleValue":0,"simplePnl":0,"simplePnlPcnt":0,"avgCostPrice":null, "simpleQty":0,"simpleCost":0,"simpleValue":0,"simplePnl":0,"simplePnlPcnt":0,"avgCostPrice":null,
"avgEntryPrice":null,"breakEvenPrice":null,"marginCallPrice":null,"liquidationPrice":null,"bankruptPrice":null, "avgEntryPrice":null,"breakEvenPrice":null,"marginCallPrice":null,"liquidationPrice":null,"bankruptPrice":null,
"timestamp":"2017-04-04T22:16:38.547Z" "timestamp":"2017-04-04T22:16:38.547Z"
}]}]`) }]}`)
err = b.wsHandleData(pressXToJSON) err = b.wsHandleData(pressXToJSON)
require.NoError(t, err) require.NoError(t, err)
} }
func TestWSDeleverageExecutionInsertHandling(t *testing.T) { func TestWSDeleverageExecutionInsertHandling(t *testing.T) {
t.Parallel() t.Parallel()
pressXToJSON := []byte(`[0, "public", "public", {"table":"execution", pressXToJSON := []byte(`{"table":"execution",
"action":"insert", "action":"insert",
"data":[{ "data":[{
"execID":"20ad1ff4-c110-a4f2-dd31-f94eaa0701fd", "execID":"20ad1ff4-c110-a4f2-dd31-f94eaa0701fd",
@@ -859,7 +859,7 @@ func TestWSDeleverageExecutionInsertHandling(t *testing.T) {
"trdMatchID":"1e849b8a-7e88-3c67-a93f-cc654d40e8ba","execCost":172306000,"execComm":-43077, "trdMatchID":"1e849b8a-7e88-3c67-a93f-cc654d40e8ba","execCost":172306000,"execComm":-43077,
"homeNotional":-1.72306,"foreignNotional":2000,"transactTime":"2017-04-04T22:16:38.472Z", "homeNotional":-1.72306,"foreignNotional":2000,"transactTime":"2017-04-04T22:16:38.472Z",
"timestamp":"2017-04-04T22:16:38.472Z" "timestamp":"2017-04-04T22:16:38.472Z"
}]}]`) }]}`)
err := b.wsHandleData(pressXToJSON) err := b.wsHandleData(pressXToJSON)
require.NoError(t, err) require.NoError(t, err)
} }
@@ -869,13 +869,13 @@ func TestWsTrades(t *testing.T) {
b := new(Bitmex) //nolint:govet // Intentional shadow to avoid future copy/paste mistakes b := new(Bitmex) //nolint:govet // Intentional shadow to avoid future copy/paste mistakes
require.NoError(t, testexch.Setup(b), "Test instance Setup must not error") require.NoError(t, testexch.Setup(b), "Test instance Setup must not error")
b.SetSaveTradeDataStatus(true) b.SetSaveTradeDataStatus(true)
msg := []byte(`[0, "public", "public", {"table":"trade","action":"insert","data":[{"timestamp":"2020-02-17T01:35:36.442Z","symbol":"ETHUSD","side":"Sell","size":100,"price":258.3,"tickDirection":"MinusTick","trdMatchID":"c427f7a0-6b26-1e10-5c4e-1bd74daf2a73","grossValue":2583000,"homeNotional":0.9904912836767037,"foreignNotional":255.84389857369254},{"timestamp":"2020-02-17T01:35:36.442Z","symbol":"ETHUSD","side":"Sell","size":100,"price":258.3,"tickDirection":"ZeroMinusTick","trdMatchID":"95eb9155-b58c-70e9-44b7-34efe50302e0","grossValue":2583000,"homeNotional":0.9904912836767037,"foreignNotional":255.84389857369254},{"timestamp":"2020-02-17T01:35:36.442Z","symbol":"ETHUSD","side":"Sell","size":100,"price":258.3,"tickDirection":"ZeroMinusTick","trdMatchID":"e607c187-f25c-86bc-cb39-8afff7aaf2d9","grossValue":2583000,"homeNotional":0.9904912836767037,"foreignNotional":255.84389857369254},{"timestamp":"2020-02-17T01:35:36.442Z","symbol":"ETHUSD","side":"Sell","size":17,"price":258.3,"tickDirection":"ZeroMinusTick","trdMatchID":"0f076814-a57d-9a59-8063-ad6b823a80ac","grossValue":439110,"homeNotional":0.1683835182250396,"foreignNotional":43.49346275752773},{"timestamp":"2020-02-17T01:35:36.442Z","symbol":"ETHUSD","side":"Sell","size":100,"price":258.25,"tickDirection":"MinusTick","trdMatchID":"f4ef3dfd-51c4-538f-37c1-e5071ba1c75d","grossValue":2582500,"homeNotional":0.9904912836767037,"foreignNotional":255.79437400950872},{"timestamp":"2020-02-17T01:35:36.442Z","symbol":"ETHUSD","side":"Sell","size":100,"price":258.25,"tickDirection":"ZeroMinusTick","trdMatchID":"81ef136b-8f4a-b1cf-78a8-fffbfa89bf40","grossValue":2582500,"homeNotional":0.9904912836767037,"foreignNotional":255.79437400950872},{"timestamp":"2020-02-17T01:35:36.442Z","symbol":"ETHUSD","side":"Sell","size":100,"price":258.25,"tickDirection":"ZeroMinusTick","trdMatchID":"65a87e8c-7563-34a4-d040-94e8513c5401","grossValue":2582500,"homeNotional":0.9904912836767037,"foreignNotional":255.79437400950872},{"timestamp":"2020-02-17T01:35:36.442Z","symbol":"ETHUSD","side":"Sell","size":15,"price":258.25,"tickDirection":"ZeroMinusTick","trdMatchID":"1d11a74e-a157-3f33-036d-35a101fba50b","grossValue":387375,"homeNotional":0.14857369255150554,"foreignNotional":38.369156101426306},{"timestamp":"2020-02-17T01:35:36.442Z","symbol":"ETHUSD","side":"Sell","size":1,"price":258.25,"tickDirection":"ZeroMinusTick","trdMatchID":"40d49df1-f018-f66f-4ca5-31d4997641d7","grossValue":25825,"homeNotional":0.009904912836767036,"foreignNotional":2.5579437400950873},{"timestamp":"2020-02-17T01:35:36.442Z","symbol":"ETHUSD","side":"Sell","size":100,"price":258.2,"tickDirection":"MinusTick","trdMatchID":"36135b51-73e5-c007-362b-a55be5830c6b","grossValue":2582000,"homeNotional":0.9904912836767037,"foreignNotional":255.7448494453249},{"timestamp":"2020-02-17T01:35:36.442Z","symbol":"ETHUSD","side":"Sell","size":100,"price":258.2,"tickDirection":"ZeroMinusTick","trdMatchID":"6ee19edb-99aa-3030-ba63-933ffb347ade","grossValue":2582000,"homeNotional":0.9904912836767037,"foreignNotional":255.7448494453249},{"timestamp":"2020-02-17T01:35:36.442Z","symbol":"ETHUSD","side":"Sell","size":100,"price":258.2,"tickDirection":"ZeroMinusTick","trdMatchID":"d44be603-cdb8-d676-e3e2-f91fb12b2a70","grossValue":2582000,"homeNotional":0.9904912836767037,"foreignNotional":255.7448494453249},{"timestamp":"2020-02-17T01:35:36.442Z","symbol":"ETHUSD","side":"Sell","size":5,"price":258.2,"tickDirection":"ZeroMinusTick","trdMatchID":"a14b43b3-50b4-c075-c54d-dfb0165de33d","grossValue":129100,"homeNotional":0.04952456418383518,"foreignNotional":12.787242472266245},{"timestamp":"2020-02-17T01:35:36.442Z","symbol":"ETHUSD","side":"Sell","size":8,"price":258.2,"tickDirection":"ZeroMinusTick","trdMatchID":"3c30e175-5194-320c-8f8c-01636c2f4a32","grossValue":206560,"homeNotional":0.07923930269413629,"foreignNotional":20.45958795562599},{"timestamp":"2020-02-17T01:35:36.442Z","symbol":"ETHUSD","side":"Sell","size":50,"price":258.2,"tickDirection":"ZeroMinusTick","trdMatchID":"5b803378-760b-4919-21fc-bfb275d39ace","grossValue":1291000,"homeNotional":0.49524564183835185,"foreignNotional":127.87242472266244},{"timestamp":"2020-02-17T01:35:36.442Z","symbol":"ETHUSD","side":"Sell","size":244,"price":258.2,"tickDirection":"ZeroMinusTick","trdMatchID":"cf57fec1-c444-b9e5-5e2d-4fb643f4fdb7","grossValue":6300080,"homeNotional":2.416798732171157,"foreignNotional":624.0174326465927}]}]`) msg := []byte(`{"table":"trade","action":"insert","data":[{"timestamp":"2020-02-17T01:35:36.442Z","symbol":"ETHUSD","side":"Sell","size":100,"price":258.3,"tickDirection":"MinusTick","trdMatchID":"c427f7a0-6b26-1e10-5c4e-1bd74daf2a73","grossValue":2583000,"homeNotional":0.9904912836767037,"foreignNotional":255.84389857369254},{"timestamp":"2020-02-17T01:35:36.442Z","symbol":"ETHUSD","side":"Sell","size":100,"price":258.3,"tickDirection":"ZeroMinusTick","trdMatchID":"95eb9155-b58c-70e9-44b7-34efe50302e0","grossValue":2583000,"homeNotional":0.9904912836767037,"foreignNotional":255.84389857369254},{"timestamp":"2020-02-17T01:35:36.442Z","symbol":"ETHUSD","side":"Sell","size":100,"price":258.3,"tickDirection":"ZeroMinusTick","trdMatchID":"e607c187-f25c-86bc-cb39-8afff7aaf2d9","grossValue":2583000,"homeNotional":0.9904912836767037,"foreignNotional":255.84389857369254},{"timestamp":"2020-02-17T01:35:36.442Z","symbol":"ETHUSD","side":"Sell","size":17,"price":258.3,"tickDirection":"ZeroMinusTick","trdMatchID":"0f076814-a57d-9a59-8063-ad6b823a80ac","grossValue":439110,"homeNotional":0.1683835182250396,"foreignNotional":43.49346275752773},{"timestamp":"2020-02-17T01:35:36.442Z","symbol":"ETHUSD","side":"Sell","size":100,"price":258.25,"tickDirection":"MinusTick","trdMatchID":"f4ef3dfd-51c4-538f-37c1-e5071ba1c75d","grossValue":2582500,"homeNotional":0.9904912836767037,"foreignNotional":255.79437400950872},{"timestamp":"2020-02-17T01:35:36.442Z","symbol":"ETHUSD","side":"Sell","size":100,"price":258.25,"tickDirection":"ZeroMinusTick","trdMatchID":"81ef136b-8f4a-b1cf-78a8-fffbfa89bf40","grossValue":2582500,"homeNotional":0.9904912836767037,"foreignNotional":255.79437400950872},{"timestamp":"2020-02-17T01:35:36.442Z","symbol":"ETHUSD","side":"Sell","size":100,"price":258.25,"tickDirection":"ZeroMinusTick","trdMatchID":"65a87e8c-7563-34a4-d040-94e8513c5401","grossValue":2582500,"homeNotional":0.9904912836767037,"foreignNotional":255.79437400950872},{"timestamp":"2020-02-17T01:35:36.442Z","symbol":"ETHUSD","side":"Sell","size":15,"price":258.25,"tickDirection":"ZeroMinusTick","trdMatchID":"1d11a74e-a157-3f33-036d-35a101fba50b","grossValue":387375,"homeNotional":0.14857369255150554,"foreignNotional":38.369156101426306},{"timestamp":"2020-02-17T01:35:36.442Z","symbol":"ETHUSD","side":"Sell","size":1,"price":258.25,"tickDirection":"ZeroMinusTick","trdMatchID":"40d49df1-f018-f66f-4ca5-31d4997641d7","grossValue":25825,"homeNotional":0.009904912836767036,"foreignNotional":2.5579437400950873},{"timestamp":"2020-02-17T01:35:36.442Z","symbol":"ETHUSD","side":"Sell","size":100,"price":258.2,"tickDirection":"MinusTick","trdMatchID":"36135b51-73e5-c007-362b-a55be5830c6b","grossValue":2582000,"homeNotional":0.9904912836767037,"foreignNotional":255.7448494453249},{"timestamp":"2020-02-17T01:35:36.442Z","symbol":"ETHUSD","side":"Sell","size":100,"price":258.2,"tickDirection":"ZeroMinusTick","trdMatchID":"6ee19edb-99aa-3030-ba63-933ffb347ade","grossValue":2582000,"homeNotional":0.9904912836767037,"foreignNotional":255.7448494453249},{"timestamp":"2020-02-17T01:35:36.442Z","symbol":"ETHUSD","side":"Sell","size":100,"price":258.2,"tickDirection":"ZeroMinusTick","trdMatchID":"d44be603-cdb8-d676-e3e2-f91fb12b2a70","grossValue":2582000,"homeNotional":0.9904912836767037,"foreignNotional":255.7448494453249},{"timestamp":"2020-02-17T01:35:36.442Z","symbol":"ETHUSD","side":"Sell","size":5,"price":258.2,"tickDirection":"ZeroMinusTick","trdMatchID":"a14b43b3-50b4-c075-c54d-dfb0165de33d","grossValue":129100,"homeNotional":0.04952456418383518,"foreignNotional":12.787242472266245},{"timestamp":"2020-02-17T01:35:36.442Z","symbol":"ETHUSD","side":"Sell","size":8,"price":258.2,"tickDirection":"ZeroMinusTick","trdMatchID":"3c30e175-5194-320c-8f8c-01636c2f4a32","grossValue":206560,"homeNotional":0.07923930269413629,"foreignNotional":20.45958795562599},{"timestamp":"2020-02-17T01:35:36.442Z","symbol":"ETHUSD","side":"Sell","size":50,"price":258.2,"tickDirection":"ZeroMinusTick","trdMatchID":"5b803378-760b-4919-21fc-bfb275d39ace","grossValue":1291000,"homeNotional":0.49524564183835185,"foreignNotional":127.87242472266244},{"timestamp":"2020-02-17T01:35:36.442Z","symbol":"ETHUSD","side":"Sell","size":244,"price":258.2,"tickDirection":"ZeroMinusTick","trdMatchID":"cf57fec1-c444-b9e5-5e2d-4fb643f4fdb7","grossValue":6300080,"homeNotional":2.416798732171157,"foreignNotional":624.0174326465927}]}`)
require.NoError(t, b.wsHandleData(msg), "Must not error handling a standard stream of trades") require.NoError(t, b.wsHandleData(msg), "Must not error handling a standard stream of trades")
msg = []byte(`[0, "public", "public", {"table":"trade","action":"insert","data":[{"timestamp":"2020-02-17T01:35:36.442Z","symbol":".BGCT","size":14,"price":258.2,"side":"sell"}]}]`) msg = []byte(`{"table":"trade","action":"insert","data":[{"timestamp":"2020-02-17T01:35:36.442Z","symbol":".BGCT","size":14,"price":258.2,"side":"sell"}]}`)
require.ErrorIs(t, b.wsHandleData(msg), exchange.ErrSymbolCannotBeMatched, "Must error correctly with an unknown symbol") require.ErrorIs(t, b.wsHandleData(msg), exchange.ErrSymbolCannotBeMatched, "Must error correctly with an unknown symbol")
msg = []byte(`[0, "public", "public", {"table":"trade","action":"insert","data":[{"timestamp":"2020-02-17T01:35:36.442Z","symbol":".BGCT","size":0,"price":258.2,"side":"sell"}]}]`) msg = []byte(`{"table":"trade","action":"insert","data":[{"timestamp":"2020-02-17T01:35:36.442Z","symbol":".BGCT","size":0,"price":258.2,"side":"sell"}]}`)
require.NoError(t, b.wsHandleData(msg), "Must not error that symbol is unknown when index trade is ignored due to zero size") require.NoError(t, b.wsHandleData(msg), "Must not error that symbol is unknown when index trade is ignored due to zero size")
} }
@@ -1152,4 +1152,7 @@ func TestSubscribe(t *testing.T) {
for _, s := range subs { for _, s := range subs {
assert.Equalf(t, subscription.UnsubscribedState, s.State(), "%s state should be unsusbscribed", s.QualifiedChannel) assert.Equalf(t, subscription.UnsubscribedState, s.State(), "%s state should be unsusbscribed", s.QualifiedChannel)
} }
err = b.Subscribe(subscription.List{{QualifiedChannel: "wibble", Channel: "wibble", Asset: asset.Spot}})
require.ErrorContains(t, err, "Unknown table: wibble", "Subscribe must receive errors through websocket.Match on request json")
} }

View File

@@ -27,7 +27,7 @@ import (
) )
const ( const (
bitmexWSURL = "wss://www.bitmex.com/realtimemd" bitmexWSURL = "wss://ws.bitmex.com/realtime"
// Public Subscription Channels // Public Subscription Channels
bitmexWSAnnouncement = "announcement" bitmexWSAnnouncement = "announcement"
@@ -86,6 +86,7 @@ func (b *Bitmex) WsConnect() error {
if !b.Websocket.IsEnabled() || !b.IsEnabled() { if !b.Websocket.IsEnabled() || !b.IsEnabled() {
return websocket.ErrWebsocketNotEnabled return websocket.ErrWebsocketNotEnabled
} }
var dialer gws.Dialer var dialer gws.Dialer
if err := b.Websocket.Conn.Dial(&dialer, http.Header{}); err != nil { if err := b.Websocket.Conn.Dial(&dialer, http.Header{}); err != nil {
return err return err
@@ -94,12 +95,8 @@ func (b *Bitmex) WsConnect() error {
b.Websocket.Wg.Add(1) b.Websocket.Wg.Add(1)
go b.wsReadData() go b.wsReadData()
ctx := context.TODO()
if err := b.wsOpenStream(ctx, b.Websocket.Conn, wsPublicStream); err != nil {
return err
}
if b.Websocket.CanUseAuthenticatedEndpoints() { if b.Websocket.CanUseAuthenticatedEndpoints() {
ctx := context.TODO()
if err := b.websocketSendAuth(ctx); err != nil { if err := b.websocketSendAuth(ctx); err != nil {
b.Websocket.SetCanUseAuthenticatedEndpoints(false) b.Websocket.SetCanUseAuthenticatedEndpoints(false)
log.Errorf(log.ExchangeSys, "%v - authentication failed: %v\n", b.Name, err) log.Errorf(log.ExchangeSys, "%v - authentication failed: %v\n", b.Name, err)
@@ -110,30 +107,10 @@ func (b *Bitmex) WsConnect() error {
} }
const ( const (
wsPublicStream = "public"
wsPrivateStream = "private"
wsSubscribeOp = "subscribe" wsSubscribeOp = "subscribe"
wsUnsubscribeOp = "unsubscribe" wsUnsubscribeOp = "unsubscribe"
wsMsgPacket = 0
wsOpenPacket = 1
wsClosePacket = 2
) )
func (b *Bitmex) wsOpenStream(ctx context.Context, c websocket.Connection, name string) error {
resp, err := c.SendMessageReturnResponse(ctx, request.Unset, "open:"+name, []any{wsOpenPacket, name, name})
if err != nil {
return err
}
var welcomeResp WebsocketWelcome
if err := json.Unmarshal(resp, &welcomeResp); err != nil {
return err
}
if b.Verbose {
log.Debugf(log.ExchangeSys, "Successfully connected to Bitmex %s websocket API at time: %s Limit: %d", name, welcomeResp.Timestamp, welcomeResp.Limit.Remaining)
}
return nil
}
// wsReadData receives and passes on websocket messages for processing // wsReadData receives and passes on websocket messages for processing
func (b *Bitmex) wsReadData() { func (b *Bitmex) wsReadData() {
defer b.Websocket.Wg.Done() defer b.Websocket.Wg.Done()
@@ -151,46 +128,53 @@ func (b *Bitmex) wsReadData() {
} }
func (b *Bitmex) wsHandleData(respRaw []byte) error { func (b *Bitmex) wsHandleData(respRaw []byte) error {
var err error
msg, _, _, err := jsonparser.Get(respRaw, "[3]")
if err != nil {
return fmt.Errorf("unknown message format: %s", respRaw)
}
// We don't need to know about errors, since we're looking optimistically into the json // We don't need to know about errors, since we're looking optimistically into the json
op, _ := jsonparser.GetString(msg, "request", "op") op, _ := jsonparser.GetString(respRaw, "request", "op")
errMsg, _ := jsonparser.GetString(msg, "error") errMsg, _ := jsonparser.GetString(respRaw, "error")
success, _ := jsonparser.GetBoolean(msg, "success") success, _ := jsonparser.GetBoolean(respRaw, "success")
version, _ := jsonparser.GetString(msg, "version") version, _ := jsonparser.GetString(respRaw, "version")
switch { switch {
case version != "": case version != "":
op = "open" var welcomeResp WebsocketWelcome
fallthrough if err := json.Unmarshal(respRaw, &welcomeResp); err != nil {
case errMsg != "", success: return err
streamID, e2 := jsonparser.GetString(respRaw, "[1]")
if e2 != nil {
return fmt.Errorf("%w parsing stream", e2)
} }
err = b.Websocket.Match.RequireMatchWithData(op+":"+streamID, msg)
if err != nil { if b.Verbose {
return fmt.Errorf("%w: %s:%s", err, op, streamID) log.Debugf(log.ExchangeSys, "%s successfully connected to websocket API at time: %s Limit: %d", b.Name, welcomeResp.Timestamp, welcomeResp.Limit.Remaining)
}
return nil
case errMsg != "", success:
var req any
if op == "authKeyExpires" {
req = op
} else {
reqBytes, _, _, err := jsonparser.Get(respRaw, "request")
if err != nil {
return err
}
req = string(reqBytes)
}
if err := b.Websocket.Match.RequireMatchWithData(req, respRaw); err != nil {
return fmt.Errorf("%w: %s", err, op)
} }
return nil return nil
} }
tableName, err := jsonparser.GetString(msg, "table") tableName, err := jsonparser.GetString(respRaw, "table")
if err != nil { if err != nil {
// Anything that's not a table isn't expected // Anything that's not a table isn't expected
return fmt.Errorf("unknown message format: %s", msg) return fmt.Errorf("unknown message format: %s", respRaw)
} }
switch tableName { switch tableName {
case bitmexWSOrderbookL2, bitmexWSOrderbookL225, bitmexWSOrderbookL10: case bitmexWSOrderbookL2, bitmexWSOrderbookL225, bitmexWSOrderbookL10:
var orderbooks OrderBookData var orderbooks OrderBookData
if err := json.Unmarshal(msg, &orderbooks); err != nil { if err := json.Unmarshal(respRaw, &orderbooks); err != nil {
return err return err
} }
if len(orderbooks.Data) == 0 { if len(orderbooks.Data) == 0 {
return fmt.Errorf("empty orderbook data received: %s", msg) return fmt.Errorf("empty orderbook data received: %s", respRaw)
} }
pair, a, err := b.GetPairAndAssetTypeRequestFormatted(orderbooks.Data[0].Symbol) pair, a, err := b.GetPairAndAssetTypeRequestFormatted(orderbooks.Data[0].Symbol)
@@ -203,10 +187,10 @@ func (b *Bitmex) wsHandleData(respRaw []byte) error {
return err return err
} }
case bitmexWSTrade: case bitmexWSTrade:
return b.handleWsTrades(msg) return b.handleWsTrades(respRaw)
case bitmexWSAnnouncement: case bitmexWSAnnouncement:
var announcement AnnouncementData var announcement AnnouncementData
if err := json.Unmarshal(msg, &announcement); err != nil { if err := json.Unmarshal(respRaw, &announcement); err != nil {
return err return err
} }
@@ -217,7 +201,7 @@ func (b *Bitmex) wsHandleData(respRaw []byte) error {
b.Websocket.DataHandler <- announcement.Data b.Websocket.DataHandler <- announcement.Data
case bitmexWSAffiliate: case bitmexWSAffiliate:
var response WsAffiliateResponse var response WsAffiliateResponse
if err := json.Unmarshal(msg, &response); err != nil { if err := json.Unmarshal(respRaw, &response); err != nil {
return err return err
} }
b.Websocket.DataHandler <- response b.Websocket.DataHandler <- response
@@ -226,7 +210,7 @@ func (b *Bitmex) wsHandleData(respRaw []byte) error {
case bitmexWSExecution: case bitmexWSExecution:
// trades of an order // trades of an order
var response WsExecutionResponse var response WsExecutionResponse
if err := json.Unmarshal(msg, &response); err != nil { if err := json.Unmarshal(respRaw, &response); err != nil {
return err return err
} }
@@ -273,7 +257,7 @@ func (b *Bitmex) wsHandleData(respRaw []byte) error {
} }
case bitmexWSOrder: case bitmexWSOrder:
var response WsOrderResponse var response WsOrderResponse
if err := json.Unmarshal(msg, &response); err != nil { if err := json.Unmarshal(respRaw, &response); err != nil {
return err return err
} }
switch response.Action { switch response.Action {
@@ -373,35 +357,35 @@ func (b *Bitmex) wsHandleData(respRaw []byte) error {
} }
case bitmexWSMargin: case bitmexWSMargin:
var response WsMarginResponse var response WsMarginResponse
if err := json.Unmarshal(msg, &response); err != nil { if err := json.Unmarshal(respRaw, &response); err != nil {
return err return err
} }
b.Websocket.DataHandler <- response b.Websocket.DataHandler <- response
case bitmexWSPosition: case bitmexWSPosition:
var response WsPositionResponse var response WsPositionResponse
if err := json.Unmarshal(msg, &response); err != nil { if err := json.Unmarshal(respRaw, &response); err != nil {
return err return err
} }
case bitmexWSPrivateNotifications: case bitmexWSPrivateNotifications:
var response WsPrivateNotificationsResponse var response WsPrivateNotificationsResponse
if err := json.Unmarshal(msg, &response); err != nil { if err := json.Unmarshal(respRaw, &response); err != nil {
return err return err
} }
b.Websocket.DataHandler <- response b.Websocket.DataHandler <- response
case bitmexWSTransact: case bitmexWSTransact:
var response WsTransactResponse var response WsTransactResponse
if err := json.Unmarshal(msg, &response); err != nil { if err := json.Unmarshal(respRaw, &response); err != nil {
return err return err
} }
b.Websocket.DataHandler <- response b.Websocket.DataHandler <- response
case bitmexWSWallet: case bitmexWSWallet:
var response WsWalletResponse var response WsWalletResponse
if err := json.Unmarshal(msg, &response); err != nil { if err := json.Unmarshal(respRaw, &response); err != nil {
return err return err
} }
b.Websocket.DataHandler <- response b.Websocket.DataHandler <- response
default: default:
b.Websocket.DataHandler <- websocket.UnhandledMessageWarning{Message: b.Name + websocket.UnhandledMessage + string(msg)} b.Websocket.DataHandler <- websocket.UnhandledMessageWarning{Message: b.Name + websocket.UnhandledMessage + string(respRaw)}
} }
return nil return nil
@@ -537,20 +521,20 @@ func (b *Bitmex) GetSubscriptionTemplate(_ *subscription.Subscription) (*templat
// Subscribe subscribes to a websocket channel // Subscribe subscribes to a websocket channel
func (b *Bitmex) Subscribe(subs subscription.List) error { func (b *Bitmex) Subscribe(subs subscription.List) error {
return common.AppendError( return common.AppendError(
b.ParallelChanOp(subs.Public(), func(l subscription.List) error { return b.manageSubs(wsSubscribeOp, l, wsPublicStream) }, len(subs)), b.ParallelChanOp(subs.Public(), func(l subscription.List) error { return b.manageSubs(wsSubscribeOp, l) }, len(subs)),
b.ParallelChanOp(subs.Private(), func(l subscription.List) error { return b.manageSubs(wsSubscribeOp, l, wsPrivateStream) }, len(subs)), b.ParallelChanOp(subs.Private(), func(l subscription.List) error { return b.manageSubs(wsSubscribeOp, l) }, len(subs)),
) )
} }
// Unsubscribe sends a websocket message to stop receiving data from the channel // Unsubscribe sends a websocket message to stop receiving data from the channel
func (b *Bitmex) Unsubscribe(subs subscription.List) error { func (b *Bitmex) Unsubscribe(subs subscription.List) error {
return common.AppendError( return common.AppendError(
b.ParallelChanOp(subs.Public(), func(l subscription.List) error { return b.manageSubs(wsUnsubscribeOp, l, wsPublicStream) }, len(subs)), b.ParallelChanOp(subs.Public(), func(l subscription.List) error { return b.manageSubs(wsUnsubscribeOp, l) }, len(subs)),
b.ParallelChanOp(subs.Private(), func(l subscription.List) error { return b.manageSubs(wsUnsubscribeOp, l, wsPrivateStream) }, len(subs)), b.ParallelChanOp(subs.Private(), func(l subscription.List) error { return b.manageSubs(wsUnsubscribeOp, l) }, len(subs)),
) )
} }
func (b *Bitmex) manageSubs(op string, subs subscription.List, stream string) error { func (b *Bitmex) manageSubs(op string, subs subscription.List) error {
req := WebsocketRequest{ req := WebsocketRequest{
Command: op, Command: op,
} }
@@ -559,8 +543,11 @@ func (b *Bitmex) manageSubs(op string, subs subscription.List, stream string) er
req.Arguments = append(req.Arguments, s.QualifiedChannel) req.Arguments = append(req.Arguments, s.QualifiedChannel)
exp[s.QualifiedChannel] = s exp[s.QualifiedChannel] = s
} }
packet := []any{wsMsgPacket, stream, stream, req} reqJSON, err := json.Marshal(req)
resps, errs := b.Websocket.Conn.SendMessageReturnResponses(context.TODO(), request.Unset, op+":"+stream, packet, len(subs)) if err != nil {
return err
}
resps, errs := b.Websocket.Conn.SendMessageReturnResponses(context.TODO(), request.Unset, string(reqJSON), req, len(subs))
for _, resp := range resps { for _, resp := range resps {
if errMsg, _ := jsonparser.GetString(resp, "error"); errMsg != "" { if errMsg, _ := jsonparser.GetString(resp, "error"); errMsg != "" {
errs = common.AppendError(errs, errors.New(errMsg)) errs = common.AppendError(errs, errors.New(errMsg))
@@ -591,23 +578,19 @@ func (b *Bitmex) websocketSendAuth(ctx context.Context) error {
return err return err
} }
timestamp := time.Now().Add(time.Hour * 1).Unix() timestamp := time.Now().Add(time.Hour * 1).Unix()
newTimestamp := strconv.FormatInt(timestamp, 10) timestampStr := strconv.FormatInt(timestamp, 10)
hmac, err := crypto.GetHMAC(crypto.HashSHA256, []byte("GET/realtime"+newTimestamp), []byte(creds.Secret)) hmac, err := crypto.GetHMAC(crypto.HashSHA256, []byte("GET/realtime"+timestampStr), []byte(creds.Secret))
if err != nil { if err != nil {
return err return err
} }
signature := crypto.HexEncodeToString(hmac) signature := crypto.HexEncodeToString(hmac)
err = b.wsOpenStream(ctx, b.Websocket.Conn, wsPrivateStream)
if err != nil {
return err
}
req := WebsocketRequest{ req := WebsocketRequest{
Command: "authKeyExpires", Command: "authKeyExpires",
Arguments: []any{creds.Key, timestamp, signature}, Arguments: []any{creds.Key, timestamp, signature},
} }
packet := []any{wsMsgPacket, wsPrivateStream, wsPrivateStream, req}
resp, err := b.Websocket.Conn.SendMessageReturnResponse(ctx, request.Unset, req.Command+":"+wsPrivateStream, packet) resp, err := b.Websocket.Conn.SendMessageReturnResponse(ctx, request.Unset, req.Command, req)
if err != nil { if err != nil {
return err return err
} }

View File

@@ -184,7 +184,7 @@ func (b *Bitmex) Setup(exch *config.Exchange) error {
return b.Websocket.SetupNewConnection(&websocket.ConnectionSetup{ return b.Websocket.SetupNewConnection(&websocket.ConnectionSetup{
ResponseCheckTimeout: exch.WebsocketResponseCheckTimeout, ResponseCheckTimeout: exch.WebsocketResponseCheckTimeout,
ResponseMaxLimit: exch.WebsocketResponseMaxLimit, ResponseMaxLimit: exch.WebsocketResponseMaxLimit,
URL: bitmexWSURL, URL: wsEndpoint,
}) })
} }