From 6c850e73e2ed871ade56b785491a337092957d52 Mon Sep 17 00:00:00 2001 From: Scott Date: Thu, 16 May 2019 16:39:16 +1000 Subject: [PATCH] Websocket connection handling and subscription management (#297) * Step one: Sets up connection handler for websockets to always be connected until a shutdown event is received. Sets up a vague subscription handler to ensure subscriptions are subscribed * Adds support for resubscriptions for bitfinex, bitstamp, bitmex and btcc. Adds subscription params for special websocket subscription requirements. Removes subscription monitor from wait group so that it can exist despite a shutdown and continuously check * Adds channel subscription support to bitmex, btse, coibasepro, coinut, gateio, gemini, hitbtc, huobi, hadax, kraken, okgroup, poloniex and zb * Implements unsubscribe for bitfinex, btcc, btse, coinbasepro, gateio, gitbtc, huobi, hadax * ManageSubscriptions now called from WSConnect and made private instead of inside individual exchanges. ManageSubscriptions can now unsubscribe. exchange_websocket_types.go now contains all exchange_websocket.go types to avoid clutter * Adds it to websocket functionality so managesubscriptions will close when not supported * Separates functions into testable functions to ensure logic works. Adds tests. Updates websocket setup to include verbosity (inherited from exchange). Adds no connection tolerance to fatal on failed reconnects * More exchange_websocket tests. Updating to use pointers. Creation of equals func to make comparison easier * Fixes okex, okcoin tests. Fixes race conditions. Removes pointer usage again. * Adds subscribe and unsubscribe to wrappers * Fixes deadlock. Fixes ws verbosity. * Updates all exchanges to properly support subscription/connection feature. Also reintroduces race conditions.... * Moves connection varialbes to struct from package to allow each websocket to have their own reconnection checks. Neatens up logs * Fixes lint/critic issues. Fixes tests. Removes unused function. * Moves websocket ratelimiter to their own const variables. Fixes more race conditions with connecting variable * Removes redundant subscribe functions. Ensuring only the exchange_websocket.go can manage subscriptions. Fixes debug logs to be verbose wrapped * Fixes issue with slice copying. Re-adds okgroup default channels * Adds nolint to append * Adds comments and adds support for gateio auth request subscriptions * Adds new test to ensure slices dont point to the same vars * removes fatals. gofmt goimports * more gofmts * Addresses PR comments, removing empty and redundant lines * Addresses PR comments. Ensures that writing to the websocket is single-threaded by adding a mutex to exchanges. Minimises wrapper code and moves subscription loops to exchange_websocket. Privatises ChannelsToSubscribe, Connecting properties and removeChannelToSubscribe func to prevent unnecessary tampering. * Removes unused mutex. FMTS and IMPORTS * Fixes request lock time change * More specific logs * Renames ws mutex. Fixes bitmex subscriptions. Increased gateio ratelimiter to 120ms. Removes ratelimiter from bitfinex, bitmex, bitstamp, btcc, btse, coibasepro, hitbtc, huobi, hadax, poloniex and zb * changes recieved typo due to not being well received * Fixes parsing issue with Huobi and hadax * Fixes data race with more locks * removes defer locks. fixes huobi/hadax verbose output * Fixes double JSONEncode for coinut. Fixes verbose output for coinut * gofmt,goimport for coinut * Fixes issue where multiple connection monitors can spawn * Removes defer exchange.WebsocketConn.Close() in defer handledata exit as connectionmonitor handles connections instead * gofmt and go import * More fmts --- exchanges/alphapoint/alphapoint_wrapper.go | 12 + exchanges/anx/anx_wrapper.go | 12 + exchanges/binance/binance.go | 3 + exchanges/binance/binance_websocket.go | 5 - exchanges/binance/binance_wrapper.go | 12 + exchanges/bitfinex/bitfinex.go | 11 +- exchanges/bitfinex/bitfinex_websocket.go | 105 +-- exchanges/bitfinex/bitfinex_wrapper.go | 14 + exchanges/bitflyer/bitflyer_wrapper.go | 12 + exchanges/bithumb/bithumb_wrapper.go | 12 + exchanges/bitmex/bitmex.go | 9 +- exchanges/bitmex/bitmex_websocket.go | 76 ++- exchanges/bitmex/bitmex_wrapper.go | 14 + exchanges/bitstamp/bitstamp.go | 9 +- exchanges/bitstamp/bitstamp_types.go | 34 + exchanges/bitstamp/bitstamp_websocket.go | 89 ++- exchanges/bitstamp/bitstamp_wrapper.go | 14 + exchanges/bittrex/bittrex_wrapper.go | 12 + exchanges/btcc/btcc.go | 10 +- exchanges/btcc/btcc_websocket.go | 164 +++-- exchanges/btcc/btcc_wrapper.go | 14 + exchanges/btcmarkets/btcmarkets_wrapper.go | 12 + exchanges/btse/btse.go | 9 +- exchanges/btse/btse_websocket.go | 91 ++- exchanges/btse/btse_wrapper.go | 14 + exchanges/coinbasepro/coinbasepro.go | 9 +- .../coinbasepro/coinbasepro_websocket.go | 112 ++-- exchanges/coinbasepro/coinbasepro_wrapper.go | 14 + exchanges/coinut/coinut.go | 9 +- exchanges/coinut/coinut_websocket.go | 119 ++-- exchanges/coinut/coinut_wrapper.go | 14 + exchanges/exchange.go | 2 + exchanges/exchange_websocket.go | 596 +++++++++++------- exchanges/exchange_websocket_test.go | 244 ++++++- exchanges/exchange_websocket_types.go | 172 +++++ exchanges/exmo/exmo_wrapper.go | 12 + exchanges/gateio/gateio.go | 10 +- exchanges/gateio/gateio_types.go | 1 + exchanges/gateio/gateio_websocket.go | 170 +++-- exchanges/gateio/gateio_wrapper.go | 14 + exchanges/gemini/gemini.go | 3 + exchanges/gemini/gemini_types.go | 2 +- exchanges/gemini/gemini_wrapper.go | 12 + exchanges/hitbtc/hitbtc.go | 9 +- exchanges/hitbtc/hitbtc_types.go | 77 +++ exchanges/hitbtc/hitbtc_websocket.go | 200 +++--- exchanges/hitbtc/hitbtc_wrapper.go | 14 + exchanges/huobi/huobi.go | 9 +- exchanges/huobi/huobi_types.go | 70 ++ exchanges/huobi/huobi_websocket.go | 152 ++--- exchanges/huobi/huobi_wrapper.go | 14 + exchanges/huobihadax/huobihadax.go | 9 +- exchanges/huobihadax/huobihadax_types.go | 5 +- exchanges/huobihadax/huobihadax_websocket.go | 91 ++- exchanges/huobihadax/huobihadax_wrapper.go | 14 + exchanges/itbit/itbit_wrapper.go | 12 + exchanges/kraken/kraken.go | 9 +- exchanges/kraken/kraken_test.go | 164 ----- exchanges/kraken/kraken_websocket.go | 214 +++---- exchanges/kraken/kraken_wrapper.go | 14 + exchanges/lakebtc/lakebtc_wrapper.go | 12 + .../localbitcoins/localbitcoins_wrapper.go | 12 + exchanges/okcoin/okcoin.go | 4 +- exchanges/okcoin/okcoin_test.go | 86 +-- exchanges/okex/okex.go | 6 +- exchanges/okex/okex_test.go | 121 +--- exchanges/okgroup/okgroup.go | 7 +- exchanges/okgroup/okgroup_websocket.go | 182 ++---- exchanges/okgroup/okgroup_wrapper.go | 14 + exchanges/poloniex/poloniex.go | 9 +- exchanges/poloniex/poloniex_websocket.go | 309 ++++----- exchanges/poloniex/poloniex_wrapper.go | 14 + exchanges/request/request.go | 1 - exchanges/yobit/yobit_wrapper.go | 12 + exchanges/zb/zb.go | 8 +- exchanges/zb/zb_websocket.go | 124 ++-- exchanges/zb/zb_websocket_types.go | 2 +- exchanges/zb/zb_wrapper.go | 13 + routines.go | 36 +- testdata/configtest.json | 2 +- tools/exchange_template/wrapper_file.tmpl | 14 + 81 files changed, 2566 insertions(+), 1783 deletions(-) create mode 100644 exchanges/exchange_websocket_types.go diff --git a/exchanges/alphapoint/alphapoint_wrapper.go b/exchanges/alphapoint/alphapoint_wrapper.go index 7a5c8081..204cf01c 100644 --- a/exchanges/alphapoint/alphapoint_wrapper.go +++ b/exchanges/alphapoint/alphapoint_wrapper.go @@ -316,3 +316,15 @@ func (a *Alphapoint) GetOrderHistory(getOrdersRequest *exchange.GetOrdersRequest return orders, nil } + +// SubscribeToWebsocketChannels appends to ChannelsToSubscribe +// which lets websocket.manageSubscriptions handle subscribing +func (a *Alphapoint) SubscribeToWebsocketChannels(channels []exchange.WebsocketChannelSubscription) error { + return common.ErrFunctionNotSupported +} + +// UnsubscribeToWebsocketChannels removes from ChannelsToSubscribe +// which lets websocket.manageSubscriptions handle unsubscribing +func (a *Alphapoint) UnsubscribeToWebsocketChannels(channels []exchange.WebsocketChannelSubscription) error { + return common.ErrFunctionNotSupported +} diff --git a/exchanges/anx/anx_wrapper.go b/exchanges/anx/anx_wrapper.go index 5c7c811c..6614a7d5 100644 --- a/exchanges/anx/anx_wrapper.go +++ b/exchanges/anx/anx_wrapper.go @@ -445,3 +445,15 @@ func (a *ANX) GetOrderHistory(getOrdersRequest *exchange.GetOrdersRequest) ([]ex return orders, nil } + +// SubscribeToWebsocketChannels appends to ChannelsToSubscribe +// which lets websocket.manageSubscriptions handle subscribing +func (a *ANX) SubscribeToWebsocketChannels(channels []exchange.WebsocketChannelSubscription) error { + return common.ErrFunctionNotSupported +} + +// UnsubscribeToWebsocketChannels removes from ChannelsToSubscribe +// which lets websocket.manageSubscriptions handle unsubscribing +func (a *ANX) UnsubscribeToWebsocketChannels(channels []exchange.WebsocketChannelSubscription) error { + return common.ErrFunctionNotSupported +} diff --git a/exchanges/binance/binance.go b/exchanges/binance/binance.go index 5a999426..74d6d37c 100644 --- a/exchanges/binance/binance.go +++ b/exchanges/binance/binance.go @@ -139,8 +139,11 @@ func (b *Binance) Setup(exch *config.ExchangeConfig) { log.Fatal(err) } err = b.WebsocketSetup(b.WSConnect, + nil, + nil, exch.Name, exch.Websocket, + exch.Verbose, binanceDefaultWebsocketURL, exch.WebsocketURL) if err != nil { diff --git a/exchanges/binance/binance_websocket.go b/exchanges/binance/binance_websocket.go index 847e0901..b96fd35f 100644 --- a/exchanges/binance/binance_websocket.go +++ b/exchanges/binance/binance_websocket.go @@ -199,11 +199,6 @@ func (b *Binance) WsHandleData() { b.Websocket.Wg.Add(1) defer func() { - err := b.WebsocketConn.Close() - if err != nil { - b.Websocket.DataHandler <- fmt.Errorf("binance_websocket.go - Unable to to close Websocket connection. Error: %s", - err) - } b.Websocket.Wg.Done() }() diff --git a/exchanges/binance/binance_wrapper.go b/exchanges/binance/binance_wrapper.go index 6ad78e9d..2833a3c4 100644 --- a/exchanges/binance/binance_wrapper.go +++ b/exchanges/binance/binance_wrapper.go @@ -413,3 +413,15 @@ func (b *Binance) GetOrderHistory(getOrdersRequest *exchange.GetOrdersRequest) ( return orders, nil } + +// SubscribeToWebsocketChannels appends to ChannelsToSubscribe +// which lets websocket.manageSubscriptions handle subscribing +func (b *Binance) SubscribeToWebsocketChannels(channels []exchange.WebsocketChannelSubscription) error { + return common.ErrFunctionNotSupported +} + +// UnsubscribeToWebsocketChannels removes from ChannelsToSubscribe +// which lets websocket.manageSubscriptions handle unsubscribing +func (b *Binance) UnsubscribeToWebsocketChannels(channels []exchange.WebsocketChannelSubscription) error { + return common.ErrFunctionNotSupported +} diff --git a/exchanges/bitfinex/bitfinex.go b/exchanges/bitfinex/bitfinex.go index 7fb3fabf..8769217e 100644 --- a/exchanges/bitfinex/bitfinex.go +++ b/exchanges/bitfinex/bitfinex.go @@ -6,6 +6,7 @@ import ( "net/http" "net/url" "strconv" + "sync" "time" "github.com/gorilla/websocket" @@ -87,6 +88,7 @@ type Bitfinex struct { exchange.Base WebsocketConn *websocket.Conn WebsocketSubdChannels map[int]WebsocketChanInfo + wsRequestMtx sync.Mutex } // SetDefaults sets the basic defaults for bitfinex @@ -114,7 +116,9 @@ func (b *Bitfinex) SetDefaults() { b.WebsocketInit() b.Websocket.Functionality = exchange.WebsocketTickerSupported | exchange.WebsocketTradeDataSupported | - exchange.WebsocketOrderbookSupported + exchange.WebsocketOrderbookSupported | + exchange.WebsocketSubscribeSupported | + exchange.WebsocketUnsubscribeSupported } // Setup takes in the supplied exchange configuration details and sets params @@ -155,8 +159,11 @@ func (b *Bitfinex) Setup(exch *config.ExchangeConfig) { log.Fatal(err) } err = b.WebsocketSetup(b.WsConnect, + b.Subscribe, + b.Unsubscribe, exch.Name, exch.Websocket, + exch.Verbose, bitfinexWebsocket, exch.WebsocketURL) if err != nil { @@ -617,7 +624,7 @@ func (b *Bitfinex) WithdrawCryptocurrency(withdrawType, wallet, address, payment &response) } -// WithdrawFIAT requests a withdrawal from a designated fiat wallet +// WithdrawFIAT Sends an authenticated request to withdraw FIAT currency func (b *Bitfinex) WithdrawFIAT(withdrawalType, walletType string, withdrawRequest *exchange.WithdrawRequest) ([]Withdrawal, error) { var response []Withdrawal req := make(map[string]interface{}) diff --git a/exchanges/bitfinex/bitfinex_websocket.go b/exchanges/bitfinex/bitfinex_websocket.go index 603a9ce6..09554df2 100644 --- a/exchanges/bitfinex/bitfinex_websocket.go +++ b/exchanges/bitfinex/bitfinex_websocket.go @@ -57,30 +57,21 @@ func (b *Bitfinex) WsPingHandler() error { req := make(map[string]string) req["event"] = "ping" - return b.WsSend(req) + return b.wsSend(req) } // WsSend sends data to the websocket server -func (b *Bitfinex) WsSend(data interface{}) error { +func (b *Bitfinex) wsSend(data interface{}) error { + b.wsRequestMtx.Lock() + defer b.wsRequestMtx.Unlock() json, err := common.JSONEncode(data) if err != nil { return err } - return b.WebsocketConn.WriteMessage(websocket.TextMessage, json) -} - -// WsSubscribe subscribes to the websocket channel -func (b *Bitfinex) WsSubscribe(channel string, params map[string]string) error { - req := make(map[string]string) - req["event"] = "subscribe" - req["channel"] = channel - - if len(params) > 0 { - for k, v := range params { - req[k] = v - } + if b.Verbose { + log.Debugf("%v sending message to websocket %v", b.Name, data) } - return b.WsSend(req) + return b.WebsocketConn.WriteMessage(websocket.TextMessage, json) } // WsSendAuth sends a autheticated event payload @@ -98,7 +89,7 @@ func (b *Bitfinex) WsSendAuth() error { req["authPayload"] = payload - return b.WsSend(req) + return b.wsSend(req) } // WsSendUnauth sends an unauthenticated payload @@ -106,7 +97,7 @@ func (b *Bitfinex) WsSendUnauth() error { req := make(map[string]string) req["event"] = "unauth" - return b.WsSend(req) + return b.wsSend(req) } // WsAddSubscriptionChannel adds a new subscription channel to the @@ -130,7 +121,6 @@ func (b *Bitfinex) WsConnect() error { return errors.New(exchange.WebsocketNotEnabled) } - var channels = []string{"book", "trades", "ticker"} var Dialer websocket.Dialer var err error @@ -145,12 +135,12 @@ func (b *Bitfinex) WsConnect() error { b.WebsocketConn, _, err = Dialer.Dial(b.Websocket.GetWebsocketURL(), http.Header{}) if err != nil { - return fmt.Errorf("unable to connect to Websocket. Error: %s", err) + return fmt.Errorf("%v unable to connect to Websocket. Error: %s", b.Name, err) } _, resp, err := b.WebsocketConn.ReadMessage() if err != nil { - return fmt.Errorf("unable to read from Websocket. Error: %s", err) + return fmt.Errorf("%v unable to read from Websocket. Error: %s", b.Name, err) } var hs WebsocketHandshake @@ -159,26 +149,13 @@ func (b *Bitfinex) WsConnect() error { return err } + b.GenerateDefaultSubscriptions() if hs.Event == "info" { if b.Verbose { log.Debugf("%s Connected to Websocket.\n", b.GetName()) } } - for _, x := range channels { - for _, y := range b.EnabledPairs { - params := make(map[string]string) - if x == "book" { - params["prec"] = "P0" - } - params["pair"] = y.String() - err = b.WsSubscribe(x, params) - if err != nil { - return err - } - } - } - if b.AuthenticatedAPISupport { err = b.WsSendAuth() if err != nil { @@ -214,11 +191,6 @@ func (b *Bitfinex) WsDataHandler() { b.Websocket.Wg.Add(1) defer func() { - err := b.WebsocketConn.Close() - if err != nil { - b.Websocket.DataHandler <- fmt.Errorf("bitfinex_websocket.go - closing websocket connection error %s", - err) - } b.Websocket.Wg.Done() }() @@ -237,12 +209,13 @@ func (b *Bitfinex) WsDataHandler() { if stream.Type == websocket.TextMessage { var result interface{} common.JSONDecode(stream.Raw, &result) - switch reflect.TypeOf(result).String() { case "map[string]interface {}": eventData := result.(map[string]interface{}) event := eventData["event"] - + if b.Verbose { + log.Debugf("%v Received message. Type '%v' Message: %v", b.Name, event, eventData) + } switch event { case "subscribed": b.WsAddSubscriptionChannel(int(eventData["chanId"].(float64)), @@ -624,3 +597,51 @@ func (b *Bitfinex) WsUpdateOrderbook(p currency.Pair, assetType string, book Web return nil } + +// GenerateDefaultSubscriptions Adds default subscriptions to websocket to be handled by ManageSubscriptions() +func (b *Bitfinex) GenerateDefaultSubscriptions() { + var channels = []string{"book", "trades", "ticker"} + subscriptions := []exchange.WebsocketChannelSubscription{} + for i := range channels { + for j := range b.EnabledPairs { + params := make(map[string]interface{}) + if channels[i] == "book" { + params["prec"] = "P0" + } + subscriptions = append(subscriptions, exchange.WebsocketChannelSubscription{ + Channel: channels[i], + Currency: b.EnabledPairs[j], + Params: params, + }) + } + } + b.Websocket.SubscribeToChannels(subscriptions) +} + +// Subscribe sends a websocket message to receive data from the channel +func (b *Bitfinex) Subscribe(channelToSubscribe exchange.WebsocketChannelSubscription) error { + req := make(map[string]interface{}) + req["event"] = "subscribe" + req["channel"] = channelToSubscribe.Channel + req["pair"] = channelToSubscribe.Currency.String() + if len(channelToSubscribe.Params) > 0 { + for k, v := range channelToSubscribe.Params { + req[k] = v + } + } + return b.wsSend(req) +} + +// Unsubscribe sends a websocket message to stop receiving data from the channel +func (b *Bitfinex) Unsubscribe(channelToSubscribe exchange.WebsocketChannelSubscription) error { + req := make(map[string]interface{}) + req["event"] = "unsubscribe" + req["channel"] = channelToSubscribe.Channel + + if len(channelToSubscribe.Params) > 0 { + for k, v := range channelToSubscribe.Params { + req[k] = v + } + } + return b.wsSend(req) +} diff --git a/exchanges/bitfinex/bitfinex_wrapper.go b/exchanges/bitfinex/bitfinex_wrapper.go index 0f667fef..c15c83ce 100644 --- a/exchanges/bitfinex/bitfinex_wrapper.go +++ b/exchanges/bitfinex/bitfinex_wrapper.go @@ -454,3 +454,17 @@ func (b *Bitfinex) GetOrderHistory(getOrdersRequest *exchange.GetOrdersRequest) return orders, nil } + +// SubscribeToWebsocketChannels appends to ChannelsToSubscribe +// which lets websocket.manageSubscriptions handle subscribing +func (b *Bitfinex) SubscribeToWebsocketChannels(channels []exchange.WebsocketChannelSubscription) error { + b.Websocket.SubscribeToChannels(channels) + return nil +} + +// UnsubscribeToWebsocketChannels removes from ChannelsToSubscribe +// which lets websocket.manageSubscriptions handle unsubscribing +func (b *Bitfinex) UnsubscribeToWebsocketChannels(channels []exchange.WebsocketChannelSubscription) error { + b.Websocket.UnsubscribeToChannels(channels) + return nil +} diff --git a/exchanges/bitflyer/bitflyer_wrapper.go b/exchanges/bitflyer/bitflyer_wrapper.go index 2feb7560..e277ea2e 100644 --- a/exchanges/bitflyer/bitflyer_wrapper.go +++ b/exchanges/bitflyer/bitflyer_wrapper.go @@ -242,3 +242,15 @@ func (b *Bitflyer) GetFeeByType(feeBuilder *exchange.FeeBuilder) (float64, error } return b.GetFee(feeBuilder) } + +// SubscribeToWebsocketChannels appends to ChannelsToSubscribe +// which lets websocket.manageSubscriptions handle subscribing +func (b *Bitflyer) SubscribeToWebsocketChannels(channels []exchange.WebsocketChannelSubscription) error { + return common.ErrFunctionNotSupported +} + +// UnsubscribeToWebsocketChannels removes from ChannelsToSubscribe +// which lets websocket.manageSubscriptions handle unsubscribing +func (b *Bitflyer) UnsubscribeToWebsocketChannels(channels []exchange.WebsocketChannelSubscription) error { + return common.ErrFunctionNotSupported +} diff --git a/exchanges/bithumb/bithumb_wrapper.go b/exchanges/bithumb/bithumb_wrapper.go index 01a3ffeb..245022d4 100644 --- a/exchanges/bithumb/bithumb_wrapper.go +++ b/exchanges/bithumb/bithumb_wrapper.go @@ -420,3 +420,15 @@ func (b *Bithumb) GetOrderHistory(getOrdersRequest *exchange.GetOrdersRequest) ( return orders, nil } + +// SubscribeToWebsocketChannels appends to ChannelsToSubscribe +// which lets websocket.manageSubscriptions handle subscribing +func (b *Bithumb) SubscribeToWebsocketChannels(channels []exchange.WebsocketChannelSubscription) error { + return common.ErrFunctionNotSupported +} + +// UnsubscribeToWebsocketChannels removes from ChannelsToSubscribe +// which lets websocket.manageSubscriptions handle unsubscribing +func (b *Bithumb) UnsubscribeToWebsocketChannels(channels []exchange.WebsocketChannelSubscription) error { + return common.ErrFunctionNotSupported +} diff --git a/exchanges/bitmex/bitmex.go b/exchanges/bitmex/bitmex.go index 54558d48..d2d94d3e 100644 --- a/exchanges/bitmex/bitmex.go +++ b/exchanges/bitmex/bitmex.go @@ -7,6 +7,7 @@ import ( "net/http" "strconv" "strings" + "sync" "time" "github.com/gorilla/websocket" @@ -23,6 +24,7 @@ import ( type Bitmex struct { exchange.Base WebsocketConn *websocket.Conn + wsRequestMtx sync.Mutex } const ( @@ -135,7 +137,9 @@ func (b *Bitmex) SetDefaults() { b.SupportsAutoPairUpdating = true b.WebsocketInit() b.Websocket.Functionality = exchange.WebsocketTradeDataSupported | - exchange.WebsocketOrderbookSupported + exchange.WebsocketOrderbookSupported | + exchange.WebsocketSubscribeSupported | + exchange.WebsocketUnsubscribeSupported } // Setup takes in the supplied exchange configuration details and sets params @@ -174,8 +178,11 @@ func (b *Bitmex) Setup(exch *config.ExchangeConfig) { log.Fatal(err) } err = b.WebsocketSetup(b.WsConnector, + b.Subscribe, + b.Unsubscribe, exch.Name, exch.Websocket, + exch.Verbose, bitmexWSURL, exch.WebsocketURL) if err != nil { diff --git a/exchanges/bitmex/bitmex_websocket.go b/exchanges/bitmex/bitmex_websocket.go index 3766b722..d025583f 100644 --- a/exchanges/bitmex/bitmex_websocket.go +++ b/exchanges/bitmex/bitmex_websocket.go @@ -105,16 +105,7 @@ func (b *Bitmex) WsConnector() error { } go b.wsHandleIncomingData() - - err = b.websocketSubscribe() - if err != nil { - closeError := b.WebsocketConn.Close() - if closeError != nil { - return fmt.Errorf("bitmex_websocket.go error - Websocket connection could not close %s", - closeError) - } - return err - } + b.GenerateDefaultSubscriptions() if b.AuthenticatedAPISupport { err := b.websocketSendAuth() @@ -143,11 +134,6 @@ func (b *Bitmex) wsHandleIncomingData() { b.Websocket.Wg.Add(1) defer func() { - err := b.WebsocketConn.Close() - if err != nil { - b.Websocket.DataHandler <- fmt.Errorf("bitmex_websocket.go - Unable to close Websocket connection. Error: %s", - err) - } b.Websocket.Wg.Done() }() @@ -170,7 +156,7 @@ func (b *Bitmex) wsHandleIncomingData() { } if common.StringContains(message, "ping") { - err = b.WebsocketConn.WriteJSON("pong") + err = b.wsSend("pong") if err != nil { b.Websocket.DataHandler <- err continue @@ -398,26 +384,42 @@ func (b *Bitmex) processOrderbook(data []OrderBookL2, action string, currencyPai return nil } -// WebsocketSubscribe subscribes to a websocket channel -func (b *Bitmex) websocketSubscribe() error { +// GenerateDefaultSubscriptions Adds default subscriptions to websocket to be handled by ManageSubscriptions() +func (b *Bitmex) GenerateDefaultSubscriptions() { contracts := b.GetEnabledCurrencies() + channels := []string{bitmexWSOrderbookL2, bitmexWSTrade} + subscriptions := []exchange.WebsocketChannelSubscription{ + { + Channel: bitmexWSAnnouncement, + }, + } + for i := range channels { + for j := range contracts { + subscriptions = append(subscriptions, exchange.WebsocketChannelSubscription{ + Channel: fmt.Sprintf("%v:%v", channels[i], contracts[j].String()), + Currency: contracts[j], + }) + } + } + b.Websocket.SubscribeToChannels(subscriptions) +} - // Subscriber +// Subscribe subscribes to a websocket channel +func (b *Bitmex) Subscribe(channelToSubscribe exchange.WebsocketChannelSubscription) error { var subscriber WebsocketRequest subscriber.Command = "subscribe" + subscriber.Arguments = append(subscriber.Arguments, channelToSubscribe.Channel) + return b.wsSend(subscriber) +} - // Announcement subscribe - subscriber.Arguments = append(subscriber.Arguments, bitmexWSAnnouncement) - - for _, contract := range contracts { - // Orderbook and Trade subscribe - // NOTE more added here in future - subscriber.Arguments = append(subscriber.Arguments, - bitmexWSOrderbookL2+":"+contract.String(), - bitmexWSTrade+":"+contract.String()) - } - - return b.WebsocketConn.WriteJSON(subscriber) +// Unsubscribe sends a websocket message to stop receiving data from the channel +func (b *Bitmex) Unsubscribe(channelToSubscribe exchange.WebsocketChannelSubscription) error { + var subscriber WebsocketRequest + subscriber.Command = "unsubscribe" + subscriber.Arguments = append(subscriber.Arguments, + channelToSubscribe.Params["args"], + channelToSubscribe.Channel+":"+channelToSubscribe.Currency.String()) + return b.wsSend(subscriber) } // WebsocketSendAuth sends an authenticated subscription @@ -433,5 +435,15 @@ func (b *Bitmex) websocketSendAuth() error { sendAuth.Command = "authKeyExpires" sendAuth.Arguments = append(sendAuth.Arguments, b.APIKey, timestamp, signature) - return b.WebsocketConn.WriteJSON(sendAuth) + return b.wsSend(sendAuth) +} + +// WsSend sends data to the websocket server +func (b *Bitmex) wsSend(data interface{}) error { + b.wsRequestMtx.Lock() + defer b.wsRequestMtx.Unlock() + if b.Verbose { + log.Debugf("%v sending message to websocket %v", b.Name, data) + } + return b.WebsocketConn.WriteJSON(data) } diff --git a/exchanges/bitmex/bitmex_wrapper.go b/exchanges/bitmex/bitmex_wrapper.go index a86f46c2..b7a06a75 100644 --- a/exchanges/bitmex/bitmex_wrapper.go +++ b/exchanges/bitmex/bitmex_wrapper.go @@ -396,3 +396,17 @@ func (b *Bitmex) GetOrderHistory(getOrdersRequest *exchange.GetOrdersRequest) ([ return orders, nil } + +// SubscribeToWebsocketChannels appends to ChannelsToSubscribe +// which lets websocket.manageSubscriptions handle subscribing +func (b *Bitmex) SubscribeToWebsocketChannels(channels []exchange.WebsocketChannelSubscription) error { + b.Websocket.SubscribeToChannels(channels) + return nil +} + +// UnsubscribeToWebsocketChannels removes from ChannelsToSubscribe +// which lets websocket.manageSubscriptions handle unsubscribing +func (b *Bitmex) UnsubscribeToWebsocketChannels(channels []exchange.WebsocketChannelSubscription) error { + b.Websocket.UnsubscribeToChannels(channels) + return nil +} diff --git a/exchanges/bitstamp/bitstamp.go b/exchanges/bitstamp/bitstamp.go index 8c9d5944..a12eac07 100644 --- a/exchanges/bitstamp/bitstamp.go +++ b/exchanges/bitstamp/bitstamp.go @@ -9,6 +9,7 @@ import ( "reflect" "strconv" "strings" + "sync" "time" "github.com/thrasher-/gocryptotrader/common" @@ -63,6 +64,7 @@ type Bitstamp struct { exchange.Base Balance Balances WebsocketConn WebsocketConn + wsRequestMtx sync.Mutex } // SetDefaults sets default for Bitstamp @@ -88,7 +90,9 @@ func (b *Bitstamp) SetDefaults() { b.APIUrl = b.APIUrlDefault b.WebsocketInit() b.Websocket.Functionality = exchange.WebsocketOrderbookSupported | - exchange.WebsocketTradeDataSupported + exchange.WebsocketTradeDataSupported | + exchange.WebsocketSubscribeSupported | + exchange.WebsocketUnsubscribeSupported } // Setup sets configuration values to bitstamp @@ -133,8 +137,11 @@ func (b *Bitstamp) Setup(exch *config.ExchangeConfig) { log.Fatal(err) } err = b.WebsocketSetup(b.WsConnect, + b.Subscribe, + b.Unsubscribe, exch.Name, exch.Websocket, + exch.Verbose, BitstampPusherKey, exch.WebsocketURL) if err != nil { diff --git a/exchanges/bitstamp/bitstamp_types.go b/exchanges/bitstamp/bitstamp_types.go index 0a51dbbd..fb2923a5 100644 --- a/exchanges/bitstamp/bitstamp_types.go +++ b/exchanges/bitstamp/bitstamp_types.go @@ -1,5 +1,7 @@ package bitstamp +import pusher "github.com/toorop/go-pusher" + // Ticker holds ticker information type Ticker struct { Last float64 `json:"last,string"` @@ -157,3 +159,35 @@ const ( internationalWithdrawal string = "international" errStr string = "error" ) + +// WebsocketConn defines a pusher websocket connection +type WebsocketConn struct { + Client *pusher.Client + Data chan *pusher.Event + Trade chan *pusher.Event +} + +// PusherOrderbook holds order book information to be pushed +type PusherOrderbook struct { + Asks [][]string `json:"asks"` + Bids [][]string `json:"bids"` + Timestamp int64 `json:"timestamp,string"` +} + +// PusherTrade holds trade information to be pushed +type PusherTrade struct { + Price float64 `json:"price"` + Amount float64 `json:"amount"` + ID int64 `json:"id"` + Type int64 `json:"type"` + Timestamp int64 `json:"timestamp,string"` + BuyOrderID int64 `json:"buy_order_id"` + SellOrderID int64 `json:"sell_order_id"` +} + +// PusherOrders defines order information +type PusherOrders struct { + ID int64 `json:"id"` + Amount float64 `json:"amount"` + Price float64 `json:""` +} diff --git a/exchanges/bitstamp/bitstamp_websocket.go b/exchanges/bitstamp/bitstamp_websocket.go index df069384..b0908fa2 100644 --- a/exchanges/bitstamp/bitstamp_websocket.go +++ b/exchanges/bitstamp/bitstamp_websocket.go @@ -15,38 +15,6 @@ import ( pusher "github.com/toorop/go-pusher" ) -// WebsocketConn defins a pusher websocket connection -type WebsocketConn struct { - Client *pusher.Client - Data chan *pusher.Event - Trade chan *pusher.Event -} - -// PusherOrderbook holds order book information to be pushed -type PusherOrderbook struct { - Asks [][]string `json:"asks"` - Bids [][]string `json:"bids"` - Timestamp int64 `json:"timestamp,string"` -} - -// PusherTrade holds trade information to be pushed -type PusherTrade struct { - Price float64 `json:"price"` - Amount float64 `json:"amount"` - ID int64 `json:"id"` - Type int64 `json:"type"` - Timestamp int64 `json:"timestamp,string"` - BuyOrderID int64 `json:"buy_order_id"` - SellOrderID int64 `json:"sell_order_id"` -} - -// PusherOrders defines order information -type PusherOrders struct { - ID int64 `json:"id"` - Amount float64 `json:"amount"` - Price float64 `json:""` -} - const ( // BitstampPusherKey holds the current pusher key BitstampPusherKey = "de504dc5763aeef9ff52" @@ -98,7 +66,7 @@ func (b *Bitstamp) WsConnect() error { if err != nil { return fmt.Errorf("%s Websocket Bind error: %s", b.GetName(), err) } - + b.GenerateDefaultSubscriptions() go b.WsReadData() for _, p := range b.GetEnabledCurrencies() { @@ -141,32 +109,49 @@ func (b *Bitstamp) WsConnect() error { Exchange: b.GetName(), } - err = b.WebsocketConn.Client.Subscribe(fmt.Sprintf("live_trades_%s", - p.Lower().String())) - - if err != nil { - return fmt.Errorf("%s Websocket Trade subscription error: %s", - b.GetName(), - err) - } - - err = b.WebsocketConn.Client.Subscribe(fmt.Sprintf("diff_order_book_%s", - p.Lower().String())) - - if err != nil { - return fmt.Errorf("%s Websocket Trade subscription error: %s", - b.GetName(), - err) - } - } return nil } +// GenerateDefaultSubscriptions Adds default subscriptions to websocket to be handled by ManageSubscriptions() +func (b *Bitstamp) GenerateDefaultSubscriptions() { + var channels = []string{"live_trades_", "diff_order_book_"} + enabledCurrencies := b.GetEnabledCurrencies() + subscriptions := []exchange.WebsocketChannelSubscription{} + for i := range channels { + for j := range enabledCurrencies { + subscriptions = append(subscriptions, exchange.WebsocketChannelSubscription{ + Channel: fmt.Sprintf("%v%v", channels[i], enabledCurrencies[j].Lower().String()), + Currency: enabledCurrencies[j], + }) + } + } + b.Websocket.SubscribeToChannels(subscriptions) +} + +// Subscribe sends a websocket message to receive data from the channel +func (b *Bitstamp) Subscribe(channelToSubscribe exchange.WebsocketChannelSubscription) error { + b.wsRequestMtx.Lock() + defer b.wsRequestMtx.Unlock() + if b.Verbose { + log.Debugf("%v sending message to websocket %v", b.Name, channelToSubscribe) + } + return b.WebsocketConn.Client.Subscribe(channelToSubscribe.Channel) +} + +// Unsubscribe sends a websocket message to stop receiving data from the channel +func (b *Bitstamp) Unsubscribe(channelToSubscribe exchange.WebsocketChannelSubscription) error { + b.wsRequestMtx.Lock() + defer b.wsRequestMtx.Unlock() + if b.Verbose { + log.Debugf("%v sending message to websocket %v", b.Name, channelToSubscribe) + } + return b.WebsocketConn.Client.Unsubscribe(channelToSubscribe.Channel) +} + // WsReadData reads data coming from bitstamp websocket connection func (b *Bitstamp) WsReadData() { b.Websocket.Wg.Add(1) - defer func() { err := b.WebsocketConn.Client.Close() if err != nil { diff --git a/exchanges/bitstamp/bitstamp_wrapper.go b/exchanges/bitstamp/bitstamp_wrapper.go index 54b86831..00183ac3 100644 --- a/exchanges/bitstamp/bitstamp_wrapper.go +++ b/exchanges/bitstamp/bitstamp_wrapper.go @@ -399,3 +399,17 @@ func (b *Bitstamp) GetOrderHistory(getOrdersRequest *exchange.GetOrdersRequest) return orders, nil } + +// SubscribeToWebsocketChannels appends to ChannelsToSubscribe +// which lets websocket.manageSubscriptions handle subscribing +func (b *Bitstamp) SubscribeToWebsocketChannels(channels []exchange.WebsocketChannelSubscription) error { + b.Websocket.SubscribeToChannels(channels) + return nil +} + +// UnsubscribeToWebsocketChannels removes from ChannelsToSubscribe +// which lets websocket.manageSubscriptions handle unsubscribing +func (b *Bitstamp) UnsubscribeToWebsocketChannels(channels []exchange.WebsocketChannelSubscription) error { + b.Websocket.UnsubscribeToChannels(channels) + return nil +} diff --git a/exchanges/bittrex/bittrex_wrapper.go b/exchanges/bittrex/bittrex_wrapper.go index 1d45cfcb..bfae5b73 100644 --- a/exchanges/bittrex/bittrex_wrapper.go +++ b/exchanges/bittrex/bittrex_wrapper.go @@ -397,3 +397,15 @@ func (b *Bittrex) GetOrderHistory(getOrdersRequest *exchange.GetOrdersRequest) ( return orders, nil } + +// SubscribeToWebsocketChannels appends to ChannelsToSubscribe +// which lets websocket.manageSubscriptions handle subscribing +func (b *Bittrex) SubscribeToWebsocketChannels(channels []exchange.WebsocketChannelSubscription) error { + return common.ErrFunctionNotSupported +} + +// UnsubscribeToWebsocketChannels removes from ChannelsToSubscribe +// which lets websocket.manageSubscriptions handle unsubscribing +func (b *Bittrex) UnsubscribeToWebsocketChannels(channels []exchange.WebsocketChannelSubscription) error { + return common.ErrFunctionNotSupported +} diff --git a/exchanges/btcc/btcc.go b/exchanges/btcc/btcc.go index d083d648..06dfd718 100644 --- a/exchanges/btcc/btcc.go +++ b/exchanges/btcc/btcc.go @@ -1,6 +1,7 @@ package btcc import ( + "sync" "time" "github.com/gorilla/websocket" @@ -23,7 +24,8 @@ const ( // been dropped type BTCC struct { exchange.Base - Conn *websocket.Conn + Conn *websocket.Conn + wsRequestMtx sync.Mutex } // SetDefaults sets default values for the exchange @@ -46,6 +48,9 @@ func (b *BTCC) SetDefaults() { request.NewRateLimit(time.Second, btccUnauthRate), common.NewHTTPClientWithTimeout(exchange.DefaultHTTPTimeout)) b.WebsocketInit() + b.Websocket.Functionality = + exchange.WebsocketSubscribeSupported | + exchange.WebsocketUnsubscribeSupported } // Setup is run on startup to setup exchange with config values @@ -86,8 +91,11 @@ func (b *BTCC) Setup(exch *config.ExchangeConfig) { log.Fatal(err) } err = b.WebsocketSetup(b.WsConnect, + b.Subscribe, + b.Unsubscribe, exch.Name, exch.Websocket, + exch.Verbose, btccSocketioAddress, exch.WebsocketURL) if err != nil { diff --git a/exchanges/btcc/btcc_websocket.go b/exchanges/btcc/btcc_websocket.go index b365d971..7c5ac45a 100644 --- a/exchanges/btcc/btcc_websocket.go +++ b/exchanges/btcc/btcc_websocket.go @@ -70,18 +70,9 @@ func (b *BTCC) WsConnect() error { } go b.WsHandleData() + b.GenerateDefaultSubscriptions() - err = b.WsSubscribeToOrderbook() - if err != nil { - return err - } - - err = b.WsSubcribeToTicker() - if err != nil { - return err - } - - return b.WsSubcribeToTrades() + return nil } // WsReadData reads data from the websocket connection @@ -251,33 +242,8 @@ func (b *BTCC) WsHandleData() { } } -// WsSubscribeAllTickers subscribes to a ticker channel -func (b *BTCC) WsSubscribeAllTickers() error { - mtx.Lock() - defer mtx.Unlock() - - return b.Conn.WriteJSON(WsOutgoing{ - Action: "SubscribeAllTickers", - }) -} - -// WsUnSubscribeAllTickers unsubscribes from a ticker channel -func (b *BTCC) WsUnSubscribeAllTickers() error { - mtx.Lock() - defer mtx.Unlock() - - return b.Conn.WriteJSON(WsOutgoing{ - Action: "UnSubscribeAllTickers", - }) -} - // WsUpdateCurrencyPairs updates currency pairs from the websocket connection func (b *BTCC) WsUpdateCurrencyPairs() error { - err := b.WsSubscribeAllTickers() - if err != nil { - return err - } - var currencyResponse WsResponseMain for { _, resp, err := b.Conn.ReadMessage() @@ -313,8 +279,6 @@ func (b *BTCC) WsUpdateCurrencyPairs() error { err) } - return b.WsUnSubscribeAllTickers() - case "Heartbeat": default: @@ -324,61 +288,6 @@ func (b *BTCC) WsUpdateCurrencyPairs() error { } } -// WsSubscribeToOrderbook subscribes to an orderbook channel -func (b *BTCC) WsSubscribeToOrderbook() error { - mtx.Lock() - defer mtx.Unlock() - - for _, pair := range b.GetEnabledCurrencies() { - formattedPair := exchange.FormatExchangeCurrency(b.GetName(), pair) - err := b.Conn.WriteJSON(WsOutgoing{ - Action: "SubOrderBook", - Symbol: formattedPair.String(), - Len: 100}) - if err != nil { - return err - } - } - return nil -} - -// WsSubcribeToTicker subscribes to a ticker channel -func (b *BTCC) WsSubcribeToTicker() error { - mtx.Lock() - defer mtx.Unlock() - - for _, pair := range b.GetEnabledCurrencies() { - formattedPair := exchange.FormatExchangeCurrency(b.GetName(), pair) - err := b.Conn.WriteJSON(WsOutgoing{ - Action: "Subscribe", - Symbol: formattedPair.String(), - }) - if err != nil { - return err - } - } - return nil -} - -// WsSubcribeToTrades subscribes to a trade channel -func (b *BTCC) WsSubcribeToTrades() error { - mtx.Lock() - defer mtx.Unlock() - - for _, pair := range b.GetEnabledCurrencies() { - formattedPair := exchange.FormatExchangeCurrency(b.GetName(), pair) - err := b.Conn.WriteJSON(WsOutgoing{ - Action: "GetTrades", - Symbol: formattedPair.String(), - Count: 100, - }) - if err != nil { - return err - } - } - return nil -} - // WsProcessOrderbookSnapshot processes a new orderbook snapshot func (b *BTCC) WsProcessOrderbookSnapshot(ob *WsOrderbookSnapshot) error { var asks, bids []orderbook.Item @@ -559,3 +468,72 @@ func (b *BTCC) WsProcessOldOrderbookSnapshot(ob WsOrderbookSnapshotOld, symbol s return nil } + +// GenerateDefaultSubscriptions Adds default subscriptions to websocket to be handled by ManageSubscriptions() +func (b *BTCC) GenerateDefaultSubscriptions() { + subscriptions := []exchange.WebsocketChannelSubscription{} + subscriptions = append(subscriptions, exchange.WebsocketChannelSubscription{ + Channel: "SubscribeAllTickers", + }) + + var channels = []string{"SubOrderBook", "GetTrades", "Subscribe"} + enabledCurrencies := b.GetEnabledCurrencies() + for i := range channels { + for j := range enabledCurrencies { + params := make(map[string]interface{}) + if channels[i] == "SubOrderBook" { + params["len"] = "100" + } else if channels[i] == "GetTrades" { + params["count"] = "100" + } + subscriptions = append(subscriptions, exchange.WebsocketChannelSubscription{ + Channel: channels[i], + Currency: enabledCurrencies[j], + Params: params, + }) + } + } + b.Websocket.SubscribeToChannels(subscriptions) +} + +// Subscribe sends a websocket message to receive data from the channel +func (b *BTCC) Subscribe(channelToSubscribe exchange.WebsocketChannelSubscription) error { + subscription := WsOutgoing{ + Action: channelToSubscribe.Channel, + Symbol: channelToSubscribe.Currency.String(), + } + if subscription.Action == "SubOrderBook" { + subscription.Len = 100 + } else if subscription.Action == "GetTrades" { + subscription.Count = 100 + } + + return b.wsSend(subscription) +} + +// Unsubscribe sends a websocket message to stop receiving data from the channel +func (b *BTCC) Unsubscribe(channelToSubscribe exchange.WebsocketChannelSubscription) error { + subscription := WsOutgoing{} + switch channelToSubscribe.Channel { + case "SubOrderBook": + subscription.Action = "UnSubOrderBook" + subscription.Symbol = channelToSubscribe.Currency.String() + case "Subscribe": + subscription.Action = "UnSubscribe" + subscription.Symbol = channelToSubscribe.Currency.String() + case "SubscribeAllTickers": + subscription.Action = "UnSubscribeAllTickers" + } + + return b.wsSend(subscription) +} + +// WsSend sends data to the websocket server +func (b *BTCC) wsSend(data interface{}) error { + b.wsRequestMtx.Lock() + defer b.wsRequestMtx.Unlock() + if b.Verbose { + log.Debugf("%v sending message to websocket %v", b.Name, data) + } + return b.Conn.WriteJSON(data) +} diff --git a/exchanges/btcc/btcc_wrapper.go b/exchanges/btcc/btcc_wrapper.go index 2f15dd3f..673640d9 100644 --- a/exchanges/btcc/btcc_wrapper.go +++ b/exchanges/btcc/btcc_wrapper.go @@ -175,3 +175,17 @@ func (b *BTCC) GetActiveOrders(getOrdersRequest *exchange.GetOrdersRequest) ([]e func (b *BTCC) GetOrderHistory(getOrdersRequest *exchange.GetOrdersRequest) ([]exchange.OrderDetail, error) { return nil, common.ErrNotYetImplemented } + +// SubscribeToWebsocketChannels appends to ChannelsToSubscribe +// which lets websocket.manageSubscriptions handle subscribing +func (b *BTCC) SubscribeToWebsocketChannels(channels []exchange.WebsocketChannelSubscription) error { + b.Websocket.SubscribeToChannels(channels) + return nil +} + +// UnsubscribeToWebsocketChannels removes from ChannelsToSubscribe +// which lets websocket.manageSubscriptions handle unsubscribing +func (b *BTCC) UnsubscribeToWebsocketChannels(channels []exchange.WebsocketChannelSubscription) error { + b.Websocket.UnsubscribeToChannels(channels) + return nil +} diff --git a/exchanges/btcmarkets/btcmarkets_wrapper.go b/exchanges/btcmarkets/btcmarkets_wrapper.go index ce4a4fb5..338300ce 100644 --- a/exchanges/btcmarkets/btcmarkets_wrapper.go +++ b/exchanges/btcmarkets/btcmarkets_wrapper.go @@ -464,3 +464,15 @@ func (b *BTCMarkets) GetOrderHistory(getOrdersRequest *exchange.GetOrdersRequest return orders, nil } + +// SubscribeToWebsocketChannels appends to ChannelsToSubscribe +// which lets websocket.manageSubscriptions handle subscribing +func (b *BTCMarkets) SubscribeToWebsocketChannels(channels []exchange.WebsocketChannelSubscription) error { + return common.ErrFunctionNotSupported +} + +// UnsubscribeToWebsocketChannels removes from ChannelsToSubscribe +// which lets websocket.manageSubscriptions handle unsubscribing +func (b *BTCMarkets) UnsubscribeToWebsocketChannels(channels []exchange.WebsocketChannelSubscription) error { + return common.ErrFunctionNotSupported +} diff --git a/exchanges/btse/btse.go b/exchanges/btse/btse.go index 80a620c9..b71c7dd8 100644 --- a/exchanges/btse/btse.go +++ b/exchanges/btse/btse.go @@ -6,6 +6,7 @@ import ( "net/http" "strconv" "strings" + "sync" "time" "github.com/gorilla/websocket" @@ -22,6 +23,7 @@ import ( type BTSE struct { exchange.Base WebsocketConn *websocket.Conn + wsRequestMtx sync.Mutex } const ( @@ -66,7 +68,9 @@ func (b *BTSE) SetDefaults() { b.SupportsRESTTickerBatching = false b.WebsocketInit() b.Websocket.Functionality = exchange.WebsocketOrderbookSupported | - exchange.WebsocketTickerSupported + exchange.WebsocketTickerSupported | + exchange.WebsocketSubscribeSupported | + exchange.WebsocketUnsubscribeSupported } // Setup takes in the supplied exchange configuration details and sets params @@ -106,8 +110,11 @@ func (b *BTSE) Setup(exch *config.ExchangeConfig) { log.Fatal(err) } err = b.WebsocketSetup(b.WsConnect, + b.Subscribe, + b.Unsubscribe, exch.Name, exch.Websocket, + exch.Verbose, btseWebsocket, exch.WebsocketURL) if err != nil { diff --git a/exchanges/btse/btse_websocket.go b/exchanges/btse/btse_websocket.go index 2e39b355..df3984c6 100644 --- a/exchanges/btse/btse_websocket.go +++ b/exchanges/btse/btse_websocket.go @@ -21,31 +21,6 @@ const ( btseWebsocket = "wss://ws.btse.com/api/ws-feed" ) -// WebsocketSubscriber subscribes to websocket channels with respect to enabled -// currencies -func (b *BTSE) WebsocketSubscriber() error { - subscribe := websocketSubscribe{ - Type: "subscribe", - Channels: []websocketChannel{ - { - Name: "snapshot", - ProductIDs: b.EnabledPairs.Strings(), - }, - { - Name: "ticker", - ProductIDs: b.EnabledPairs.Strings(), - }, - }, - } - - data, err := common.JSONEncode(subscribe) - if err != nil { - return err - } - - return b.WebsocketConn.WriteMessage(websocket.TextMessage, data) -} - // WsConnect connects the websocket client func (b *BTSE) WsConnect() error { if !b.Websocket.IsEnabled() || !b.IsEnabled() { @@ -72,12 +47,12 @@ func (b *BTSE) WsConnect() error { b.Name, err) } - err = b.WebsocketSubscriber() if err != nil { return err } go b.WsHandleData() + b.GenerateDefaultSubscriptions() return nil } @@ -98,11 +73,6 @@ func (b *BTSE) WsHandleData() { b.Websocket.Wg.Add(1) defer func() { - err := b.WebsocketConn.Close() - if err != nil { - b.Websocket.DataHandler <- fmt.Errorf("%s - Unable to to close Websocket connection. Error: %s", - b.Name, err) - } b.Websocket.Wg.Done() }() @@ -137,7 +107,6 @@ func (b *BTSE) WsHandleData() { b.Websocket.DataHandler <- err continue } - switch msgType.Type { case "ticker": var t wsTicker @@ -234,3 +203,61 @@ func (b *BTSE) wsProcessSnapshot(snapshot *websocketOrderbookSnapshot) error { return nil } + +// GenerateDefaultSubscriptions Adds default subscriptions to websocket to be handled by ManageSubscriptions() +func (b *BTSE) GenerateDefaultSubscriptions() { + var channels = []string{"snapshot", "ticker"} + enabledCurrencies := b.GetEnabledCurrencies() + subscriptions := []exchange.WebsocketChannelSubscription{} + for i := range channels { + for j := range enabledCurrencies { + subscriptions = append(subscriptions, exchange.WebsocketChannelSubscription{ + Channel: channels[i], + Currency: enabledCurrencies[j], + }) + } + } + b.Websocket.SubscribeToChannels(subscriptions) +} + +// Subscribe sends a websocket message to receive data from the channel +func (b *BTSE) Subscribe(channelToSubscribe exchange.WebsocketChannelSubscription) error { + subscribe := websocketSubscribe{ + Type: "subscribe", + Channels: []websocketChannel{ + { + Name: channelToSubscribe.Channel, + ProductIDs: []string{channelToSubscribe.Currency.String()}, + }, + }, + } + return b.wsSend(subscribe) +} + +// Unsubscribe sends a websocket message to stop receiving data from the channel +func (b *BTSE) Unsubscribe(channelToSubscribe exchange.WebsocketChannelSubscription) error { + subscribe := websocketSubscribe{ + Type: "unsubscribe", + Channels: []websocketChannel{ + { + Name: channelToSubscribe.Channel, + ProductIDs: []string{channelToSubscribe.Currency.String()}, + }, + }, + } + return b.wsSend(subscribe) +} + +// WsSend sends data to the websocket server +func (b *BTSE) wsSend(data interface{}) error { + b.wsRequestMtx.Lock() + defer b.wsRequestMtx.Unlock() + if b.Verbose { + log.Debugf("%v sending message to websocket %v", b.Name, data) + } + json, err := common.JSONEncode(data) + if err != nil { + return err + } + return b.WebsocketConn.WriteMessage(websocket.TextMessage, json) +} diff --git a/exchanges/btse/btse_wrapper.go b/exchanges/btse/btse_wrapper.go index 5e7d3963..3cd625e0 100644 --- a/exchanges/btse/btse_wrapper.go +++ b/exchanges/btse/btse_wrapper.go @@ -358,3 +358,17 @@ func (b *BTSE) GetFeeByType(feeBuilder *exchange.FeeBuilder) (float64, error) { } return b.GetFee(feeBuilder) } + +// SubscribeToWebsocketChannels appends to ChannelsToSubscribe +// which lets websocket.manageSubscriptions handle subscribing +func (b *BTSE) SubscribeToWebsocketChannels(channels []exchange.WebsocketChannelSubscription) error { + b.Websocket.SubscribeToChannels(channels) + return nil +} + +// UnsubscribeToWebsocketChannels removes from ChannelsToSubscribe +// which lets websocket.manageSubscriptions handle unsubscribing +func (b *BTSE) UnsubscribeToWebsocketChannels(channels []exchange.WebsocketChannelSubscription) error { + b.Websocket.UnsubscribeToChannels(channels) + return nil +} diff --git a/exchanges/coinbasepro/coinbasepro.go b/exchanges/coinbasepro/coinbasepro.go index e6518d77..fb557de4 100644 --- a/exchanges/coinbasepro/coinbasepro.go +++ b/exchanges/coinbasepro/coinbasepro.go @@ -8,6 +8,7 @@ import ( "net/url" "strconv" "strings" + "sync" "time" "github.com/gorilla/websocket" @@ -61,6 +62,7 @@ const ( type CoinbasePro struct { exchange.Base WebsocketConn *websocket.Conn + wsRequestMtx sync.Mutex } // SetDefaults sets default values for the exchange @@ -88,7 +90,9 @@ func (c *CoinbasePro) SetDefaults() { c.APIUrl = c.APIUrlDefault c.WebsocketInit() c.Websocket.Functionality = exchange.WebsocketTickerSupported | - exchange.WebsocketOrderbookSupported + exchange.WebsocketOrderbookSupported | + exchange.WebsocketSubscribeSupported | + exchange.WebsocketUnsubscribeSupported } // Setup initialises the exchange parameters with the current configuration @@ -132,8 +136,11 @@ func (c *CoinbasePro) Setup(exch *config.ExchangeConfig) { log.Fatal(err) } err = c.WebsocketSetup(c.WsConnect, + c.Subscribe, + c.Unsubscribe, exch.Name, exch.Websocket, + exch.Verbose, coinbaseproWebsocketURL, exch.WebsocketURL) if err != nil { diff --git a/exchanges/coinbasepro/coinbasepro_websocket.go b/exchanges/coinbasepro/coinbasepro_websocket.go index af1adcc9..a773d928 100644 --- a/exchanges/coinbasepro/coinbasepro_websocket.go +++ b/exchanges/coinbasepro/coinbasepro_websocket.go @@ -13,46 +13,13 @@ import ( "github.com/thrasher-/gocryptotrader/currency" exchange "github.com/thrasher-/gocryptotrader/exchanges" "github.com/thrasher-/gocryptotrader/exchanges/orderbook" + log "github.com/thrasher-/gocryptotrader/logger" ) const ( coinbaseproWebsocketURL = "wss://ws-feed.pro.coinbase.com" ) -// WebsocketSubscriber subscribes to websocket channels with respect to enabled -// currencies -func (c *CoinbasePro) WebsocketSubscriber() error { - var currencies []string - for _, x := range c.EnabledPairs.Strings() { - currency := x[0:3] + "-" + x[3:] - currencies = append(currencies, currency) - } - - var channels = []WsChannels{ - { - Name: "heartbeat", - ProductIDs: currencies, - }, - { - Name: "ticker", - ProductIDs: currencies, - }, - { - Name: "level2", - ProductIDs: currencies, - }, - } - - subscribe := WebsocketSubscribe{Type: "subscribe", Channels: channels} - - data, err := common.JSONEncode(subscribe) - if err != nil { - return err - } - - return c.WebsocketConn.WriteMessage(websocket.TextMessage, data) -} - // WsConnect initiates a websocket connection func (c *CoinbasePro) WsConnect() error { if !c.Websocket.IsEnabled() || !c.IsEnabled() { @@ -79,11 +46,7 @@ func (c *CoinbasePro) WsConnect() error { err) } - err = c.WebsocketSubscriber() - if err != nil { - return err - } - + c.GenerateDefaultSubscriptions() go c.WsHandleData() return nil @@ -95,7 +58,6 @@ func (c *CoinbasePro) WsReadData() (exchange.WebsocketResponse, error) { if err != nil { return exchange.WebsocketResponse{}, err } - c.Websocket.TrafficAlert <- struct{}{} return exchange.WebsocketResponse{Raw: resp}, nil } @@ -105,11 +67,6 @@ func (c *CoinbasePro) WsHandleData() { c.Websocket.Wg.Add(1) defer func() { - err := c.WebsocketConn.Close() - if err != nil { - c.Websocket.DataHandler <- fmt.Errorf("coinbasepro_websocket.go - Unable to to close Websocket connection. Error: %s", - err) - } c.Websocket.Wg.Done() }() @@ -117,14 +74,12 @@ func (c *CoinbasePro) WsHandleData() { select { case <-c.Websocket.ShutdownC: return - default: resp, err := c.WsReadData() if err != nil { c.Websocket.DataHandler <- err return } - type MsgType struct { Type string `json:"type"` Sequence int64 `json:"sequence"` @@ -283,3 +238,66 @@ func (c *CoinbasePro) ProcessUpdate(update WebsocketL2Update) error { return nil } + +// GenerateDefaultSubscriptions Adds default subscriptions to websocket to be handled by ManageSubscriptions() +func (c *CoinbasePro) GenerateDefaultSubscriptions() { + var channels = []string{"heartbeat", "level2", "ticker"} + enabledCurrencies := c.GetEnabledCurrencies() + subscriptions := []exchange.WebsocketChannelSubscription{} + for i := range channels { + for j := range enabledCurrencies { + enabledCurrencies[j].Delimiter = "-" + subscriptions = append(subscriptions, exchange.WebsocketChannelSubscription{ + Channel: channels[i], + Currency: enabledCurrencies[j], + }) + } + } + c.Websocket.SubscribeToChannels(subscriptions) +} + +// Subscribe sends a websocket message to receive data from the channel +func (c *CoinbasePro) Subscribe(channelToSubscribe exchange.WebsocketChannelSubscription) error { + subscribe := WebsocketSubscribe{ + Type: "subscribe", + Channels: []WsChannels{ + { + Name: channelToSubscribe.Channel, + ProductIDs: []string{ + channelToSubscribe.Currency.String(), + }, + }, + }, + } + return c.wsSend(subscribe) +} + +// Unsubscribe sends a websocket message to stop receiving data from the channel +func (c *CoinbasePro) Unsubscribe(channelToSubscribe exchange.WebsocketChannelSubscription) error { + subscribe := WebsocketSubscribe{ + Type: "unsubscribe", + Channels: []WsChannels{ + { + Name: channelToSubscribe.Channel, + ProductIDs: []string{ + channelToSubscribe.Currency.String(), + }, + }, + }, + } + return c.wsSend(subscribe) +} + +// WsSend sends data to the websocket server +func (c *CoinbasePro) wsSend(data interface{}) error { + c.wsRequestMtx.Lock() + defer c.wsRequestMtx.Unlock() + if c.Verbose { + log.Debugf("%v sending message to websocket %v", c.Name, data) + } + json, err := common.JSONEncode(data) + if err != nil { + return err + } + return c.WebsocketConn.WriteMessage(websocket.TextMessage, json) +} diff --git a/exchanges/coinbasepro/coinbasepro_wrapper.go b/exchanges/coinbasepro/coinbasepro_wrapper.go index baf3d83e..62e1883d 100644 --- a/exchanges/coinbasepro/coinbasepro_wrapper.go +++ b/exchanges/coinbasepro/coinbasepro_wrapper.go @@ -382,3 +382,17 @@ func (c *CoinbasePro) GetOrderHistory(getOrdersRequest *exchange.GetOrdersReques return orders, nil } + +// SubscribeToWebsocketChannels appends to ChannelsToSubscribe +// which lets websocket.manageSubscriptions handle subscribing +func (c *CoinbasePro) SubscribeToWebsocketChannels(channels []exchange.WebsocketChannelSubscription) error { + c.Websocket.SubscribeToChannels(channels) + return nil +} + +// UnsubscribeToWebsocketChannels removes from ChannelsToSubscribe +// which lets websocket.manageSubscriptions handle unsubscribing +func (c *CoinbasePro) UnsubscribeToWebsocketChannels(channels []exchange.WebsocketChannelSubscription) error { + c.Websocket.UnsubscribeToChannels(channels) + return nil +} diff --git a/exchanges/coinut/coinut.go b/exchanges/coinut/coinut.go index c67612f8..70d91c00 100644 --- a/exchanges/coinut/coinut.go +++ b/exchanges/coinut/coinut.go @@ -6,6 +6,7 @@ import ( "errors" "fmt" "net/http" + "sync" "time" "github.com/gorilla/websocket" @@ -48,6 +49,7 @@ type COINUT struct { exchange.Base WebsocketConn *websocket.Conn InstrumentMap map[string]int + wsRequestMtx sync.Mutex } // SetDefaults sets current default values @@ -77,7 +79,9 @@ func (c *COINUT) SetDefaults() { c.WebsocketInit() c.Websocket.Functionality = exchange.WebsocketTickerSupported | exchange.WebsocketOrderbookSupported | - exchange.WebsocketTradeDataSupported + exchange.WebsocketTradeDataSupported | + exchange.WebsocketSubscribeSupported | + exchange.WebsocketUnsubscribeSupported } // Setup sets the current exchange configuration @@ -118,8 +122,11 @@ func (c *COINUT) Setup(exch *config.ExchangeConfig) { log.Fatal(err) } err = c.WebsocketSetup(c.WsConnect, + c.Subscribe, + c.Unsubscribe, exch.Name, exch.Websocket, + exch.Verbose, coinutWebsocketURL, exch.WebsocketURL) if err != nil { diff --git a/exchanges/coinut/coinut_websocket.go b/exchanges/coinut/coinut_websocket.go index 432b97b2..22218d70 100644 --- a/exchanges/coinut/coinut_websocket.go +++ b/exchanges/coinut/coinut_websocket.go @@ -2,7 +2,6 @@ package coinut import ( "errors" - "fmt" "net/http" "net/url" "time" @@ -12,9 +11,11 @@ import ( "github.com/thrasher-/gocryptotrader/currency" exchange "github.com/thrasher-/gocryptotrader/exchanges" "github.com/thrasher-/gocryptotrader/exchanges/orderbook" + log "github.com/thrasher-/gocryptotrader/logger" ) const coinutWebsocketURL = "wss://wsapi.coinut.com" +const coinutWebsocketRateLimit = 30 * time.Millisecond var nNonce map[int64]string var channels map[string]chan []byte @@ -43,11 +44,6 @@ func (c *COINUT) WsHandleData() { c.Websocket.Wg.Add(1) defer func() { - err := c.WebsocketConn.Close() - if err != nil { - c.Websocket.DataHandler <- fmt.Errorf("coinut_websocket.go - Unable to to close Websocket connection. Error: %s", - err) - } c.Websocket.Wg.Done() }() @@ -69,7 +65,6 @@ func (c *COINUT) WsHandleData() { c.Websocket.DataHandler <- err continue } - switch incoming.Reply { case "hb": channels["hb"] <- resp.Raw @@ -203,10 +198,7 @@ func (c *COINUT) WsConnect() error { populatedList = true } - err = c.WsSubscribe() - if err != nil { - return err - } + c.GenerateDefaultSubscriptions() // define bi-directional communication channels = make(map[string]chan []byte) @@ -230,17 +222,11 @@ func (c *COINUT) GetNonce() int64 { // WsSetInstrumentList fetches instrument list and propagates a local cache func (c *COINUT) WsSetInstrumentList() error { - req, err := common.JSONEncode(wsRequest{ + err := c.wsSend(wsRequest{ Request: "inst_list", SecType: "SPOT", Nonce: c.GetNonce(), }) - - if err != nil { - return err - } - - err = c.WebsocketConn.WriteMessage(websocket.TextMessage, req) if err != nil { return err } @@ -270,48 +256,6 @@ func (c *COINUT) WsSetInstrumentList() error { return nil } -// WsSubscribe subscribes to websocket streams -func (c *COINUT) WsSubscribe() error { - pairs := c.GetEnabledCurrencies() - - for _, p := range pairs { - ticker := wsRequest{ - Request: "inst_tick", - InstID: instrumentListByString[p.String()], - Subscribe: true, - Nonce: c.GetNonce(), - } - - tickjson, err := common.JSONEncode(ticker) - if err != nil { - return err - } - - err = c.WebsocketConn.WriteMessage(websocket.TextMessage, tickjson) - if err != nil { - return err - } - - ob := wsRequest{ - Request: "inst_order_book", - InstID: instrumentListByString[p.String()], - Subscribe: true, - Nonce: c.GetNonce(), - } - - objson, err := common.JSONEncode(ob) - if err != nil { - return err - } - - err = c.WebsocketConn.WriteMessage(websocket.TextMessage, objson) - if err != nil { - return err - } - } - return nil -} - // WsProcessOrderbookSnapshot processes the orderbook snapshot func (c *COINUT) WsProcessOrderbookSnapshot(ob *WsOrderbookSnapshot) error { var bids []orderbook.Item @@ -361,3 +305,58 @@ func (c *COINUT) WsProcessOrderbookUpdate(ob *WsOrderbookUpdate) error { c.GetName(), "SPOT") } + +// GenerateDefaultSubscriptions Adds default subscriptions to websocket to be handled by ManageSubscriptions() +func (c *COINUT) GenerateDefaultSubscriptions() { + var channels = []string{"inst_tick", "inst_order_book"} + subscriptions := []exchange.WebsocketChannelSubscription{} + enabledCurrencies := c.GetEnabledCurrencies() + for i := range channels { + for j := range enabledCurrencies { + subscriptions = append(subscriptions, exchange.WebsocketChannelSubscription{ + Channel: channels[i], + Currency: enabledCurrencies[j], + }) + } + } + c.Websocket.SubscribeToChannels(subscriptions) +} + +// Subscribe sends a websocket message to receive data from the channel +func (c *COINUT) Subscribe(channelToSubscribe exchange.WebsocketChannelSubscription) error { + subscribe := wsRequest{ + Request: channelToSubscribe.Channel, + InstID: instrumentListByString[channelToSubscribe.Currency.String()], + Subscribe: true, + Nonce: c.GetNonce(), + } + return c.wsSend(subscribe) +} + +// Unsubscribe sends a websocket message to stop receiving data from the channel +func (c *COINUT) Unsubscribe(channelToSubscribe exchange.WebsocketChannelSubscription) error { + subscribe := wsRequest{ + Request: channelToSubscribe.Channel, + InstID: instrumentListByString[channelToSubscribe.Currency.String()], + Subscribe: false, + Nonce: c.GetNonce(), + } + return c.wsSend(subscribe) +} + +// WsSend sends data to the websocket server +func (c *COINUT) wsSend(data interface{}) error { + c.wsRequestMtx.Lock() + defer c.wsRequestMtx.Unlock() + + json, err := common.JSONEncode(data) + if err != nil { + return err + } + if c.Verbose { + log.Debugf("%v sending message to websocket %v", c.Name, string(json)) + } + // Basic rate limiter + time.Sleep(coinutWebsocketRateLimit) + return c.WebsocketConn.WriteMessage(websocket.TextMessage, json) +} diff --git a/exchanges/coinut/coinut_wrapper.go b/exchanges/coinut/coinut_wrapper.go index 916ae934..5807adbe 100644 --- a/exchanges/coinut/coinut_wrapper.go +++ b/exchanges/coinut/coinut_wrapper.go @@ -503,3 +503,17 @@ func (c *COINUT) GetOrderHistory(getOrdersRequest *exchange.GetOrdersRequest) ([ return orders, nil } + +// SubscribeToWebsocketChannels appends to ChannelsToSubscribe +// which lets websocket.manageSubscriptions handle subscribing +func (c *COINUT) SubscribeToWebsocketChannels(channels []exchange.WebsocketChannelSubscription) error { + c.Websocket.SubscribeToChannels(channels) + return nil +} + +// UnsubscribeToWebsocketChannels removes from ChannelsToSubscribe +// which lets websocket.manageSubscriptions handle unsubscribing +func (c *COINUT) UnsubscribeToWebsocketChannels(channels []exchange.WebsocketChannelSubscription) error { + c.Websocket.UnsubscribeToChannels(channels) + return nil +} diff --git a/exchanges/exchange.go b/exchanges/exchange.go index 4902bf5c..a163c8dd 100644 --- a/exchanges/exchange.go +++ b/exchanges/exchange.go @@ -323,6 +323,8 @@ type IBotExchange interface { WithdrawFiatFunds(withdrawRequest *WithdrawRequest) (string, error) WithdrawFiatFundsToInternationalBank(withdrawRequest *WithdrawRequest) (string, error) GetWebsocket() (*Websocket, error) + SubscribeToWebsocketChannels(channels []WebsocketChannelSubscription) error + UnsubscribeToWebsocketChannels(channels []WebsocketChannelSubscription) error } // SupportsRESTTickerBatchUpdates returns whether or not the diff --git a/exchanges/exchange_websocket.go b/exchanges/exchange_websocket.go index 57d73650..63ba1c7e 100644 --- a/exchanges/exchange_websocket.go +++ b/exchanges/exchange_websocket.go @@ -10,37 +10,7 @@ import ( "github.com/thrasher-/gocryptotrader/config" "github.com/thrasher-/gocryptotrader/currency" "github.com/thrasher-/gocryptotrader/exchanges/orderbook" -) - -// Websocket functionality list and state consts -const ( - NoWebsocketSupport uint32 = 0 - WebsocketTickerSupported uint32 = 1 << (iota - 1) - WebsocketOrderbookSupported - WebsocketKlineSupported - WebsocketTradeDataSupported - WebsocketAccountSupported - WebsocketAllowsRequests - - WebsocketTickerSupportedText = "TICKER STREAMING SUPPORTED" - WebsocketOrderbookSupportedText = "ORDERBOOK STREAMING SUPPORTED" - WebsocketKlineSupportedText = "KLINE STREAMING SUPPORTED" - WebsocketTradeDataSupportedText = "TRADE STREAMING SUPPORTED" - WebsocketAccountSupportedText = "ACCOUNT STREAMING SUPPORTED" - WebsocketAllowsRequestsText = "WEBSOCKET REQUESTS SUPPORTED" - NoWebsocketSupportText = "WEBSOCKET NOT SUPPORTED" - UnknownWebsocketFunctionality = "UNKNOWN FUNCTIONALITY BITMASK" - - // WebsocketNotEnabled alerts of a disabled websocket - WebsocketNotEnabled = "exchange_websocket_not_enabled" - // WebsocketTrafficLimitTime defines a standard time for no traffic from the - // websocket connection - WebsocketTrafficLimitTime = 5 * time.Second - // WebsocketStateTimeout defines a const for when a websocket connection - // times out, will be handled by the routine management system - WebsocketStateTimeout = "TIMEOUT" - - websocketRestablishConnection = 1 * time.Second + log "github.com/thrasher-/gocryptotrader/logger" ) // WebsocketInit initialises the websocket struct @@ -56,8 +26,11 @@ func (e *Base) WebsocketInit() { // WebsocketSetup sets main variables for websocket connection func (e *Base) WebsocketSetup(connector func() error, + subscriber func(channelToSubscribe WebsocketChannelSubscription) error, + unsubscriber func(channelToUnsubscribe WebsocketChannelSubscription) error, exchangeName string, - wsEnabled bool, + wsEnabled, + verbose bool, defaultURL, runningURL string) error { @@ -65,118 +38,26 @@ func (e *Base) WebsocketSetup(connector func() error, e.Websocket.Connected = make(chan struct{}, 1) e.Websocket.Disconnected = make(chan struct{}, 1) e.Websocket.TrafficAlert = make(chan struct{}, 1) + e.Websocket.verbose = verbose + e.Websocket.SetChannelSubscriber(subscriber) + e.Websocket.SetChannelUnsubscriber(unsubscriber) err := e.Websocket.SetWsStatusAndConnection(wsEnabled) if err != nil { return err } - e.Websocket.SetDefaultURL(defaultURL) e.Websocket.SetConnector(connector) e.Websocket.SetWebsocketURL(runningURL) e.Websocket.SetExchangeName(exchangeName) e.Websocket.init = false + e.Websocket.noConnectionCheckLimit = 5 + e.Websocket.reconnectionLimit = 10 return nil } -// Websocket defines a return type for websocket connections via the interface -// wrapper for routine processing in routines.go -type Websocket struct { - proxyAddr string - defaultURL string - runningURL string - exchangeName string - enabled bool - init bool - connected bool - connector func() error - m sync.Mutex - - // Connected denotes a channel switch for diversion of request flow - Connected chan struct{} - - // Disconnected denotes a channel switch for diversion of request flow - Disconnected chan struct{} - - // DataHandler pipes websocket data to an exchange websocket data handler - DataHandler chan interface{} - - // ShutdownC is the main shutdown channel used within an exchange package - // called by its own defined Shutdown function - ShutdownC chan struct{} - - // Orderbook is a local cache of orderbooks - Orderbook WebsocketOrderbookLocal - - // Wg defines a wait group for websocket routines for cleanly shutting down - // routines - Wg sync.WaitGroup - - // TrafficAlert monitors if there is a halt in traffic throughput - TrafficAlert chan struct{} - - // Functionality defines websocket stream capabilities - Functionality uint32 -} - -// trafficMonitor monitors traffic and switches connection modes for websocket -func (w *Websocket) trafficMonitor(wg *sync.WaitGroup) { - w.Wg.Add(1) - wg.Done() // Makes sure we are unlocking after we add to waitgroup - - defer func() { - if w.connected { - w.Disconnected <- struct{}{} - } - w.Wg.Done() - }() - - // Define an initial traffic timer which will be a delay then fall over to - // WebsocketTrafficLimitTime after first response - trafficTimer := time.NewTimer(5 * time.Second) - - for { - select { - case <-w.ShutdownC: // Returns on shutdown channel close - return - case <-w.TrafficAlert: // Resets timer on traffic - if !w.connected { - w.Connected <- struct{}{} - w.connected = true - } - - trafficTimer.Reset(WebsocketTrafficLimitTime) - - case <-trafficTimer.C: // Falls through when timer runs out - newtimer := time.NewTimer(10 * time.Second) // New secondary timer set - if w.connected { - // If connected divert traffic to rest - w.Disconnected <- struct{}{} - w.connected = false - } - - select { - case <-w.ShutdownC: // Returns on shutdown channel close - return - - case <-newtimer.C: // If secondary timer runs state timeout is sent to the data handler - w.DataHandler <- WebsocketStateTimeout - return - - case <-w.TrafficAlert: // If in this time response traffic comes through - trafficTimer.Reset(WebsocketTrafficLimitTime) - if !w.connected { - // If not connected divert traffic from REST to websocket - w.Connected <- struct{}{} - w.connected = true - } - } - } - } -} - // Connect intiates a websocket connection by using a package defined connection // function func (w *Websocket) Connect() error { @@ -188,13 +69,15 @@ func (w *Websocket) Connect() error { } if w.connected { + w.connecting = false return errors.New("exchange_websocket.go error - already connected, cannot connect again") } + w.connecting = true w.ShutdownC = make(chan struct{}, 1) - err := w.connector() if err != nil { + w.connecting = false return fmt.Errorf("exchange_websocket.go connection error %s", err) } @@ -202,41 +85,130 @@ func (w *Websocket) Connect() error { if !w.connected { w.Connected <- struct{}{} w.connected = true + w.connecting = false } var anotherWG sync.WaitGroup anotherWG.Add(1) go w.trafficMonitor(&anotherWG) anotherWG.Wait() + if !w.connectionMonitorRunning { + go w.wsConnectionMonitor() + } + go w.manageSubscriptions() return nil } +// WsConnectionMonitor ensures that the WS keeps connecting +func (w *Websocket) wsConnectionMonitor() { + w.m.Lock() + w.connectionMonitorRunning = true + w.m.Unlock() + defer func() { + w.connectionMonitorRunning = false + }() + + for { + time.Sleep(connectionMonitorDelay) + w.m.Lock() + if !w.enabled { + w.m.Unlock() + w.DataHandler <- fmt.Errorf("%v WsConnectionMonitor: websocket disabled, shutting down", w.exchangeName) + err := w.Shutdown() + if err != nil { + log.Error(err) + } + if w.verbose { + log.Debugf("%v WsConnectionMonitor exiting", w.exchangeName) + } + return + } + w.m.Unlock() + err := w.checkConnection() + if err != nil { + log.Error(err) + } + } +} + +// checkConnection ensures the connection is maintained +// Will reconnect on disconnect +func (w *Websocket) checkConnection() error { + if w.verbose { + log.Debugf("%v checking connection", w.exchangeName) + } + switch { + case !w.IsConnected() && !w.IsConnecting(): + w.m.Lock() + defer w.m.Unlock() + if w.verbose { + log.Debugf("%v no connection. Attempt %v/%v", w.exchangeName, w.noConnectionChecks, w.noConnectionCheckLimit) + } + if w.noConnectionChecks >= w.noConnectionCheckLimit { + if w.verbose { + log.Debugf("%v resetting connection", w.exchangeName) + } + w.connecting = true + go w.WebsocketReset() + w.noConnectionChecks = 0 + } + w.noConnectionChecks++ + case w.IsConnecting(): + if w.reconnectionChecks >= w.reconnectionLimit { + return fmt.Errorf("%v websocket failed to reconnect after %v seconds", + w.exchangeName, + w.reconnectionLimit*int(connectionMonitorDelay.Seconds())) + } + if w.verbose { + log.Debugf("%v Busy reconnecting", w.exchangeName) + } + w.reconnectionChecks++ + default: + w.noConnectionChecks = 0 + w.reconnectionChecks = 0 + } + return nil +} + // IsConnected exposes websocket connection status func (w *Websocket) IsConnected() bool { + w.m.Lock() + defer w.m.Unlock() return w.connected } +// IsConnecting checks whether websocket is busy connecting +func (w *Websocket) IsConnecting() bool { + w.m.Lock() + defer w.m.Unlock() + return w.connecting +} + // Shutdown attempts to shut down a websocket connection and associated routines // by using a package defined shutdown function func (w *Websocket) Shutdown() error { w.m.Lock() - defer func() { w.Orderbook.FlushCache() w.m.Unlock() }() - - if !w.connected { - return errors.New("exchange_websocket.go error - System not connected to shut down") + if !w.connected && w.ShutdownC == nil { + return fmt.Errorf("%v cannot shutdown a disconnected websocket", w.exchangeName) } - timer := time.NewTimer(5 * time.Second) + if w.verbose { + log.Debugf("%v shutting down websocket channels", w.exchangeName) + } + timer := time.NewTimer(15 * time.Second) c := make(chan struct{}, 1) go func(c chan struct{}) { close(w.ShutdownC) w.Wg.Wait() + if w.verbose { + log.Debugf("%v completed websocket channel shutdown", w.exchangeName) + } c <- struct{}{} }(c) @@ -245,11 +217,105 @@ func (w *Websocket) Shutdown() error { w.connected = false return nil case <-timer.C: - return fmt.Errorf("%s - Websocket routines failed to shutdown", + return fmt.Errorf("%s websocket routines failed to shutdown after 15 seconds", w.GetName()) } } +// WebsocketReset sends the shutdown command, waits for channel/func closure and then reconnects +func (w *Websocket) WebsocketReset() error { + err := w.Shutdown() + if err != nil { + // does not return here to allow connection to be made if already shut down + log.Errorf("%v shutdown error: %v", w.exchangeName, err) + } + log.Infof("%v reconnecting to websocket", w.exchangeName) + w.m.Lock() + w.init = true + w.m.Unlock() + err = w.Connect() + if err != nil { + log.Errorf("%v connection error: %v", w.exchangeName, err) + } + return err +} + +// trafficMonitor monitors traffic and switches connection modes for websocket +func (w *Websocket) trafficMonitor(wg *sync.WaitGroup) { + w.Wg.Add(1) + wg.Done() // Makes sure we are unlocking after we add to waitgroup + defer func() { + if w.connected { + w.Disconnected <- struct{}{} + } + w.Wg.Done() + }() + + // Define an initial traffic timer which will be a delay then fall over to + // WebsocketTrafficLimitTime after first response + trafficTimer := time.NewTimer(5 * time.Second) + for { + select { + case <-w.ShutdownC: // Returns on shutdown channel close + if w.verbose { + log.Debugf("%v trafficMonitor shutdown message received", w.exchangeName) + } + return + case <-w.TrafficAlert: // Resets timer on traffic + w.m.Lock() + if !w.connected { + w.Connected <- struct{}{} + w.connected = true + } + w.m.Unlock() + if w.verbose { + log.Debugf("%v received a traffic alert", w.exchangeName) + } + trafficTimer.Reset(WebsocketTrafficLimitTime) + case <-trafficTimer.C: // Falls through when timer runs out + newtimer := time.NewTimer(10 * time.Second) // New secondary timer set + if w.verbose { + log.Debugf("%v has not received a traffic alert in 5 seconds.", w.exchangeName) + } + w.m.Lock() + if w.connected { + // If connected divert traffic to rest + w.Disconnected <- struct{}{} + w.connected = false + } + w.m.Unlock() + + select { + case <-w.ShutdownC: // Returns on shutdown channel close + w.m.Lock() + w.connected = false + w.m.Unlock() + return + + case <-newtimer.C: // If secondary timer runs state timeout is sent to the data handler + if w.verbose { + log.Debugf("%v has not received a traffic alert in 15 seconds, exiting", w.exchangeName) + } + w.DataHandler <- fmt.Errorf("trafficMonitor %v", WebsocketStateTimeout) + return + + case <-w.TrafficAlert: // If in this time response traffic comes through + trafficTimer.Reset(WebsocketTrafficLimitTime) + w.m.Lock() + if !w.connected { + // If not connected dive rt traffic from REST to websocket + w.Connected <- struct{}{} + if w.verbose { + log.Debugf("%v has received a traffic alert. Setting status to connected", w.exchangeName) + } + w.connected = true + } + w.m.Unlock() + } + } + } +} + // SetWebsocketURL sets websocket URL func (w *Websocket) SetWebsocketURL(websocketURL string) { if websocketURL == "" || websocketURL == config.WebsocketURLNonDefaultMessage { @@ -267,29 +333,36 @@ func (w *Websocket) GetWebsocketURL() string { // SetWsStatusAndConnection sets if websocket is enabled // it will also connect/disconnect the websocket connection func (w *Websocket) SetWsStatusAndConnection(enabled bool) error { + w.m.Lock() if w.enabled == enabled { if w.init { + w.m.Unlock() return nil } + w.m.Unlock() return fmt.Errorf("exchange_websocket.go error - already set as %t", enabled) } - w.enabled = enabled if !w.init { if enabled { if w.connected { + w.m.Unlock() return nil } + w.m.Unlock() return w.Connect() } if !w.connected { + w.m.Unlock() return nil } + w.m.Unlock() return w.Shutdown() } + w.m.Unlock() return nil } @@ -305,14 +378,12 @@ func (w *Websocket) SetProxyAddress(proxyAddr string) error { } w.proxyAddr = proxyAddr - if !w.init && w.enabled { if w.connected { err := w.Shutdown() if err != nil { return err } - return w.Connect() } return w.Connect() } @@ -349,14 +420,6 @@ func (w *Websocket) GetName() string { return w.exchangeName } -// WebsocketOrderbookLocal defines a local cache of orderbooks for amending, -// appending and deleting changes and updates the main store in orderbook.go -type WebsocketOrderbookLocal struct { - ob []*orderbook.Base - lastUpdated time.Time - m sync.Mutex -} - // Update updates a local cache using bid targets and ask targets then updates // main cache in orderbook.go // Volume == 0; deletion at price target @@ -567,70 +630,6 @@ func (w *WebsocketOrderbookLocal) FlushCache() { w.m.Unlock() } -// WebsocketResponse defines generalised data from the websocket connection -type WebsocketResponse struct { - Type int - Raw []byte -} - -// WebsocketOrderbookUpdate defines a websocket event in which the orderbook -// has been updated in the orderbook package -type WebsocketOrderbookUpdate struct { - Pair currency.Pair - Asset string - Exchange string -} - -// TradeData defines trade data -type TradeData struct { - Timestamp time.Time - CurrencyPair currency.Pair - AssetType string - Exchange string - EventType string - EventTime int64 - Price float64 - Amount float64 - Side string -} - -// TickerData defines ticker feed -type TickerData struct { - Timestamp time.Time - Pair currency.Pair - AssetType string - Exchange string - ClosePrice float64 - Quantity float64 - OpenPrice float64 - HighPrice float64 - LowPrice float64 -} - -// KlineData defines kline feed -type KlineData struct { - Timestamp time.Time - Pair currency.Pair - AssetType string - Exchange string - StartTime time.Time - CloseTime time.Time - Interval string - OpenPrice float64 - ClosePrice float64 - HighPrice float64 - LowPrice float64 - Volume float64 -} - -// WebsocketPositionUpdated reflects a change in orders/contracts on an exchange -type WebsocketPositionUpdated struct { - Timestamp time.Time - Pair currency.Pair - AssetType string - Exchange string -} - // GetFunctionality returns a functionality bitmask for the websocket // connection func (w *Websocket) GetFunctionality() uint32 { @@ -668,6 +667,12 @@ func (w *Websocket) FormatFunctionality() string { case WebsocketAllowsRequests: functionality = append(functionality, WebsocketAllowsRequestsText) + case WebsocketSubscribeSupported: + functionality = append(functionality, WebsocketSubscribeSupportedText) + + case WebsocketUnsubscribeSupported: + functionality = append(functionality, WebsocketUnsubscribeSupportedText) + default: functionality = append(functionality, fmt.Sprintf("%s[1<<%v]", UnknownWebsocketFunctionality, i)) @@ -681,3 +686,172 @@ func (w *Websocket) FormatFunctionality() string { return NoWebsocketSupportText } + +// SetChannelSubscriber sets the function to use the base subscribe func +func (w *Websocket) SetChannelSubscriber(subscriber func(channelToSubscribe WebsocketChannelSubscription) error) { + w.channelSubscriber = subscriber +} + +// SetChannelUnsubscriber sets the function to use the base unsubscribe func +func (w *Websocket) SetChannelUnsubscriber(unsubscriber func(channelToUnsubscribe WebsocketChannelSubscription) error) { + w.channelUnsubscriber = unsubscriber +} + +// ManageSubscriptions ensures the subscriptions specified continue to be subscribed to +func (w *Websocket) manageSubscriptions() error { + if !w.SupportsFunctionality(WebsocketSubscribeSupported) && !w.SupportsFunctionality(WebsocketUnsubscribeSupported) { + return fmt.Errorf("%v does not support channel subscriptions, exiting ManageSubscriptions()", w.exchangeName) + } + w.Wg.Add(1) + defer func() { + if w.verbose { + log.Debugf("%v ManageSubscriptions exiting", w.exchangeName) + } + w.Wg.Done() + }() + for { + select { + case <-w.ShutdownC: + w.subscribedChannels = []WebsocketChannelSubscription{} + if w.verbose { + log.Debugf("%v shutdown manageSubscriptions", w.exchangeName) + } + return nil + default: + time.Sleep(manageSubscriptionsDelay) + if w.verbose { + log.Debugf("%v checking subscriptions", w.exchangeName) + } + // Subscribe to channels Pending a subscription + if w.SupportsFunctionality(WebsocketSubscribeSupported) { + err := w.subscribeToChannels() + if err != nil { + w.DataHandler <- err + } + } + if w.SupportsFunctionality(WebsocketUnsubscribeSupported) { + err := w.unsubscribeToChannels() + if err != nil { + w.DataHandler <- err + } + } + } + } +} + +// subscribeToChannels compares channelsToSubscribe to subscribedChannels +// and subscribes to any channels not present in subscribedChannels +func (w *Websocket) subscribeToChannels() error { + w.subscriptionLock.Lock() + defer w.subscriptionLock.Unlock() + for i := 0; i < len(w.channelsToSubscribe); i++ { + channelIsSubscribed := false + for j := 0; j < len(w.subscribedChannels); j++ { + if w.subscribedChannels[j].Equal(&w.channelsToSubscribe[i]) { + channelIsSubscribed = true + break + } + } + if !channelIsSubscribed { + if w.verbose { + log.Debugf("%v Subscribing to %v %v", w.exchangeName, w.channelsToSubscribe[i].Channel, w.channelsToSubscribe[i].Currency.String()) + } + err := w.channelSubscriber(w.channelsToSubscribe[i]) + if err != nil { + return err + } + w.subscribedChannels = append(w.subscribedChannels, w.channelsToSubscribe[i]) + } + } + return nil +} + +// unsubscribeToChannels compares subscribedChannels to channelsToSubscribe +// and unsubscribes to any channels not present in channelsToSubscribe +func (w *Websocket) unsubscribeToChannels() error { + w.subscriptionLock.Lock() + defer w.subscriptionLock.Unlock() + for i := 0; i < len(w.subscribedChannels); i++ { + subscriptionFound := false + for j := 0; j < len(w.channelsToSubscribe); j++ { + if w.channelsToSubscribe[j].Equal(&w.subscribedChannels[i]) { + subscriptionFound = true + break + } + } + if !subscriptionFound { + err := w.channelUnsubscriber(w.subscribedChannels[i]) + if err != nil { + return err + } + } + } + // Now that the slices should match, assign rather than looping and appending the differences + w.subscribedChannels = append(w.channelsToSubscribe[:0:0], w.channelsToSubscribe...) //nolint:gocritic + + return nil +} + +// removeChannelToSubscribe removes an entry from w.channelsToSubscribe +// so an unsubscribe event can be triggered +func (w *Websocket) removeChannelToSubscribe(subscribedChannel WebsocketChannelSubscription) { + w.subscriptionLock.Lock() + defer w.subscriptionLock.Unlock() + channelLength := len(w.channelsToSubscribe) + i := 0 + for j := 0; j < len(w.channelsToSubscribe); j++ { + if !w.channelsToSubscribe[j].Equal(&subscribedChannel) { + w.channelsToSubscribe[i] = w.channelsToSubscribe[j] + i++ + } + } + w.channelsToSubscribe = w.channelsToSubscribe[:i] + if channelLength == len(w.channelsToSubscribe) { + w.DataHandler <- fmt.Errorf("%v removeChannelToSubscribe() Channel %v Currency %v could not be removed because it was not found", + w.exchangeName, + subscribedChannel.Channel, + subscribedChannel.Currency) + } +} + +// ResubscribeToChannel calls unsubscribe func and +// removes it from subscribedChannels to trigger a subscribe event +func (w *Websocket) ResubscribeToChannel(subscribedChannel WebsocketChannelSubscription) { + w.subscriptionLock.Lock() + defer w.subscriptionLock.Unlock() + err := w.channelUnsubscriber(subscribedChannel) + if err != nil { + w.DataHandler <- err + } + // Remove the channel from the list of subscribed channels + // ManageSubscriptions will automatically resubscribe + i := 0 + for j := 0; j < len(w.subscribedChannels); j++ { + if !w.subscribedChannels[j].Equal(&subscribedChannel) { + w.subscribedChannels[i] = w.subscribedChannels[j] + i++ + } + } + w.subscribedChannels = w.subscribedChannels[:i] +} + +// SubscribeToChannels appends supplied channels to channelsToSubscribe +func (w *Websocket) SubscribeToChannels(channels []WebsocketChannelSubscription) { + for i := range channels { + w.channelsToSubscribe = append(w.channelsToSubscribe, channels[i]) + } + w.noConnectionChecks = 0 +} + +// UnsubscribeToChannels removes supplied channels from channelsToSubscribe +func (w *Websocket) UnsubscribeToChannels(channels []WebsocketChannelSubscription) { + for i := range channels { + w.removeChannelToSubscribe(channels[i]) + } +} + +// Equal two WebsocketChannelSubscription to determine equality +func (w *WebsocketChannelSubscription) Equal(subscribedChannel *WebsocketChannelSubscription) bool { + return strings.EqualFold(w.Channel, subscribedChannel.Channel) && + strings.EqualFold(w.Currency.String(), subscribedChannel.Currency.String()) +} diff --git a/exchanges/exchange_websocket_test.go b/exchanges/exchange_websocket_test.go index f7cd0a5d..8210d5bd 100644 --- a/exchanges/exchange_websocket_test.go +++ b/exchanges/exchange_websocket_test.go @@ -1,6 +1,8 @@ package exchange import ( + "fmt" + "strings" "testing" "time" @@ -28,8 +30,11 @@ func TestWebsocket(t *testing.T) { } wsTest.WebsocketSetup(func() error { return nil }, + func(test WebsocketChannelSubscription) error { return nil }, + func(test WebsocketChannelSubscription) error { return nil }, "testName", true, + false, "testDefaultURL", "testRunningURL") @@ -71,13 +76,12 @@ func TestWebsocket(t *testing.T) { } } }() - // -- Not connected shutdown err := wsTest.Websocket.Shutdown() if err == nil { t.Fatal("test failed - should not be connected to able to shut down") } - + wsTest.Websocket.Wg.Wait() // -- Normal connect err = wsTest.Websocket.Connect() if err != nil { @@ -255,7 +259,6 @@ func TestUpdate(t *testing.T) { {Price: 1337, Amount: 100}, // Append {Price: 1336, Amount: 0}, // Ghost delete } - err := wsTest.Websocket.Orderbook.Update(bidTargets, askTargets, LTCUSDPAIR, @@ -328,3 +331,238 @@ func TestFunctionality(t *testing.T) { t.Fatal("Test Failed - SupportsFunctionality error should be true") } } + +// placeholderSubscriber basic function to test subscriptions +func placeholderSubscriber(channelToSubscribe WebsocketChannelSubscription) error { + return nil +} + +// TestSubscribe logic test +func TestSubscribe(t *testing.T) { + w := Websocket{ + channelsToSubscribe: []WebsocketChannelSubscription{ + { + Channel: "hello", + }, + }, + subscribedChannels: []WebsocketChannelSubscription{}, + } + w.SetChannelSubscriber(placeholderSubscriber) + w.subscribeToChannels() + if len(w.subscribedChannels) != 1 { + t.Errorf("Subscription did not occur") + } +} + +// TestUnsubscribe logic test +func TestUnsubscribe(t *testing.T) { + w := Websocket{ + channelsToSubscribe: []WebsocketChannelSubscription{}, + subscribedChannels: []WebsocketChannelSubscription{ + { + Channel: "hello", + }, + }, + } + w.SetChannelUnsubscriber(placeholderSubscriber) + w.unsubscribeToChannels() + if len(w.subscribedChannels) != 0 { + t.Errorf("Unsubscription did not occur") + } +} + +// TestSubscriptionWithExistingEntry logic test +func TestSubscriptionWithExistingEntry(t *testing.T) { + w := Websocket{ + channelsToSubscribe: []WebsocketChannelSubscription{ + { + Channel: "hello", + }, + }, + subscribedChannels: []WebsocketChannelSubscription{ + { + Channel: "hello", + }, + }, + } + w.SetChannelSubscriber(placeholderSubscriber) + w.subscribeToChannels() + if len(w.subscribedChannels) != 1 { + t.Errorf("Subscription should not have occured") + } +} + +// TestUnsubscriptionWithExistingEntry logic test +func TestUnsubscriptionWithExistingEntry(t *testing.T) { + w := Websocket{ + channelsToSubscribe: []WebsocketChannelSubscription{ + { + Channel: "hello", + }, + }, + subscribedChannels: []WebsocketChannelSubscription{ + { + Channel: "hello", + }, + }, + } + w.SetChannelUnsubscriber(placeholderSubscriber) + w.unsubscribeToChannels() + if len(w.subscribedChannels) != 1 { + t.Errorf("Unsubscription should not have occured") + } +} + +// TestManageSubscriptionsWithoutFunctionality logic test +func TestManageSubscriptionsWithoutFunctionality(t *testing.T) { + w := Websocket{ + ShutdownC: make(chan struct{}, 1), + } + err := w.manageSubscriptions() + if err == nil { + t.Error("Requires functionality to work") + } +} + +// TestManageSubscriptionsStartStop logic test +func TestManageSubscriptionsStartStop(t *testing.T) { + w := Websocket{ + ShutdownC: make(chan struct{}, 1), + Functionality: WebsocketSubscribeSupported | WebsocketUnsubscribeSupported, + } + go w.manageSubscriptions() + time.Sleep(time.Second) + close(w.ShutdownC) +} + +// TestWsConnectionMonitorNoConnection logic test +func TestWsConnectionMonitorNoConnection(t *testing.T) { + w := Websocket{} + w.DataHandler = make(chan interface{}, 1) + w.ShutdownC = make(chan struct{}, 1) + w.exchangeName = "hello" + go w.wsConnectionMonitor() + err := <-w.DataHandler + if !strings.EqualFold(err.(error).Error(), + fmt.Sprintf("%v WsConnectionMonitor: websocket disabled, shutting down", w.exchangeName)) { + t.Errorf("expecting error 'WsConnectionMonitor: websocket disabled, shutting down', received '%v'", err) + } +} + +// TestWsNoConnectionTolerance logic test +func TestWsNoConnectionTolerance(t *testing.T) { + w := Websocket{} + w.DataHandler = make(chan interface{}, 1) + w.ShutdownC = make(chan struct{}, 1) + w.enabled = true + w.noConnectionCheckLimit = 500 + w.checkConnection() + if w.noConnectionChecks == 0 { + t.Errorf("Expected noConnectionTolerance to increment, received '%v'", w.noConnectionChecks) + } +} + +// TestConnecting logic test +func TestConnecting(t *testing.T) { + w := Websocket{} + w.DataHandler = make(chan interface{}, 1) + w.ShutdownC = make(chan struct{}, 1) + w.enabled = true + w.connecting = true + w.reconnectionLimit = 500 + w.checkConnection() + if w.reconnectionChecks != 1 { + t.Errorf("Expected reconnectionLimit to increment, received '%v'", w.reconnectionChecks) + } +} + +// TestReconnectionLimit logic test +func TestReconnectionLimit(t *testing.T) { + w := Websocket{} + w.DataHandler = make(chan interface{}, 1) + w.ShutdownC = make(chan struct{}, 1) + w.enabled = true + w.connecting = true + w.reconnectionChecks = 99 + w.reconnectionLimit = 1 + err := w.checkConnection() + if err == nil { + t.Error("Expected error") + } +} + +// TestRemoveChannelToSubscribe logic test +func TestRemoveChannelToSubscribe(t *testing.T) { + subscription := WebsocketChannelSubscription{ + Channel: "hello", + } + w := Websocket{ + channelsToSubscribe: []WebsocketChannelSubscription{ + subscription, + }, + } + w.SetChannelUnsubscriber(placeholderSubscriber) + w.removeChannelToSubscribe(subscription) + if len(w.subscribedChannels) != 0 { + t.Errorf("Unsubscription did not occur") + } +} + +// TestRemoveChannelToSubscribeWithNoSubscription logic test +func TestRemoveChannelToSubscribeWithNoSubscription(t *testing.T) { + subscription := WebsocketChannelSubscription{ + Channel: "hello", + } + w := Websocket{ + channelsToSubscribe: []WebsocketChannelSubscription{}, + } + w.DataHandler = make(chan interface{}, 1) + w.SetChannelUnsubscriber(placeholderSubscriber) + go w.removeChannelToSubscribe(subscription) + err := <-w.DataHandler + if !strings.Contains(err.(error).Error(), "could not be removed because it was not found") { + t.Error("Expected not found error") + } +} + +// TestResubscribeToChannel logic test +func TestResubscribeToChannel(t *testing.T) { + subscription := WebsocketChannelSubscription{ + Channel: "hello", + } + w := Websocket{ + channelsToSubscribe: []WebsocketChannelSubscription{}, + } + w.DataHandler = make(chan interface{}, 1) + w.SetChannelUnsubscriber(placeholderSubscriber) + w.SetChannelSubscriber(placeholderSubscriber) + w.ResubscribeToChannel(subscription) +} + +// TestSliceCopyDoesntImpactBoth logic test +func TestSliceCopyDoesntImpactBoth(t *testing.T) { + w := Websocket{ + channelsToSubscribe: []WebsocketChannelSubscription{ + { + Channel: "hello1", + }, + { + Channel: "hello2", + }, + }, + subscribedChannels: []WebsocketChannelSubscription{ + { + Channel: "hello3", + }, + }, + } + w.SetChannelUnsubscriber(placeholderSubscriber) + w.unsubscribeToChannels() + if len(w.subscribedChannels) != 2 { + t.Errorf("Unsubscription did not occur") + } + w.subscribedChannels[0].Channel = "test" + if strings.EqualFold(w.subscribedChannels[0].Channel, w.channelsToSubscribe[0].Channel) { + t.Errorf("Slice has not been copies appropriately") + } +} diff --git a/exchanges/exchange_websocket_types.go b/exchanges/exchange_websocket_types.go new file mode 100644 index 00000000..b488dfd8 --- /dev/null +++ b/exchanges/exchange_websocket_types.go @@ -0,0 +1,172 @@ +package exchange + +import ( + "sync" + "time" + + "github.com/thrasher-/gocryptotrader/currency" + "github.com/thrasher-/gocryptotrader/exchanges/orderbook" +) + +// Websocket functionality list and state consts +const ( + NoWebsocketSupport uint32 = 0 + WebsocketTickerSupported uint32 = 1 << (iota - 1) + WebsocketOrderbookSupported + WebsocketKlineSupported + WebsocketTradeDataSupported + WebsocketAccountSupported + WebsocketAllowsRequests + WebsocketSubscribeSupported + WebsocketUnsubscribeSupported + + WebsocketTickerSupportedText = "TICKER STREAMING SUPPORTED" + WebsocketOrderbookSupportedText = "ORDERBOOK STREAMING SUPPORTED" + WebsocketKlineSupportedText = "KLINE STREAMING SUPPORTED" + WebsocketTradeDataSupportedText = "TRADE STREAMING SUPPORTED" + WebsocketAccountSupportedText = "ACCOUNT STREAMING SUPPORTED" + WebsocketAllowsRequestsText = "WEBSOCKET REQUESTS SUPPORTED" + NoWebsocketSupportText = "WEBSOCKET NOT SUPPORTED" + UnknownWebsocketFunctionality = "UNKNOWN FUNCTIONALITY BITMASK" + WebsocketSubscribeSupportedText = "WEBSOCKET SUBSCRIBE SUPPORTED" + WebsocketUnsubscribeSupportedText = "WEBSOCKET UNSUBSCRIBE SUPPORTED" + + // WebsocketNotEnabled alerts of a disabled websocket + WebsocketNotEnabled = "exchange_websocket_not_enabled" + // WebsocketTrafficLimitTime defines a standard time for no traffic from the + // websocket connection + WebsocketTrafficLimitTime = 5 * time.Second + websocketRestablishConnection = time.Second + manageSubscriptionsDelay = 5 * time.Second + // connection monitor time delays and limits + connectionMonitorDelay = 2 * time.Second + // WebsocketStateTimeout defines a const for when a websocket connection + // times out, will be handled by the routine management system + WebsocketStateTimeout = "TIMEOUT" +) + +// Websocket defines a return type for websocket connections via the interface +// wrapper for routine processing in routines.go +type Websocket struct { + proxyAddr string + defaultURL string + runningURL string + exchangeName string + enabled bool + init bool + connected bool + connecting bool + verbose bool + connector func() error + m sync.Mutex + subscriptionLock sync.Mutex + connectionMonitorRunning bool + reconnectionLimit int + noConnectionChecks int + reconnectionChecks int + noConnectionCheckLimit int + // Subscriptions stuff + subscribedChannels []WebsocketChannelSubscription + channelsToSubscribe []WebsocketChannelSubscription + channelSubscriber func(channelToSubscribe WebsocketChannelSubscription) error + channelUnsubscriber func(channelToUnsubscribe WebsocketChannelSubscription) error + // Connected denotes a channel switch for diversion of request flow + Connected chan struct{} + // Disconnected denotes a channel switch for diversion of request flow + Disconnected chan struct{} + // DataHandler pipes websocket data to an exchange websocket data handler + DataHandler chan interface{} + // ShutdownC is the main shutdown channel which controls all websocket go funcs + ShutdownC chan struct{} + ShutdownConnectionMonitor chan struct{} + // Orderbook is a local cache of orderbooks + Orderbook WebsocketOrderbookLocal + + // Wg defines a wait group for websocket routines for cleanly shutting down + // routines + Wg sync.WaitGroup + // TrafficAlert monitors if there is a halt in traffic throughput + TrafficAlert chan struct{} + // Functionality defines websocket stream capabilities + Functionality uint32 +} + +// WebsocketChannelSubscription container for websocket subscriptions +// Currently only a one at a time thing to avoid complexity +type WebsocketChannelSubscription struct { + Channel string + Currency currency.Pair + Params map[string]interface{} +} + +// WebsocketOrderbookLocal defines a local cache of orderbooks for amending, +// appending and deleting changes and updates the main store in orderbook.go +type WebsocketOrderbookLocal struct { + ob []*orderbook.Base + lastUpdated time.Time + m sync.Mutex +} + +// WebsocketResponse defines generalised data from the websocket connection +type WebsocketResponse struct { + Type int + Raw []byte +} + +// WebsocketOrderbookUpdate defines a websocket event in which the orderbook +// has been updated in the orderbook package +type WebsocketOrderbookUpdate struct { + Pair currency.Pair + Asset string + Exchange string +} + +// TradeData defines trade data +type TradeData struct { + Timestamp time.Time + CurrencyPair currency.Pair + AssetType string + Exchange string + EventType string + EventTime int64 + Price float64 + Amount float64 + Side string +} + +// TickerData defines ticker feed +type TickerData struct { + Timestamp time.Time + Pair currency.Pair + AssetType string + Exchange string + ClosePrice float64 + Quantity float64 + OpenPrice float64 + HighPrice float64 + LowPrice float64 +} + +// KlineData defines kline feed +type KlineData struct { + Timestamp time.Time + Pair currency.Pair + AssetType string + Exchange string + StartTime time.Time + CloseTime time.Time + Interval string + OpenPrice float64 + ClosePrice float64 + HighPrice float64 + LowPrice float64 + Volume float64 +} + +// WebsocketPositionUpdated reflects a change in orders/contracts on an exchange +type WebsocketPositionUpdated struct { + Timestamp time.Time + Pair currency.Pair + AssetType string + Exchange string +} diff --git a/exchanges/exmo/exmo_wrapper.go b/exchanges/exmo/exmo_wrapper.go index dc2e2e09..0f1d8cc0 100644 --- a/exchanges/exmo/exmo_wrapper.go +++ b/exchanges/exmo/exmo_wrapper.go @@ -392,3 +392,15 @@ func (e *EXMO) GetOrderHistory(getOrdersRequest *exchange.GetOrdersRequest) ([]e return orders, nil } + +// SubscribeToWebsocketChannels appends to ChannelsToSubscribe +// which lets websocket.manageSubscriptions handle subscribing +func (e *EXMO) SubscribeToWebsocketChannels(channels []exchange.WebsocketChannelSubscription) error { + return common.ErrFunctionNotSupported +} + +// UnsubscribeToWebsocketChannels removes from ChannelsToSubscribe +// which lets websocket.manageSubscriptions handle unsubscribing +func (e *EXMO) UnsubscribeToWebsocketChannels(channels []exchange.WebsocketChannelSubscription) error { + return common.ErrFunctionNotSupported +} diff --git a/exchanges/gateio/gateio.go b/exchanges/gateio/gateio.go index f3743b8e..7936bb1c 100644 --- a/exchanges/gateio/gateio.go +++ b/exchanges/gateio/gateio.go @@ -7,6 +7,7 @@ import ( "net/http" "strconv" "strings" + "sync" "time" "github.com/gorilla/websocket" @@ -49,6 +50,7 @@ const ( type Gateio struct { WebsocketConn *websocket.Conn exchange.Base + wsRequestMtx sync.Mutex } // SetDefaults sets default values for the exchange @@ -78,7 +80,9 @@ func (g *Gateio) SetDefaults() { g.Websocket.Functionality = exchange.WebsocketTickerSupported | exchange.WebsocketTradeDataSupported | exchange.WebsocketOrderbookSupported | - exchange.WebsocketKlineSupported + exchange.WebsocketKlineSupported | + exchange.WebsocketSubscribeSupported | + exchange.WebsocketUnsubscribeSupported } // Setup sets user configuration @@ -120,8 +124,11 @@ func (g *Gateio) Setup(exch *config.ExchangeConfig) { log.Fatal(err) } err = g.WebsocketSetup(g.WsConnect, + g.Subscribe, + g.Unsubscribe, exch.Name, exch.Websocket, + exch.Verbose, gateioWebsocketEndpoint, exch.WebsocketURL) if err != nil { @@ -461,6 +468,7 @@ func (g *Gateio) GetTradeHistory(symbol string) (TradHistoryResponse, error) { return result, nil } +// GenerateSignature returns hash for authenticated requests func (g *Gateio) GenerateSignature(message string) []byte { return common.GetHMAC(common.HashSHA512, []byte(message), []byte(g.APISecret)) } diff --git a/exchanges/gateio/gateio_types.go b/exchanges/gateio/gateio_types.go index 33a59db9..b55dbadf 100644 --- a/exchanges/gateio/gateio_types.go +++ b/exchanges/gateio/gateio_types.go @@ -35,6 +35,7 @@ var ( TimeIntervalDay = TimeInterval(60 * 60 * 24) ) +// IDs for requests const ( IDGeneric = 0000 IDSignIn = 1010 diff --git a/exchanges/gateio/gateio_websocket.go b/exchanges/gateio/gateio_websocket.go index c9ddc82d..f973a4c2 100644 --- a/exchanges/gateio/gateio_websocket.go +++ b/exchanges/gateio/gateio_websocket.go @@ -7,6 +7,7 @@ import ( "net/http" "net/url" "strconv" + "strings" "time" "github.com/gorilla/websocket" @@ -18,8 +19,9 @@ import ( ) const ( - gateioWebsocketEndpoint = "wss://ws.gate.io/v3/" - gatioWsMethodPing = "ping" + gateioWebsocketEndpoint = "wss://ws.gate.io/v3/" + gatioWsMethodPing = "ping" + gateioWebsocketRateLimit = 120 * time.Millisecond ) // WsConnect initiates a websocket connection @@ -54,8 +56,9 @@ func (g *Gateio) WsConnect() error { } go g.WsHandleData() + g.GenerateDefaultSubscriptions() - return g.WsSubscribe() + return nil } func (g *Gateio) wsServerSignIn() error { @@ -67,84 +70,7 @@ func (g *Gateio) wsServerSignIn() error { Method: "server.sign", Params: []interface{}{g.APIKey, signature, nonce}, } - return g.WebsocketConn.WriteJSON(signinWsRequest) -} - -// WsSubscribe subscribes to the full websocket suite on ZB exchange -func (g *Gateio) WsSubscribe() error { - enabled := g.GetEnabledCurrencies() - - for _, c := range enabled { - ticker := WebsocketRequest{ - ID: 1337, - Method: "ticker.subscribe", - Params: []interface{}{c.String()}, - } - - err := g.WebsocketConn.WriteJSON(ticker) - if err != nil { - return err - } - - trade := WebsocketRequest{ - ID: 1337, - Method: "trades.subscribe", - Params: []interface{}{c.String()}, - } - - err = g.WebsocketConn.WriteJSON(trade) - if err != nil { - return err - } - - depth := WebsocketRequest{ - ID: 1337, - Method: "depth.subscribe", - Params: []interface{}{c.String(), 30, "0.1"}, - } - - err = g.WebsocketConn.WriteJSON(depth) - if err != nil { - return err - } - - kline := WebsocketRequest{ - ID: 1337, - Method: "kline.subscribe", - Params: []interface{}{c.String(), 1800}, - } - - err = g.WebsocketConn.WriteJSON(kline) - if err != nil { - return err - } - } - - if g.AuthenticatedAPISupport { - balance := WebsocketRequest{ - ID: IDBalance, - Method: "balance.subscribe", - Params: []interface{}{}, - } - - err := g.WebsocketConn.WriteJSON(balance) - if err != nil { - return err - } - - for _, c := range enabled { - orderNotification := WebsocketRequest{ - ID: IDGeneric, - Method: "order.subscribe", - Params: []interface{}{c.String()}, - } - err := g.WebsocketConn.WriteJSON(orderNotification) - if err != nil { - return err - } - } - } - return nil + return g.wsSend(signinWsRequest) } // WsReadData reads from the websocket connection and returns the websocket @@ -165,11 +91,6 @@ func (g *Gateio) WsHandleData() { g.Websocket.Wg.Add(1) defer func() { - err := g.WebsocketConn.Close() - if err != nil { - g.Websocket.DataHandler <- fmt.Errorf("gateio_websocket.go - Unable to to close Websocket connection. Error: %s", - err) - } g.Websocket.Wg.Done() }() @@ -182,6 +103,8 @@ func (g *Gateio) WsHandleData() { resp, err := g.WsReadData() if err != nil { g.Websocket.DataHandler <- err + // Read data error messages can overwhelm and panic the application + time.Sleep(time.Second) continue } @@ -416,13 +339,72 @@ func (g *Gateio) WsHandleData() { } } +// GenerateDefaultSubscriptions Adds default subscriptions to websocket to be handled by ManageSubscriptions() +func (g *Gateio) GenerateDefaultSubscriptions() { + var channels = []string{"ticker.subscribe", "trades.subscribe", "depth.subscribe", "kline.subscribe"} + if g.AuthenticatedAPISupport { + channels = append(channels, "balance.subscribe", "order.subscribe") + } + + subscriptions := []exchange.WebsocketChannelSubscription{} + enabledCurrencies := g.GetEnabledCurrencies() + for i := range channels { + for j := range enabledCurrencies { + params := make(map[string]interface{}) + if strings.EqualFold(channels[i], "depth.subscribe") { + params["limit"] = 30 + params["interval"] = "0.1" + } else if strings.EqualFold(channels[i], "kline.subscribe") { + params["interval"] = 1800 + } + subscriptions = append(subscriptions, exchange.WebsocketChannelSubscription{ + Channel: channels[i], + Currency: enabledCurrencies[j], + Params: params, + }) + } + } + g.Websocket.SubscribeToChannels(subscriptions) +} + +// Subscribe sends a websocket message to receive data from the channel +func (g *Gateio) Subscribe(channelToSubscribe exchange.WebsocketChannelSubscription) error { + params := []interface{}{channelToSubscribe.Currency.String()} + for _, paramValue := range channelToSubscribe.Params { + params = append(params, paramValue) + } + + subscribe := WebsocketRequest{ + ID: IDGeneric, + Method: channelToSubscribe.Channel, + Params: params, + } + + if strings.EqualFold(channelToSubscribe.Channel, "balance.subscribe") { + subscribe.ID = IDBalance + } + + return g.wsSend(subscribe) +} + +// Unsubscribe sends a websocket message to stop receiving data from the channel +func (g *Gateio) Unsubscribe(channelToSubscribe exchange.WebsocketChannelSubscription) error { + unsbuscribeText := strings.Replace(channelToSubscribe.Channel, "subscribe", "unsubscribe", 1) + subscribe := WebsocketRequest{ + ID: IDGeneric, + Method: unsbuscribeText, + Params: []interface{}{channelToSubscribe.Currency.String(), 1800}, + } + return g.wsSend(subscribe) +} + func (g *Gateio) wsGetBalance() error { balanceWsRequest := WebsocketRequest{ ID: IDBalance, Method: "balance.query", Params: []interface{}{}, } - return g.WebsocketConn.WriteJSON(balanceWsRequest) + return g.wsSend(balanceWsRequest) } func (g *Gateio) wsGetOrderInfo(market string, offset, limit int) error { @@ -435,5 +417,17 @@ func (g *Gateio) wsGetOrderInfo(market string, offset, limit int) error { limit, }, } - return g.WebsocketConn.WriteJSON(order) + return g.wsSend(order) +} + +// WsSend sends data to the websocket server +func (g *Gateio) wsSend(data interface{}) error { + g.wsRequestMtx.Lock() + defer g.wsRequestMtx.Unlock() + if g.Verbose { + log.Debugf("%v sending message to websocket %v", g.Name, data) + } + // Basic rate limiter + time.Sleep(gateioWebsocketRateLimit) + return g.WebsocketConn.WriteJSON(data) } diff --git a/exchanges/gateio/gateio_wrapper.go b/exchanges/gateio/gateio_wrapper.go index ba35dd33..b7e91baa 100644 --- a/exchanges/gateio/gateio_wrapper.go +++ b/exchanges/gateio/gateio_wrapper.go @@ -445,3 +445,17 @@ func (g *Gateio) GetOrderHistory(getOrdersRequest *exchange.GetOrdersRequest) ([ return orders, nil } + +// SubscribeToWebsocketChannels appends to ChannelsToSubscribe +// which lets websocket.manageSubscriptions handle subscribing +func (g *Gateio) SubscribeToWebsocketChannels(channels []exchange.WebsocketChannelSubscription) error { + g.Websocket.SubscribeToChannels(channels) + return nil +} + +// UnsubscribeToWebsocketChannels removes from ChannelsToSubscribe +// which lets websocket.manageSubscriptions handle unsubscribing +func (g *Gateio) UnsubscribeToWebsocketChannels(channels []exchange.WebsocketChannelSubscription) error { + g.Websocket.UnsubscribeToChannels(channels) + return nil +} diff --git a/exchanges/gemini/gemini.go b/exchanges/gemini/gemini.go index 6b9ac2e1..cc9a0811 100644 --- a/exchanges/gemini/gemini.go +++ b/exchanges/gemini/gemini.go @@ -167,8 +167,11 @@ func (g *Gemini) Setup(exch *config.ExchangeConfig) { log.Fatal(err) } err = g.WebsocketSetup(g.WsConnect, + nil, + nil, exch.Name, exch.Websocket, + exch.Verbose, geminiWebsocketEndpoint, exch.WebsocketURL) if err != nil { diff --git a/exchanges/gemini/gemini_types.go b/exchanges/gemini/gemini_types.go index 5f4c8743..71d1313e 100644 --- a/exchanges/gemini/gemini_types.go +++ b/exchanges/gemini/gemini_types.go @@ -214,7 +214,7 @@ type Event struct { Remaining float64 `json:"remaining,string"` Side string `json:"side"` MakerSide string `json:"makerSide"` - Amount float64 `json:"amount"` + Amount float64 `json:"amount,string"` } // ReadData defines read data from the websocket connection diff --git a/exchanges/gemini/gemini_wrapper.go b/exchanges/gemini/gemini_wrapper.go index d6b5718d..44bd055b 100644 --- a/exchanges/gemini/gemini_wrapper.go +++ b/exchanges/gemini/gemini_wrapper.go @@ -357,3 +357,15 @@ func (g *Gemini) GetOrderHistory(getOrdersRequest *exchange.GetOrdersRequest) ([ return orders, nil } + +// SubscribeToWebsocketChannels appends to ChannelsToSubscribe +// which lets websocket.manageSubscriptions handle subscribing +func (g *Gemini) SubscribeToWebsocketChannels(channels []exchange.WebsocketChannelSubscription) error { + return common.ErrFunctionNotSupported +} + +// UnsubscribeToWebsocketChannels removes from ChannelsToSubscribe +// which lets websocket.manageSubscriptions handle unsubscribing +func (g *Gemini) UnsubscribeToWebsocketChannels(channels []exchange.WebsocketChannelSubscription) error { + return common.ErrFunctionNotSupported +} diff --git a/exchanges/hitbtc/hitbtc.go b/exchanges/hitbtc/hitbtc.go index e50a090b..63f439f2 100644 --- a/exchanges/hitbtc/hitbtc.go +++ b/exchanges/hitbtc/hitbtc.go @@ -7,6 +7,7 @@ import ( "net/http" "net/url" "strconv" + "sync" "time" "github.com/gorilla/websocket" @@ -54,6 +55,7 @@ const ( type HitBTC struct { exchange.Base WebsocketConn *websocket.Conn + wsRequestMtx sync.Mutex } // SetDefaults sets default settings for hitbtc @@ -80,7 +82,9 @@ func (h *HitBTC) SetDefaults() { h.APIUrl = h.APIUrlDefault h.WebsocketInit() h.Websocket.Functionality = exchange.WebsocketTickerSupported | - exchange.WebsocketOrderbookSupported + exchange.WebsocketOrderbookSupported | + exchange.WebsocketSubscribeSupported | + exchange.WebsocketUnsubscribeSupported } // Setup sets user exchange configuration settings @@ -121,8 +125,11 @@ func (h *HitBTC) Setup(exch *config.ExchangeConfig) { log.Fatal(err) } err = h.WebsocketSetup(h.WsConnect, + h.Subscribe, + h.Unsubscribe, exch.Name, exch.Websocket, + exch.Verbose, hitbtcWebsocketAddress, exch.WebsocketURL) if err != nil { diff --git a/exchanges/hitbtc/hitbtc_types.go b/exchanges/hitbtc/hitbtc_types.go index 4833d6db..ecc9ef30 100644 --- a/exchanges/hitbtc/hitbtc_types.go +++ b/exchanges/hitbtc/hitbtc_types.go @@ -293,3 +293,80 @@ type LendingHistory struct { Open string `json:"open"` Close string `json:"close"` } + +type capture struct { + Method string `json:"method"` + Result bool `json:"result"` + Error struct { + Code int `json:"code"` + Message string `json:"message"` + } `json:"error"` +} + +// WsRequest defines a request obj for the JSON-RPC and gets a websocket +// response +type WsRequest struct { + Method string `json:"method"` + Params interface{} `json:"params,omitempty"` + ID interface{} `json:"id"` +} + +// WsNotification defines a notification obj for the JSON-RPC this does not get +// a websocket response +type WsNotification struct { + JSONRPCVersion string `json:"jsonrpc"` + Method string `json:"method"` + Params interface{} `json:"params"` +} + +type params struct { + Symbol string `json:"symbol"` + Period string `json:"period,omitempty"` + Limit int64 `json:"limit,omitempty"` +} + +// WsTicker defines websocket ticker feed return params +type WsTicker struct { + Params struct { + Ask float64 `json:"ask,string"` + Bid float64 `json:"bid,string"` + Last float64 `json:"last,string"` + Open float64 `json:"open,string"` + Low float64 `json:"low,string"` + High float64 `json:"high,string"` + Volume float64 `json:"volume,string"` + VolumeQuote float64 `json:"volumeQuote,string"` + Timestamp string `json:"timestamp"` + Symbol string `json:"symbol"` + } `json:"params"` +} + +// WsOrderbook defines websocket orderbook feed return params +type WsOrderbook struct { + Params struct { + Ask []struct { + Price float64 `json:"price,string"` + Size float64 `json:"size,string"` + } `json:"ask"` + Bid []struct { + Price float64 `json:"price,string"` + Size float64 `json:"size,string"` + } `json:"bid"` + Symbol string `json:"symbol"` + Sequence int64 `json:"sequence"` + } `json:"params"` +} + +// WsTrade defines websocket trade feed return params +type WsTrade struct { + Params struct { + Data []struct { + ID int64 `json:"id"` + Price float64 `json:"price,string"` + Quantity float64 `json:"quantity,string"` + Side string `json:"side"` + Timestamp string `json:"timestamp"` + } `json:"data"` + Symbol string `json:"symbol"` + } `json:"params"` +} diff --git a/exchanges/hitbtc/hitbtc_websocket.go b/exchanges/hitbtc/hitbtc_websocket.go index b06d5ba8..1178d2f7 100644 --- a/exchanges/hitbtc/hitbtc_websocket.go +++ b/exchanges/hitbtc/hitbtc_websocket.go @@ -5,6 +5,7 @@ import ( "fmt" "net/http" "net/url" + "strings" "time" "github.com/gorilla/websocket" @@ -12,6 +13,7 @@ import ( "github.com/thrasher-/gocryptotrader/currency" exchange "github.com/thrasher-/gocryptotrader/exchanges" "github.com/thrasher-/gocryptotrader/exchanges/orderbook" + log "github.com/thrasher-/gocryptotrader/logger" ) const ( @@ -43,58 +45,8 @@ func (h *HitBTC) WsConnect() error { } go h.WsHandleData() + h.GenerateDefaultSubscriptions() - return h.WsSubscribe() -} - -// WsSubscribe subscribes to the relevant channels -func (h *HitBTC) WsSubscribe() error { - enabledPairs := h.GetEnabledCurrencies() - for _, p := range enabledPairs { - pF := exchange.FormatExchangeCurrency(h.GetName(), p) - - tickerSubReq, err := common.JSONEncode(WsNotification{ - JSONRPCVersion: rpcVersion, - Method: "subscribeTicker", - Params: params{Symbol: pF.String()}, - }) - if err != nil { - return err - } - - err = h.WebsocketConn.WriteMessage(websocket.TextMessage, tickerSubReq) - if err != nil { - return nil - } - - orderbookSubReq, err := common.JSONEncode(WsNotification{ - JSONRPCVersion: rpcVersion, - Method: "subscribeOrderbook", - Params: params{Symbol: pF.String()}, - }) - if err != nil { - return err - } - - err = h.WebsocketConn.WriteMessage(websocket.TextMessage, orderbookSubReq) - if err != nil { - return nil - } - - tradeSubReq, err := common.JSONEncode(WsNotification{ - JSONRPCVersion: rpcVersion, - Method: "subscribeTrades", - Params: params{Symbol: pF.String()}, - }) - if err != nil { - return err - } - - err = h.WebsocketConn.WriteMessage(websocket.TextMessage, tradeSubReq) - if err != nil { - return nil - } - } return nil } @@ -114,11 +66,6 @@ func (h *HitBTC) WsHandleData() { h.Websocket.Wg.Add(1) defer func() { - err := h.WebsocketConn.Close() - if err != nil { - h.Websocket.DataHandler <- fmt.Errorf("hitbtc_websocket.go - Unable to to close Websocket connection. Error: %s", - err) - } h.Websocket.Wg.Done() }() @@ -290,77 +237,84 @@ func (h *HitBTC) WsProcessOrderbookUpdate(ob WsOrderbook) error { return nil } -type capture struct { - Method string `json:"method"` - Result bool `json:"result"` - Error struct { - Code int `json:"code"` - Message string `json:"message"` - } `json:"error"` +// GenerateDefaultSubscriptions Adds default subscriptions to websocket to be handled by ManageSubscriptions() +func (h *HitBTC) GenerateDefaultSubscriptions() { + var channels = []string{"subscribeTicker", "subscribeOrderbook", "subscribeTrades", "subscribeCandles"} + subscriptions := []exchange.WebsocketChannelSubscription{} + enabledCurrencies := h.GetEnabledCurrencies() + for i := range channels { + for j := range enabledCurrencies { + enabledCurrencies[j].Delimiter = "" + subscriptions = append(subscriptions, exchange.WebsocketChannelSubscription{ + Channel: channels[i], + Currency: enabledCurrencies[j], + }) + } + } + h.Websocket.SubscribeToChannels(subscriptions) } -// WsRequest defines a request obj for the JSON-RPC and gets a websocket -// response -type WsRequest struct { - Method string `json:"method"` - Params interface{} `json:"params,omitempty"` - ID interface{} `json:"id"` +// Subscribe sends a websocket message to receive data from the channel +func (h *HitBTC) Subscribe(channelToSubscribe exchange.WebsocketChannelSubscription) error { + subscribe := WsNotification{ + JSONRPCVersion: rpcVersion, + Method: channelToSubscribe.Channel, + Params: params{ + Symbol: channelToSubscribe.Currency.String(), + }, + } + if strings.EqualFold(channelToSubscribe.Channel, "subscribeTrades") { + subscribe.Params = params{ + Symbol: channelToSubscribe.Currency.String(), + Limit: 100, + } + } else if strings.EqualFold(channelToSubscribe.Channel, "subscribeCandles") { + subscribe.Params = params{ + Symbol: channelToSubscribe.Currency.String(), + Period: "M30", + Limit: 100, + } + } + + return h.wsSend(subscribe) } -// WsNotification defines a notification obj for the JSON-RPC this does not get -// a websocket response -type WsNotification struct { - JSONRPCVersion string `json:"jsonrpc"` - Method string `json:"method"` - Params interface{} `json:"params"` +// Unsubscribe sends a websocket message to stop receiving data from the channel +func (h *HitBTC) Unsubscribe(channelToSubscribe exchange.WebsocketChannelSubscription) error { + unsubscribeChannel := strings.Replace(channelToSubscribe.Channel, "subscribe", "unsubscribe", 1) + subscribe := WsNotification{ + JSONRPCVersion: rpcVersion, + Method: unsubscribeChannel, + Params: params{ + Symbol: channelToSubscribe.Currency.String(), + }, + } + if strings.EqualFold(unsubscribeChannel, "unsubscribeTrades") { + subscribe.Params = params{ + Symbol: channelToSubscribe.Currency.String(), + Limit: 100, + } + } else if strings.EqualFold(unsubscribeChannel, "unsubscribeCandles") { + subscribe.Params = params{ + Symbol: channelToSubscribe.Currency.String(), + Period: "M30", + Limit: 100, + } + } + + return h.wsSend(subscribe) } -type params struct { - Symbol string `json:"symbol"` -} - -// WsTicker defines websocket ticker feed return params -type WsTicker struct { - Params struct { - Ask float64 `json:"ask,string"` - Bid float64 `json:"bid,string"` - Last float64 `json:"last,string"` - Open float64 `json:"open,string"` - Low float64 `json:"low,string"` - High float64 `json:"high,string"` - Volume float64 `json:"volume,string"` - VolumeQuote float64 `json:"volumeQuote,string"` - Timestamp string `json:"timestamp"` - Symbol string `json:"symbol"` - } `json:"params"` -} - -// WsOrderbook defines websocket orderbook feed return params -type WsOrderbook struct { - Params struct { - Ask []struct { - Price float64 `json:"price,string"` - Size float64 `json:"size,string"` - } `json:"ask"` - Bid []struct { - Price float64 `json:"price,string"` - Size float64 `json:"size,string"` - } `json:"bid"` - Symbol string `json:"symbol"` - Sequence int64 `json:"sequence"` - } `json:"params"` -} - -// WsTrade defines websocket trade feed return params -type WsTrade struct { - Params struct { - Data []struct { - ID int64 `json:"id"` - Price float64 `json:"price,string"` - Quantity float64 `json:"quantity,string"` - Side string `json:"side"` - Timestamp string `json:"timestamp"` - } `json:"data"` - Symbol string `json:"symbol"` - } `json:"params"` +// WsSend sends data to the websocket server +func (h *HitBTC) wsSend(data interface{}) error { + h.wsRequestMtx.Lock() + defer h.wsRequestMtx.Unlock() + json, err := common.JSONEncode(data) + if err != nil { + return err + } + if h.Verbose { + log.Debugf("%v sending message to websocket %v", h.Name, data) + } + return h.WebsocketConn.WriteMessage(websocket.TextMessage, json) } diff --git a/exchanges/hitbtc/hitbtc_wrapper.go b/exchanges/hitbtc/hitbtc_wrapper.go index 2aa596fe..ec149444 100644 --- a/exchanges/hitbtc/hitbtc_wrapper.go +++ b/exchanges/hitbtc/hitbtc_wrapper.go @@ -380,3 +380,17 @@ func (h *HitBTC) GetOrderHistory(getOrdersRequest *exchange.GetOrdersRequest) ([ return orders, nil } + +// SubscribeToWebsocketChannels appends to ChannelsToSubscribe +// which lets websocket.manageSubscriptions handle subscribing +func (h *HitBTC) SubscribeToWebsocketChannels(channels []exchange.WebsocketChannelSubscription) error { + h.Websocket.SubscribeToChannels(channels) + return nil +} + +// UnsubscribeToWebsocketChannels removes from ChannelsToSubscribe +// which lets websocket.manageSubscriptions handle unsubscribing +func (h *HitBTC) UnsubscribeToWebsocketChannels(channels []exchange.WebsocketChannelSubscription) error { + h.Websocket.UnsubscribeToChannels(channels) + return nil +} diff --git a/exchanges/huobi/huobi.go b/exchanges/huobi/huobi.go index 091a9a24..80931dcb 100644 --- a/exchanges/huobi/huobi.go +++ b/exchanges/huobi/huobi.go @@ -14,6 +14,7 @@ import ( "net/url" "strconv" "strings" + "sync" "time" "github.com/gorilla/websocket" @@ -68,6 +69,7 @@ type HUOBI struct { exchange.Base AccountID string WebsocketConn *websocket.Conn + wsRequestMtx sync.Mutex } // SetDefaults sets default values for the exchange @@ -95,7 +97,9 @@ func (h *HUOBI) SetDefaults() { h.WebsocketInit() h.Websocket.Functionality = exchange.WebsocketKlineSupported | exchange.WebsocketOrderbookSupported | - exchange.WebsocketTradeDataSupported + exchange.WebsocketTradeDataSupported | + exchange.WebsocketSubscribeSupported | + exchange.WebsocketUnsubscribeSupported } // Setup sets user configuration @@ -138,8 +142,11 @@ func (h *HUOBI) Setup(exch *config.ExchangeConfig) { log.Fatal(err) } err = h.WebsocketSetup(h.WsConnect, + h.Subscribe, + h.Unsubscribe, exch.Name, exch.Websocket, + exch.Verbose, huobiSocketIOAddress, exch.WebsocketURL) if err != nil { diff --git a/exchanges/huobi/huobi_types.go b/exchanges/huobi/huobi_types.go index 19f49aaf..0f90346d 100644 --- a/exchanges/huobi/huobi_types.go +++ b/exchanges/huobi/huobi_types.go @@ -259,3 +259,73 @@ var ( TimeIntervalMohth = TimeInterval("1mon") TimeIntervalYear = TimeInterval("1year") ) + +// WsRequest defines a request data structure +type WsRequest struct { + Topic string `json:"req,omitempty"` + Subscribe string `json:"sub,omitempty"` + Unsubscribe string `json:"unsub,omitempty"` + ClientGeneratedID string `json:"id,omitempty"` +} + +// WsResponse defines a response from the websocket connection when there +// is an error +type WsResponse struct { + TS int64 `json:"ts"` + Status string `json:"status"` + ErrorCode string `json:"err-code"` + ErrorMessage string `json:"err-msg"` + Ping int64 `json:"ping"` + Channel string `json:"ch"` + Subscribed string `json:"subbed"` +} + +// WsHeartBeat defines a heartbeat request +type WsHeartBeat struct { + ClientNonce int64 `json:"ping"` +} + +// WsDepth defines market depth websocket response +type WsDepth struct { + Channel string `json:"ch"` + Timestamp int64 `json:"ts"` + Tick struct { + Bids []interface{} `json:"bids"` + Asks []interface{} `json:"asks"` + Timestamp int64 `json:"ts"` + Version int64 `json:"version"` + } `json:"tick"` +} + +// WsKline defines market kline websocket response +type WsKline struct { + Channel string `json:"ch"` + Timestamp int64 `json:"ts"` + Tick struct { + ID int64 `json:"id"` + Open float64 `json:"open"` + Close float64 `json:"close"` + Low float64 `json:"low"` + High float64 `json:"high"` + Amount float64 `json:"amount"` + Volume float64 `json:"vol"` + Count int64 `json:"count"` + } +} + +// WsTrade defines market trade websocket response +type WsTrade struct { + Channel string `json:"ch"` + Timestamp int64 `json:"ts"` + Tick struct { + ID int64 `json:"id"` + Timestamp int64 `json:"ts"` + Data []struct { + Amount float64 `json:"amount"` + Timestamp int64 `json:"ts"` + ID float64 `json:"id,string"` + Price float64 `json:"price"` + Direction string `json:"direction"` + } `json:"data"` + } +} diff --git a/exchanges/huobi/huobi_websocket.go b/exchanges/huobi/huobi_websocket.go index 25808aa5..f59d576b 100644 --- a/exchanges/huobi/huobi_websocket.go +++ b/exchanges/huobi/huobi_websocket.go @@ -6,7 +6,6 @@ import ( "errors" "fmt" "io/ioutil" - "math/big" "net/http" "net/url" "time" @@ -51,7 +50,8 @@ func (h *HUOBI) WsConnect() error { go h.WsHandleData() - return h.WsSubscribe() + h.GenerateDefaultSubscriptions() + return nil } // WsReadData reads data from the websocket connection @@ -83,11 +83,6 @@ func (h *HUOBI) WsHandleData() { h.Websocket.Wg.Add(1) defer func() { - err := h.WebsocketConn.Close() - if err != nil { - h.Websocket.DataHandler <- fmt.Errorf("huobi_websocket.go - Unable to to close Websocket connection. Error: %s", - err) - } h.Websocket.Wg.Done() }() @@ -222,115 +217,52 @@ func (h *HUOBI) WsProcessOrderbook(ob *WsDepth, symbol string) error { return nil } -// WsSubscribe susbcribes to the current websocket streams based on the enabled -// pair -func (h *HUOBI) WsSubscribe() error { - pairs := h.GetEnabledCurrencies() - - for _, p := range pairs { - fPair := exchange.FormatExchangeCurrency(h.GetName(), p) - - depthTopic := fmt.Sprintf(wsMarketDepth, fPair.String()) - depthJSON, err := common.JSONEncode(WsRequest{Subscribe: depthTopic}) - if err != nil { - return err - } - - err = h.WebsocketConn.WriteMessage(websocket.TextMessage, depthJSON) - if err != nil { - return err - } - - klineTopic := fmt.Sprintf(wsMarketKline, fPair.String()) - KlineJSON, err := common.JSONEncode(WsRequest{Subscribe: klineTopic}) - if err != nil { - return err - } - - err = h.WebsocketConn.WriteMessage(websocket.TextMessage, KlineJSON) - if err != nil { - return err - } - - tradeTopic := fmt.Sprintf(wsMarketTrade, fPair.String()) - tradeJSON, err := common.JSONEncode(WsRequest{Subscribe: tradeTopic}) - if err != nil { - return err - } - - err = h.WebsocketConn.WriteMessage(websocket.TextMessage, tradeJSON) - if err != nil { - return err +// GenerateDefaultSubscriptions Adds default subscriptions to websocket to be handled by ManageSubscriptions() +func (h *HUOBI) GenerateDefaultSubscriptions() { + var channels = []string{wsMarketKline, wsMarketDepth, wsMarketTrade} + enabledCurrencies := h.GetEnabledCurrencies() + subscriptions := []exchange.WebsocketChannelSubscription{} + for i := range channels { + for j := range enabledCurrencies { + enabledCurrencies[j].Delimiter = "" + channel := fmt.Sprintf(channels[i], enabledCurrencies[j].Lower().String()) + subscriptions = append(subscriptions, exchange.WebsocketChannelSubscription{ + Channel: channel, + Currency: enabledCurrencies[j], + }) } } - return nil + h.Websocket.SubscribeToChannels(subscriptions) } -// WsRequest defines a request data structure -type WsRequest struct { - Topic string `json:"req,omitempty"` - Subscribe string `json:"sub,omitempty"` - ClientGeneratedID string `json:"id,omitempty"` -} - -// WsResponse defines a response from the websocket connection when there -// is an error -type WsResponse struct { - TS int64 `json:"ts"` - Status string `json:"status"` - ErrorCode string `json:"err-code"` - ErrorMessage string `json:"err-msg"` - Ping int64 `json:"ping"` - Channel string `json:"ch"` - Subscribed string `json:"subbed"` -} - -// WsHeartBeat defines a heartbeat request -type WsHeartBeat struct { - ClientNonce int64 `json:"ping"` -} - -// WsDepth defines market depth websocket response -type WsDepth struct { - Channel string `json:"ch"` - Timestamp int64 `json:"ts"` - Tick struct { - Bids []interface{} `json:"bids"` - Asks []interface{} `json:"asks"` - Timestamp int64 `json:"ts"` - Version int64 `json:"version"` - } `json:"tick"` -} - -// WsKline defines market kline websocket response -type WsKline struct { - Channel string `json:"ch"` - Timestamp int64 `json:"ts"` - Tick struct { - ID int64 `json:"id"` - Open float64 `json:"open"` - Close float64 `json:"close"` - Low float64 `json:"low"` - High float64 `json:"high"` - Amount float64 `json:"amount"` - Volume float64 `json:"vol"` - Count int64 `json:"count"` +// Subscribe sends a websocket message to receive data from the channel +func (h *HUOBI) Subscribe(channelToSubscribe exchange.WebsocketChannelSubscription) error { + subscriptionRequest := WsRequest{Subscribe: channelToSubscribe.Channel} + if h.Verbose { + log.Debugf("Subscription: %v", subscriptionRequest) } + subscription, err := common.JSONEncode(subscriptionRequest) + if err != nil { + return err + } + return h.wsSend(subscription) } -// WsTrade defines market trade websocket response -type WsTrade struct { - Channel string `json:"ch"` - Timestamp int64 `json:"ts"` - Tick struct { - ID int64 `json:"id"` - Timestamp int64 `json:"ts"` - Data []struct { - Amount float64 `json:"amount"` - Timestamp int64 `json:"ts"` - ID big.Int `json:"id,number"` - Price float64 `json:"price"` - Direction string `json:"direction"` - } `json:"data"` +// Unsubscribe sends a websocket message to stop receiving data from the channel +func (h *HUOBI) Unsubscribe(channelToSubscribe exchange.WebsocketChannelSubscription) error { + subscription, err := common.JSONEncode(WsRequest{Unsubscribe: channelToSubscribe.Channel}) + if err != nil { + return err } + return h.wsSend(subscription) +} + +// WsSend sends data to the websocket server +func (h *HUOBI) wsSend(data []byte) error { + h.wsRequestMtx.Lock() + defer h.wsRequestMtx.Unlock() + if h.Verbose { + log.Debugf("%v sending message to websocket %s", h.Name, string(data)) + } + return h.WebsocketConn.WriteMessage(websocket.TextMessage, data) } diff --git a/exchanges/huobi/huobi_wrapper.go b/exchanges/huobi/huobi_wrapper.go index 7e77ca64..a92cadaf 100644 --- a/exchanges/huobi/huobi_wrapper.go +++ b/exchanges/huobi/huobi_wrapper.go @@ -511,3 +511,17 @@ func setOrderSideAndType(requestType string, orderDetail *exchange.OrderDetail) orderDetail.OrderType = exchange.LimitOrderType } } + +// SubscribeToWebsocketChannels appends to ChannelsToSubscribe +// which lets websocket.manageSubscriptions handle subscribing +func (h *HUOBI) SubscribeToWebsocketChannels(channels []exchange.WebsocketChannelSubscription) error { + h.Websocket.SubscribeToChannels(channels) + return nil +} + +// UnsubscribeToWebsocketChannels removes from ChannelsToSubscribe +// which lets websocket.manageSubscriptions handle unsubscribing +func (h *HUOBI) UnsubscribeToWebsocketChannels(channels []exchange.WebsocketChannelSubscription) error { + h.Websocket.UnsubscribeToChannels(channels) + return nil +} diff --git a/exchanges/huobihadax/huobihadax.go b/exchanges/huobihadax/huobihadax.go index 192748cc..7c3333c8 100644 --- a/exchanges/huobihadax/huobihadax.go +++ b/exchanges/huobihadax/huobihadax.go @@ -8,6 +8,7 @@ import ( "net/http" "net/url" "strconv" + "sync" "time" "github.com/gorilla/websocket" @@ -63,6 +64,7 @@ const ( type HUOBIHADAX struct { WebsocketConn *websocket.Conn exchange.Base + wsRequestMtx sync.Mutex } // SetDefaults sets default values for the exchange @@ -90,7 +92,9 @@ func (h *HUOBIHADAX) SetDefaults() { h.WebsocketInit() h.Websocket.Functionality = exchange.WebsocketKlineSupported | exchange.WebsocketTradeDataSupported | - exchange.WebsocketOrderbookSupported + exchange.WebsocketOrderbookSupported | + exchange.WebsocketSubscribeSupported | + exchange.WebsocketUnsubscribeSupported } // Setup sets user configuration @@ -132,8 +136,11 @@ func (h *HUOBIHADAX) Setup(exch *config.ExchangeConfig) { log.Fatal(err) } err = h.WebsocketSetup(h.WsConnect, + h.Subscribe, + h.Unsubscribe, exch.Name, exch.Websocket, + exch.Verbose, huobiGlobalWebsocketEndpoint, exch.WebsocketURL) if err != nil { diff --git a/exchanges/huobihadax/huobihadax_types.go b/exchanges/huobihadax/huobihadax_types.go index 8bc591c5..c533c317 100644 --- a/exchanges/huobihadax/huobihadax_types.go +++ b/exchanges/huobihadax/huobihadax_types.go @@ -1,7 +1,5 @@ package huobihadax -import "math/big" - // Response stores the Huobi response information type Response struct { Status string `json:"status"` @@ -258,6 +256,7 @@ type History struct { type WsRequest struct { Topic string `json:"req,omitempty"` Subscribe string `json:"sub,omitempty"` + Unsubscribe string `json:"unsub,omitempty"` ClientGeneratedID string `json:"id,omitempty"` } @@ -316,7 +315,7 @@ type WsTrade struct { Data []struct { Amount float64 `json:"amount"` Timestamp int64 `json:"ts"` - ID big.Int `json:"id,number"` + ID float64 `json:"id,string"` Price float64 `json:"price"` Direction string `json:"direction"` } `json:"data"` diff --git a/exchanges/huobihadax/huobihadax_websocket.go b/exchanges/huobihadax/huobihadax_websocket.go index 8dfcdd28..b3fcdbe5 100644 --- a/exchanges/huobihadax/huobihadax_websocket.go +++ b/exchanges/huobihadax/huobihadax_websocket.go @@ -15,6 +15,7 @@ import ( "github.com/thrasher-/gocryptotrader/currency" exchange "github.com/thrasher-/gocryptotrader/exchanges" "github.com/thrasher-/gocryptotrader/exchanges/orderbook" + log "github.com/thrasher-/gocryptotrader/logger" ) const ( @@ -51,7 +52,8 @@ func (h *HUOBIHADAX) WsConnect() error { go h.WsHandleData() - return h.WsSubscribe() + h.GenerateDefaultSubscriptions() + return nil } // WsReadData reads data from the websocket connection @@ -83,11 +85,6 @@ func (h *HUOBIHADAX) WsHandleData() { h.Websocket.Wg.Add(1) defer func() { - err := h.WebsocketConn.Close() - if err != nil { - h.Websocket.DataHandler <- fmt.Errorf("huobi_websocket.go - Unable to to close Websocket connection. Error: %s", - err) - } h.Websocket.Wg.Done() }() @@ -223,46 +220,48 @@ func (h *HUOBIHADAX) WsProcessOrderbook(ob *WsDepth, symbol string) error { return nil } -// WsSubscribe susbcribes to the current websocket streams based on the enabled -// pair -func (h *HUOBIHADAX) WsSubscribe() error { - pairs := h.GetEnabledCurrencies() - - for _, p := range pairs { - fPair := exchange.FormatExchangeCurrency(h.GetName(), p) - - depthTopic := fmt.Sprintf(wsMarketDepth, fPair.String()) - depthJSON, err := common.JSONEncode(WsRequest{Subscribe: depthTopic}) - if err != nil { - return err - } - - err = h.WebsocketConn.WriteMessage(websocket.TextMessage, depthJSON) - if err != nil { - return err - } - - klineTopic := fmt.Sprintf(wsMarketKline, fPair.String()) - KlineJSON, err := common.JSONEncode(WsRequest{Subscribe: klineTopic}) - if err != nil { - return err - } - - err = h.WebsocketConn.WriteMessage(websocket.TextMessage, KlineJSON) - if err != nil { - return err - } - - tradeTopic := fmt.Sprintf(wsMarketTrade, fPair.String()) - tradeJSON, err := common.JSONEncode(WsRequest{Subscribe: tradeTopic}) - if err != nil { - return err - } - - err = h.WebsocketConn.WriteMessage(websocket.TextMessage, tradeJSON) - if err != nil { - return err +// GenerateDefaultSubscriptions Adds default subscriptions to websocket to be handled by ManageSubscriptions() +func (h *HUOBIHADAX) GenerateDefaultSubscriptions() { + var channels = []string{wsMarketKline, wsMarketDepth, wsMarketTrade} + enabledCurrencies := h.GetEnabledCurrencies() + subscriptions := []exchange.WebsocketChannelSubscription{} + for i := range channels { + for j := range enabledCurrencies { + enabledCurrencies[j].Delimiter = "" + channel := fmt.Sprintf(channels[i], enabledCurrencies[j].Lower().String()) + subscriptions = append(subscriptions, exchange.WebsocketChannelSubscription{ + Channel: channel, + Currency: enabledCurrencies[j], + }) } } - return nil + h.Websocket.SubscribeToChannels(subscriptions) +} + +// Subscribe sends a websocket message to receive data from the channel +func (h *HUOBIHADAX) Subscribe(channelToSubscribe exchange.WebsocketChannelSubscription) error { + subscription, err := common.JSONEncode(WsRequest{Subscribe: channelToSubscribe.Channel}) + if err != nil { + return err + } + return h.wsSend(subscription) +} + +// Unsubscribe sends a websocket message to stop receiving data from the channel +func (h *HUOBIHADAX) Unsubscribe(channelToSubscribe exchange.WebsocketChannelSubscription) error { + subscription, err := common.JSONEncode(WsRequest{Unsubscribe: channelToSubscribe.Channel}) + if err != nil { + return err + } + return h.wsSend(subscription) +} + +// WsSend sends data to the websocket server +func (h *HUOBIHADAX) wsSend(data []byte) error { + h.wsRequestMtx.Lock() + defer h.wsRequestMtx.Unlock() + if h.Verbose { + log.Debugf("%v sending message to websocket %s", h.Name, string(data)) + } + return h.WebsocketConn.WriteMessage(websocket.TextMessage, data) } diff --git a/exchanges/huobihadax/huobihadax_wrapper.go b/exchanges/huobihadax/huobihadax_wrapper.go index 847a99e0..03d354e7 100644 --- a/exchanges/huobihadax/huobihadax_wrapper.go +++ b/exchanges/huobihadax/huobihadax_wrapper.go @@ -450,3 +450,17 @@ func (h *HUOBIHADAX) GetOrderHistory(getOrdersRequest *exchange.GetOrdersRequest return orders, nil } + +// SubscribeToWebsocketChannels appends to ChannelsToSubscribe +// which lets websocket.manageSubscriptions handle subscribing +func (h *HUOBIHADAX) SubscribeToWebsocketChannels(channels []exchange.WebsocketChannelSubscription) error { + h.Websocket.SubscribeToChannels(channels) + return nil +} + +// UnsubscribeToWebsocketChannels removes from ChannelsToSubscribe +// which lets websocket.manageSubscriptions handle unsubscribing +func (h *HUOBIHADAX) UnsubscribeToWebsocketChannels(channels []exchange.WebsocketChannelSubscription) error { + h.Websocket.UnsubscribeToChannels(channels) + return nil +} diff --git a/exchanges/itbit/itbit_wrapper.go b/exchanges/itbit/itbit_wrapper.go index a5279dc3..8af6facb 100644 --- a/exchanges/itbit/itbit_wrapper.go +++ b/exchanges/itbit/itbit_wrapper.go @@ -406,3 +406,15 @@ func (i *ItBit) GetOrderHistory(getOrdersRequest *exchange.GetOrdersRequest) ([] return orders, nil } + +// SubscribeToWebsocketChannels appends to ChannelsToSubscribe +// which lets websocket.manageSubscriptions handle subscribing +func (i *ItBit) SubscribeToWebsocketChannels(channels []exchange.WebsocketChannelSubscription) error { + return common.ErrFunctionNotSupported +} + +// UnsubscribeToWebsocketChannels removes from ChannelsToSubscribe +// which lets websocket.manageSubscriptions handle unsubscribing +func (i *ItBit) UnsubscribeToWebsocketChannels(channels []exchange.WebsocketChannelSubscription) error { + return common.ErrFunctionNotSupported +} diff --git a/exchanges/kraken/kraken.go b/exchanges/kraken/kraken.go index fed15ee5..0350a490 100644 --- a/exchanges/kraken/kraken.go +++ b/exchanges/kraken/kraken.go @@ -60,7 +60,7 @@ type Kraken struct { exchange.Base WebsocketConn *websocket.Conn CryptoFee, FiatFee float64 - mu sync.Mutex + wsRequestMtx sync.Mutex } // SetDefaults sets current default settings @@ -94,7 +94,9 @@ func (k *Kraken) SetDefaults() { k.Websocket.Functionality = exchange.WebsocketTickerSupported | exchange.WebsocketTradeDataSupported | exchange.WebsocketKlineSupported | - exchange.WebsocketOrderbookSupported + exchange.WebsocketOrderbookSupported | + exchange.WebsocketSubscribeSupported | + exchange.WebsocketUnsubscribeSupported } @@ -136,8 +138,11 @@ func (k *Kraken) Setup(exch *config.ExchangeConfig) { log.Fatal(err) } err = k.WebsocketSetup(k.WsConnect, + k.Subscribe, + k.Unsubscribe, exch.Name, exch.Websocket, + exch.Verbose, krakenWSURL, exch.WebsocketURL) if err != nil { diff --git a/exchanges/kraken/kraken_test.go b/exchanges/kraken/kraken_test.go index 97973b7e..9a93c465 100644 --- a/exchanges/kraken/kraken_test.go +++ b/exchanges/kraken/kraken_test.go @@ -748,167 +748,3 @@ func TestOrderBookOutOfOrder(t *testing.T) { t.Error("Expected out of order orderbook error") } } - -// TestSubscribeToChannel websocket test -func TestSubscribeToChannel(t *testing.T) { - if k.Name == "" { - k.SetDefaults() - TestSetup(t) - } - if !k.Websocket.IsEnabled() { - t.Skip("Websocket not enabled, skipping") - } - if k.WebsocketConn == nil { - k.Websocket.Connect() - } - - err := k.WsSubscribeToChannel("ticker", []string{"XTZ/USD"}, 1) - if err != nil { - t.Error(err) - } -} - -// TestSubscribeToNonExistentChannel websocket test -func TestSubscribeToNonExistentChannel(t *testing.T) { - if k.Name == "" { - k.SetDefaults() - TestSetup(t) - } - if !k.Websocket.IsEnabled() { - t.Skip("Websocket not enabled, skipping") - } - if k.WebsocketConn == nil { - k.Websocket.Connect() - } - err := k.WsSubscribeToChannel("ticker", []string{"pewdiepie"}, 1) - if err != nil { - t.Error(err) - } - subscriptionError := false - for i := 0; i < 7; i++ { - response := <-k.Websocket.DataHandler - if err, ok := response.(error); ok && err != nil { - subscriptionError = true - break - } - } - if !subscriptionError { - t.Error("Expected error") - } -} - -// TestSubscribeUnsubscribeToChannel websocket test -func TestSubscribeUnsubscribeToChannel(t *testing.T) { - if k.Name == "" { - k.SetDefaults() - TestSetup(t) - } - if !k.Websocket.IsEnabled() { - t.Skip("Websocket not enabled, skipping") - } - if k.WebsocketConn == nil { - k.Websocket.Connect() - } - err := k.WsSubscribeToChannel("ticker", []string{"XRP/JPY"}, 1) - if err != nil { - t.Error(err) - } - err = k.WsUnsubscribeToChannel("ticker", []string{"XRP/JPY"}, 2) - if err != nil { - t.Error(err) - } -} - -// TestUnsubscribeWithoutSubscription websocket test -func TestUnsubscribeWithoutSubscription(t *testing.T) { - if k.Name == "" { - k.SetDefaults() - TestSetup(t) - } - if !k.Websocket.IsEnabled() { - t.Skip("Websocket not enabled, skipping") - } - if k.WebsocketConn == nil { - k.Websocket.Connect() - } - err := k.WsUnsubscribeToChannel("ticker", []string{"QTUM/EUR"}, 3) - if err != nil { - t.Error(err) - } - unsubscriptionError := false - for i := 0; i < 5; i++ { - response := <-k.Websocket.DataHandler - t.Log(response) - if err, ok := response.(error); ok && err != nil { - if err.Error() == "requestID: '3'. Error: Subscription Not Found" { - unsubscriptionError = true - break - } - } - } - if !unsubscriptionError { - t.Error("Expected error") - } -} - -// TestUnsubscribeWithChannelID websocket test -func TestUnsubscribeWithChannelID(t *testing.T) { - if k.Name == "" { - k.SetDefaults() - TestSetup(t) - } - if !k.Websocket.IsEnabled() { - t.Skip("Websocket not enabled, skipping") - } - if k.WebsocketConn == nil { - k.Websocket.Connect() - } - err := k.WsUnsubscribeToChannelByChannelID(100) - if err != nil { - t.Error(err) - } - unsubscriptionError := false - for i := 0; i < 5; i++ { - response := <-k.Websocket.DataHandler - if err, ok := response.(error); ok && err != nil { - if err.Error() == "Not subscribed to the requested channelID" { - unsubscriptionError = true - break - } - } - } - if !unsubscriptionError { - t.Error("Expected error") - } -} - -// TestUnsubscribeFromNonExistentChannel websocket test -func TestUnsubscribeFromNonExistentChannel(t *testing.T) { - if k.Name == "" { - k.SetDefaults() - TestSetup(t) - } - if !k.Websocket.IsEnabled() { - t.Skip("Websocket not enabled, skipping") - } - if k.WebsocketConn == nil { - k.Websocket.Connect() - } - err := k.WsUnsubscribeToChannel("ticker", []string{"tseries"}, 0) - if err != nil { - t.Error(err) - } - unsubscriptionError := false - for i := 0; i < 5; i++ { - response := <-k.Websocket.DataHandler - if err, ok := response.(error); ok && err != nil { - if err.Error() == "Currency pair not in ISO 4217-A3 format tseries" { - unsubscriptionError = true - break - } - } - } - if !unsubscriptionError { - t.Error("Expected error") - } -} diff --git a/exchanges/kraken/kraken_websocket.go b/exchanges/kraken/kraken_websocket.go index 20eee9e3..14ebc815 100644 --- a/exchanges/kraken/kraken_websocket.go +++ b/exchanges/kraken/kraken_websocket.go @@ -11,7 +11,6 @@ import ( "net/url" "sort" "strconv" - "strings" "sync" "time" @@ -47,6 +46,7 @@ const ( // Only supported asset type krakenWsAssetType = "SPOT" orderbookBufferLimit = 3 + krakenWsRateLimit = 50 * time.Millisecond ) // orderbookMutex Ensures if two entries arrive at once, only one can be processed at a time @@ -62,16 +62,20 @@ var krakenOrderBooks map[int64]orderbook.Base var orderbookBuffer map[int64][]orderbook.Base var subscribeToDefaultChannels = true +// Channels require a topic and a currency +// Format [[ticker,but-t4u],[orderbook,nce-btt]] +var defaultSubscribedChannels = []string{krakenWsTicker, krakenWsTrade, krakenWsOrderbook, krakenWsOHLC, krakenWsSpread} + // writeToWebsocket sends a message to the websocket endpoint func (k *Kraken) writeToWebsocket(message []byte) error { - k.mu.Lock() - defer k.mu.Unlock() + k.wsRequestMtx.Lock() + defer k.wsRequestMtx.Unlock() if k.Verbose { log.Debugf("Sending message to WS: %v", string(message)) } // Really basic WS rate limit - time.Sleep(30 * time.Millisecond) + time.Sleep(krakenWsRateLimit) return k.WebsocketConn.WriteMessage(websocket.TextMessage, message) } @@ -110,24 +114,10 @@ func (k *Kraken) WsConnect() error { go k.WsHandleData() go k.wsPingHandler() if subscribeToDefaultChannels { - k.WsSubscribeToDefaults() + k.GenerateDefaultSubscriptions() } - return nil -} -// WsSubscribeToDefaults subscribes to the websocket channels -func (k *Kraken) WsSubscribeToDefaults() { - channelsToSubscribe := []string{krakenWsTicker, krakenWsTrade, krakenWsOrderbook, krakenWsOHLC, krakenWsSpread} - for _, pair := range k.EnabledPairs { - // Kraken WS formats pairs with / but config and REST use - - formattedPair := strings.ToUpper(strings.Replace(pair.String(), "-", "/", 1)) - for _, channel := range channelsToSubscribe { - err := k.WsSubscribeToChannel(channel, []string{formattedPair}, 0) - if err != nil { - k.Websocket.DataHandler <- err - } - } - } + return nil } // WsReadData reads data from the websocket connection @@ -164,6 +154,7 @@ func (k *Kraken) wsPingHandler() { k.Websocket.Wg.Add(1) defer k.Websocket.Wg.Done() ticker := time.NewTicker(time.Second * 27) + for { select { case <-k.Websocket.ShutdownC: @@ -187,11 +178,6 @@ func (k *Kraken) wsPingHandler() { func (k *Kraken) WsHandleData() { k.Websocket.Wg.Add(1) defer func() { - err := k.WebsocketConn.Close() - if err != nil { - k.Websocket.DataHandler <- fmt.Errorf("%v unable to to close Websocket connection. Error: %s", - k.GetName(), err) - } k.Websocket.Wg.Done() }() @@ -202,8 +188,10 @@ func (k *Kraken) WsHandleData() { default: resp, err := k.WsReadData() if err != nil { - k.Websocket.DataHandler <- err - return + k.Websocket.DataHandler <- fmt.Errorf("%v WsHandleData: %v", + k.Name, + err) + time.Sleep(time.Second) } // event response handling var eventResponse WebsocketEventResponse @@ -219,8 +207,6 @@ func (k *Kraken) WsHandleData() { k.WsHandleDataResponse(dataResponse) continue } - // Unknown data handling - k.Websocket.DataHandler <- fmt.Errorf("unrecognised response: %v", string(resp.Raw)) continue } } @@ -312,64 +298,23 @@ func (k *Kraken) WsHandleEventResponse(response *WebsocketEventResponse) { } } -// WsSubscribeToChannel sends a request to WS to subscribe to supplied channel name and pairs -func (k *Kraken) WsSubscribeToChannel(topic string, currencies []string, requestID int64) error { - resp := WebsocketSubscriptionEventRequest{ - Event: krakenWsSubscribe, - Pairs: currencies, - Subscription: WebsocketSubscriptionData{ - Name: topic, - }, - } - if requestID > 0 { - resp.RequestID = requestID - } - json, err := common.JSONEncode(resp) - if err != nil { - return err - } - return k.writeToWebsocket(json) -} - -// WsUnsubscribeToChannel sends a request to WS to unsubscribe to supplied channel name and pairs -func (k *Kraken) WsUnsubscribeToChannel(topic string, currencies []string, requestID int64) error { - resp := WebsocketSubscriptionEventRequest{ - Event: krakenWsUnsubscribe, - Pairs: currencies, - Subscription: WebsocketSubscriptionData{ - Name: topic, - }, - } - if requestID > 0 { - resp.RequestID = requestID - } - json, err := common.JSONEncode(resp) - if err != nil { - return err - } - return k.writeToWebsocket(json) -} - -// WsUnsubscribeToChannelByChannelID sends a request to WS to unsubscribe to supplied channel ID -func (k *Kraken) WsUnsubscribeToChannelByChannelID(channelID int64) error { - resp := WebsocketUnsubscribeByChannelIDEventRequest{ - Event: krakenWsUnsubscribe, - ChannelID: channelID, - } - json, err := common.JSONEncode(resp) - if err != nil { - return err - } - return k.writeToWebsocket(json) -} - // addNewSubscriptionChannelData stores channel ids, pairs and subscription types to an array // allowing correlation between subscriptions and returned data func addNewSubscriptionChannelData(response *WebsocketEventResponse) { for i := range subscriptionChannelPair { - if response.ChannelID == subscriptionChannelPair[i].ChannelID { - return + if response.ChannelID != subscriptionChannelPair[i].ChannelID { + continue } + // kill the stale orderbooks due to resubscribing + if orderbookBuffer == nil { + orderbookBuffer = make(map[int64][]orderbook.Base) + } + orderbookBuffer[response.ChannelID] = []orderbook.Base{} + if krakenOrderBooks == nil { + krakenOrderBooks = make(map[int64]orderbook.Base) + } + krakenOrderBooks[response.ChannelID] = orderbook.Base{} + return } // We change the / to - to maintain compatibility with REST/config @@ -391,47 +336,6 @@ func getSubscriptionChannelData(id int64) WebsocketChannelData { return WebsocketChannelData{} } -// ResubscribeToChannel will attempt to unsubscribe and resubscribe to a channel -func (k *Kraken) ResubscribeToChannel(channel string, pair currency.Pair) { - // Kraken WS formats pairs with / but config and REST use - - formattedPair := strings.ToUpper(strings.Replace(pair.String(), "-", "/", 1)) - if krakenWsResubscribeFailureLimit > 0 { - var successfulUnsubscribe bool - for i := 0; i < krakenWsResubscribeFailureLimit; i++ { - err := k.WsUnsubscribeToChannel(channel, []string{formattedPair}, 0) - if err != nil { - log.Error(err) - time.Sleep(krakenWsResubscribeDelayInSeconds * time.Second) - continue - } - successfulUnsubscribe = true - break - } - if !successfulUnsubscribe { - log.Fatalf("%v websocket channel %v failed to unsubscribe after %v attempts", - k.GetName(), channel, krakenWsResubscribeFailureLimit) - } - successfulSubscribe := true - for i := 0; i < krakenWsResubscribeFailureLimit; i++ { - err := k.WsSubscribeToChannel(channel, []string{formattedPair}, 0) - if err != nil { - log.Error(err) - time.Sleep(krakenWsResubscribeDelayInSeconds * time.Second) - continue - } - successfulSubscribe = true - break - } - if !successfulSubscribe { - log.Fatalf("%v websocket channel %v failed to resubscribe after %v attempts", - k.GetName(), channel, krakenWsResubscribeFailureLimit) - } - } else { - log.Fatalf("%v websocket channel %v cannot resubscribe. Limit: %v", - k.GetName(), channel, krakenWsResubscribeFailureLimit) - } -} - // wsProcessTickers converts ticker data and sends it to the datahandler func (k *Kraken) wsProcessTickers(channelData *WebsocketChannelData, data interface{}) { tickerData := data.(map[string]interface{}) @@ -509,13 +413,17 @@ func (k *Kraken) wsProcessOrderBook(channelData *WebsocketChannelData, data inte _, asksExist := obData["a"] _, bidsExist := obData["b"] if asksExist || bidsExist { - k.mu.Lock() - defer k.mu.Unlock() + k.wsRequestMtx.Lock() + defer k.wsRequestMtx.Unlock() k.wsProcessOrderBookBuffer(channelData, obData) if len(orderbookBuffer[channelData.ChannelID]) >= orderbookBufferLimit { err := k.wsProcessOrderBookUpdate(channelData) if err != nil { - k.ResubscribeToChannel(channelData.Subscription, channelData.Pair) + subscriptionToRemove := exchange.WebsocketChannelSubscription{ + Channel: krakenWsOrderbook, + Currency: channelData.Pair, + } + k.Websocket.ResubscribeToChannel(subscriptionToRemove) } } } @@ -838,3 +746,57 @@ func (k *Kraken) wsProcessCandles(channelData *WebsocketChannelData, data interf Volume: volume, } } + +// GenerateDefaultSubscriptions Adds default subscriptions to websocket to be handled by ManageSubscriptions() +func (k *Kraken) GenerateDefaultSubscriptions() { + enabledCurrencies := k.GetEnabledCurrencies() + subscriptions := []exchange.WebsocketChannelSubscription{} + for i := range defaultSubscribedChannels { + for j := range enabledCurrencies { + enabledCurrencies[j].Delimiter = "/" + subscriptions = append(subscriptions, exchange.WebsocketChannelSubscription{ + Channel: defaultSubscribedChannels[i], + Currency: enabledCurrencies[j], + }) + } + } + k.Websocket.SubscribeToChannels(subscriptions) +} + +// Subscribe sends a websocket message to receive data from the channel +func (k *Kraken) Subscribe(channelToSubscribe exchange.WebsocketChannelSubscription) error { + resp := WebsocketSubscriptionEventRequest{ + Event: krakenWsSubscribe, + Pairs: []string{channelToSubscribe.Currency.String()}, + Subscription: WebsocketSubscriptionData{ + Name: channelToSubscribe.Channel, + }, + } + json, err := common.JSONEncode(resp) + if err != nil { + if k.Verbose { + log.Debugf("%v subscribe error: %v", k.Name, err) + } + return err + } + return k.writeToWebsocket(json) +} + +// Unsubscribe sends a websocket message to stop receiving data from the channel +func (k *Kraken) Unsubscribe(channelToSubscribe exchange.WebsocketChannelSubscription) error { + resp := WebsocketSubscriptionEventRequest{ + Event: krakenWsUnsubscribe, + Pairs: []string{channelToSubscribe.Currency.String()}, + Subscription: WebsocketSubscriptionData{ + Name: channelToSubscribe.Channel, + }, + } + json, err := common.JSONEncode(resp) + if err != nil { + if k.Verbose { + log.Debugf("%v unsubscribe error: %v", k.Name, err) + } + return err + } + return k.writeToWebsocket(json) +} diff --git a/exchanges/kraken/kraken_wrapper.go b/exchanges/kraken/kraken_wrapper.go index d19e9079..c3f8fd72 100644 --- a/exchanges/kraken/kraken_wrapper.go +++ b/exchanges/kraken/kraken_wrapper.go @@ -396,3 +396,17 @@ func (k *Kraken) GetOrderHistory(getOrdersRequest *exchange.GetOrdersRequest) ([ return orders, nil } + +// SubscribeToWebsocketChannels appends to ChannelsToSubscribe +// which lets websocket.manageSubscriptions handle subscribing +func (k *Kraken) SubscribeToWebsocketChannels(channels []exchange.WebsocketChannelSubscription) error { + k.Websocket.SubscribeToChannels(channels) + return nil +} + +// UnsubscribeToWebsocketChannels removes from ChannelsToSubscribe +// which lets websocket.manageSubscriptions handle unsubscribing +func (k *Kraken) UnsubscribeToWebsocketChannels(channels []exchange.WebsocketChannelSubscription) error { + k.Websocket.UnsubscribeToChannels(channels) + return nil +} diff --git a/exchanges/lakebtc/lakebtc_wrapper.go b/exchanges/lakebtc/lakebtc_wrapper.go index b57823c4..dac301be 100644 --- a/exchanges/lakebtc/lakebtc_wrapper.go +++ b/exchanges/lakebtc/lakebtc_wrapper.go @@ -351,3 +351,15 @@ func (l *LakeBTC) GetOrderHistory(getOrdersRequest *exchange.GetOrdersRequest) ( return orders, nil } + +// SubscribeToWebsocketChannels appends to ChannelsToSubscribe +// which lets websocket.manageSubscriptions handle subscribing +func (l *LakeBTC) SubscribeToWebsocketChannels(channels []exchange.WebsocketChannelSubscription) error { + return common.ErrFunctionNotSupported +} + +// UnsubscribeToWebsocketChannels removes from ChannelsToSubscribe +// which lets websocket.manageSubscriptions handle unsubscribing +func (l *LakeBTC) UnsubscribeToWebsocketChannels(channels []exchange.WebsocketChannelSubscription) error { + return common.ErrFunctionNotSupported +} diff --git a/exchanges/localbitcoins/localbitcoins_wrapper.go b/exchanges/localbitcoins/localbitcoins_wrapper.go index 8d084bbe..865243d9 100644 --- a/exchanges/localbitcoins/localbitcoins_wrapper.go +++ b/exchanges/localbitcoins/localbitcoins_wrapper.go @@ -428,3 +428,15 @@ func (l *LocalBitcoins) GetOrderHistory(getOrdersRequest *exchange.GetOrdersRequ return orders, nil } + +// SubscribeToWebsocketChannels appends to ChannelsToSubscribe +// which lets websocket.manageSubscriptions handle subscribing +func (l *LocalBitcoins) SubscribeToWebsocketChannels(channels []exchange.WebsocketChannelSubscription) error { + return common.ErrFunctionNotSupported +} + +// UnsubscribeToWebsocketChannels removes from ChannelsToSubscribe +// which lets websocket.manageSubscriptions handle unsubscribing +func (l *LocalBitcoins) UnsubscribeToWebsocketChannels(channels []exchange.WebsocketChannelSubscription) error { + return common.ErrFunctionNotSupported +} diff --git a/exchanges/okcoin/okcoin.go b/exchanges/okcoin/okcoin.go index c541eb1f..bc0a2572 100644 --- a/exchanges/okcoin/okcoin.go +++ b/exchanges/okcoin/okcoin.go @@ -54,5 +54,7 @@ func (o *OKCoin) SetDefaults() { o.Websocket.Functionality = exchange.WebsocketTickerSupported | exchange.WebsocketTradeDataSupported | exchange.WebsocketKlineSupported | - exchange.WebsocketOrderbookSupported + exchange.WebsocketOrderbookSupported | + exchange.WebsocketSubscribeSupported | + exchange.WebsocketUnsubscribeSupported } diff --git a/exchanges/okcoin/okcoin_test.go b/exchanges/okcoin/okcoin_test.go index 0fa413a5..917f71c8 100644 --- a/exchanges/okcoin/okcoin_test.go +++ b/exchanges/okcoin/okcoin_test.go @@ -24,6 +24,7 @@ const ( var o OKCoin var testSetupRan bool var spotCurrency = currency.NewPairWithDelimiter(currency.BTC.String(), currency.USD.String(), "-").Lower().String() +var websocketEnabled bool // TestSetDefaults Sets standard default settings for running a test func TestSetDefaults(t *testing.T) { @@ -56,11 +57,13 @@ func TestSetup(t *testing.T) { o.ExchangeName = OKGroupExchange cfg := config.GetConfig() cfg.LoadConfig("../../testdata/configtest.json") - okcoinConfig, err := cfg.GetExchangeConfig(OKGroupExchange) if err != nil { t.Fatalf("Test Failed - %v Setup() init error", OKGroupExchange) } + if okcoinConfig.Websocket { + websocketEnabled = true + } okcoinConfig.AuthenticatedAPISupport = true okcoinConfig.APIKey = apiKey @@ -69,6 +72,7 @@ func TestSetup(t *testing.T) { okcoinConfig.WebsocketURL = o.WebsocketURL o.Setup(&okcoinConfig) testSetupRan = true + o.Websocket.DataHandler = make(chan interface{}, 999) } func areTestAPIKeysSet() bool { @@ -92,8 +96,11 @@ func testStandardErrorHandling(t *testing.T, err error) { func setupWSConnection() error { o.Enabled = true err := o.WebsocketSetup(o.WsConnect, + nil, + nil, o.Name, true, + o.Verbose, o.WebsocketURL, o.WebsocketURL) o.Websocket.DataHandler = make(chan interface{}, 500) @@ -812,15 +819,11 @@ func TestGetMarginTransactionDetails(t *testing.T) { // TestWsLogin API endpoint test func TestWsLogin(t *testing.T) { TestSetRealOrderDefaults(t) - if o.WebsocketConn == nil { - o.Websocket.Shutdown() - err := setupWSConnection() - if err != nil { - t.Error(err) - } + if !websocketEnabled { + t.Skip("Websocket not enabled, skipping") } - if !o.Websocket.IsConnected() { - t.Skip() + if !o.Websocket.IsConnecting() || !o.Websocket.IsConnected() { + o.Websocket.Connect() } err := o.WsLogin() if err != nil { @@ -841,28 +844,24 @@ func TestWsLogin(t *testing.T) { // TestSubscribeToChannel API endpoint test func TestSubscribeToChannel(t *testing.T) { TestSetDefaults(t) - if o.WebsocketConn == nil { - err := setupWSConnection() - if err != nil { - t.Error(err) - } + if !websocketEnabled { + t.Skip("Websocket not enabled, skipping") } - if !o.Websocket.IsConnected() { - t.Skip() + if !o.Websocket.IsConnecting() || !o.Websocket.IsConnected() { + o.Websocket.Connect() } - defer disconnectFromWS() - channelName := "spot/depth:LTC-BTC" - err := o.WsSubscribeToChannel(channelName) - if err != nil { - t.Error(err) - return + subscription := exchange.WebsocketChannelSubscription{ + Channel: "spot/depth", + Currency: currency.NewPairDelimiter("LTC-BTC", "-"), } + + o.Subscribe(subscription) var errorReceived bool for i := 0; i < 5; i++ { response := <-o.Websocket.DataHandler if err, ok := response.(error); ok && err != nil { t.Log(response) - if strings.Contains(response.(error).Error(), channelName) { + if strings.Contains(response.(error).Error(), subscription.Channel) { errorReceived = true } } @@ -876,30 +875,23 @@ func TestSubscribeToChannel(t *testing.T) { // Attempts to subscribe to a channel that doesn't exist // Then captures the error response func TestSubscribeToNonExistantChannel(t *testing.T) { - defer disconnectFromWS() TestSetDefaults(t) - if o.WebsocketConn == nil { - o.Websocket.Shutdown() - err := setupWSConnection() - if err != nil { - t.Error(err) - } + if !websocketEnabled { + t.Skip("Websocket not enabled, skipping") } - if !o.Websocket.IsConnected() { - t.Skip("Could not connect to websocket. Skipping") + if !o.Websocket.IsConnecting() || !o.Websocket.IsConnected() { + o.Websocket.Connect() } - channelName := "badChannel" - err := o.WsSubscribeToChannel(channelName) - if err != nil { - t.Error(err) - return + subscription := exchange.WebsocketChannelSubscription{ + Channel: "badChannel", } + o.Subscribe(subscription) var errorReceived bool for i := 0; i < 5; i++ { response := <-o.Websocket.DataHandler if err, ok := response.(error); ok && err != nil { t.Log(response) - if strings.Contains(response.(error).Error(), channelName) { + if strings.Contains(response.(error).Error(), subscription.Channel) { errorReceived = true } } @@ -943,14 +935,9 @@ func TestGetWsChannelWithoutOrderType(t *testing.T) { // TestOrderBookUpdateChecksumCalculator logic test func TestOrderBookUpdateChecksumCalculator(t *testing.T) { TestSetDefaults(t) - if o.WebsocketConn == nil { - o.Websocket.Shutdown() - err := setupWSConnection() - if err != nil { - t.Error(err) - } + if !websocketEnabled { + t.Skip("Websocket not enabled, skipping") } - disconnectFromWS() original := `{"table":"spot/depth","action":"partial","data":[{"instrument_id":"BTC-USDT","asks":[["3864.6786","0.145",1],["3864.7682","0.005",1],["3864.9851","0.57",1],["3864.9852","0.30137754",1],["3864.9986","2.81818419",1],["3864.9995","0.002",1],["3865","0.0597",1],["3865.0309","0.4",1],["3865.1995","0.004",1],["3865.3995","0.004",1],["3865.5995","0.004",1],["3865.7995","0.004",1],["3865.9995","0.004",1],["3866.0961","0.25865886",1],["3866.1995","0.004",1],["3866.3995","0.004",1],["3866.4004","0.3243",2],["3866.5995","0.004",1],["3866.7633","0.44247086",1],["3866.7995","0.004",1],["3866.9197","0.511",1],["3867.256","0.51716256",1],["3867.3951","0.02588112",1],["3867.4014","0.025",1],["3867.4566","0.02499999",1],["3867.4675","4.01155057",5],["3867.5515","1.1",1],["3867.6113","0.009",1],["3867.7349","0.026",1],["3867.7781","0.03738652",1],["3867.9163","0.0521",1],["3868.0381","0.34354941",1],["3868.0436","0.051",1],["3868.0657","0.90552172",3],["3868.1819","0.03863346",1],["3868.2013","0.194",1],["3868.346","0.051",1],["3868.3863","0.01155",1],["3868.7716","0.009",1],["3868.947","0.025",1],["3868.98","0.001",1],["3869.0764","1.03487931",1],["3869.2773","0.07724578",1],["3869.4039","0.025",1],["3869.4068","1.03",1],["3869.7068","2.06976398",1],["3870","0.5",1],["3870.0465","0.01",1],["3870.7042","0.02099651",1],["3870.9451","2.07047375",1],["3871.5254","1.2",1],["3871.5596","0.001",1],["3871.6605","0.01035032",1],["3871.7179","2.07047375",1],["3871.8816","0.51751625",1],["3872.1","0.75",1],["3872.2464","0.0646",1],["3872.3747","0.283",1],["3872.4039","0.2",1],["3872.7655","0.23179307",1],["3872.8005","2.06976398",1],["3873.1509","2",1],["3873.3215","0.26",1],["3874.1392","0.001",1],["3874.1487","3.88224364",4],["3874.1685","1.8",1],["3874.5571","0.08974762",1],["3874.734","2.06976398",1],["3874.99","0.3",1],["3875","1.001",2],["3875.0041","1.03505051",1],["3875.45","0.3",1],["3875.4766","0.15",1],["3875.7057","0.51751625",1],["3876","0.001",1],["3876.68","0.3",1],["3876.7188","0.001",1],["3877","0.75",1],["3877.31","0.035",1],["3877.38","0.3",1],["3877.7","0.3",1],["3877.88","0.3",1],["3878.0364","0.34770122",1],["3878.4525","0.48579748",1],["3878.4955","0.02812511",1],["3878.8855","0.00258579",1],["3878.9605","0.895",1],["3879","0.001",1],["3879.2984","0.002",2],["3879.432","0.001",1],["3879.6313","6",1],["3879.9999","0.002",2],["3880","1.25132834",5],["3880.2526","0.04075162",1],["3880.7145","0.0647",1],["3881.2469","1.883",1],["3881.878","0.002",2],["3884.4576","0.002",2],["3885","0.002",2],["3885.2233","0.28304103",1],["3885.7416","18",1],["3886","0.001",1],["3886.1554","5.4",1],["3887","0.001",1],["3887.0372","0.002",2],["3887.2559","0.05214011",1],["3887.9238","0.0019",1],["3888","0.15810538",4],["3889","0.001",1],["3889.5175","0.50510653",1],["3889.6168","0.002",2],["3889.9999","0.001",1],["3890","2.34968109",4],["3890.5222","0.00257806",1],["3891.2659","5",1],["3891.9999","0.00893897",1],["3892.1964","0.002",2],["3892.4358","0.0176",1],["3893.1388","1.4279",1],["3894","0.0026321",1],["3894.776","0.001",1],["3895","1.501",2],["3895.379","0.25881288",1],["3897","0.05",1],["3897.3556","0.001",1],["3897.8432","0.73708079",1],["3898","3.31353018",7],["3898.4462","4.757",1],["3898.6","0.47159638",1],["3898.8769","0.0129",1],["3899","6",2],["3899.6516","0.025",1],["3899.9352","0.001",1],["3899.9999","0.013",2],["3900","22.37447743",24],["3900.9999","0.07763916",1],["3901","0.10192487",1],["3902.1937","0.00257034",1],["3902.3991","1.5532141",1],["3902.5148","0.001",1],["3904","1.49331984",1],["3904.9999","0.95905447",1],["3905","0.501",2],["3905.0944","0.001",1],["3905.61","0.099",1],["3905.6801","0.54343686",1],["3906.2901","0.0258",1],["3907.674","0.001",1],["3907.85","1.35778084",1],["3908","0.03846153",1],["3908.23","1.95189531",1],["3908.906","0.03148978",1],["3909","0.001",1],["3909.9999","0.01398721",2],["3910","0.016",2],["3910.2536","0.001",1],["3912.5406","0.88270517",1],["3912.8332","0.001",1],["3913","1.2640608",1],["3913.87","1.69114184",1],["3913.9003","0.00256266",1],["3914","1.21766411",1],["3915","0.001",1],["3915.4128","0.001",1],["3915.7425","6.848",1],["3916","0.0050949",1],["3917.36","1.28658296",1],["3917.9924","0.001",1],["3919","0.001",1],["3919.9999","0.001",1],["3920","1.21171832",3],["3920.0002","0.20217038",1],["3920.572","0.001",1],["3921","0.128",1],["3923.0756","0.00148064",1],["3923.1516","0.001",1],["3923.86","1.38831714",1],["3925","0.01867801",2],["3925.642","0.00255499",1],["3925.7312","0.001",1],["3926","0.04290757",1],["3927","0.023",1],["3927.3175","0.01212865",1],["3927.65","1.51375612",1],["3928","0.5",1],["3928.3108","0.001",1],["3929","0.001",1],["3929.9999","0.01519338",2],["3930","0.0174985",3],["3930.21","1.49335799",1],["3930.8904","0.001",1],["3932.2999","0.01953",1],["3932.8962","7.96",1],["3933.0387","11.808",1],["3933.47","0.001",1],["3934","1.40839932",1],["3935","0.001",1],["3936.8","0.62879518",1],["3937.23","1.56977841",1],["3937.4189","0.00254735",1]],"bids":[["3864.5217","0.00540709",1],["3864.5216","0.14068758",2],["3864.2275","0.01033576",1],["3864.0989","0.00825047",1],["3864.0273","0.38",1],["3864.0272","0.4",1],["3863.9957","0.01083539",1],["3863.9184","0.01653723",1],["3863.8282","0.25588165",1],["3863.8153","0.154",1],["3863.7791","1.14122492",1],["3863.6866","0.01733662",1],["3863.6093","0.02645958",1],["3863.3775","0.02773862",1],["3863.0297","0.513",1],["3863.0286","1.1028564",2],["3862.8489","0.01",1],["3862.5972","0.01890179",1],["3862.3431","0.01152944",1],["3862.313","0.009",1],["3862.2445","0.90551002",3],["3862.0734","0.014",1],["3862.0539","0.64976067",1],["3861.8586","0.025",1],["3861.7888","0.025",1],["3861.7673","0.008",1],["3861.5785","0.01",1],["3861.3895","0.005",1],["3861.3338","0.25875855",1],["3861.161","0.01",1],["3861.1111","0.03863352",1],["3861.0732","0.51703882",1],["3860.9116","0.17754895",1],["3860.75","0.19",1],["3860.6554","0.015",1],["3860.6172","0.005",1],["3860.6088","0.008",1],["3860.4724","0.12940042",1],["3860.4424","0.25880084",1],["3860.42","0.01",1],["3860.3725","0.51760102",1],["3859.8449","0.005",1],["3859.8285","0.03738652",1],["3859.7638","0.07726703",1],["3859.4502","0.008",1],["3859.3772","0.05173471",1],["3859.3409","0.194",1],["3859","5",1],["3858.827","0.0521",1],["3858.8208","0.001",1],["3858.679","0.26",1],["3858.4814","0.07477305",1],["3858.1669","1.03503422",1],["3857.6005","0.006",1],["3857.4005","0.004",1],["3857.2005","0.004",1],["3857.1871","1.218",1],["3857.0005","0.004",1],["3856.8135","0.0646",1],["3856.8005","0.004",1],["3856.2412","0.001",1],["3856.2349","1.03503422",1],["3856.0197","0.01037339",1],["3855.8781","0.23178117",1],["3855.8005","0.004",1],["3855.7165","0.00259355",1],["3855.4858","0.25875855",1],["3854.4584","0.01",1],["3853.6616","0.001",1],["3853.1373","0.92",1],["3852.5072","0.48599702",1],["3851.3926","0.13008333",1],["3851.082","0.001",1],["3850.9317","2",1],["3850.6359","0.34770165",1],["3850.2058","0.51751624",1],["3850.0823","0.15",1],["3850.0042","0.5175171",1],["3850","0.001",1],["3849.6325","1.8",1],["3849.41","0.3",1],["3848.9686","1.85",1],["3848.7426","0.18511466",1],["3848.52","0.3",1],["3848.5024","0.001",1],["3848.42","0.3",1],["3848.1618","2.204",1],["3847.77","0.3",1],["3847.48","0.3",1],["3847.3581","2.05",1],["3846.8259","0.0646",1],["3846.59","0.3",1],["3846.49","0.3",1],["3845.9228","0.001",1],["3844.184","0.00260133",1],["3844.0092","6.3",1],["3843.3432","0.001",1],["3841","0.06300963",1],["3840.7636","0.001",1],["3840","0.201",3],["3839.7681","18",1],["3839.5328","0.05214011",1],["3838.184","0.001",1],["3837.2344","0.27589557",1],["3836.6479","5.2",1],["3836","2.37196773",3],["3835.6044","0.001",1],["3833.6053","0.25873556",1],["3833.0248","0.001",1],["3833","0.8726502",1],["3832.6859","0.00260913",1],["3832","0.007",1],["3831.637","6",1],["3831.0602","0.001",1],["3830.4452","0.001",1],["3830","0.20375718",4],["3829.7125","0.07833486",1],["3829.6283","0.3519681",1],["3829","0.0039261",1],["3827.8656","0.001",1],["3826.0001","0.53251232",1],["3826","0.0509",1],["3825.7834","0.00698562",1],["3825.286","0.001",1],["3823.0001","0.03010127",1],["3822.8014","0.00261588",1],["3822.7064","0.001",1],["3822.2","1",1],["3822.1121","0.35994101",1],["3821.2222","0.00261696",1],["3821","0.001",1],["3820.1268","0.001",1],["3820","1.12992803",4],["3819","0.01331195",2],["3817.5472","0.001",1],["3816","1.13807184",2],["3815.8343","0.32463428",1],["3815.7834","0.00525295",1],["3815","28.99386799",4],["3814.9676","0.001",1],["3813","0.91303023",4],["3812.388","0.002",2],["3811.2257","0.07",1],["3810","0.32573997",2],["3809.8084","0.001",1],["3809.7928","0.00262481",1],["3807.2288","0.001",1],["3806.8421","0.07003461",1],["3806","0.19",1],["3805.8041","0.05678805",1],["3805","1.01",2],["3804.6492","0.001",1],["3804.3551","0.1",1],["3803","0.005",1],["3802.22","2.05042631",1],["3802.0696","0.001",1],["3802","1.63290092",1],["3801.2257","0.07",1],["3801","57.4",3],["3800.9853","0.02492278",1],["3800.8421","0.06503533",1],["3800.7844","0.02812628",1],["3800.0001","0.00409473",1],["3800","17.91401074",15],["3799.49","0.001",1],["3799","0.1",1],["3796.9104","0.001",1],["3796","9.00128053",2],["3795.5441","0.0028",1],["3794.3308","0.001",1],["3791","55",1],["3790.7777","0.07",1],["3790","12.03238184",7],["3789","1",1],["3788","0.21110454",2],["3787.2959","9",1],["3786.592","0.001",1],["3786","9.01916822",2],["3785","12.87914268",5],["3784.0124","0.001",1],["3781.4328","0.002",2],["3781","56.3",2],["3780.7777","0.07",1],["3780","23.41537654",10],["3778.8532","0.002",2],["3776","9",1],["3774","0.003",1],["3772.2481","0.06901672",1],["3771","55.1",2],["3770.7777","0.07",1],["3770","7.30268416",5],["3769","0.25",1],["3768","1.3725",3],["3766.66","0.02",1],["3766","7.64837924",2],["3765.58","1.22775492",1],["3762.58","1.22873383",1],["3761","51.68262164",1],["3760.8031","0.0399",1],["3760.7777","0.07",1]],"timestamp":"2019-03-06T23:19:17.705Z","checksum":-1785549915}]}` update := `{"table":"spot/depth","action":"update","data":[{"instrument_id":"BTC-USDT","asks":[["3864.6786","0",0],["3864.9852","0",0],["3865.9994","0.48402971",1],["3866.4004","0.001",1],["3866.7995","0.3273",2],["3867.4566","0",0],["3867.7031","0.025",1],["3868.0436","0",0],["3868.346","0",0],["3868.3695","0.051",1],["3870.9243","0.642",1],["3874.9942","0.51751796",1],["3875.7057","0",0],["3939","0.001",1]],"bids":[["3864.55","0.0565449",1],["3863.8282","0",0],["3863.8153","0",0],["3863.7898","0.01320077",1],["3863.4807","0.02112123",1],["3863.3002","0.04233533",1],["3863.1717","0.03379397",1],["3863.0685","0.04438179",1],["3863.0286","0.7362564",1],["3862.9912","0.06773651",1],["3862.8626","0.05407035",1],["3862.7595","0.07101087",1],["3862.313","0.3756",2],["3862.1848","0.012",1],["3862.0734","0",0],["3861.8391","0.025",1],["3861.7888","0",0],["3856.6716","0.38893641",1],["3768","0",0],["3766.66","0",0],["3766","0",0],["3765.58","0",0],["3762.58","0",0],["3761","0",0],["3760.8031","0",0],["3760.7777","0",0]],"timestamp":"2019-03-06T23:19:18.239Z","checksum":-1587788848}]}` var dataResponse okgroup.WebsocketDataResponse @@ -978,14 +965,9 @@ func TestOrderBookUpdateChecksumCalculator(t *testing.T) { // TestOrderBookUpdateChecksumCalculatorWithDash logic test func TestOrderBookUpdateChecksumCalculatorWith8DecimalPlaces(t *testing.T) { TestSetDefaults(t) - if o.WebsocketConn == nil { - o.Websocket.Shutdown() - err := setupWSConnection() - if err != nil { - t.Error(err) - } + if !websocketEnabled { + t.Skip("Websocket not enabled, skipping") } - disconnectFromWS() original := `{"table":"spot/depth","action":"partial","data":[{"instrument_id":"WAVES-BTC","asks":[["0.000714","1.15414979",1],["0.000715","3.3",2],["0.000717","426.71348",2],["0.000719","140.84507042",1],["0.00072","590.77",1],["0.000721","991.77",1],["0.000724","0.3532032",1],["0.000725","58.82698567",1],["0.000726","1033.15469748",2],["0.000729","0.35320321",1],["0.00073","352.77",1],["0.000735","0.38469748",1],["0.000736","625.77",1],["0.00075191","152.44796961",1],["0.00075192","114.3359772",1],["0.00075193","85.7519829",1],["0.00075194","64.31398718",1],["0.00075195","48.23549038",1],["0.00075196","36.17661779",1],["0.00075199","61.04804253",1],["0.0007591","70.71318474",1],["0.0007621","53.03488855",1],["0.00076211","39.77616642",1],["0.00076212","29.83212481",1],["0.0007635","22.37409361",1],["0.00076351","29.36599786",2],["0.00076352","9.43907074",1],["0.00076353","7.07930306",1],["0.00076354","14.15860612",1],["0.00076355","3.53965153",1],["0.00076369","3.53965153",1],["0.0008","34.36841101",1],["0.00082858","1.69936503",1],["0.00083232","2.8",1],["0.00084","15.69220129",1],["0.00085","4.42785042",1],["0.00088","0.1",1],["0.000891","0.1",1],["0.0009","12.41486491",2],["0.00093","5",1],["0.0012","12.31486492",1],["0.00531314","6.91803114",1],["0.00799999","0.02",1],["0.0084","0.05989",1],["0.00931314","5.18852336",1],["0.0799999","0.02",1],["0.499","6.00423396",1],["0.5","0.4995",1],["0.799999","0.02",1],["4.99","2",1],["5","3.98583144",1],["7.99999999","0.02",1],["79.99999999","0.02",1],["799.99999999","0.02986704",1]],"bids":[["0.000709","222.91679881",3],["0.000703","0.47161952",1],["0.000701","140.73015789",2],["0.0007","0.3",1],["0.000699","401",1],["0.000698","232.61801667",2],["0.000689","0.71396896",1],["0.000688","0.69910125",1],["0.000613","227.54771052",1],["0.0005","0.01",1],["0.00026789","3.69905341",1],["0.000238","2.4",1],["0.00022","0.53",1],["0.0000055","374.09871696",1],["0.00000056","222",1],["0.00000055","736.84761363",1],["0.0000002","999",1],["0.00000009","1222.22222417",1],["0.00000008","20868.64520447",1],["0.00000002","110000",1],["0.00000001","10000",1]],"timestamp":"2019-03-12T22:22:42.274Z","checksum":1319037905}]}` update := `{"table":"spot/depth","action":"update","data":[{"instrument_id":"WAVES-BTC","asks":[["0.000715","100.48199596",3],["0.000716","62.21679881",1]],"bids":[["0.000713","38.95772168",1]],"timestamp":"2019-03-12T22:22:42.938Z","checksum":-131160897}]}` var dataResponse okgroup.WebsocketDataResponse diff --git a/exchanges/okex/okex.go b/exchanges/okex/okex.go index 8e697d74..01f17834 100644 --- a/exchanges/okex/okex.go +++ b/exchanges/okex/okex.go @@ -79,7 +79,9 @@ func (o *OKEX) SetDefaults() { o.Websocket.Functionality = exchange.WebsocketTickerSupported | exchange.WebsocketTradeDataSupported | exchange.WebsocketKlineSupported | - exchange.WebsocketOrderbookSupported + exchange.WebsocketOrderbookSupported | + exchange.WebsocketSubscribeSupported | + exchange.WebsocketUnsubscribeSupported } // GetFuturesPostions Get the information of all holding positions in futures trading. @@ -252,7 +254,7 @@ func (o *OKEX) GetFuturesTokenInfoForCurrency(instrumentID string) (resp okgroup } // GetFuturesFilledOrder Get the recent 300 transactions of all contracts. Pagination is not supported here. -// The whole book will be returned for one request. WebSocket is recommended here. +// The whole book will be returned for one request. Websocket is recommended here. func (o *OKEX) GetFuturesFilledOrder(request okgroup.GetFuturesFilledOrderRequest) (resp []okgroup.GetFuturesFilledOrdersResponse, _ error) { requestURL := fmt.Sprintf("%v/%v/%v%v", okgroup.OKGroupInstruments, request.InstrumentID, okgroup.OKGroupTrades, okgroup.FormatParameters(request)) return resp, o.SendHTTPRequest(http.MethodGet, okGroupFuturesSubsection, requestURL, nil, &resp, true) diff --git a/exchanges/okex/okex_test.go b/exchanges/okex/okex_test.go index be3dd882..7b2d6d8f 100644 --- a/exchanges/okex/okex_test.go +++ b/exchanges/okex/okex_test.go @@ -2,7 +2,6 @@ package okex import ( "fmt" - "strings" "testing" "time" @@ -25,6 +24,7 @@ const ( var testSetupRan bool var o = OKEX{} var spotCurrency = currency.NewPairWithDelimiter(currency.BTC.String(), currency.USDT.String(), "-").Lower().String() +var websocketEnabled bool // TestSetDefaults Sets standard default settings for running a test func TestSetDefaults(t *testing.T) { @@ -62,7 +62,9 @@ func TestSetup(t *testing.T) { if err != nil { t.Fatalf("Test Failed - %v Setup() init error", OKGroupExchange) } - + if okexConfig.Websocket { + websocketEnabled = true + } okexConfig.AuthenticatedAPISupport = true okexConfig.APIKey = apiKey okexConfig.APISecret = apiSecret @@ -70,6 +72,7 @@ func TestSetup(t *testing.T) { okexConfig.WebsocketURL = o.WebsocketURL o.Setup(&okexConfig) testSetupRan = true + o.Websocket.DataHandler = make(chan interface{}, 999) } func areTestAPIKeysSet() bool { @@ -93,8 +96,11 @@ func testStandardErrorHandling(t *testing.T, err error) { func setupWSConnection() error { if !o.Websocket.IsEnabled() { err := o.WebsocketSetup(o.WsConnect, + o.Subscribe, + o.Unsubscribe, o.Name, true, + o.Verbose, o.WebsocketURL, o.WebsocketURL) o.Websocket.DataHandler = make(chan interface{}, 500) @@ -106,11 +112,6 @@ func setupWSConnection() error { return nil } -// disconnectFromWS disconnect to WS, but pass back error so test can handle it if needed -func disconnectFromWS() error { - return o.Websocket.Shutdown() -} - // TestGetAccountCurrencies API endpoint test func TestGetAccountCurrencies(t *testing.T) { TestSetDefaults(t) @@ -1578,16 +1579,11 @@ func TestGetETTSettlementPriceHistory(t *testing.T) { // TestWsLogin API endpoint test func TestWsLogin(t *testing.T) { TestSetRealOrderDefaults(t) - t.Parallel() - if o.WebsocketConn == nil { - o.Websocket.Shutdown() - err := setupWSConnection() - if err != nil { - t.Error(err) - } + if !websocketEnabled { + t.Skip("Websocket not enabled, skipping") } - if !o.Websocket.IsConnected() { - t.Skip("Could not connect to websocket. Skipping") + if !o.Websocket.IsConnecting() || !o.Websocket.IsConnected() { + o.Websocket.Connect() } err := o.WsLogin() if err != nil { @@ -1606,79 +1602,6 @@ func TestWsLogin(t *testing.T) { } } -// TestSubscribeToChannel API endpoint test -func TestSubscribeToChannel(t *testing.T) { - TestSetDefaults(t) - if o.WebsocketConn == nil { - o.Websocket.Shutdown() - err := setupWSConnection() - if err != nil { - t.Error(err) - } - } - if !o.Websocket.IsConnected() { - t.Skip("Could not connect to websocket. Skipping") - } - channelName := "spot/depth:LTC-BTC" - err := o.WsSubscribeToChannel(channelName) - if err != nil { - t.Error(err) - return - } - var errorReceived bool - for i := 0; i < 5; i++ { - response := <-o.Websocket.DataHandler - if err, ok := response.(error); ok && err != nil { - t.Log(response) - if strings.Contains(response.(error).Error(), channelName) { - errorReceived = true - } - } - } - - if errorReceived { - t.Error("Expecting subscription to channel") - } -} - -// TestSubscribeToNonExistantChannel Logic test -// Attempts to subscribe to a channel that doesn't exist -// Then captures the error response -func TestSubscribeToNonExistantChannel(t *testing.T) { - defer disconnectFromWS() - TestSetDefaults(t) - if o.WebsocketConn == nil { - o.Websocket.Shutdown() - err := setupWSConnection() - if err != nil { - t.Error(err) - } - } - if !o.Websocket.IsConnected() { - t.Skip("Could not connect to websocket. Skipping") - } - channelName := "badChannel" - err := o.WsSubscribeToChannel(channelName) - if err != nil { - t.Error(err) - return - } - var errorReceived bool - for i := 0; i < 7; i++ { - response := <-o.Websocket.DataHandler - if err, ok := response.(error); ok && err != nil { - t.Log(response) - if strings.Contains(response.(error).Error(), channelName) { - errorReceived = true - } - } - } - - if !errorReceived { - t.Error("Expecting OKEX error - 30040 message: Channel badChannel doesn't exist") - } -} - // TestGetAssetTypeFromTableName logic test func TestGetAssetTypeFromTableName(t *testing.T) { str := "spot/candle300s:BTC-USDT" @@ -1714,15 +1637,9 @@ func TestGetWsChannelWithoutOrderType(t *testing.T) { // TestOrderBookUpdateChecksumCalculator logic test func TestOrderBookUpdateChecksumCalculator(t *testing.T) { TestSetDefaults(t) - t.Parallel() - if o.WebsocketConn == nil { - o.Websocket.Shutdown() - err := setupWSConnection() - if err != nil { - t.Error(err) - } + if !websocketEnabled { + t.Skip("Websocket not enabled, skipping") } - disconnectFromWS() original := `{"table":"spot/depth","action":"partial","data":[{"instrument_id":"BTC-USDT","asks":[["3864.6786","0.145",1],["3864.7682","0.005",1],["3864.9851","0.57",1],["3864.9852","0.30137754",1],["3864.9986","2.81818419",1],["3864.9995","0.002",1],["3865","0.0597",1],["3865.0309","0.4",1],["3865.1995","0.004",1],["3865.3995","0.004",1],["3865.5995","0.004",1],["3865.7995","0.004",1],["3865.9995","0.004",1],["3866.0961","0.25865886",1],["3866.1995","0.004",1],["3866.3995","0.004",1],["3866.4004","0.3243",2],["3866.5995","0.004",1],["3866.7633","0.44247086",1],["3866.7995","0.004",1],["3866.9197","0.511",1],["3867.256","0.51716256",1],["3867.3951","0.02588112",1],["3867.4014","0.025",1],["3867.4566","0.02499999",1],["3867.4675","4.01155057",5],["3867.5515","1.1",1],["3867.6113","0.009",1],["3867.7349","0.026",1],["3867.7781","0.03738652",1],["3867.9163","0.0521",1],["3868.0381","0.34354941",1],["3868.0436","0.051",1],["3868.0657","0.90552172",3],["3868.1819","0.03863346",1],["3868.2013","0.194",1],["3868.346","0.051",1],["3868.3863","0.01155",1],["3868.7716","0.009",1],["3868.947","0.025",1],["3868.98","0.001",1],["3869.0764","1.03487931",1],["3869.2773","0.07724578",1],["3869.4039","0.025",1],["3869.4068","1.03",1],["3869.7068","2.06976398",1],["3870","0.5",1],["3870.0465","0.01",1],["3870.7042","0.02099651",1],["3870.9451","2.07047375",1],["3871.5254","1.2",1],["3871.5596","0.001",1],["3871.6605","0.01035032",1],["3871.7179","2.07047375",1],["3871.8816","0.51751625",1],["3872.1","0.75",1],["3872.2464","0.0646",1],["3872.3747","0.283",1],["3872.4039","0.2",1],["3872.7655","0.23179307",1],["3872.8005","2.06976398",1],["3873.1509","2",1],["3873.3215","0.26",1],["3874.1392","0.001",1],["3874.1487","3.88224364",4],["3874.1685","1.8",1],["3874.5571","0.08974762",1],["3874.734","2.06976398",1],["3874.99","0.3",1],["3875","1.001",2],["3875.0041","1.03505051",1],["3875.45","0.3",1],["3875.4766","0.15",1],["3875.7057","0.51751625",1],["3876","0.001",1],["3876.68","0.3",1],["3876.7188","0.001",1],["3877","0.75",1],["3877.31","0.035",1],["3877.38","0.3",1],["3877.7","0.3",1],["3877.88","0.3",1],["3878.0364","0.34770122",1],["3878.4525","0.48579748",1],["3878.4955","0.02812511",1],["3878.8855","0.00258579",1],["3878.9605","0.895",1],["3879","0.001",1],["3879.2984","0.002",2],["3879.432","0.001",1],["3879.6313","6",1],["3879.9999","0.002",2],["3880","1.25132834",5],["3880.2526","0.04075162",1],["3880.7145","0.0647",1],["3881.2469","1.883",1],["3881.878","0.002",2],["3884.4576","0.002",2],["3885","0.002",2],["3885.2233","0.28304103",1],["3885.7416","18",1],["3886","0.001",1],["3886.1554","5.4",1],["3887","0.001",1],["3887.0372","0.002",2],["3887.2559","0.05214011",1],["3887.9238","0.0019",1],["3888","0.15810538",4],["3889","0.001",1],["3889.5175","0.50510653",1],["3889.6168","0.002",2],["3889.9999","0.001",1],["3890","2.34968109",4],["3890.5222","0.00257806",1],["3891.2659","5",1],["3891.9999","0.00893897",1],["3892.1964","0.002",2],["3892.4358","0.0176",1],["3893.1388","1.4279",1],["3894","0.0026321",1],["3894.776","0.001",1],["3895","1.501",2],["3895.379","0.25881288",1],["3897","0.05",1],["3897.3556","0.001",1],["3897.8432","0.73708079",1],["3898","3.31353018",7],["3898.4462","4.757",1],["3898.6","0.47159638",1],["3898.8769","0.0129",1],["3899","6",2],["3899.6516","0.025",1],["3899.9352","0.001",1],["3899.9999","0.013",2],["3900","22.37447743",24],["3900.9999","0.07763916",1],["3901","0.10192487",1],["3902.1937","0.00257034",1],["3902.3991","1.5532141",1],["3902.5148","0.001",1],["3904","1.49331984",1],["3904.9999","0.95905447",1],["3905","0.501",2],["3905.0944","0.001",1],["3905.61","0.099",1],["3905.6801","0.54343686",1],["3906.2901","0.0258",1],["3907.674","0.001",1],["3907.85","1.35778084",1],["3908","0.03846153",1],["3908.23","1.95189531",1],["3908.906","0.03148978",1],["3909","0.001",1],["3909.9999","0.01398721",2],["3910","0.016",2],["3910.2536","0.001",1],["3912.5406","0.88270517",1],["3912.8332","0.001",1],["3913","1.2640608",1],["3913.87","1.69114184",1],["3913.9003","0.00256266",1],["3914","1.21766411",1],["3915","0.001",1],["3915.4128","0.001",1],["3915.7425","6.848",1],["3916","0.0050949",1],["3917.36","1.28658296",1],["3917.9924","0.001",1],["3919","0.001",1],["3919.9999","0.001",1],["3920","1.21171832",3],["3920.0002","0.20217038",1],["3920.572","0.001",1],["3921","0.128",1],["3923.0756","0.00148064",1],["3923.1516","0.001",1],["3923.86","1.38831714",1],["3925","0.01867801",2],["3925.642","0.00255499",1],["3925.7312","0.001",1],["3926","0.04290757",1],["3927","0.023",1],["3927.3175","0.01212865",1],["3927.65","1.51375612",1],["3928","0.5",1],["3928.3108","0.001",1],["3929","0.001",1],["3929.9999","0.01519338",2],["3930","0.0174985",3],["3930.21","1.49335799",1],["3930.8904","0.001",1],["3932.2999","0.01953",1],["3932.8962","7.96",1],["3933.0387","11.808",1],["3933.47","0.001",1],["3934","1.40839932",1],["3935","0.001",1],["3936.8","0.62879518",1],["3937.23","1.56977841",1],["3937.4189","0.00254735",1]],"bids":[["3864.5217","0.00540709",1],["3864.5216","0.14068758",2],["3864.2275","0.01033576",1],["3864.0989","0.00825047",1],["3864.0273","0.38",1],["3864.0272","0.4",1],["3863.9957","0.01083539",1],["3863.9184","0.01653723",1],["3863.8282","0.25588165",1],["3863.8153","0.154",1],["3863.7791","1.14122492",1],["3863.6866","0.01733662",1],["3863.6093","0.02645958",1],["3863.3775","0.02773862",1],["3863.0297","0.513",1],["3863.0286","1.1028564",2],["3862.8489","0.01",1],["3862.5972","0.01890179",1],["3862.3431","0.01152944",1],["3862.313","0.009",1],["3862.2445","0.90551002",3],["3862.0734","0.014",1],["3862.0539","0.64976067",1],["3861.8586","0.025",1],["3861.7888","0.025",1],["3861.7673","0.008",1],["3861.5785","0.01",1],["3861.3895","0.005",1],["3861.3338","0.25875855",1],["3861.161","0.01",1],["3861.1111","0.03863352",1],["3861.0732","0.51703882",1],["3860.9116","0.17754895",1],["3860.75","0.19",1],["3860.6554","0.015",1],["3860.6172","0.005",1],["3860.6088","0.008",1],["3860.4724","0.12940042",1],["3860.4424","0.25880084",1],["3860.42","0.01",1],["3860.3725","0.51760102",1],["3859.8449","0.005",1],["3859.8285","0.03738652",1],["3859.7638","0.07726703",1],["3859.4502","0.008",1],["3859.3772","0.05173471",1],["3859.3409","0.194",1],["3859","5",1],["3858.827","0.0521",1],["3858.8208","0.001",1],["3858.679","0.26",1],["3858.4814","0.07477305",1],["3858.1669","1.03503422",1],["3857.6005","0.006",1],["3857.4005","0.004",1],["3857.2005","0.004",1],["3857.1871","1.218",1],["3857.0005","0.004",1],["3856.8135","0.0646",1],["3856.8005","0.004",1],["3856.2412","0.001",1],["3856.2349","1.03503422",1],["3856.0197","0.01037339",1],["3855.8781","0.23178117",1],["3855.8005","0.004",1],["3855.7165","0.00259355",1],["3855.4858","0.25875855",1],["3854.4584","0.01",1],["3853.6616","0.001",1],["3853.1373","0.92",1],["3852.5072","0.48599702",1],["3851.3926","0.13008333",1],["3851.082","0.001",1],["3850.9317","2",1],["3850.6359","0.34770165",1],["3850.2058","0.51751624",1],["3850.0823","0.15",1],["3850.0042","0.5175171",1],["3850","0.001",1],["3849.6325","1.8",1],["3849.41","0.3",1],["3848.9686","1.85",1],["3848.7426","0.18511466",1],["3848.52","0.3",1],["3848.5024","0.001",1],["3848.42","0.3",1],["3848.1618","2.204",1],["3847.77","0.3",1],["3847.48","0.3",1],["3847.3581","2.05",1],["3846.8259","0.0646",1],["3846.59","0.3",1],["3846.49","0.3",1],["3845.9228","0.001",1],["3844.184","0.00260133",1],["3844.0092","6.3",1],["3843.3432","0.001",1],["3841","0.06300963",1],["3840.7636","0.001",1],["3840","0.201",3],["3839.7681","18",1],["3839.5328","0.05214011",1],["3838.184","0.001",1],["3837.2344","0.27589557",1],["3836.6479","5.2",1],["3836","2.37196773",3],["3835.6044","0.001",1],["3833.6053","0.25873556",1],["3833.0248","0.001",1],["3833","0.8726502",1],["3832.6859","0.00260913",1],["3832","0.007",1],["3831.637","6",1],["3831.0602","0.001",1],["3830.4452","0.001",1],["3830","0.20375718",4],["3829.7125","0.07833486",1],["3829.6283","0.3519681",1],["3829","0.0039261",1],["3827.8656","0.001",1],["3826.0001","0.53251232",1],["3826","0.0509",1],["3825.7834","0.00698562",1],["3825.286","0.001",1],["3823.0001","0.03010127",1],["3822.8014","0.00261588",1],["3822.7064","0.001",1],["3822.2","1",1],["3822.1121","0.35994101",1],["3821.2222","0.00261696",1],["3821","0.001",1],["3820.1268","0.001",1],["3820","1.12992803",4],["3819","0.01331195",2],["3817.5472","0.001",1],["3816","1.13807184",2],["3815.8343","0.32463428",1],["3815.7834","0.00525295",1],["3815","28.99386799",4],["3814.9676","0.001",1],["3813","0.91303023",4],["3812.388","0.002",2],["3811.2257","0.07",1],["3810","0.32573997",2],["3809.8084","0.001",1],["3809.7928","0.00262481",1],["3807.2288","0.001",1],["3806.8421","0.07003461",1],["3806","0.19",1],["3805.8041","0.05678805",1],["3805","1.01",2],["3804.6492","0.001",1],["3804.3551","0.1",1],["3803","0.005",1],["3802.22","2.05042631",1],["3802.0696","0.001",1],["3802","1.63290092",1],["3801.2257","0.07",1],["3801","57.4",3],["3800.9853","0.02492278",1],["3800.8421","0.06503533",1],["3800.7844","0.02812628",1],["3800.0001","0.00409473",1],["3800","17.91401074",15],["3799.49","0.001",1],["3799","0.1",1],["3796.9104","0.001",1],["3796","9.00128053",2],["3795.5441","0.0028",1],["3794.3308","0.001",1],["3791","55",1],["3790.7777","0.07",1],["3790","12.03238184",7],["3789","1",1],["3788","0.21110454",2],["3787.2959","9",1],["3786.592","0.001",1],["3786","9.01916822",2],["3785","12.87914268",5],["3784.0124","0.001",1],["3781.4328","0.002",2],["3781","56.3",2],["3780.7777","0.07",1],["3780","23.41537654",10],["3778.8532","0.002",2],["3776","9",1],["3774","0.003",1],["3772.2481","0.06901672",1],["3771","55.1",2],["3770.7777","0.07",1],["3770","7.30268416",5],["3769","0.25",1],["3768","1.3725",3],["3766.66","0.02",1],["3766","7.64837924",2],["3765.58","1.22775492",1],["3762.58","1.22873383",1],["3761","51.68262164",1],["3760.8031","0.0399",1],["3760.7777","0.07",1]],"timestamp":"2019-03-06T23:19:17.705Z","checksum":-1785549915}]}` update := `{"table":"spot/depth","action":"update","data":[{"instrument_id":"BTC-USDT","asks":[["3864.6786","0",0],["3864.9852","0",0],["3865.9994","0.48402971",1],["3866.4004","0.001",1],["3866.7995","0.3273",2],["3867.4566","0",0],["3867.7031","0.025",1],["3868.0436","0",0],["3868.346","0",0],["3868.3695","0.051",1],["3870.9243","0.642",1],["3874.9942","0.51751796",1],["3875.7057","0",0],["3939","0.001",1]],"bids":[["3864.55","0.0565449",1],["3863.8282","0",0],["3863.8153","0",0],["3863.7898","0.01320077",1],["3863.4807","0.02112123",1],["3863.3002","0.04233533",1],["3863.1717","0.03379397",1],["3863.0685","0.04438179",1],["3863.0286","0.7362564",1],["3862.9912","0.06773651",1],["3862.8626","0.05407035",1],["3862.7595","0.07101087",1],["3862.313","0.3756",2],["3862.1848","0.012",1],["3862.0734","0",0],["3861.8391","0.025",1],["3861.7888","0",0],["3856.6716","0.38893641",1],["3768","0",0],["3766.66","0",0],["3766","0",0],["3765.58","0",0],["3762.58","0",0],["3761","0",0],["3760.8031","0",0],["3760.7777","0",0]],"timestamp":"2019-03-06T23:19:18.239Z","checksum":-1587788848}]}` var dataResponse okgroup.WebsocketDataResponse @@ -1750,15 +1667,9 @@ func TestOrderBookUpdateChecksumCalculator(t *testing.T) { // TestOrderBookUpdateChecksumCalculatorWithDash logic test func TestOrderBookUpdateChecksumCalculatorWith8DecimalPlaces(t *testing.T) { TestSetDefaults(t) - t.Parallel() - if o.WebsocketConn == nil { - o.Websocket.Shutdown() - err := setupWSConnection() - if err != nil { - t.Error(err) - } + if !websocketEnabled { + t.Skip("Websocket not enabled, skipping") } - disconnectFromWS() original := `{"table":"spot/depth","action":"partial","data":[{"instrument_id":"WAVES-BTC","asks":[["0.000714","1.15414979",1],["0.000715","3.3",2],["0.000717","426.71348",2],["0.000719","140.84507042",1],["0.00072","590.77",1],["0.000721","991.77",1],["0.000724","0.3532032",1],["0.000725","58.82698567",1],["0.000726","1033.15469748",2],["0.000729","0.35320321",1],["0.00073","352.77",1],["0.000735","0.38469748",1],["0.000736","625.77",1],["0.00075191","152.44796961",1],["0.00075192","114.3359772",1],["0.00075193","85.7519829",1],["0.00075194","64.31398718",1],["0.00075195","48.23549038",1],["0.00075196","36.17661779",1],["0.00075199","61.04804253",1],["0.0007591","70.71318474",1],["0.0007621","53.03488855",1],["0.00076211","39.77616642",1],["0.00076212","29.83212481",1],["0.0007635","22.37409361",1],["0.00076351","29.36599786",2],["0.00076352","9.43907074",1],["0.00076353","7.07930306",1],["0.00076354","14.15860612",1],["0.00076355","3.53965153",1],["0.00076369","3.53965153",1],["0.0008","34.36841101",1],["0.00082858","1.69936503",1],["0.00083232","2.8",1],["0.00084","15.69220129",1],["0.00085","4.42785042",1],["0.00088","0.1",1],["0.000891","0.1",1],["0.0009","12.41486491",2],["0.00093","5",1],["0.0012","12.31486492",1],["0.00531314","6.91803114",1],["0.00799999","0.02",1],["0.0084","0.05989",1],["0.00931314","5.18852336",1],["0.0799999","0.02",1],["0.499","6.00423396",1],["0.5","0.4995",1],["0.799999","0.02",1],["4.99","2",1],["5","3.98583144",1],["7.99999999","0.02",1],["79.99999999","0.02",1],["799.99999999","0.02986704",1]],"bids":[["0.000709","222.91679881",3],["0.000703","0.47161952",1],["0.000701","140.73015789",2],["0.0007","0.3",1],["0.000699","401",1],["0.000698","232.61801667",2],["0.000689","0.71396896",1],["0.000688","0.69910125",1],["0.000613","227.54771052",1],["0.0005","0.01",1],["0.00026789","3.69905341",1],["0.000238","2.4",1],["0.00022","0.53",1],["0.0000055","374.09871696",1],["0.00000056","222",1],["0.00000055","736.84761363",1],["0.0000002","999",1],["0.00000009","1222.22222417",1],["0.00000008","20868.64520447",1],["0.00000002","110000",1],["0.00000001","10000",1]],"timestamp":"2019-03-12T22:22:42.274Z","checksum":1319037905}]}` update := `{"table":"spot/depth","action":"update","data":[{"instrument_id":"WAVES-BTC","asks":[["0.000715","100.48199596",3],["0.000716","62.21679881",1]],"bids":[["0.000713","38.95772168",1]],"timestamp":"2019-03-12T22:22:42.938Z","checksum":-131160897}]}` var dataResponse okgroup.WebsocketDataResponse diff --git a/exchanges/okgroup/okgroup.go b/exchanges/okgroup/okgroup.go index 0a0c9605..bc6f63e1 100644 --- a/exchanges/okgroup/okgroup.go +++ b/exchanges/okgroup/okgroup.go @@ -88,7 +88,7 @@ type OKGroup struct { exchange.Base ExchangeName string WebsocketConn *websocket.Conn - mu sync.Mutex + wsRequestMtx sync.Mutex // Spot and contract market error codes as per https://www.okex.com/rest_request.html ErrorCodes map[string]error // Stores for corresponding variable checks @@ -141,8 +141,11 @@ func (o *OKGroup) Setup(exch *config.ExchangeConfig) { log.Fatal(err) } err = o.WebsocketSetup(o.WsConnect, + o.Subscribe, + o.Unsubscribe, exch.Name, exch.Websocket, + exch.Verbose, o.WebsocketURL, exch.WebsocketURL) if err != nil { @@ -359,7 +362,7 @@ func (o *OKGroup) GetSpotTokenPairDetails() (resp []GetSpotTokenPairDetailsRespo } // GetSpotOrderBook Getting the order book of a trading pair. Pagination is not supported here. -// The whole book will be returned for one request. WebSocket is recommended here. +// The whole book will be returned for one request. Websocket is recommended here. func (o *OKGroup) GetSpotOrderBook(request GetSpotOrderBookRequest) (resp GetSpotOrderBookResponse, _ error) { requestURL := fmt.Sprintf("%v/%v/%v%v", OKGroupInstruments, request.InstrumentID, OKGroupGetSpotOrderBook, FormatParameters(request)) return resp, o.SendHTTPRequest(http.MethodGet, okGroupTokenSubsection, requestURL, nil, &resp, false) diff --git a/exchanges/okgroup/okgroup_websocket.go b/exchanges/okgroup/okgroup_websocket.go index d86c5ab6..88280ddf 100644 --- a/exchanges/okgroup/okgroup_websocket.go +++ b/exchanges/okgroup/okgroup_websocket.go @@ -143,18 +143,23 @@ const ( okGroupWsFuturesAccount = okGroupWsFuturesSubsection + okGroupWsAccount okGroupWsFuturesPosition = okGroupWsFuturesSubsection + okGroupWsPosition okGroupWsFuturesOrder = okGroupWsFuturesSubsection + okGroupWsOrder + + okGroupWsRateLimit = 30 * time.Millisecond ) // orderbookMutex Ensures if two entries arrive at once, only one can be processed at a time var orderbookMutex sync.Mutex +var defaultSubscribedChannels = []string{okGroupWsSpotDepth, okGroupWsSpotCandle300s, okGroupWsSpotTicker, okGroupWsSpotTrade} // writeToWebsocket sends a message to the websocket endpoint func (o *OKGroup) writeToWebsocket(message string) error { - o.mu.Lock() - defer o.mu.Unlock() + o.wsRequestMtx.Lock() + defer o.wsRequestMtx.Unlock() if o.Verbose { - log.Debugf("Sending message to WS: %v", message) + log.Debugf("%v sending message to WS: %v", o.Name, message) } + // Really basic WS rate limit + time.Sleep(okGroupWsRateLimit) return o.WebsocketConn.WriteMessage(websocket.TextMessage, []byte(message)) } @@ -165,13 +170,11 @@ func (o *OKGroup) WsConnect() error { } var dialer websocket.Dialer - if o.Websocket.GetProxyAddress() != "" { proxy, err := url.Parse(o.Websocket.GetProxyAddress()) if err != nil { return err } - dialer.Proxy = http.ProxyURL(proxy) } @@ -187,41 +190,20 @@ func (o *OKGroup) WsConnect() error { err) } if o.Verbose { - log.Debugf("Successful connection to %v", o.Websocket.GetWebsocketURL()) + log.Debugf("Successful connection to %v", + o.Websocket.GetWebsocketURL()) } - - var wg sync.WaitGroup + wg := sync.WaitGroup{} wg.Add(2) go o.WsHandleData(&wg) go o.wsPingHandler(&wg) - - err = o.WsSubscribeToDefaults() - if err != nil { - return fmt.Errorf("error: Could not subscribe to the OKEX websocket %s", - err) - } + o.GenerateDefaultSubscriptions() // Ensures that we start the routines and we dont race when shutdown occurs wg.Wait() return nil } -// WsSubscribeToDefaults subscribes to the websocket channels -func (o *OKGroup) WsSubscribeToDefaults() (err error) { - channelsToSubscribe := []string{okGroupWsSpotDepth, okGroupWsSpotCandle300s, okGroupWsSpotTicker, okGroupWsSpotTrade} - for _, pair := range o.EnabledPairs { - formattedPair := strings.ToUpper(strings.Replace(pair.String(), "_", "-", 1)) - for _, channel := range channelsToSubscribe { - err = o.WsSubscribeToChannel(fmt.Sprintf("%v:%s", channel, formattedPair)) - if err != nil { - return - } - } - } - - return nil -} - // WsReadData reads data from the websocket connection func (o *OKGroup) WsReadData() (exchange.WebsocketResponse, error) { mType, resp, err := o.WebsocketConn.ReadMessage() @@ -255,7 +237,7 @@ func (o *OKGroup) wsPingHandler(wg *sync.WaitGroup) { o.Websocket.Wg.Add(1) defer o.Websocket.Wg.Done() - ticker := time.NewTicker(time.Second * 27) + ticker := time.NewTicker(time.Second * 10) wg.Done() @@ -271,7 +253,6 @@ func (o *OKGroup) wsPingHandler(wg *sync.WaitGroup) { } if err != nil { o.Websocket.DataHandler <- err - return } } } @@ -281,11 +262,6 @@ func (o *OKGroup) wsPingHandler(wg *sync.WaitGroup) { func (o *OKGroup) WsHandleData(wg *sync.WaitGroup) { o.Websocket.Wg.Add(1) defer func() { - err := o.WebsocketConn.Close() - if err != nil { - o.Websocket.DataHandler <- fmt.Errorf("okex_websocket.go - Unable to to close Websocket connection. Error: %s", - err) - } o.Websocket.Wg.Done() }() @@ -295,11 +271,12 @@ func (o *OKGroup) WsHandleData(wg *sync.WaitGroup) { select { case <-o.Websocket.ShutdownC: return + default: resp, err := o.WsReadData() if err != nil { + time.Sleep(time.Second) o.Websocket.DataHandler <- err - return } var dataResponse WebsocketDataResponse err = common.JSONDecode(resp.Raw, &dataResponse) @@ -326,46 +303,10 @@ func (o *OKGroup) WsHandleData(wg *sync.WaitGroup) { } continue } - o.Websocket.DataHandler <- fmt.Errorf("unrecognised response: %v", resp.Raw) - continue } } } -// WsSubscribeToChannel sends a request to WS to subscribe to supplied channel -func (o *OKGroup) WsSubscribeToChannel(topic string) error { - resp := WebsocketEventRequest{ - Operation: "subscribe", - Arguments: []string{topic}, - } - json, err := common.JSONEncode(resp) - if err != nil { - return err - } - err = o.writeToWebsocket(string(json)) - if err != nil { - return err - } - return nil -} - -// WsUnsubscribeToChannel sends a request to WS to unsubscribe to supplied channel -func (o *OKGroup) WsUnsubscribeToChannel(topic string) error { - resp := WebsocketEventRequest{ - Operation: "unsubscribe", - Arguments: []string{topic}, - } - json, err := common.JSONEncode(resp) - if err != nil { - return err - } - err = o.writeToWebsocket(string(json)) - if err != nil { - return err - } - return nil -} - // WsLogin sends a login request to websocket to enable access to authenticated endpoints func (o *OKGroup) WsLogin() error { utcTime := time.Now().UTC() @@ -440,9 +381,12 @@ func (o *OKGroup) WsHandleDataResponse(response *WebsocketDataResponse) { orderbookMutex.Lock() err := o.WsProcessOrderBook(response) if err != nil { - log.Error(err) - subscriptionChannel := fmt.Sprintf("%v:%v", response.Table, response.Data[0].InstrumentID) - o.ResubscribeToChannel(subscriptionChannel) + pair := currency.NewPairDelimiter(response.Data[0].InstrumentID, "-") + channelToResubscribe := exchange.WebsocketChannelSubscription{ + Channel: response.Table, + Currency: pair, + } + o.Websocket.ResubscribeToChannel(channelToResubscribe) } orderbookMutex.Unlock() case okGroupWsTicker: @@ -460,42 +404,6 @@ func (o *OKGroup) WsHandleDataResponse(response *WebsocketDataResponse) { } } -// ResubscribeToChannel will attempt to unsubscribe and resubscribe to a channel -func (o *OKGroup) ResubscribeToChannel(channel string) { - if okGroupWsResubscribeFailureLimit > 0 { - var successfulUnsubscribe bool - for i := 0; i < okGroupWsResubscribeFailureLimit; i++ { - err := o.WsUnsubscribeToChannel(channel) - if err != nil { - log.Error(err) - time.Sleep(okGroupWsResubscribeDelayInSeconds * time.Second) - continue - } - successfulUnsubscribe = true - break - } - if !successfulUnsubscribe { - log.Fatalf("%v websocket channel %v failed to unsubscribe after %v attempts", o.GetName(), channel, okGroupWsResubscribeFailureLimit) - } - successfulSubscribe := true - for i := 0; i < okGroupWsResubscribeFailureLimit; i++ { - err := o.WsSubscribeToChannel(channel) - if err != nil { - log.Error(err) - time.Sleep(okGroupWsResubscribeDelayInSeconds * time.Second) - continue - } - successfulSubscribe = true - break - } - if !successfulSubscribe { - log.Fatalf("%v websocket channel %v failed to resubscribe after %v attempts", o.GetName(), channel, okGroupWsResubscribeFailureLimit) - } - } else { - log.Fatalf("%v websocket channel %v cannot resubscribe. Limit: %v", o.GetName(), channel, okGroupWsResubscribeFailureLimit) - } -} - // logDataResponse will log the details of any websocket data event // where there is no websocket datahandler for it func logDataResponse(response *WebsocketDataResponse) { @@ -767,3 +675,51 @@ func (o *OKGroup) CalculateUpdateOrderbookChecksum(orderbookData *orderbook.Base checksum = strings.TrimSuffix(checksum, ":") return int32(crc32.ChecksumIEEE([]byte(checksum))) } + +// GenerateDefaultSubscriptions Adds default subscriptions to websocket to be handled by ManageSubscriptions() +func (o *OKGroup) GenerateDefaultSubscriptions() { + enabledCurrencies := o.GetEnabledCurrencies() + subscriptions := []exchange.WebsocketChannelSubscription{} + for i := range defaultSubscribedChannels { + for j := range enabledCurrencies { + enabledCurrencies[j].Delimiter = "-" + subscriptions = append(subscriptions, exchange.WebsocketChannelSubscription{ + Channel: defaultSubscribedChannels[i], + Currency: enabledCurrencies[j], + }) + } + } + o.Websocket.SubscribeToChannels(subscriptions) +} + +// Subscribe sends a websocket message to receive data from the channel +func (o *OKGroup) Subscribe(channelToSubscribe exchange.WebsocketChannelSubscription) error { + resp := WebsocketEventRequest{ + Operation: "subscribe", + Arguments: []string{fmt.Sprintf("%v:%v", channelToSubscribe.Channel, channelToSubscribe.Currency.String())}, + } + json, err := common.JSONEncode(resp) + if err != nil { + if o.Verbose { + log.Debugf("%v subscribe error: %v", o.Name, err) + } + return err + } + return o.writeToWebsocket(string(json)) +} + +// Unsubscribe sends a websocket message to stop receiving data from the channel +func (o *OKGroup) Unsubscribe(channelToSubscribe exchange.WebsocketChannelSubscription) error { + resp := WebsocketEventRequest{ + Operation: "unsubscribe", + Arguments: []string{fmt.Sprintf("%v:%v", channelToSubscribe.Channel, channelToSubscribe.Currency.String())}, + } + json, err := common.JSONEncode(resp) + if err != nil { + if o.Verbose { + log.Debugf("%v unsubscribe error: %v", o.Name, err) + } + return err + } + return o.writeToWebsocket(string(json)) +} diff --git a/exchanges/okgroup/okgroup_wrapper.go b/exchanges/okgroup/okgroup_wrapper.go index 1465360d..b74cf7fd 100644 --- a/exchanges/okgroup/okgroup_wrapper.go +++ b/exchanges/okgroup/okgroup_wrapper.go @@ -429,3 +429,17 @@ func (o *OKGroup) GetFeeByType(feeBuilder *exchange.FeeBuilder) (float64, error) func (o *OKGroup) GetWithdrawCapabilities() uint32 { return o.GetWithdrawPermissions() } + +// SubscribeToWebsocketChannels appends to ChannelsToSubscribe +// which lets websocket.manageSubscriptions handle subscribing +func (o *OKGroup) SubscribeToWebsocketChannels(channels []exchange.WebsocketChannelSubscription) error { + o.Websocket.SubscribeToChannels(channels) + return nil +} + +// UnsubscribeToWebsocketChannels removes from ChannelsToSubscribe +// which lets websocket.manageSubscriptions handle unsubscribing +func (o *OKGroup) UnsubscribeToWebsocketChannels(channels []exchange.WebsocketChannelSubscription) error { + o.Websocket.UnsubscribeToChannels(channels) + return nil +} diff --git a/exchanges/poloniex/poloniex.go b/exchanges/poloniex/poloniex.go index e2e1f1b9..3d0b8e0d 100644 --- a/exchanges/poloniex/poloniex.go +++ b/exchanges/poloniex/poloniex.go @@ -8,6 +8,7 @@ import ( "net/http" "net/url" "strconv" + "sync" "time" "github.com/gorilla/websocket" @@ -62,6 +63,7 @@ const ( type Poloniex struct { exchange.Base WebsocketConn *websocket.Conn + wsRequestMtx sync.Mutex } // SetDefaults sets default settings for poloniex @@ -89,7 +91,9 @@ func (p *Poloniex) SetDefaults() { p.WebsocketInit() p.Websocket.Functionality = exchange.WebsocketTradeDataSupported | exchange.WebsocketOrderbookSupported | - exchange.WebsocketTickerSupported + exchange.WebsocketTickerSupported | + exchange.WebsocketSubscribeSupported | + exchange.WebsocketUnsubscribeSupported } // Setup sets user exchange configuration settings @@ -130,8 +134,11 @@ func (p *Poloniex) Setup(exch *config.ExchangeConfig) { log.Fatal(err) } err = p.WebsocketSetup(p.WsConnect, + p.Subscribe, + p.Unsubscribe, exch.Name, exch.Websocket, + exch.Verbose, poloniexWebsocketAddress, exch.WebsocketURL) if err != nil { diff --git a/exchanges/poloniex/poloniex_websocket.go b/exchanges/poloniex/poloniex_websocket.go index 38e51d57..40e6c83c 100644 --- a/exchanges/poloniex/poloniex_websocket.go +++ b/exchanges/poloniex/poloniex_websocket.go @@ -6,6 +6,7 @@ import ( "net/http" "net/url" "strconv" + "strings" "time" "github.com/gorilla/websocket" @@ -65,41 +66,8 @@ func (p *Poloniex) WsConnect() error { } go p.WsHandleData() + p.GenerateDefaultSubscriptions() - return p.WsSubscribe() -} - -// WsSubscribe subscribes to the websocket feeds -func (p *Poloniex) WsSubscribe() error { - tickerJSON, err := common.JSONEncode(WsCommand{ - Command: "subscribe", - Channel: wsTickerDataID}) - if err != nil { - return err - } - - err = p.WebsocketConn.WriteMessage(websocket.TextMessage, tickerJSON) - if err != nil { - return err - } - - pairs := p.GetEnabledCurrencies() - for _, nextPair := range pairs { - fPair := exchange.FormatExchangeCurrency(p.GetName(), nextPair) - - orderbookJSON, err := common.JSONEncode(WsCommand{ - Command: "subscribe", - Channel: fPair.String(), - }) - if err != nil { - return err - } - - err = p.WebsocketConn.WriteMessage(websocket.TextMessage, orderbookJSON) - if err != nil { - return err - } - } return nil } @@ -129,11 +97,6 @@ func (p *Poloniex) WsHandleData() { p.Websocket.Wg.Add(1) defer func() { - err := p.WebsocketConn.Close() - if err != nil { - p.Websocket.DataHandler <- fmt.Errorf("poloniex_websocket.go - Unable to to close Websocket connection. Error: %s", - err) - } p.Websocket.Wg.Done() }() @@ -155,119 +118,122 @@ func (p *Poloniex) WsHandleData() { p.Websocket.DataHandler <- err continue } - - data := result.([]interface{}) - chanID := int(data[0].(float64)) - - if len(data) == 2 && chanID != wsHeartbeat { - if checkSubscriptionSuccess(data) { - if p.Verbose { - log.Debugf("poloniex websocket subscribed to channel successfully. %d", chanID) + switch data := result.(type) { + case map[string]interface{}: + // subscription error + p.Websocket.DataHandler <- errors.New(data["error"].(string)) + case []interface{}: + chanID := int(data[0].(float64)) + if len(data) == 2 && chanID != wsHeartbeat { + if checkSubscriptionSuccess(data) { + if p.Verbose { + log.Debugf("poloniex websocket subscribed to channel successfully. %d", chanID) + } + } else { + if p.Verbose { + log.Debugf("poloniex websocket subscription to channel failed. %d", chanID) + } } - } else { - if p.Verbose { - log.Debugf("poloniex websocket subscription to channel failed. %d", chanID) + continue + } + + switch chanID { + case wsAccountNotificationID: + case wsTickerDataID: + tickerData := data[2].([]interface{}) + var t WsTicker + t.LastPrice, _ = strconv.ParseFloat(tickerData[1].(string), 64) + t.LowestAsk, _ = strconv.ParseFloat(tickerData[2].(string), 64) + t.HighestBid, _ = strconv.ParseFloat(tickerData[3].(string), 64) + t.PercentageChange, _ = strconv.ParseFloat(tickerData[4].(string), 64) + t.BaseCurrencyVolume24H, _ = strconv.ParseFloat(tickerData[5].(string), 64) + t.QuoteCurrencyVolume24H, _ = strconv.ParseFloat(tickerData[6].(string), 64) + isFrozen := false + if tickerData[7].(float64) == 1 { + isFrozen = true } - } - continue - } + t.IsFrozen = isFrozen + t.HighestTradeIn24H, _ = strconv.ParseFloat(tickerData[8].(string), 64) + t.LowestTradePrice24H, _ = strconv.ParseFloat(tickerData[9].(string), 64) - switch chanID { - case wsAccountNotificationID: - case wsTickerDataID: - tickerData := data[2].([]interface{}) - var t WsTicker - t.LastPrice, _ = strconv.ParseFloat(tickerData[1].(string), 64) - t.LowestAsk, _ = strconv.ParseFloat(tickerData[2].(string), 64) - t.HighestBid, _ = strconv.ParseFloat(tickerData[3].(string), 64) - t.PercentageChange, _ = strconv.ParseFloat(tickerData[4].(string), 64) - t.BaseCurrencyVolume24H, _ = strconv.ParseFloat(tickerData[5].(string), 64) - t.QuoteCurrencyVolume24H, _ = strconv.ParseFloat(tickerData[6].(string), 64) - isFrozen := false - if tickerData[7].(float64) == 1 { - isFrozen = true - } - t.IsFrozen = isFrozen - t.HighestTradeIn24H, _ = strconv.ParseFloat(tickerData[8].(string), 64) - t.LowestTradePrice24H, _ = strconv.ParseFloat(tickerData[9].(string), 64) + p.Websocket.DataHandler <- exchange.TickerData{ + Timestamp: time.Now(), + Exchange: p.GetName(), + AssetType: "SPOT", + LowPrice: t.LowestAsk, + HighPrice: t.HighestBid, + } + case ws24HourExchangeVolumeID: + case wsHeartbeat: + default: + if len(data) > 2 { + subData := data[2].([]interface{}) - p.Websocket.DataHandler <- exchange.TickerData{ - Timestamp: time.Now(), - Exchange: p.GetName(), - AssetType: "SPOT", - LowPrice: t.LowestAsk, - HighPrice: t.HighestBid, - } - case ws24HourExchangeVolumeID: - case wsHeartbeat: - default: - if len(data) > 2 { - subData := data[2].([]interface{}) + for x := range subData { + dataL2 := subData[x] + dataL3 := dataL2.([]interface{}) - for x := range subData { - dataL2 := subData[x] - dataL3 := dataL2.([]interface{}) + switch getWSDataType(dataL2) { + case "i": + dataL3map := dataL3[1].(map[string]interface{}) + currencyPair, ok := dataL3map["currencyPair"].(string) + if !ok { + p.Websocket.DataHandler <- errors.New("poloniex.go error - could not find currency pair in map") + continue + } - switch getWSDataType(dataL2) { - case "i": - dataL3map := dataL3[1].(map[string]interface{}) - currencyPair, ok := dataL3map["currencyPair"].(string) - if !ok { - p.Websocket.DataHandler <- errors.New("poloniex.go error - could not find currency pair in map") - continue - } + orderbookData, ok := dataL3map["orderBook"].([]interface{}) + if !ok { + p.Websocket.DataHandler <- errors.New("poloniex.go error - could not find orderbook data in map") + continue + } - orderbookData, ok := dataL3map["orderBook"].([]interface{}) - if !ok { - p.Websocket.DataHandler <- errors.New("poloniex.go error - could not find orderbook data in map") - continue - } + err := p.WsProcessOrderbookSnapshot(orderbookData, currencyPair) + if err != nil { + p.Websocket.DataHandler <- err + continue + } - err := p.WsProcessOrderbookSnapshot(orderbookData, currencyPair) - if err != nil { - p.Websocket.DataHandler <- err - continue - } + p.Websocket.DataHandler <- exchange.WebsocketOrderbookUpdate{ + Exchange: p.GetName(), + Asset: "SPOT", + Pair: currency.NewPairFromString(currencyPair), + } + case "o": + currencyPair := CurrencyPairID[chanID] + err := p.WsProcessOrderbookUpdate(dataL3, currencyPair) + if err != nil { + p.Websocket.DataHandler <- err + continue + } - p.Websocket.DataHandler <- exchange.WebsocketOrderbookUpdate{ - Exchange: p.GetName(), - Asset: "SPOT", - Pair: currency.NewPairFromString(currencyPair), - } - case "o": - currencyPair := CurrencyPairID[chanID] - err := p.WsProcessOrderbookUpdate(dataL3, currencyPair) - if err != nil { - p.Websocket.DataHandler <- err - continue - } + p.Websocket.DataHandler <- exchange.WebsocketOrderbookUpdate{ + Exchange: p.GetName(), + Asset: "SPOT", + Pair: currency.NewPairFromString(currencyPair), + } + case "t": + currencyPair := CurrencyPairID[chanID] + var trade WsTrade + trade.Symbol = CurrencyPairID[chanID] + trade.TradeID, _ = strconv.ParseInt(dataL3[1].(string), 10, 64) + // 1 for buy 0 for sell + side := "buy" + if dataL3[2].(float64) != 1 { + side = "sell" + } + trade.Side = side + trade.Volume, _ = strconv.ParseFloat(dataL3[3].(string), 64) + trade.Price, _ = strconv.ParseFloat(dataL3[4].(string), 64) + trade.Timestamp = int64(dataL3[5].(float64)) - p.Websocket.DataHandler <- exchange.WebsocketOrderbookUpdate{ - Exchange: p.GetName(), - Asset: "SPOT", - Pair: currency.NewPairFromString(currencyPair), - } - case "t": - currencyPair := CurrencyPairID[chanID] - var trade WsTrade - trade.Symbol = CurrencyPairID[chanID] - trade.TradeID, _ = strconv.ParseInt(dataL3[1].(string), 10, 64) - // 1 for buy 0 for sell - side := "buy" - if dataL3[2].(float64) != 1 { - side = "sell" - } - trade.Side = side - trade.Volume, _ = strconv.ParseFloat(dataL3[3].(string), 64) - trade.Price, _ = strconv.ParseFloat(dataL3[4].(string), 64) - trade.Timestamp = int64(dataL3[5].(float64)) - - p.Websocket.DataHandler <- exchange.TradeData{ - Timestamp: time.Unix(trade.Timestamp, 0), - CurrencyPair: currency.NewPairFromString(currencyPair), - Side: trade.Side, - Amount: trade.Volume, - Price: trade.Price, + p.Websocket.DataHandler <- exchange.TradeData{ + Timestamp: time.Unix(trade.Timestamp, 0), + CurrencyPair: currency.NewPairFromString(currencyPair), + Side: trade.Side, + Amount: trade.Volume, + Price: trade.Price, + } } } } @@ -468,3 +434,62 @@ var CurrencyPairID = map[int]string{ 226: "USDC_USDT", 225: "USDC_ETH", } + +// GenerateDefaultSubscriptions Adds default subscriptions to websocket to be handled by ManageSubscriptions() +func (p *Poloniex) GenerateDefaultSubscriptions() { + subscriptions := []exchange.WebsocketChannelSubscription{} + // Tickerdata is its own channel + subscriptions = append(subscriptions, exchange.WebsocketChannelSubscription{ + Channel: fmt.Sprintf("%v", wsTickerDataID), + }) + + enabledCurrencies := p.GetEnabledCurrencies() + for j := range enabledCurrencies { + enabledCurrencies[j].Delimiter = "_" + subscriptions = append(subscriptions, exchange.WebsocketChannelSubscription{ + Channel: "orderbook", + Currency: enabledCurrencies[j], + }) + } + p.Websocket.SubscribeToChannels(subscriptions) +} + +// Subscribe sends a websocket message to receive data from the channel +func (p *Poloniex) Subscribe(channelToSubscribe exchange.WebsocketChannelSubscription) error { + subscriptionRequest := WsCommand{ + Command: "subscribe", + } + if strings.EqualFold(fmt.Sprintf("%v", wsTickerDataID), channelToSubscribe.Channel) { + subscriptionRequest.Channel = wsTickerDataID + } else { + subscriptionRequest.Channel = channelToSubscribe.Currency.String() + } + return p.wsSend(subscriptionRequest) +} + +// Unsubscribe sends a websocket message to stop receiving data from the channel +func (p *Poloniex) Unsubscribe(channelToSubscribe exchange.WebsocketChannelSubscription) error { + unsubscriptionRequest := WsCommand{ + Command: "unsubscribe", + } + if strings.EqualFold(fmt.Sprintf("%v", wsTickerDataID), channelToSubscribe.Channel) { + unsubscriptionRequest.Channel = wsTickerDataID + } else { + unsubscriptionRequest.Channel = channelToSubscribe.Currency.String() + } + return p.wsSend(unsubscriptionRequest) +} + +// WsSend sends data to the websocket server +func (p *Poloniex) wsSend(data interface{}) error { + p.wsRequestMtx.Lock() + defer p.wsRequestMtx.Unlock() + json, err := common.JSONEncode(data) + if err != nil { + return err + } + if p.Verbose { + log.Debugf("%v sending message to websocket %v", p.Name, data) + } + return p.WebsocketConn.WriteMessage(websocket.TextMessage, json) +} diff --git a/exchanges/poloniex/poloniex_wrapper.go b/exchanges/poloniex/poloniex_wrapper.go index 7b1d6c82..5e93a95a 100644 --- a/exchanges/poloniex/poloniex_wrapper.go +++ b/exchanges/poloniex/poloniex_wrapper.go @@ -396,3 +396,17 @@ func (p *Poloniex) GetOrderHistory(getOrdersRequest *exchange.GetOrdersRequest) return orders, nil } + +// SubscribeToWebsocketChannels appends to ChannelsToSubscribe +// which lets websocket.manageSubscriptions handle subscribing +func (p *Poloniex) SubscribeToWebsocketChannels(channels []exchange.WebsocketChannelSubscription) error { + p.Websocket.SubscribeToChannels(channels) + return nil +} + +// UnsubscribeToWebsocketChannels removes from ChannelsToSubscribe +// which lets websocket.manageSubscriptions handle unsubscribing +func (p *Poloniex) UnsubscribeToWebsocketChannels(channels []exchange.WebsocketChannelSubscription) error { + p.Websocket.UnsubscribeToChannels(channels) + return nil +} diff --git a/exchanges/request/request.go b/exchanges/request/request.go index ff25174e..052a6bf9 100644 --- a/exchanges/request/request.go +++ b/exchanges/request/request.go @@ -330,7 +330,6 @@ func (r *Requester) DoRequest(req *http.Request, path string, body io.Reader, re if resp.StatusCode != 200 && resp.StatusCode != 201 && resp.StatusCode != 202 { err = fmt.Errorf("unsuccessful HTTP status code: %d", resp.StatusCode) - if verbose { err = fmt.Errorf("%s\n%s", err.Error(), fmt.Sprintf("%s exchange raw response: %s", r.Name, string(contents))) diff --git a/exchanges/yobit/yobit_wrapper.go b/exchanges/yobit/yobit_wrapper.go index eef57754..6a541c0c 100644 --- a/exchanges/yobit/yobit_wrapper.go +++ b/exchanges/yobit/yobit_wrapper.go @@ -363,3 +363,15 @@ func (y *Yobit) GetOrderHistory(getOrdersRequest *exchange.GetOrdersRequest) ([] return orders, nil } + +// SubscribeToWebsocketChannels appends to ChannelsToSubscribe +// which lets websocket.manageSubscriptions handle subscribing +func (y *Yobit) SubscribeToWebsocketChannels(channels []exchange.WebsocketChannelSubscription) error { + return common.ErrFunctionNotSupported +} + +// UnsubscribeToWebsocketChannels removes from ChannelsToSubscribe +// which lets websocket.manageSubscriptions handle unsubscribing +func (y *Yobit) UnsubscribeToWebsocketChannels(channels []exchange.WebsocketChannelSubscription) error { + return common.ErrFunctionNotSupported +} diff --git a/exchanges/zb/zb.go b/exchanges/zb/zb.go index 1e13230a..575ed837 100644 --- a/exchanges/zb/zb.go +++ b/exchanges/zb/zb.go @@ -8,6 +8,7 @@ import ( "net/url" "strconv" "strings" + "sync" "time" "github.com/thrasher-/gocryptotrader/currency" @@ -49,6 +50,7 @@ const ( type ZB struct { WebsocketConn *websocket.Conn exchange.Base + wsRequestMtx sync.Mutex } // SetDefaults sets default values for the exchange @@ -78,7 +80,8 @@ func (z *ZB) SetDefaults() { z.WebsocketInit() z.Websocket.Functionality = exchange.WebsocketTickerSupported | exchange.WebsocketOrderbookSupported | - exchange.WebsocketTradeDataSupported + exchange.WebsocketTradeDataSupported | + exchange.WebsocketSubscribeSupported } // Setup sets user configuration @@ -120,8 +123,11 @@ func (z *ZB) Setup(exch *config.ExchangeConfig) { log.Fatal(err) } err = z.WebsocketSetup(z.WsConnect, + z.Subscribe, + nil, exch.Name, exch.Websocket, + exch.Verbose, zbWebsocketAPI, exch.WebsocketURL) if err != nil { diff --git a/exchanges/zb/zb_websocket.go b/exchanges/zb/zb_websocket.go index f3671541..b219cee9 100644 --- a/exchanges/zb/zb_websocket.go +++ b/exchanges/zb/zb_websocket.go @@ -12,6 +12,7 @@ import ( "github.com/thrasher-/gocryptotrader/currency" exchange "github.com/thrasher-/gocryptotrader/exchanges" "github.com/thrasher-/gocryptotrader/exchanges/orderbook" + log "github.com/thrasher-/gocryptotrader/logger" ) const ( @@ -42,75 +43,7 @@ func (z *ZB) WsConnect() error { } go z.WsHandleData() - - return z.WsSubscribe() -} - -// WsSubscribe subscribes to the full websocket suite on ZB exchange -func (z *ZB) WsSubscribe() error { - markets := Subscription{ - Event: "addChannel", - Channel: "markets", - } - - reqMarkets, err := common.JSONEncode(markets) - if err != nil { - return err - } - - err = z.WebsocketConn.WriteMessage(websocket.TextMessage, reqMarkets) - if err != nil { - return err - } - - for _, c := range z.GetEnabledCurrencies() { - cPair := c.Base.Lower().String() + c.Quote.Lower().String() - - ticker := Subscription{ - Event: "addChannel", - Channel: fmt.Sprintf("%s_ticker", cPair), - } - - reqTicker, err := common.JSONEncode(ticker) - if err != nil { - return err - } - - err = z.WebsocketConn.WriteMessage(websocket.TextMessage, reqTicker) - if err != nil { - return err - } - - depth := Subscription{ - Event: "addChannel", - Channel: fmt.Sprintf("%s_depth", cPair), - } - - reqDepth, err := common.JSONEncode(depth) - if err != nil { - return err - } - - err = z.WebsocketConn.WriteMessage(websocket.TextMessage, reqDepth) - if err != nil { - return err - } - - trades := Subscription{ - Event: "addChannel", - Channel: fmt.Sprintf("%s_trades", cPair), - } - - reqTrades, err := common.JSONEncode(trades) - if err != nil { - return err - } - - err = z.WebsocketConn.WriteMessage(websocket.TextMessage, reqTrades) - if err != nil { - return err - } - } + z.GenerateDefaultSubscriptions() return nil } @@ -133,22 +66,18 @@ func (z *ZB) WsHandleData() { z.Websocket.Wg.Add(1) defer func() { - err := z.WebsocketConn.Close() - if err != nil { - z.Websocket.DataHandler <- fmt.Errorf("zb_websocket.go - Unable to to close Websocket connection. Error: %s", - err) - } z.Websocket.Wg.Done() }() for { select { case <-z.Websocket.ShutdownC: - + return default: resp, err := z.WsReadData() if err != nil { z.Websocket.DataHandler <- err + time.Sleep(time.Second) continue } @@ -158,7 +87,6 @@ func (z *ZB) WsHandleData() { z.Websocket.DataHandler <- err continue } - switch { case common.StringContains(result.Channel, "markets"): if !result.Success { @@ -309,3 +237,47 @@ var wsErrCodes = map[int64]string{ 4001: "API interface is locked", 4002: "Request too frequently", } + +// GenerateDefaultSubscriptions Adds default subscriptions to websocket to be handled by ManageSubscriptions() +func (z *ZB) GenerateDefaultSubscriptions() { + subscriptions := []exchange.WebsocketChannelSubscription{} + // Tickerdata is its own channel + subscriptions = append(subscriptions, exchange.WebsocketChannelSubscription{ + Channel: "markets", + }) + channels := []string{"%s_ticker", "%s_depth", "%s_trades"} + enabledCurrencies := z.GetEnabledCurrencies() + for i := range channels { + for j := range enabledCurrencies { + enabledCurrencies[j].Delimiter = "" + subscriptions = append(subscriptions, exchange.WebsocketChannelSubscription{ + Channel: fmt.Sprintf(channels[i], enabledCurrencies[j].Lower().String()), + Currency: enabledCurrencies[j].Lower(), + }) + } + } + z.Websocket.SubscribeToChannels(subscriptions) +} + +// Subscribe sends a websocket message to receive data from the channel +func (z *ZB) Subscribe(channelToSubscribe exchange.WebsocketChannelSubscription) error { + subscriptionRequest := Subscription{ + Event: "addChannel", + Channel: channelToSubscribe.Channel, + } + return z.wsSend(subscriptionRequest) +} + +// WsSend sends data to the websocket server +func (z *ZB) wsSend(data interface{}) error { + z.wsRequestMtx.Lock() + defer z.wsRequestMtx.Unlock() + json, err := common.JSONEncode(data) + if err != nil { + return err + } + if z.Verbose { + log.Debugf("%v sending message to websocket %v", z.Name, data) + } + return z.WebsocketConn.WriteMessage(websocket.TextMessage, json) +} diff --git a/exchanges/zb/zb_websocket_types.go b/exchanges/zb/zb_websocket_types.go index 5fe85e6e..691d11cf 100644 --- a/exchanges/zb/zb_websocket_types.go +++ b/exchanges/zb/zb_websocket_types.go @@ -14,7 +14,7 @@ type Generic struct { Success bool `json:"success"` Channel string `json:"channel"` Message string `json:"message"` - No int64 `json:"no"` + No string `json:"no"` Data json.RawMessage `json:"data"` } diff --git a/exchanges/zb/zb_wrapper.go b/exchanges/zb/zb_wrapper.go index 0502f9f6..cbf1bb9d 100644 --- a/exchanges/zb/zb_wrapper.go +++ b/exchanges/zb/zb_wrapper.go @@ -413,3 +413,16 @@ func (z *ZB) GetOrderHistory(getOrdersRequest *exchange.GetOrdersRequest) ([]exc return orders, nil } + +// SubscribeToWebsocketChannels appends to ChannelsToSubscribe +// which lets websocket.manageSubscriptions handle subscribing +func (z *ZB) SubscribeToWebsocketChannels(channels []exchange.WebsocketChannelSubscription) error { + z.Websocket.SubscribeToChannels(channels) + return nil +} + +// UnsubscribeToWebsocketChannels removes from ChannelsToSubscribe +// which lets websocket.manageSubscriptions handle unsubscribing +func (z *ZB) UnsubscribeToWebsocketChannels(channels []exchange.WebsocketChannelSubscription) error { + return common.ErrFunctionNotSupported +} diff --git a/routines.go b/routines.go index d061e3df..99b5ce93 100644 --- a/routines.go +++ b/routines.go @@ -178,8 +178,8 @@ func relayWebsocketEvent(result interface{}, event, assetType, exchangeName stri } err := BroadcastWebsocketMessage(evt) if err != nil { - log.Errorf("Failed to broadcast websocket event. Error: %s", - err) + log.Errorf("Failed to broadcast websocket event %v. Error: %s", + event, err) } } @@ -405,7 +405,7 @@ func WebsocketDataHandler(ws *exchange.Websocket, verbose bool) { case error: switch { case common.StringContains(d.Error(), "close 1006"): - go WebsocketReconnect(ws, verbose) + go ws.WebsocketReset() continue default: log.Errorf("routines.go exchange %s websocket error - %s", ws.GetName(), data) @@ -440,33 +440,3 @@ func WebsocketDataHandler(ws *exchange.Websocket, verbose bool) { } } } - -// WebsocketReconnect tries to reconnect to a websocket stream -func WebsocketReconnect(ws *exchange.Websocket, verbose bool) { - if verbose { - log.Debugf("Websocket reconnection requested for %s", ws.GetName()) - } - - err := ws.Shutdown() - if err != nil { - log.Error(err) - return - } - - wg.Add(1) - defer wg.Done() - - tick := time.NewTicker(3 * time.Second) - for { - select { - case <-shutdowner: - return - - case <-tick.C: - err = ws.Connect() - if err == nil { - return - } - } - } -} diff --git a/testdata/configtest.json b/testdata/configtest.json index 95b1ebe1..0a749a05 100644 --- a/testdata/configtest.json +++ b/testdata/configtest.json @@ -1085,7 +1085,7 @@ "name": "OKCOIN International", "enabled": true, "verbose": false, - "websocket": false, + "websocket": true, "useSandbox": false, "restPollingDelay": 10, "httpTimeout": 15000000000, diff --git a/tools/exchange_template/wrapper_file.tmpl b/tools/exchange_template/wrapper_file.tmpl index 143068b3..d2aaec0e 100644 --- a/tools/exchange_template/wrapper_file.tmpl +++ b/tools/exchange_template/wrapper_file.tmpl @@ -185,4 +185,18 @@ func ({{.Variable}} *{{.CapitalName}}) GetFeeByType(feeBuilder *exchange.FeeBuil return 0, common.ErrNotYetImplemented } +// SubscribeToWebsocketChannels appends to ChannelsToSubscribe +// which lets websocket.manageSubscriptions handle subscribing +func ({{.Variable}} *{{.CapitalName}}) SubscribeToWebsocketChannels(channels []exchange.WebsocketChannelSubscription) error { + {{.Variable}}.Websocket.SubscribeToChannels(channels) + return nil +} + +// UnsubscribeToWebsocketChannels removes from ChannelsToSubscribe +// which lets websocket.manageSubscriptions handle unsubscribing +func ({{.Variable}} *{{.CapitalName}}) UnsubscribeToWebsocketChannels(channels []exchange.WebsocketChannelSubscription) error { + {{.Variable}}.Websocket.UnubscribeToChannels(channels) + return nil +} + {{end}}