mirror of
https://github.com/d0zingcat/gocryptotrader.git
synced 2026-05-16 15:09:57 +00:00
* orderbook/buffer: data integrity and resubscription pass * btcmarkets: REMOVE THAT LIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIINE!!!!!!!!!!!!!!!!! * buffer: reinstate publish, refaactor, invalidate more and comments * buffer/orderbook: improve update and snapshot performance. Move Update type to orderbook package to util. pointer through entire function calls. (cleanup). Change action string to uint8 for easier comparison. Add parsing helper. Update current test benchmark comments. * dispatch: change publish func to variadic id param * dispatch: remove sender receiver wait time as this adds overhead and complexity. update tests. * dispatch: don't create pointers for every job container * rpcserver: fix assertion issues with data publishing change * linter: fixes * glorious: nits addr * depth: change validation handling to incorporate and store err * linter: fix more issues * dispatch: fix race * travis: update before fetching * depth: wrap and return wrapped error in invalidate call and fix tests * btcmarkets: fix commenting * workflow: check * workflow: check * orderbook: check error * buffer/depth: return invalidation error and fix tests * gctcli: display errors on orderbook streams * buffer: remove unused types * orderbook/bitmex: shift function to bitmex * orderbook: Add specific comments to unexported functions that don't have locking require locking. * orderbook: restrict published data functionality to orderbook.Outbound interface * common: add assertion failure helper for error * dispatch: remove atomics, add mutex protection, remove add/remove worker, redo main tests * dispatch: export function * engine: revert and change sub logger to manager * engine: remove old test * dispatch: add common variable ;) * btcmarket: don't overflow int in tests on 32bit systems * ci: force 1.17.7 usage for go * Revert "ci: force 1.17.7 usage for go" This reverts commit af2f95563bf218cf2b9f36a9fcf3258e2c6a2d91. * golangci: bump version add and remove linter items * Revert "golangci: bump version add and remove linter items" This reverts commit 3c98bffc9d030e39faca0387ea40c151df2ab06b. * dispatch: remove unsused mutex from mux * order: slight optimizations * nits: glorious * dispatch: fix regression on uuid generation and input inline with master * linter: fix * linter: fix * glorious: nit - rm slice segration * account: fix test after merge * coinbasepro: revert change * account: close channel instead of needing a receiver, push alert in routine to prepare for waiter. Co-authored-by: Ryan O'Hara-Reid <ryan.oharareid@thrasher.io>
715 lines
19 KiB
Go
715 lines
19 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/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"
|
|
)
|
|
|
|
// WsConnect initiates a new websocket connection
|
|
func (b *Bitmex) WsConnect() error {
|
|
if !b.Websocket.IsEnabled() || !b.IsEnabled() {
|
|
return errors.New(stream.WebsocketNotEnabled)
|
|
}
|
|
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 {
|
|
log.Errorf(log.ExchangeSys,
|
|
"%v - authentication failed: %v\n",
|
|
b.Name,
|
|
err)
|
|
} else {
|
|
authsubs, err := b.GenerateAuthenticatedSubscriptions()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
return b.Websocket.SubscribeToChannels(authsubs)
|
|
}
|
|
}
|
|
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 p currency.Pair
|
|
p, err = currency.NewPairFromString(orderbooks.Data[0].Symbol)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
var a asset.Item
|
|
a, err = b.GetPairAssetType(p)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
err = b.processOrderbook(orderbooks.Data,
|
|
orderbooks.Action,
|
|
p,
|
|
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
|
|
p, err = currency.NewPairFromString(tradeHolder.Data[i].Symbol)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
var a asset.Item
|
|
a, err = b.GetPairAssetType(p)
|
|
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
|
|
p, err = currency.NewPairFromString(response.Data[i].Symbol)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
var a asset.Item
|
|
a, err = b.GetPairAssetType(p)
|
|
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.Modify{
|
|
Exchange: b.Name,
|
|
ID: 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,
|
|
ID: 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.Modify{
|
|
Price: response.Data[x].Price,
|
|
Amount: response.Data[x].OrderQuantity,
|
|
Exchange: b.Name,
|
|
ID: 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.Items, 0, len(data)),
|
|
Bids: make(orderbook.Items, 0, len(data)),
|
|
}
|
|
|
|
for i := range data {
|
|
item := orderbook.Item{
|
|
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
|
|
|
|
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.Item, 0, len(data))
|
|
bids := make([]orderbook.Item, 0, len(data))
|
|
for i := range data {
|
|
nItem := orderbook.Item{
|
|
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,
|
|
})
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// GenerateDefaultSubscriptions Adds default subscriptions to websocket to be handled by ManageSubscriptions()
|
|
func (b *Bitmex) GenerateDefaultSubscriptions() ([]stream.ChannelSubscription, error) {
|
|
channels := []string{bitmexWSOrderbookL2, bitmexWSTrade}
|
|
subscriptions := []stream.ChannelSubscription{
|
|
{
|
|
Channel: bitmexWSAnnouncement,
|
|
},
|
|
}
|
|
|
|
assets := b.GetAssetTypes(true)
|
|
for x := range assets {
|
|
contracts, err := b.GetEnabledPairs(assets[x])
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
for y := range contracts {
|
|
for z := range channels {
|
|
if assets[x] == asset.Index && channels[z] == bitmexWSOrderbookL2 {
|
|
// There are no L2 orderbook for index assets
|
|
continue
|
|
}
|
|
subscriptions = append(subscriptions, stream.ChannelSubscription{
|
|
Channel: channels[z] + ":" + contracts[y].String(),
|
|
Currency: contracts[y],
|
|
Asset: assets[x],
|
|
})
|
|
}
|
|
}
|
|
}
|
|
return subscriptions, nil
|
|
}
|
|
|
|
// GenerateAuthenticatedSubscriptions Adds authenticated subscriptions to websocket to be handled by ManageSubscriptions()
|
|
func (b *Bitmex) GenerateAuthenticatedSubscriptions() ([]stream.ChannelSubscription, error) {
|
|
if !b.Websocket.CanUseAuthenticatedEndpoints() {
|
|
return nil, nil
|
|
}
|
|
contracts, err := b.GetEnabledPairs(asset.PerpetualContract)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
channels := []string{bitmexWSExecution,
|
|
bitmexWSPosition,
|
|
}
|
|
subscriptions := []stream.ChannelSubscription{
|
|
{
|
|
Channel: bitmexWSAffiliate,
|
|
},
|
|
{
|
|
Channel: bitmexWSOrder,
|
|
},
|
|
{
|
|
Channel: bitmexWSMargin,
|
|
},
|
|
{
|
|
Channel: bitmexWSPrivateNotifications,
|
|
},
|
|
{
|
|
Channel: bitmexWSTransact,
|
|
},
|
|
{
|
|
Channel: bitmexWSWallet,
|
|
},
|
|
}
|
|
for i := range channels {
|
|
for j := range contracts {
|
|
subscriptions = append(subscriptions, stream.ChannelSubscription{
|
|
Channel: channels[i] + ":" + contracts[j].String(),
|
|
Currency: contracts[j],
|
|
Asset: asset.PerpetualContract,
|
|
})
|
|
}
|
|
}
|
|
return subscriptions, nil
|
|
}
|
|
|
|
// Subscribe subscribes to a websocket channel
|
|
func (b *Bitmex) Subscribe(channelsToSubscribe []stream.ChannelSubscription) error {
|
|
var subscriber WebsocketRequest
|
|
subscriber.Command = "subscribe"
|
|
|
|
for i := range channelsToSubscribe {
|
|
subscriber.Arguments = append(subscriber.Arguments,
|
|
channelsToSubscribe[i].Channel)
|
|
}
|
|
err := b.Websocket.Conn.SendJSONMessage(subscriber)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
b.Websocket.AddSuccessfulSubscriptions(channelsToSubscribe...)
|
|
return nil
|
|
}
|
|
|
|
// Unsubscribe sends a websocket message to stop receiving data from the channel
|
|
func (b *Bitmex) Unsubscribe(channelsToUnsubscribe []stream.ChannelSubscription) error {
|
|
var unsubscriber WebsocketRequest
|
|
unsubscriber.Command = "unsubscribe"
|
|
|
|
for i := range channelsToUnsubscribe {
|
|
unsubscriber.Arguments = append(unsubscriber.Arguments,
|
|
channelsToUnsubscribe[i].Channel)
|
|
}
|
|
err := b.Websocket.Conn.SendJSONMessage(unsubscriber)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
b.Websocket.RemoveSuccessfulUnsubscriptions(channelsToUnsubscribe...)
|
|
return nil
|
|
}
|
|
|
|
// 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)
|
|
}
|