mirror of
https://github.com/d0zingcat/gocryptotrader.git
synced 2026-05-17 23:16:52 +00:00
* Websocket: Use ErrSubscribedAlready instead of errChannelAlreadySubscribed * Subscriptions: Replace Pair with Pairs Given that some subscriptions have multiple pairs, support that as the standard. * Docs: Update subscriptions in add new exch * RPC: Update Subscription Pairs * Linter: Disable testifylint.Len We deliberately use Equal over Len to avoid spamming the contents of large Slices * Websocket: Add suffix to state consts * Binance: Subscription Pairs support * Bitfinex: Subscription Pairs support * Bithumb: Subscription Pairs support * Bitmex: Subscription Pairs support * Bitstamp: Subscription Pairs support * BTCMarkets: Subscription Pairs support * BTSE: Subscription Pairs support * Coinbase: Subscription Pairs support * Coinut: Subscription Pairs support * GateIO: Subscription Pairs support * Gemini: Subscription Pairs support and improvement * Hitbtc: Subscription Pairs support * Huboi: Subscription Pairs support * Kucoin: Subscription Pairs support * Okcoin: Subscription Pairs support * Poloniex: Subscription Pairs support * Kraken: Add subscription Pairs support Note: This is a naieve implementation because we want to rebase the kraken websocket rewrite on top of this * Bybit: Subscription Pairs support * Okx: Subscription Pairs support * Bitmex: Subsription configuration * Fixes unauthenticated websocket left as CanUseAuth * Fixes auth subs happening privately * CoinbasePro: Subscription Configuration * Consolidate ProductIDs when all subscriptions are for the same list * Websocket: Log actual sent message when Verbose * Subscriptions: Improve clarity of which key is which in Match * Subscriptions: Lint fix for HugeParam * Subscriptions: Add AddPairs and move keys from test * Subscriptions: Simplify subscription keys and add key types * Subscriptions: Add List.GroupPairs Rename sub.AddPairs * Subscription: Fix ExactKey not matching 0 pairs * Subscriptions: Remove unused IdentityKey and HasPairKey * Subscriptions: Fix GetKey test * Subscriptions: Test coverage improvements * Websocket: Change State on Add/Remove * Subscriptions: Improve error context * Subscriptions: Fix Enable: false subs not ignored * Bitfinex: Fix WsAuth test failing on DataHandler DataHandler is eaten by dataMonitor now, so we need to use ToRoutine * Deribit: Subscription Pairs support * Websocket: Accept nil lists for checkSubscriptions If the user passes in a nil (implicitly empty) list, we would not panic. Therefore the burden of correctness about that data lies with them. The list of subscriptions is empty, and that's okay, and possibly convenient * Websocket: Add context to NilPointer errors * Subscriptions: Add context to nil errors * Exchange: Fix error expectations in UnsubToWSChans
680 lines
18 KiB
Go
680 lines
18 KiB
Go
package bitmex
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"net/http"
|
|
"strconv"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/gorilla/websocket"
|
|
"github.com/thrasher-corp/gocryptotrader/common/crypto"
|
|
"github.com/thrasher-corp/gocryptotrader/currency"
|
|
"github.com/thrasher-corp/gocryptotrader/exchanges/asset"
|
|
"github.com/thrasher-corp/gocryptotrader/exchanges/order"
|
|
"github.com/thrasher-corp/gocryptotrader/exchanges/orderbook"
|
|
"github.com/thrasher-corp/gocryptotrader/exchanges/stream"
|
|
"github.com/thrasher-corp/gocryptotrader/exchanges/subscription"
|
|
"github.com/thrasher-corp/gocryptotrader/exchanges/trade"
|
|
"github.com/thrasher-corp/gocryptotrader/log"
|
|
)
|
|
|
|
const (
|
|
bitmexWSURL = "wss://www.bitmex.com/realtime"
|
|
|
|
// Public Subscription Channels
|
|
bitmexWSAnnouncement = "announcement"
|
|
bitmexWSChat = "chat"
|
|
bitmexWSConnected = "connected"
|
|
bitmexWSFunding = "funding"
|
|
bitmexWSInstrument = "instrument"
|
|
bitmexWSInsurance = "insurance"
|
|
bitmexWSLiquidation = "liquidation"
|
|
bitmexWSOrderbookL2 = "orderBookL2"
|
|
bitmexWSOrderbookL225 = "orderBookL2_25"
|
|
bitmexWSOrderbookL10 = "orderBook10"
|
|
bitmexWSPublicNotifications = "publicNotifications"
|
|
bitmexWSQuote = "quote"
|
|
bitmexWSQuote1m = "quoteBin1m"
|
|
bitmexWSQuote5m = "quoteBin5m"
|
|
bitmexWSQuote1h = "quoteBin1h"
|
|
bitmexWSQuote1d = "quoteBin1d"
|
|
bitmexWSSettlement = "settlement"
|
|
bitmexWSTrade = "trade"
|
|
bitmexWSTrade1m = "tradeBin1m"
|
|
bitmexWSTrade5m = "tradeBin5m"
|
|
bitmexWSTrade1h = "tradeBin1h"
|
|
bitmexWSTrade1d = "tradeBin1d"
|
|
|
|
// Authenticated Subscription Channels
|
|
bitmexWSAffiliate = "affiliate"
|
|
bitmexWSExecution = "execution"
|
|
bitmexWSOrder = "order"
|
|
bitmexWSMargin = "margin"
|
|
bitmexWSPosition = "position"
|
|
bitmexWSPrivateNotifications = "privateNotifications"
|
|
bitmexWSTransact = "transact"
|
|
bitmexWSWallet = "wallet"
|
|
|
|
bitmexActionInitialData = "partial"
|
|
bitmexActionInsertData = "insert"
|
|
bitmexActionDeleteData = "delete"
|
|
bitmexActionUpdateData = "update"
|
|
)
|
|
|
|
var subscriptionNames = map[string]string{
|
|
subscription.OrderbookChannel: bitmexWSOrderbookL2,
|
|
subscription.AllTradesChannel: bitmexWSTrade,
|
|
}
|
|
|
|
// WsConnect initiates a new websocket connection
|
|
func (b *Bitmex) WsConnect() error {
|
|
if !b.Websocket.IsEnabled() || !b.IsEnabled() {
|
|
return stream.ErrWebsocketNotEnabled
|
|
}
|
|
var dialer websocket.Dialer
|
|
err := b.Websocket.Conn.Dial(&dialer, http.Header{})
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
resp := b.Websocket.Conn.ReadMessage()
|
|
if resp.Raw == nil {
|
|
return errors.New("connection closed")
|
|
}
|
|
var welcomeResp WebsocketWelcome
|
|
err = json.Unmarshal(resp.Raw, &welcomeResp)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
if b.Verbose {
|
|
log.Debugf(log.ExchangeSys,
|
|
"Successfully connected to Bitmex %s at time: %s Limit: %d",
|
|
welcomeResp.Info,
|
|
welcomeResp.Timestamp,
|
|
welcomeResp.Limit.Remaining)
|
|
}
|
|
|
|
b.Websocket.Wg.Add(1)
|
|
go b.wsReadData()
|
|
|
|
if b.Websocket.CanUseAuthenticatedEndpoints() {
|
|
err = b.websocketSendAuth(context.TODO())
|
|
if err != nil {
|
|
b.Websocket.SetCanUseAuthenticatedEndpoints(false)
|
|
log.Errorf(log.ExchangeSys, "%v - authentication failed: %v\n", b.Name, err)
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// wsReadData receives and passes on websocket messages for processing
|
|
func (b *Bitmex) wsReadData() {
|
|
defer b.Websocket.Wg.Done()
|
|
|
|
for {
|
|
resp := b.Websocket.Conn.ReadMessage()
|
|
if resp.Raw == nil {
|
|
return
|
|
}
|
|
err := b.wsHandleData(resp.Raw)
|
|
if err != nil {
|
|
b.Websocket.DataHandler <- err
|
|
}
|
|
}
|
|
}
|
|
|
|
func (b *Bitmex) wsHandleData(respRaw []byte) error {
|
|
quickCapture := make(map[string]interface{})
|
|
err := json.Unmarshal(respRaw, &quickCapture)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
var respError WebsocketErrorResponse
|
|
if _, ok := quickCapture["status"]; ok {
|
|
err = json.Unmarshal(respRaw, &respError)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
if _, ok := quickCapture["success"]; ok {
|
|
var decodedResp WebsocketSubscribeResp
|
|
err = json.Unmarshal(respRaw, &decodedResp)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
if decodedResp.Success {
|
|
if len(quickCapture) == 3 {
|
|
if b.Verbose {
|
|
log.Debugf(log.ExchangeSys, "%s websocket: Successfully subscribed to %s",
|
|
b.Name, decodedResp.Subscribe)
|
|
}
|
|
} else {
|
|
b.Websocket.SetCanUseAuthenticatedEndpoints(true)
|
|
if b.Verbose {
|
|
log.Debugf(log.ExchangeSys, "%s websocket: Successfully authenticated websocket connection",
|
|
b.Name)
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
b.Websocket.DataHandler <- fmt.Errorf("%s websocket error: Unable to subscribe %s",
|
|
b.Name, decodedResp.Subscribe)
|
|
} else if _, ok := quickCapture["table"]; ok {
|
|
var decodedResp WebsocketMainResponse
|
|
err = json.Unmarshal(respRaw, &decodedResp)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
switch decodedResp.Table {
|
|
case bitmexWSOrderbookL2, bitmexWSOrderbookL225, bitmexWSOrderbookL10:
|
|
var orderbooks OrderBookData
|
|
err = json.Unmarshal(respRaw, &orderbooks)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if len(orderbooks.Data) == 0 {
|
|
return fmt.Errorf("%s - Empty orderbook data received: %s", b.Name, respRaw)
|
|
}
|
|
|
|
var pair currency.Pair
|
|
var a asset.Item
|
|
pair, a, err = b.GetPairAndAssetTypeRequestFormatted(orderbooks.Data[0].Symbol)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
err = b.processOrderbook(orderbooks.Data, orderbooks.Action, pair, a)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
case bitmexWSTrade:
|
|
if !b.IsSaveTradeDataEnabled() {
|
|
return nil
|
|
}
|
|
var tradeHolder TradeData
|
|
err = json.Unmarshal(respRaw, &tradeHolder)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
var trades []trade.Data
|
|
for i := range tradeHolder.Data {
|
|
if tradeHolder.Data[i].Price == 0 {
|
|
// Please note that indices (symbols starting with .) post trades at intervals to the trade feed.
|
|
// These have a size of 0 and are used only to indicate a changing price.
|
|
continue
|
|
}
|
|
var p currency.Pair
|
|
var a asset.Item
|
|
p, a, err = b.GetPairAndAssetTypeRequestFormatted(tradeHolder.Data[i].Symbol)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
var oSide order.Side
|
|
oSide, err = order.StringToOrderSide(tradeHolder.Data[i].Side)
|
|
if err != nil {
|
|
b.Websocket.DataHandler <- order.ClassificationError{
|
|
Exchange: b.Name,
|
|
Err: err,
|
|
}
|
|
}
|
|
|
|
trades = append(trades, trade.Data{
|
|
TID: tradeHolder.Data[i].TrdMatchID,
|
|
Exchange: b.Name,
|
|
CurrencyPair: p,
|
|
AssetType: a,
|
|
Side: oSide,
|
|
Price: tradeHolder.Data[i].Price,
|
|
Amount: float64(tradeHolder.Data[i].Size),
|
|
Timestamp: tradeHolder.Data[i].Timestamp,
|
|
})
|
|
}
|
|
return b.AddTradesToBuffer(trades...)
|
|
case bitmexWSAnnouncement:
|
|
var announcement AnnouncementData
|
|
err = json.Unmarshal(respRaw, &announcement)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
if announcement.Action == bitmexActionInitialData {
|
|
return nil
|
|
}
|
|
|
|
b.Websocket.DataHandler <- announcement.Data
|
|
case bitmexWSAffiliate:
|
|
var response WsAffiliateResponse
|
|
err = json.Unmarshal(respRaw, &response)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
b.Websocket.DataHandler <- response
|
|
case bitmexWSInstrument:
|
|
// ticker
|
|
case bitmexWSExecution:
|
|
// trades of an order
|
|
var response WsExecutionResponse
|
|
err = json.Unmarshal(respRaw, &response)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
for i := range response.Data {
|
|
var p currency.Pair
|
|
var a asset.Item
|
|
p, a, err = b.GetPairAndAssetTypeRequestFormatted(response.Data[i].Symbol)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
var oStatus order.Status
|
|
oStatus, err = order.StringToOrderStatus(response.Data[i].OrdStatus)
|
|
if err != nil {
|
|
b.Websocket.DataHandler <- order.ClassificationError{
|
|
Exchange: b.Name,
|
|
OrderID: response.Data[i].OrderID,
|
|
Err: err,
|
|
}
|
|
}
|
|
var oSide order.Side
|
|
oSide, err = order.StringToOrderSide(response.Data[i].Side)
|
|
if err != nil {
|
|
b.Websocket.DataHandler <- order.ClassificationError{
|
|
Exchange: b.Name,
|
|
OrderID: response.Data[i].OrderID,
|
|
Err: err,
|
|
}
|
|
}
|
|
b.Websocket.DataHandler <- &order.Detail{
|
|
Exchange: b.Name,
|
|
OrderID: response.Data[i].OrderID,
|
|
AccountID: strconv.FormatInt(response.Data[i].Account, 10),
|
|
AssetType: a,
|
|
Pair: p,
|
|
Status: oStatus,
|
|
Trades: []order.TradeHistory{
|
|
{
|
|
Price: response.Data[i].Price,
|
|
Amount: response.Data[i].OrderQuantity,
|
|
Exchange: b.Name,
|
|
TID: response.Data[i].ExecID,
|
|
Side: oSide,
|
|
Timestamp: response.Data[i].Timestamp,
|
|
IsMaker: false,
|
|
},
|
|
},
|
|
}
|
|
}
|
|
case bitmexWSOrder:
|
|
var response WsOrderResponse
|
|
err = json.Unmarshal(respRaw, &response)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
switch response.Action {
|
|
case "update", "insert":
|
|
for x := range response.Data {
|
|
var p currency.Pair
|
|
var a asset.Item
|
|
p, a, err = b.GetRequestFormattedPairAndAssetType(response.Data[x].Symbol)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
var oSide order.Side
|
|
oSide, err = order.StringToOrderSide(response.Data[x].Side)
|
|
if err != nil {
|
|
b.Websocket.DataHandler <- order.ClassificationError{
|
|
Exchange: b.Name,
|
|
OrderID: response.Data[x].OrderID,
|
|
Err: err,
|
|
}
|
|
}
|
|
var oType order.Type
|
|
oType, err = order.StringToOrderType(response.Data[x].OrderType)
|
|
if err != nil {
|
|
b.Websocket.DataHandler <- order.ClassificationError{
|
|
Exchange: b.Name,
|
|
OrderID: response.Data[x].OrderID,
|
|
Err: err,
|
|
}
|
|
}
|
|
var oStatus order.Status
|
|
oStatus, err = order.StringToOrderStatus(response.Data[x].OrderStatus)
|
|
if err != nil {
|
|
b.Websocket.DataHandler <- order.ClassificationError{
|
|
Exchange: b.Name,
|
|
OrderID: response.Data[x].OrderID,
|
|
Err: err,
|
|
}
|
|
}
|
|
b.Websocket.DataHandler <- &order.Detail{
|
|
Price: response.Data[x].Price,
|
|
Amount: response.Data[x].OrderQuantity,
|
|
Exchange: b.Name,
|
|
OrderID: response.Data[x].OrderID,
|
|
AccountID: strconv.FormatInt(response.Data[x].Account, 10),
|
|
Type: oType,
|
|
Side: oSide,
|
|
Status: oStatus,
|
|
AssetType: a,
|
|
Date: response.Data[x].TransactTime,
|
|
Pair: p,
|
|
}
|
|
}
|
|
case "delete":
|
|
for x := range response.Data {
|
|
var p currency.Pair
|
|
var a asset.Item
|
|
p, a, err = b.GetRequestFormattedPairAndAssetType(response.Data[x].Symbol)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
var oSide order.Side
|
|
oSide, err = order.StringToOrderSide(response.Data[x].Side)
|
|
if err != nil {
|
|
b.Websocket.DataHandler <- order.ClassificationError{
|
|
Exchange: b.Name,
|
|
OrderID: response.Data[x].OrderID,
|
|
Err: err,
|
|
}
|
|
}
|
|
var oType order.Type
|
|
oType, err = order.StringToOrderType(response.Data[x].OrderType)
|
|
if err != nil {
|
|
b.Websocket.DataHandler <- order.ClassificationError{
|
|
Exchange: b.Name,
|
|
OrderID: response.Data[x].OrderID,
|
|
Err: err,
|
|
}
|
|
}
|
|
var oStatus order.Status
|
|
oStatus, err = order.StringToOrderStatus(response.Data[x].OrderStatus)
|
|
if err != nil {
|
|
b.Websocket.DataHandler <- order.ClassificationError{
|
|
Exchange: b.Name,
|
|
OrderID: response.Data[x].OrderID,
|
|
Err: err,
|
|
}
|
|
}
|
|
b.Websocket.DataHandler <- &order.Detail{
|
|
Price: response.Data[x].Price,
|
|
Amount: response.Data[x].OrderQuantity,
|
|
Exchange: b.Name,
|
|
OrderID: response.Data[x].OrderID,
|
|
AccountID: strconv.FormatInt(response.Data[x].Account, 10),
|
|
Type: oType,
|
|
Side: oSide,
|
|
Status: oStatus,
|
|
AssetType: a,
|
|
Date: response.Data[x].TransactTime,
|
|
Pair: p,
|
|
}
|
|
}
|
|
default:
|
|
b.Websocket.DataHandler <- fmt.Errorf("%s - Unsupported order update %+v", b.Name, response)
|
|
}
|
|
case bitmexWSMargin:
|
|
var response WsMarginResponse
|
|
err = json.Unmarshal(respRaw, &response)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
b.Websocket.DataHandler <- response
|
|
case bitmexWSPosition:
|
|
var response WsPositionResponse
|
|
err = json.Unmarshal(respRaw, &response)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
case bitmexWSPrivateNotifications:
|
|
var response WsPrivateNotificationsResponse
|
|
err = json.Unmarshal(respRaw, &response)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
b.Websocket.DataHandler <- response
|
|
case bitmexWSTransact:
|
|
var response WsTransactResponse
|
|
err = json.Unmarshal(respRaw, &response)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
b.Websocket.DataHandler <- response
|
|
case bitmexWSWallet:
|
|
var response WsWalletResponse
|
|
err = json.Unmarshal(respRaw, &response)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
b.Websocket.DataHandler <- response
|
|
default:
|
|
b.Websocket.DataHandler <- stream.UnhandledMessageWarning{Message: b.Name + stream.UnhandledMessage + string(respRaw)}
|
|
return nil
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// ProcessOrderbook processes orderbook updates
|
|
func (b *Bitmex) processOrderbook(data []OrderBookL2, action string, p currency.Pair, a asset.Item) error {
|
|
if len(data) < 1 {
|
|
return errors.New("no orderbook data")
|
|
}
|
|
|
|
switch action {
|
|
case bitmexActionInitialData:
|
|
book := orderbook.Base{
|
|
Asks: make(orderbook.Tranches, 0, len(data)),
|
|
Bids: make(orderbook.Tranches, 0, len(data)),
|
|
}
|
|
|
|
for i := range data {
|
|
item := orderbook.Tranche{
|
|
Price: data[i].Price,
|
|
Amount: float64(data[i].Size),
|
|
ID: data[i].ID,
|
|
}
|
|
switch {
|
|
case strings.EqualFold(data[i].Side, order.Sell.String()):
|
|
book.Asks = append(book.Asks, item)
|
|
case strings.EqualFold(data[i].Side, order.Buy.String()):
|
|
book.Bids = append(book.Bids, item)
|
|
default:
|
|
return fmt.Errorf("could not process websocket orderbook update, order side could not be matched for %s",
|
|
data[i].Side)
|
|
}
|
|
}
|
|
book.Asks.Reverse() // Reverse asks for correct alignment
|
|
book.Asset = a
|
|
book.Pair = p
|
|
book.Exchange = b.Name
|
|
book.VerifyOrderbook = b.CanVerifyOrderbook
|
|
book.LastUpdated = data[0].Timestamp
|
|
|
|
err := b.Websocket.Orderbook.LoadSnapshot(&book)
|
|
if err != nil {
|
|
return fmt.Errorf("process orderbook error - %s",
|
|
err)
|
|
}
|
|
default:
|
|
updateAction, err := b.GetActionFromString(action)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
asks := make([]orderbook.Tranche, 0, len(data))
|
|
bids := make([]orderbook.Tranche, 0, len(data))
|
|
for i := range data {
|
|
nItem := orderbook.Tranche{
|
|
Price: data[i].Price,
|
|
Amount: float64(data[i].Size),
|
|
ID: data[i].ID,
|
|
}
|
|
if strings.EqualFold(data[i].Side, "Sell") {
|
|
asks = append(asks, nItem)
|
|
continue
|
|
}
|
|
bids = append(bids, nItem)
|
|
}
|
|
|
|
err = b.Websocket.Orderbook.Update(&orderbook.Update{
|
|
Bids: bids,
|
|
Asks: asks,
|
|
Pair: p,
|
|
Asset: a,
|
|
Action: updateAction,
|
|
UpdateTime: data[0].Timestamp,
|
|
})
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// generateSubscriptions returns Adds default subscriptions to websocket to be handled by ManageSubscriptions()
|
|
func (b *Bitmex) generateSubscriptions() (subscription.List, error) {
|
|
authed := b.Websocket.CanUseAuthenticatedEndpoints()
|
|
|
|
assetPairs := map[asset.Item]currency.Pairs{}
|
|
for _, a := range b.GetAssetTypes(true) {
|
|
p, err := b.GetEnabledPairs(a)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
f, err := b.GetPairFormat(a, true)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
assetPairs[a] = p.Format(f)
|
|
}
|
|
|
|
subs := subscription.List{}
|
|
for _, baseSub := range b.Features.Subscriptions {
|
|
if !authed && baseSub.Authenticated {
|
|
continue
|
|
}
|
|
|
|
if baseSub.Asset == asset.Empty {
|
|
// Skip pair handling for subs which don't have an asset
|
|
subs = append(subs, baseSub.Clone())
|
|
continue
|
|
}
|
|
|
|
for a, p := range assetPairs {
|
|
if baseSub.Channel == bitmexWSOrderbookL2 && a == asset.Index {
|
|
continue // There are no L2 orderbook for index assets
|
|
}
|
|
if baseSub.Asset != asset.All && baseSub.Asset != a {
|
|
continue
|
|
}
|
|
s := baseSub.Clone()
|
|
s.Asset = a
|
|
s.Pairs = p
|
|
subs = append(subs, s)
|
|
}
|
|
}
|
|
|
|
return subs, nil
|
|
}
|
|
|
|
// Subscribe subscribes to a websocket channel
|
|
func (b *Bitmex) Subscribe(subs subscription.List) error {
|
|
req := WebsocketRequest{
|
|
Command: "subscribe",
|
|
}
|
|
for _, s := range subs {
|
|
for _, p := range s.Pairs {
|
|
cName := channelName(s.Channel)
|
|
req.Arguments = append(req.Arguments, cName+":"+p.String())
|
|
}
|
|
}
|
|
err := b.Websocket.Conn.SendJSONMessage(req)
|
|
if err == nil {
|
|
err = b.Websocket.AddSuccessfulSubscriptions(subs...)
|
|
}
|
|
return err
|
|
}
|
|
|
|
// Unsubscribe sends a websocket message to stop receiving data from the channel
|
|
func (b *Bitmex) Unsubscribe(subs subscription.List) error {
|
|
req := WebsocketRequest{
|
|
Command: "unsubscribe",
|
|
}
|
|
|
|
for _, s := range subs {
|
|
for _, p := range s.Pairs {
|
|
cName := channelName(s.Channel)
|
|
req.Arguments = append(req.Arguments, cName+":"+p.String())
|
|
}
|
|
}
|
|
err := b.Websocket.Conn.SendJSONMessage(req)
|
|
if err == nil {
|
|
err = b.Websocket.RemoveSubscriptions(subs...)
|
|
}
|
|
return err
|
|
}
|
|
|
|
// channelName converts global channel Names used in config of channel input into bitmex channel names
|
|
// returns the name unchanged if no match is found
|
|
func channelName(name string) string {
|
|
if s, ok := subscriptionNames[name]; ok {
|
|
return s
|
|
}
|
|
return name
|
|
}
|
|
|
|
// WebsocketSendAuth sends an authenticated subscription
|
|
func (b *Bitmex) websocketSendAuth(ctx context.Context) error {
|
|
creds, err := b.GetCredentials(ctx)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
b.Websocket.SetCanUseAuthenticatedEndpoints(true)
|
|
timestamp := time.Now().Add(time.Hour * 1).Unix()
|
|
newTimestamp := strconv.FormatInt(timestamp, 10)
|
|
hmac, err := crypto.GetHMAC(crypto.HashSHA256,
|
|
[]byte("GET/realtime"+newTimestamp),
|
|
[]byte(creds.Secret))
|
|
if err != nil {
|
|
return err
|
|
}
|
|
signature := crypto.HexEncodeToString(hmac)
|
|
|
|
var sendAuth WebsocketRequest
|
|
sendAuth.Command = "authKeyExpires"
|
|
sendAuth.Arguments = append(sendAuth.Arguments, creds.Key, timestamp,
|
|
signature)
|
|
err = b.Websocket.Conn.SendJSONMessage(sendAuth)
|
|
if err != nil {
|
|
b.Websocket.SetCanUseAuthenticatedEndpoints(false)
|
|
return err
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// GetActionFromString matches a string action to an internal action.
|
|
func (b *Bitmex) GetActionFromString(s string) (orderbook.Action, error) {
|
|
switch s {
|
|
case "update":
|
|
return orderbook.Amend, nil
|
|
case "delete":
|
|
return orderbook.Delete, nil
|
|
case "insert":
|
|
return orderbook.Insert, nil
|
|
case "update/insert":
|
|
return orderbook.UpdateInsert, nil
|
|
}
|
|
return 0, fmt.Errorf("%s %w", s, orderbook.ErrInvalidAction)
|
|
}
|