Initial overhaul of websocket connection and feeds (#189)

* Initial overhaul of websocket connection and feeds
* Added proxy support
* Piped to routines.go

* Added new websocket file in exchanges
Refactored orderbook handling into exchange_websocket.go
Added better error responses for binance_websocket.go
General clean for binance_websocket.go

* General fixes - bitfinex_websocket.go
Refactored orderbook cache code - bitfinex_websocket.go
Removed fatal error with unhandled type - routines.go

* Added general improvements to bitmex_websocket.go
Refactored orderbook handling to exchange_websocket.go
Added variable in Item struct in orderbook.go for looking up orders by ID

* Fix issue when routines are blocked due to Data Handler not started
Updated traffic handler
General fixes for bitstamp_websocket.go

* General fixes for coinbasepro_websocket.go

* General fixes for coinut_websocket.go
Fixed error return in exchange_websocket.go

* Removed comments in coinut_wrapper.go
Refactor orderbook logic from hitbtc_websocket.go to exchange_websocket.go

* General fixes

* Removed comments
General fixes

* Updated routines.go

* After rebase fix

* Fixed update config pairs in okcoin.go

* fixed config currency issue in okcoin.go for okcoin China

* exchange_websocket.go
*Removed unused const dec
*Removed state change routine
*Improved trafficMonitor routine
*Increased verbosity for error returns
*Removed uneeded mutex locks

exchange_websocket_test.go
*Added new tests for websocket and orderbook updating

routines.go
*Removed string cased

* Fixed race conditions on sync.waitgroup in exchanges_websocket.go

* Changes variable name in config.go

* Removes unnecessary comment

* Removes indefinite lock on error return

* Removes unnecessary comment

* Adds support for BTCC websocket
Drops support for BTCC REST

* Rewords comment in exchange_websocket.go
Moves types to poloniex_types.go

* Moves types to coinut_types.go

* Removes uneeded range for accessing array variables for coinbase_websocket.go
Removes comments in coinut_types.go

* Adds verbosity flag to GCT
Suppresses verbose output from routines.go

* Fixes setting proxy for REST and Websocket per exchange
Upgrades error handling
Drops unused *url.Url variable in exchange type

* Adds test for setting proxy

* Fixes bug that closes connection due to incorrect timeout time through a proxy connection

* Clarify verbose flag message
This commit is contained in:
Ryan O'Hara-Reid
2018-10-24 14:22:41 +11:00
committed by Adrian Gallagher
parent 7315e6604c
commit d3c2800fe0
99 changed files with 6515 additions and 3031 deletions

View File

@@ -9,6 +9,7 @@ import (
"strconv"
"time"
"github.com/gorilla/websocket"
"github.com/thrasher-/gocryptotrader/common"
"github.com/thrasher-/gocryptotrader/config"
"github.com/thrasher-/gocryptotrader/exchanges"
@@ -49,6 +50,7 @@ const (
// HitBTC is the overarching type across the hitbtc package
type HitBTC struct {
exchange.Base
WebsocketConn *websocket.Conn
}
// SetDefaults sets default settings for hitbtc
@@ -57,7 +59,6 @@ func (p *HitBTC) SetDefaults() {
p.Enabled = false
p.Fee = 0
p.Verbose = false
p.Websocket = false
p.RESTPollingDelay = 10
p.RequestCurrencyPairFormat.Delimiter = ""
p.RequestCurrencyPairFormat.Uppercase = true
@@ -72,6 +73,7 @@ func (p *HitBTC) SetDefaults() {
common.NewHTTPClientWithTimeout(exchange.DefaultHTTPTimeout))
p.APIUrlDefault = apiURL
p.APIUrl = p.APIUrlDefault
p.WebsocketInit()
}
// Setup sets user exchange configuration settings
@@ -86,7 +88,7 @@ func (p *HitBTC) Setup(exch config.ExchangeConfig) {
p.SetHTTPClientUserAgent(exch.HTTPUserAgent)
p.RESTPollingDelay = exch.RESTPollingDelay // Max 60000ms
p.Verbose = exch.Verbose
p.Websocket = exch.Websocket
p.Websocket.SetEnabled(exch.Websocket)
p.BaseCurrencies = common.SplitStrings(exch.BaseCurrencies, ",")
p.AvailablePairs = common.SplitStrings(exch.AvailablePairs, ",")
p.EnabledPairs = common.SplitStrings(exch.EnabledPairs, ",")
@@ -106,6 +108,18 @@ func (p *HitBTC) Setup(exch config.ExchangeConfig) {
if err != nil {
log.Fatal(err)
}
err = p.SetClientProxyAddress(exch.ProxyAddress)
if err != nil {
log.Fatal(err)
}
err = p.WebsocketSetup(p.WsConnect,
exch.Name,
exch.Websocket,
hitbtcWebsocketAddress,
exch.WebsocketURL)
if err != nil {
log.Fatal(err)
}
}
}

View File

@@ -1,188 +1,372 @@
package hitbtc
import (
"errors"
"fmt"
"log"
"strconv"
"net/http"
"net/url"
"time"
"github.com/beatgammit/turnpike"
"github.com/gorilla/websocket"
"github.com/thrasher-/gocryptotrader/common"
"github.com/thrasher-/gocryptotrader/currency/pair"
"github.com/thrasher-/gocryptotrader/exchanges"
"github.com/thrasher-/gocryptotrader/exchanges/orderbook"
)
const (
hitbtcWebsocketAddress = "wss://api.hitbtc.com"
hitbtcWebsocketRealm = "realm1"
hitbtcWebsocketTicker = "ticker"
hitbtcWebsocketTrollbox = "trollbox"
hitbtcWebsocketAddress = "wss://api.hitbtc.com/api/2/ws"
rpcVersion = "2.0"
)
// WebsocketTicker holds ticker data
type WebsocketTicker struct {
CurrencyPair string
Last float64
LowestAsk float64
HighestBid float64
PercentChange float64
BaseVolume float64
QuoteVolume float64
IsFrozen bool
High float64
Low float64
}
// OnTicker converts ticker to websocket ticker
func OnTicker(args []interface{}, kwargs map[string]interface{}) {
ticker := WebsocketTicker{}
ticker.CurrencyPair = args[0].(string)
ticker.Last, _ = strconv.ParseFloat(args[1].(string), 64)
ticker.LowestAsk, _ = strconv.ParseFloat(args[2].(string), 64)
ticker.HighestBid, _ = strconv.ParseFloat(args[3].(string), 64)
ticker.PercentChange, _ = strconv.ParseFloat(args[4].(string), 64)
ticker.BaseVolume, _ = strconv.ParseFloat(args[5].(string), 64)
ticker.QuoteVolume, _ = strconv.ParseFloat(args[6].(string), 64)
if args[7].(float64) != 0 {
ticker.IsFrozen = true
} else {
ticker.IsFrozen = false
// WsConnect starts a new connection with the websocket API
func (h *HitBTC) WsConnect() error {
if !h.Websocket.IsEnabled() || !h.IsEnabled() {
return errors.New(exchange.WebsocketNotEnabled)
}
ticker.High, _ = strconv.ParseFloat(args[8].(string), 64)
ticker.Low, _ = strconv.ParseFloat(args[9].(string), 64)
}
var dialer websocket.Dialer
// WebsocketTrollboxMessage contains trollbox message information
type WebsocketTrollboxMessage struct {
MessageNumber float64
Username string
Message string
Reputation float64
}
// OnTrollbox converts trollbox messages
func OnTrollbox(args []interface{}, kwargs map[string]interface{}) {
message := WebsocketTrollboxMessage{}
message.MessageNumber, _ = args[1].(float64)
message.Username = args[2].(string)
message.Message = args[3].(string)
if len(args) == 5 {
message.Reputation = args[4].(float64)
}
}
// OnDepthOrTrade converts depth and trade data
func OnDepthOrTrade(args []interface{}, kwargs map[string]interface{}) {
for x := range args {
data := args[x].(map[string]interface{})
msgData := data["data"].(map[string]interface{})
msgType := data["type"].(string)
switch msgType {
case "orderBookModify":
{
type HitBTCWebsocketOrderbookModify struct {
Type string
Rate float64
Amount float64
}
orderModify := HitBTCWebsocketOrderbookModify{}
orderModify.Type = msgData["type"].(string)
rateStr := msgData["rate"].(string)
orderModify.Rate, _ = strconv.ParseFloat(rateStr, 64)
amountStr := msgData["amount"].(string)
orderModify.Amount, _ = strconv.ParseFloat(amountStr, 64)
}
case "orderBookRemove":
{
type HitBTCWebsocketOrderbookRemove struct {
Type string
Rate float64
}
orderRemoval := HitBTCWebsocketOrderbookRemove{}
orderRemoval.Type = msgData["type"].(string)
rateStr := msgData["rate"].(string)
orderRemoval.Rate, _ = strconv.ParseFloat(rateStr, 64)
}
case "newTrade":
{
type HitBTCWebsocketNewTrade struct {
Type string
TradeID int64
Rate float64
Amount float64
Date string
Total float64
}
trade := HitBTCWebsocketNewTrade{}
trade.Type = msgData["type"].(string)
tradeIDstr := msgData["tradeID"].(string)
trade.TradeID, _ = strconv.ParseInt(tradeIDstr, 10, 64)
rateStr := msgData["rate"].(string)
trade.Rate, _ = strconv.ParseFloat(rateStr, 64)
amountStr := msgData["amount"].(string)
trade.Amount, _ = strconv.ParseFloat(amountStr, 64)
totalStr := msgData["total"].(string)
trade.Rate, _ = strconv.ParseFloat(totalStr, 64)
trade.Date = msgData["date"].(string)
}
}
}
}
// WebsocketClient initiates a websocket client
func (p *HitBTC) WebsocketClient() {
for p.Enabled && p.Websocket {
c, err := turnpike.NewWebsocketClient(turnpike.JSON, hitbtcWebsocketAddress, nil)
if h.Websocket.GetProxyAddress() != "" {
proxy, err := url.Parse(h.Websocket.GetProxyAddress())
if err != nil {
log.Printf("%s Unable to connect to Websocket. Error: %s\n", p.GetName(), err)
continue
return err
}
if p.Verbose {
log.Printf("%s Connected to Websocket.\n", p.GetName())
}
dialer.Proxy = http.ProxyURL(proxy)
}
_, err = c.JoinRealm(hitbtcWebsocketRealm, nil)
var err error
h.WebsocketConn, _, err = dialer.Dial(hitbtcWebsocketAddress, http.Header{})
if err != nil {
return err
}
go h.WsReadData()
go h.WsHandleData()
err = h.WsSubscribe()
if err != nil {
return err
}
return nil
}
// 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 {
log.Printf("%s Unable to join realm. Error: %s\n", p.GetName(), err)
continue
return err
}
if p.Verbose {
log.Printf("%s Joined Websocket realm.\n", p.GetName())
err = h.WebsocketConn.WriteMessage(websocket.TextMessage, tickerSubReq)
if err != nil {
return nil
}
c.ReceiveDone = make(chan bool)
if err := c.Subscribe(hitbtcWebsocketTicker, OnTicker); err != nil {
log.Printf("%s Error subscribing to ticker channel: %s\n", p.GetName(), err)
orderbookSubReq, err := common.JSONEncode(WsNotification{
JSONRPCVersion: rpcVersion,
Method: "subscribeOrderbook",
Params: params{Symbol: pF.String()},
})
if err != nil {
return err
}
if err := c.Subscribe(hitbtcWebsocketTrollbox, OnTrollbox); err != nil {
log.Printf("%s Error subscribing to trollbox channel: %s\n", p.GetName(), err)
err = h.WebsocketConn.WriteMessage(websocket.TextMessage, orderbookSubReq)
if err != nil {
return nil
}
for x := range p.EnabledPairs {
currency := p.EnabledPairs[x]
if err := c.Subscribe(currency, OnDepthOrTrade); err != nil {
log.Printf("%s Error subscribing to %s channel: %s\n", p.GetName(), currency, err)
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
}
// WsReadData reads from the websocket connection
func (h *HitBTC) WsReadData() {
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()
}()
for {
select {
case <-h.Websocket.ShutdownC:
return
default:
_, resp, err := h.WebsocketConn.ReadMessage()
if err != nil {
h.Websocket.DataHandler <- err
return
}
}
if p.Verbose {
log.Printf("%s Subscribed to websocket channels.\n", p.GetName())
h.Websocket.TrafficAlert <- struct{}{}
h.Websocket.Intercomm <- exchange.WebsocketResponse{Raw: resp}
}
<-c.ReceiveDone
log.Printf("%s Websocket client disconnected.\n", p.GetName())
}
}
// WsHandleData handles websocket data
func (h *HitBTC) WsHandleData() {
h.Websocket.Wg.Add(1)
defer h.Websocket.Wg.Done()
for {
select {
case <-h.Websocket.ShutdownC:
case resp := <-h.Websocket.Intercomm:
var init capture
err := common.JSONDecode(resp.Raw, &init)
if err != nil {
log.Fatal(err)
}
if init.Error.Message != "" || init.Error.Code != 0 {
h.Websocket.DataHandler <- fmt.Errorf("hitbtc.go error - Code: %d, Message: %s",
init.Error.Code,
init.Error.Message)
continue
}
if init.Result {
continue
}
switch init.Method {
case "ticker":
var ticker WsTicker
err := common.JSONDecode(resp.Raw, &ticker)
if err != nil {
log.Fatal(err)
}
ts, err := time.Parse(time.RFC3339, ticker.Params.Timestamp)
if err != nil {
log.Fatal(err)
}
h.Websocket.DataHandler <- exchange.TickerData{
Exchange: h.GetName(),
AssetType: "SPOT",
Pair: pair.NewCurrencyPairFromString(ticker.Params.Symbol),
Quantity: ticker.Params.Volume,
Timestamp: ts,
OpenPrice: ticker.Params.Open,
HighPrice: ticker.Params.High,
LowPrice: ticker.Params.Low,
}
case "snapshotOrderbook":
var obSnapshot WsOrderbook
err := common.JSONDecode(resp.Raw, &obSnapshot)
if err != nil {
log.Fatal(err)
}
err = h.WsProcessOrderbookSnapshot(obSnapshot)
if err != nil {
log.Fatal(err)
}
case "updateOrderbook":
var obUpdate WsOrderbook
err := common.JSONDecode(resp.Raw, &obUpdate)
if err != nil {
log.Fatal(err)
}
h.WsProcessOrderbookUpdate(obUpdate)
case "snapshotTrades":
var tradeSnapshot WsTrade
err := common.JSONDecode(resp.Raw, &tradeSnapshot)
if err != nil {
log.Fatal(err)
}
case "updateTrades":
var tradeUpdates WsTrade
err := common.JSONDecode(resp.Raw, &tradeUpdates)
if err != nil {
log.Fatal(err)
}
}
}
}
}
// WsProcessOrderbookSnapshot processes a full orderbook snapshot to a local cache
func (h *HitBTC) WsProcessOrderbookSnapshot(ob WsOrderbook) error {
if len(ob.Params.Bid) == 0 || len(ob.Params.Ask) == 0 {
return errors.New("hitbtc.go error - no orderbooks to process")
}
var bids []orderbook.Item
for _, bid := range ob.Params.Bid {
bids = append(bids, orderbook.Item{Amount: bid.Size, Price: bid.Price})
}
var asks []orderbook.Item
for _, ask := range ob.Params.Ask {
asks = append(asks, orderbook.Item{Amount: ask.Size, Price: ask.Price})
}
p := pair.NewCurrencyPairFromString(ob.Params.Symbol)
var newOrderbook orderbook.Base
newOrderbook.Asks = asks
newOrderbook.Bids = bids
newOrderbook.AssetType = "SPOT"
newOrderbook.CurrencyPair = ob.Params.Symbol
newOrderbook.LastUpdated = time.Now()
newOrderbook.Pair = p
err := h.Websocket.Orderbook.LoadSnapshot(newOrderbook, h.GetName())
if err != nil {
return err
}
h.Websocket.DataHandler <- exchange.WebsocketOrderbookUpdate{
Exchange: h.GetName(),
Asset: "SPOT",
Pair: p,
}
return nil
}
// WsProcessOrderbookUpdate updates a local cache
func (h *HitBTC) WsProcessOrderbookUpdate(ob WsOrderbook) error {
if len(ob.Params.Bid) == 0 && len(ob.Params.Ask) == 0 {
return errors.New("hitbtc_websocket.go error - no data")
}
var bids, asks []orderbook.Item
for _, bid := range ob.Params.Bid {
bids = append(bids, orderbook.Item{Price: bid.Price, Amount: bid.Size})
}
for _, ask := range ob.Params.Ask {
asks = append(asks, orderbook.Item{Price: ask.Price, Amount: ask.Size})
}
p := pair.NewCurrencyPairFromString(ob.Params.Symbol)
err := h.Websocket.Orderbook.Update(bids, asks, p, time.Now(), h.GetName(), "SPOT")
if err != nil {
return err
}
h.Websocket.DataHandler <- exchange.WebsocketOrderbookUpdate{
Exchange: h.GetName(),
Asset: "SPOT",
Pair: p,
}
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"`
}
// 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"`
}
// 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"`
}

View File

@@ -24,15 +24,11 @@ func (h *HitBTC) Start(wg *sync.WaitGroup) {
// Run implements the HitBTC wrapper
func (h *HitBTC) Run() {
if h.Verbose {
log.Printf("%s Websocket: %s (url: %s).\n", h.GetName(), common.IsEnabled(h.Websocket), hitbtcWebsocketAddress)
log.Printf("%s Websocket: %s (url: %s).\n", h.GetName(), common.IsEnabled(h.Websocket.IsEnabled()), hitbtcWebsocketAddress)
log.Printf("%s polling delay: %ds.\n", h.GetName(), h.RESTPollingDelay)
log.Printf("%s %d currencies enabled: %s.\n", h.GetName(), len(h.EnabledPairs), h.EnabledPairs)
}
if h.Websocket {
go h.WebsocketClient()
}
exchangeProducts, err := h.GetSymbolsDetailed()
if err != nil {
log.Printf("%s Failed to get available symbols.\n", h.GetName())
@@ -207,3 +203,8 @@ func (h *HitBTC) WithdrawFiatExchangeFunds(currency pair.CurrencyItem, amount fl
func (h *HitBTC) WithdrawFiatExchangeFundsToInternationalBank(currency pair.CurrencyItem, amount float64) (string, error) {
return "", errors.New("not yet implemented")
}
// GetWebsocket returns a pointer to the exchange websocket
func (h *HitBTC) GetWebsocket() (*exchange.Websocket, error) {
return h.Websocket, nil
}