diff --git a/config/config.go b/config/config.go index 4827b73f..3807e1c2 100644 --- a/config/config.go +++ b/config/config.go @@ -56,7 +56,8 @@ const ( WarningExchangeAuthAPIDefaultOrEmptyValues = "WARNING -- Exchange %s: Authenticated API support disabled due to default/empty APIKey/Secret/ClientID values." WarningCurrencyExchangeProvider = "WARNING -- Currency exchange provider invalid valid. Reset to Fixer." WarningPairsLastUpdatedThresholdExceeded = "WARNING -- Exchange %s: Last manual update of available currency pairs has exceeded %d days. Manual update required!" - APIURLDefaultMessage = "NON_DEFAULT_HTTP_LINK_TO_EXCHANGE_API" + APIURLNonDefaultMessage = "NON_DEFAULT_HTTP_LINK_TO_EXCHANGE_API" + WebsocketURLNonDefaultMessage = "NON_DEFAULT_HTTP_LINK_TO_WEBSOCKET_EXCHANGE_API" ) // Variables here are used for configuration @@ -129,6 +130,8 @@ type ExchangeConfig struct { APIAuthPEMKey string `json:"apiAuthPemKey,omitempty"` APIURL string `json:"apiUrl"` APIURLSecondary string `json:"apiUrlSecondary"` + ProxyAddress string `json:"proxyAddress"` + WebsocketURL string `json:"websocketUrl"` ClientID string `json:"clientId,omitempty"` AvailablePairs string `json:"availablePairs"` EnabledPairs string `json:"enabledPairs"` @@ -672,17 +675,23 @@ func (c *Config) CheckExchangeConfigValues() error { c.Exchanges[i].Name = "CoinbasePro" } - if exch.APIURL != APIURLDefaultMessage { - if exch.APIURL == "" { - // Set default if nothing set - c.Exchanges[i].APIURL = APIURLDefaultMessage + if exch.WebsocketURL != WebsocketURLNonDefaultMessage { + if exch.WebsocketURL == "" { + c.Exchanges[i].WebsocketURL = WebsocketURLNonDefaultMessage } } - if exch.APIURLSecondary != APIURLDefaultMessage { + if exch.APIURL != APIURLNonDefaultMessage { + if exch.APIURL == "" { + // Set default if nothing set + c.Exchanges[i].APIURL = APIURLNonDefaultMessage + } + } + + if exch.APIURLSecondary != APIURLNonDefaultMessage { if exch.APIURLSecondary == "" { // Set default if nothing set - c.Exchanges[i].APIURLSecondary = APIURLDefaultMessage + c.Exchanges[i].APIURLSecondary = APIURLNonDefaultMessage } } diff --git a/exchanges/alphapoint/alphapoint.go b/exchanges/alphapoint/alphapoint.go index ddca63f3..5fac7db1 100644 --- a/exchanges/alphapoint/alphapoint.go +++ b/exchanges/alphapoint/alphapoint.go @@ -55,7 +55,10 @@ func (a *Alphapoint) SetDefaults() { a.AssetTypes = []string{ticker.Spot} a.SupportsAutoPairUpdating = false a.SupportsRESTTickerBatching = false - a.Requester = request.New(a.Name, request.NewRateLimit(time.Minute*10, alphapointAuthRate), request.NewRateLimit(time.Minute*10, alphapointUnauthRate), common.NewHTTPClientWithTimeout(exchange.DefaultHTTPTimeout)) + a.Requester = request.New(a.Name, + request.NewRateLimit(time.Minute*10, alphapointAuthRate), + request.NewRateLimit(time.Minute*10, alphapointUnauthRate), + common.NewHTTPClientWithTimeout(exchange.DefaultHTTPTimeout)) } // GetTicker returns current ticker information from Alphapoint for a selected diff --git a/exchanges/alphapoint/alphapoint_websocket.go b/exchanges/alphapoint/alphapoint_websocket.go index 3dc8b458..6d42ffef 100644 --- a/exchanges/alphapoint/alphapoint_websocket.go +++ b/exchanges/alphapoint/alphapoint_websocket.go @@ -14,7 +14,7 @@ const ( // WebsocketClient starts a new webstocket connection func (a *Alphapoint) WebsocketClient() { - for a.Enabled && a.Websocket { + for a.Enabled { var Dialer websocket.Dialer var err error a.WebsocketConn, _, err = Dialer.Dial(a.WebsocketURL, http.Header{}) @@ -35,7 +35,7 @@ func (a *Alphapoint) WebsocketClient() { return } - for a.Enabled && a.Websocket { + for a.Enabled { msgType, resp, err := a.WebsocketConn.ReadMessage() if err != nil { log.Println(err) diff --git a/exchanges/alphapoint/alphapoint_wrapper.go b/exchanges/alphapoint/alphapoint_wrapper.go index 3d052b23..5f67d7b6 100644 --- a/exchanges/alphapoint/alphapoint_wrapper.go +++ b/exchanges/alphapoint/alphapoint_wrapper.go @@ -171,3 +171,8 @@ func (a *Alphapoint) WithdrawCryptoExchangeFunds(address string, cryptocurrency func (a *Alphapoint) WithdrawFiatExchangeFunds(currency pair.CurrencyItem, amount float64) (string, error) { return "", errors.New("not yet implemented") } + +// GetWebsocket returns a pointer to the exchange websocket +func (a *Alphapoint) GetWebsocket() (*exchange.Websocket, error) { + return nil, errors.New("not yet implemented") +} diff --git a/exchanges/anx/anx.go b/exchanges/anx/anx.go index a7d4e9a7..0f88a2dc 100644 --- a/exchanges/anx/anx.go +++ b/exchanges/anx/anx.go @@ -47,7 +47,6 @@ func (a *ANX) SetDefaults() { a.TakerFee = 0.6 a.MakerFee = 0.3 a.Verbose = false - a.Websocket = false a.RESTPollingDelay = 10 a.RequestCurrencyPairFormat.Delimiter = "" a.RequestCurrencyPairFormat.Uppercase = true @@ -64,6 +63,7 @@ func (a *ANX) SetDefaults() { common.NewHTTPClientWithTimeout(exchange.DefaultHTTPTimeout)) a.APIUrlDefault = anxAPIURL a.APIUrl = a.APIUrlDefault + a.WebsocketInit() } //Setup is run on startup to setup exchange with config values @@ -78,7 +78,6 @@ func (a *ANX) Setup(exch config.ExchangeConfig) { a.SetHTTPClientUserAgent(exch.HTTPUserAgent) a.RESTPollingDelay = exch.RESTPollingDelay a.Verbose = exch.Verbose - a.Websocket = exch.Websocket a.BaseCurrencies = common.SplitStrings(exch.BaseCurrencies, ",") a.AvailablePairs = common.SplitStrings(exch.AvailablePairs, ",") a.EnabledPairs = common.SplitStrings(exch.EnabledPairs, ",") @@ -98,6 +97,10 @@ func (a *ANX) Setup(exch config.ExchangeConfig) { if err != nil { log.Fatal(err) } + err = a.SetClientProxyAddress(exch.ProxyAddress) + if err != nil { + log.Fatal(err) + } } } diff --git a/exchanges/anx/anx_test.go b/exchanges/anx/anx_test.go index 4f8441b0..10eb9dfe 100644 --- a/exchanges/anx/anx_test.go +++ b/exchanges/anx/anx_test.go @@ -26,7 +26,7 @@ func TestSetDefaults(t *testing.T) { if anx.Verbose != false { t.Error("Test Failed - ANX SetDefaults() incorrect values set") } - if anx.Websocket != false { + if anx.Websocket.IsEnabled() != false { t.Error("Test Failed - ANX SetDefaults() incorrect values set") } if anx.RESTPollingDelay != 10 { @@ -61,7 +61,7 @@ func TestSetup(t *testing.T) { if anx.Verbose != false { t.Error("Test Failed - ANX Setup() incorrect values set") } - if anx.Websocket != false { + if anx.Websocket.IsEnabled() != false { t.Error("Test Failed - ANX Setup() incorrect values set") } if len(anx.BaseCurrencies) <= 0 { diff --git a/exchanges/anx/anx_wrapper.go b/exchanges/anx/anx_wrapper.go index 29afcb63..8eb24939 100644 --- a/exchanges/anx/anx_wrapper.go +++ b/exchanges/anx/anx_wrapper.go @@ -244,3 +244,8 @@ func (a *ANX) WithdrawFiatExchangeFunds(currency pair.CurrencyItem, amount float func (a *ANX) WithdrawFiatExchangeFundsToInternationalBank(currency pair.CurrencyItem, amount float64) (string, error) { return "", errors.New("not yet implemented") } + +// GetWebsocket returns a pointer to the exchange websocket +func (a *ANX) GetWebsocket() (*exchange.Websocket, error) { + return nil, errors.New("not yet implemented") +} diff --git a/exchanges/binance/binance.go b/exchanges/binance/binance.go index 9c3f35d3..b649e536 100644 --- a/exchanges/binance/binance.go +++ b/exchanges/binance/binance.go @@ -22,7 +22,7 @@ type Binance struct { exchange.Base WebsocketConn *websocket.Conn - // valid string list that a required by the exchange + // Valid string list that is required by the exchange validLimits []int validIntervals []TimeInterval } @@ -61,7 +61,6 @@ func (b *Binance) SetDefaults() { b.Name = "Binance" b.Enabled = false b.Verbose = false - b.Websocket = false b.RESTPollingDelay = 10 b.RequestCurrencyPairFormat.Delimiter = "" b.RequestCurrencyPairFormat.Uppercase = true @@ -77,6 +76,7 @@ func (b *Binance) SetDefaults() { common.NewHTTPClientWithTimeout(exchange.DefaultHTTPTimeout)) b.APIUrlDefault = apiURL b.APIUrl = b.APIUrlDefault + b.WebsocketInit() } // Setup takes in the supplied exchange configuration details and sets params @@ -91,7 +91,6 @@ func (b *Binance) Setup(exch config.ExchangeConfig) { b.SetHTTPClientUserAgent(exch.HTTPUserAgent) b.RESTPollingDelay = exch.RESTPollingDelay b.Verbose = exch.Verbose - b.Websocket = exch.Websocket b.BaseCurrencies = common.SplitStrings(exch.BaseCurrencies, ",") b.AvailablePairs = common.SplitStrings(exch.AvailablePairs, ",") b.EnabledPairs = common.SplitStrings(exch.EnabledPairs, ",") @@ -111,6 +110,18 @@ func (b *Binance) Setup(exch config.ExchangeConfig) { if err != nil { log.Fatal(err) } + err = b.SetClientProxyAddress(exch.ProxyAddress) + if err != nil { + log.Fatal(err) + } + err = b.WebsocketSetup(b.WSConnect, + exch.Name, + exch.Websocket, + binanceDefaultWebsocketURL, + exch.WebsocketURL) + if err != nil { + log.Fatal(err) + } } } @@ -199,6 +210,8 @@ func (b *Binance) GetOrderBook(obd OrderBookDataRequestParams) (OrderBook, error } } } + + orderbook.LastUpdateID = resp.LastUpdateID return orderbook, nil } diff --git a/exchanges/binance/binance_types.go b/exchanges/binance/binance_types.go index 380102bc..8fe6b1a8 100644 --- a/exchanges/binance/binance_types.go +++ b/exchanges/binance/binance_types.go @@ -61,9 +61,10 @@ type OrderBookData struct { // OrderBook actual structured data that can be used for orderbook type OrderBook struct { - Code int - Msg string - Bids []struct { + LastUpdateID int64 + Code int + Msg string + Bids []struct { Price float64 Quantity float64 } @@ -73,6 +74,24 @@ type OrderBook struct { } } +// DepthUpdateParams is used as an embedded type for WebsocketDepthStream +type DepthUpdateParams []struct { + PriceLevel float64 + Quantity float64 + ingnore []interface{} +} + +// WebsocketDepthStream is the difference for the update depth stream +type WebsocketDepthStream struct { + Event string `json:"e"` + Timestamp int64 `json:"E"` + Pair string `json:"s"` + FirstUpdateID int64 `json:"U"` + LastUpdateID int64 `json:"u"` + UpdateBids []interface{} `json:"b"` + UpdateAsks []interface{} `json:"a"` +} + // RecentTradeRequestParams represents Klines request data. type RecentTradeRequestParams struct { Symbol string `json:"symbol"` // Required field. example LTCBTC, BTCUSDT diff --git a/exchanges/binance/binance_websocket.go b/exchanges/binance/binance_websocket.go index 896cc3ef..9b3b4f48 100644 --- a/exchanges/binance/binance_websocket.go +++ b/exchanges/binance/binance_websocket.go @@ -1,98 +1,358 @@ package binance import ( - "log" + "errors" + "fmt" "net/http" + "net/url" + "strconv" "strings" + "sync" "time" "github.com/gorilla/websocket" "github.com/thrasher-/gocryptotrader/common" + "github.com/thrasher-/gocryptotrader/currency/pair" + exchange "github.com/thrasher-/gocryptotrader/exchanges" + "github.com/thrasher-/gocryptotrader/exchanges/orderbook" ) const ( binanceDefaultWebsocketURL = "wss://stream.binance.com:9443" - binancePingPeriod = 20 * time.Second ) -// WebsocketClient starts and handles the websocket client connection -func (b *Binance) WebsocketClient() { - for b.Enabled && b.Websocket { - var Dialer websocket.Dialer - var err error - // myenabledPairs := strings.ToLower(strings.Replace(strings.Join(b.EnabledPairs, "@ticker/"), "-", "", -1)) + "@trade" +var lastUpdateID map[string]int64 +var m sync.Mutex - myenabledPairsTicker := strings.ToLower(strings.Replace(strings.Join(b.EnabledPairs, "@ticker/"), "-", "", -1)) + "@ticker" - myenabledPairsTrade := strings.ToLower(strings.Replace(strings.Join(b.EnabledPairs, "@trade/"), "-", "", -1)) + "@trade" - myenabledPairsKline := strings.ToLower(strings.Replace(strings.Join(b.EnabledPairs, "@kline_1m/"), "-", "", -1)) + "@kline_1m" - wsurl := b.WebsocketURL + "/stream?streams=" + myenabledPairsTicker + "/" + myenabledPairsTrade + "/" + myenabledPairsKline +// SeedLocalCache seeds depth data +func (b *Binance) SeedLocalCache(p pair.CurrencyPair) error { + var newOrderBook orderbook.Base - // b.WebsocketConn, _, err = Dialer.Dial(binanceDefaultWebsocketURL+myenabledPairs, http.Header{}) - b.WebsocketConn, _, err = Dialer.Dial(wsurl, http.Header{}) + formattedPair := exchange.FormatExchangeCurrency(b.Name, p) + orderbookNew, err := b.GetOrderBook( + OrderBookDataRequestParams{ + Symbol: formattedPair.String(), + Limit: 1000, + }) + + if err != nil { + return err + } + + m.Lock() + if lastUpdateID == nil { + lastUpdateID = make(map[string]int64) + } + + lastUpdateID[formattedPair.String()] = orderbookNew.LastUpdateID + m.Unlock() + + for _, bids := range orderbookNew.Bids { + newOrderBook.Bids = append(newOrderBook.Bids, + orderbook.Item{Amount: bids.Quantity, Price: bids.Price}) + } + for _, Asks := range orderbookNew.Asks { + newOrderBook.Asks = append(newOrderBook.Asks, + orderbook.Item{Amount: Asks.Quantity, Price: Asks.Price}) + } + + newOrderBook.Pair = pair.NewCurrencyPairFromString(formattedPair.String()) + newOrderBook.CurrencyPair = formattedPair.String() + newOrderBook.LastUpdated = time.Now() + newOrderBook.AssetType = "SPOT" + + return b.Websocket.Orderbook.LoadSnapshot(newOrderBook, b.GetName()) +} + +// UpdateLocalCache updates and returns the most recent iteration of the orderbook +func (b *Binance) UpdateLocalCache(ob WebsocketDepthStream) error { + m.Lock() + ID, ok := lastUpdateID[ob.Pair] + if !ok { + m.Unlock() + return errors.New("binance_websocket.go - Unable to find lastUpdateID") + } + + if ob.LastUpdateID+1 <= ID || ID >= ob.LastUpdateID+1 { + // Drop update, out of order + m.Unlock() + return nil + } + + lastUpdateID[ob.Pair] = ob.LastUpdateID + m.Unlock() + + var updateBid, updateAsk []orderbook.Item + + for _, bidsToUpdate := range ob.UpdateBids { + var priceToBeUpdated orderbook.Item + for i, bids := range bidsToUpdate.([]interface{}) { + switch i { + case 0: + priceToBeUpdated.Price, _ = strconv.ParseFloat(bids.(string), 64) + case 1: + priceToBeUpdated.Amount, _ = strconv.ParseFloat(bids.(string), 64) + } + } + updateBid = append(updateBid, priceToBeUpdated) + } + + for _, asksToUpdate := range ob.UpdateAsks { + var priceToBeUpdated orderbook.Item + for i, asks := range asksToUpdate.([]interface{}) { + switch i { + case 0: + priceToBeUpdated.Price, _ = strconv.ParseFloat(asks.(string), 64) + case 1: + priceToBeUpdated.Amount, _ = strconv.ParseFloat(asks.(string), 64) + } + } + updateAsk = append(updateBid, priceToBeUpdated) + } + + updatedTime := time.Unix(ob.Timestamp, 0) + currencyPair := pair.NewCurrencyPairFromString(ob.Pair) + + return b.Websocket.Orderbook.Update(updateBid, + updateAsk, + currencyPair, + updatedTime, + b.GetName(), + "SPOT") +} + +// WSConnect intiates a websocket connection +func (b *Binance) WSConnect() error { + if !b.Websocket.IsEnabled() || !b.IsEnabled() { + return errors.New(exchange.WebsocketNotEnabled) + } + + var Dialer websocket.Dialer + var err error + + ticker := strings.ToLower( + strings.Replace( + strings.Join(b.EnabledPairs, "@ticker/"), "-", "", -1)) + "@ticker" + trade := strings.ToLower( + strings.Replace( + strings.Join(b.EnabledPairs, "@trade/"), "-", "", -1)) + "@trade" + kline := strings.ToLower( + strings.Replace( + strings.Join(b.EnabledPairs, "@kline_1m/"), "-", "", -1)) + "@kline_1m" + depth := strings.ToLower( + strings.Replace( + strings.Join(b.EnabledPairs, "@depth/"), "-", "", -1)) + "@depth" + + wsurl := b.Websocket.GetWebsocketURL() + + "/stream?streams=" + + ticker + + "/" + + trade + + "/" + + kline + + "/" + + depth + + if b.Websocket.GetProxyAddress() != "" { + url, err := url.Parse(b.Websocket.GetProxyAddress()) if err != nil { - log.Printf("%s Unable to connect to Websocket. Error: %s\n", b.Name, err) - continue + return fmt.Errorf("binance_websocket.go - Unable to connect to parse proxy address. Error: %s", + err) } - if b.Verbose { - log.Printf("%s Connected to Websocket.\n", b.Name) - } + Dialer.Proxy = http.ProxyURL(url) + } - for b.Enabled && b.Websocket { + for _, ePair := range b.GetEnabledCurrencies() { + err := b.SeedLocalCache(ePair) + if err != nil { + return err + } + } + + b.WebsocketConn, _, err = Dialer.Dial(wsurl, http.Header{}) + if err != nil { + return fmt.Errorf("binance_websocket.go - Unable to connect to Websocket. Error: %s", + err) + } + + go b.WsHandleData() + + return nil +} + +// WSReadData reads from the websocket connection +func (b *Binance) WSReadData() { + 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() + }() + + for { + select { + case <-b.Websocket.ShutdownC: + return + + default: msgType, resp, err := b.WebsocketConn.ReadMessage() if err != nil { - log.Println(err) - break + b.Websocket.DataHandler <- fmt.Errorf("binance_websocket.go - Websocket Read Data. Error: %s", + err) + return } - switch msgType { + b.Websocket.TrafficAlert <- struct{}{} + b.Websocket.Intercomm <- exchange.WebsocketResponse{Type: msgType, Raw: resp} + } + } +} + +// WsHandleData handles websocket data from WsReadData +func (b *Binance) WsHandleData() { + b.Websocket.Wg.Add(1) + defer b.Websocket.Wg.Done() + + go b.WSReadData() + + for { + select { + case <-b.Websocket.ShutdownC: + return + + case read := <-b.Websocket.Intercomm: + switch read.Type { case websocket.TextMessage: multiStreamData := MultiStreamData{} - err := common.JSONDecode(resp, &multiStreamData) - + err := common.JSONDecode(read.Raw, &multiStreamData) if err != nil { - log.Println("Could not load multi stream data.", string(resp)) + b.Websocket.DataHandler <- fmt.Errorf("binance_websocket.go - Could not load multi stream data: %s", + string(read.Raw)) continue } if strings.Contains(multiStreamData.Stream, "trade") { trade := TradeStream{} - err := common.JSONDecode(multiStreamData.Data, &trade) + err := common.JSONDecode(multiStreamData.Data, &trade) if err != nil { - log.Println("Could not convert to a TradeStream structure") + b.Websocket.DataHandler <- fmt.Errorf("binance_websocket.go - Could not unmarshal trade data: %s", + err) continue } - log.Println("Trade received", trade.Symbol, trade.TimeStamp, trade.TradeID, trade.Price, trade.Quantity) + + price, err := strconv.ParseFloat(trade.Price, 64) + if err != nil { + b.Websocket.DataHandler <- fmt.Errorf("binance_websocket.go - price conversion error: %s", + err) + continue + } + + amount, err := strconv.ParseFloat(trade.Quantity, 64) + if err != nil { + b.Websocket.DataHandler <- fmt.Errorf("binance_websocket.go - amount conversion error: %s", + err) + continue + } + + b.Websocket.DataHandler <- exchange.TradeData{ + CurrencyPair: pair.NewCurrencyPairFromString(trade.Symbol), + Timestamp: time.Unix(0, trade.TimeStamp), + Price: price, + Amount: amount, + Exchange: b.GetName(), + AssetType: "SPOT", + Side: trade.EventType, + } + continue + } else if strings.Contains(multiStreamData.Stream, "ticker") { ticker := TickerStream{} err := common.JSONDecode(multiStreamData.Data, &ticker) if err != nil { - log.Println("Could not convert to a TickerStream structure") + b.Websocket.DataHandler <- fmt.Errorf("binance_websocket.go - Could not convert to a TickerStream structure %s", + err.Error()) continue } - log.Println("Ticker received", ticker.Symbol, ticker.EventTime, ticker.TotalTradedVolume, ticker.LastTradeID) + var wsTicker exchange.TickerData + + wsTicker.Timestamp = time.Unix(0, ticker.EventTime) + wsTicker.Pair = pair.NewCurrencyPairFromString(ticker.Symbol) + wsTicker.AssetType = "SPOT" + wsTicker.Exchange = b.GetName() + wsTicker.ClosePrice, _ = strconv.ParseFloat(ticker.CurrDayClose, 64) + wsTicker.Quantity, _ = strconv.ParseFloat(ticker.TotalTradedVolume, 64) + wsTicker.OpenPrice, _ = strconv.ParseFloat(ticker.OpenPrice, 64) + wsTicker.HighPrice, _ = strconv.ParseFloat(ticker.HighPrice, 64) + wsTicker.LowPrice, _ = strconv.ParseFloat(ticker.LowPrice, 64) + + b.Websocket.DataHandler <- wsTicker + continue + } else if strings.Contains(multiStreamData.Stream, "kline") { kline := KlineStream{} err := common.JSONDecode(multiStreamData.Data, &kline) if err != nil { - log.Println("Could not convert to a KlineStream structure") + b.Websocket.DataHandler <- fmt.Errorf("binance_websocket.go - Could not convert to a KlineStream structure %s", + err) continue } - log.Println("Kline received", kline.Symbol, kline.EventTime, kline.Kline.HighPrice, kline.Kline.LowPrice) - } - type MsgType struct { - MessageType string `json:"messageType"` + var wsKline exchange.KlineData + + wsKline.Timestamp = time.Unix(0, kline.EventTime) + wsKline.Pair = pair.NewCurrencyPairFromString(kline.Symbol) + wsKline.AssetType = "SPOT" + wsKline.Exchange = b.GetName() + wsKline.StartTime = time.Unix(0, kline.Kline.StartTime) + wsKline.CloseTime = time.Unix(0, kline.Kline.CloseTime) + wsKline.Interval = kline.Kline.Interval + wsKline.OpenPrice, _ = strconv.ParseFloat(kline.Kline.OpenPrice, 64) + wsKline.ClosePrice, _ = strconv.ParseFloat(kline.Kline.ClosePrice, 64) + wsKline.HighPrice, _ = strconv.ParseFloat(kline.Kline.HighPrice, 64) + wsKline.LowPrice, _ = strconv.ParseFloat(kline.Kline.LowPrice, 64) + wsKline.Volume, _ = strconv.ParseFloat(kline.Kline.Volume, 64) + + b.Websocket.DataHandler <- wsKline + continue + + } else if common.StringContains(multiStreamData.Stream, "depth") { + depth := WebsocketDepthStream{} + + err := common.JSONDecode(multiStreamData.Data, &depth) + if err != nil { + b.Websocket.DataHandler <- fmt.Errorf("binance_websocket.go - Could not convert to depthStream structure %s", + err) + continue + } + + err = b.UpdateLocalCache(depth) + if err != nil { + b.Websocket.DataHandler <- fmt.Errorf("binance_websocket.go - UpdateLocalCache error: %s", + err) + continue + } + + currencyPair := pair.NewCurrencyPairFromString(depth.Pair) + + b.Websocket.DataHandler <- exchange.WebsocketOrderbookUpdate{ + Pair: currencyPair, + Asset: "SPOT", + Exchange: b.GetName(), + } + continue } } } - b.WebsocketConn.Close() - log.Printf("%s Websocket client disconnected.", b.Name) } } diff --git a/exchanges/binance/binance_wrapper.go b/exchanges/binance/binance_wrapper.go index a1f5e43a..dea21b28 100644 --- a/exchanges/binance/binance_wrapper.go +++ b/exchanges/binance/binance_wrapper.go @@ -24,15 +24,11 @@ func (b *Binance) Start(wg *sync.WaitGroup) { // Run implements the OKEX wrapper func (b *Binance) Run() { if b.Verbose { - log.Printf("%s Websocket: %s. (url: %s).\n", b.GetName(), common.IsEnabled(b.Websocket), b.WebsocketURL) + log.Printf("%s Websocket: %s. (url: %s).\n", b.GetName(), common.IsEnabled(b.Websocket.IsEnabled()), b.Websocket.GetWebsocketURL()) log.Printf("%s polling delay: %ds.\n", b.GetName(), b.RESTPollingDelay) log.Printf("%s %d currencies enabled: %s.\n", b.GetName(), len(b.EnabledPairs), b.EnabledPairs) } - if b.Websocket { - go b.WebsocketClient() - } - symbols, err := b.GetExchangeValidCurrencyPairs() if err != nil { log.Printf("%s Failed to get exchange info.\n", b.GetName()) @@ -193,3 +189,8 @@ func (b *Binance) WithdrawFiatExchangeFunds(currency pair.CurrencyItem, amount f func (b *Binance) WithdrawFiatExchangeFundsToInternationalBank(currency pair.CurrencyItem, amount float64) (string, error) { return "", errors.New("not yet implemented") } + +// GetWebsocket returns a pointer to the exchange websocket +func (b *Binance) GetWebsocket() (*exchange.Websocket, error) { + return b.Websocket, nil +} diff --git a/exchanges/bitfinex/bitfinex.go b/exchanges/bitfinex/bitfinex.go index 79377b7e..28dedc99 100644 --- a/exchanges/bitfinex/bitfinex.go +++ b/exchanges/bitfinex/bitfinex.go @@ -91,7 +91,6 @@ func (b *Bitfinex) SetDefaults() { b.Name = "Bitfinex" b.Enabled = false b.Verbose = false - b.Websocket = false b.RESTPollingDelay = 10 b.WebsocketSubdChannels = make(map[int]WebsocketChanInfo) b.RequestCurrencyPairFormat.Delimiter = "" @@ -107,6 +106,7 @@ func (b *Bitfinex) SetDefaults() { common.NewHTTPClientWithTimeout(exchange.DefaultHTTPTimeout)) b.APIUrlDefault = bitfinexAPIURLBase b.APIUrl = b.APIUrlDefault + b.WebsocketInit() } // Setup takes in the supplied exchange configuration details and sets params @@ -121,7 +121,7 @@ func (b *Bitfinex) Setup(exch config.ExchangeConfig) { b.SetHTTPClientUserAgent(exch.HTTPUserAgent) b.RESTPollingDelay = exch.RESTPollingDelay b.Verbose = exch.Verbose - b.Websocket = exch.Websocket + b.Websocket.SetEnabled(exch.Websocket) b.BaseCurrencies = common.SplitStrings(exch.BaseCurrencies, ",") b.AvailablePairs = common.SplitStrings(exch.AvailablePairs, ",") b.EnabledPairs = common.SplitStrings(exch.EnabledPairs, ",") @@ -141,6 +141,18 @@ func (b *Bitfinex) Setup(exch config.ExchangeConfig) { if err != nil { log.Fatal(err) } + err = b.SetClientProxyAddress(exch.ProxyAddress) + if err != nil { + log.Fatal(err) + } + err = b.WebsocketSetup(b.WsConnect, + exch.Name, + exch.Websocket, + bitfinexWebsocket, + exch.WebsocketURL) + if err != nil { + log.Fatal(err) + } } } diff --git a/exchanges/bitfinex/bitfinex_test.go b/exchanges/bitfinex/bitfinex_test.go index e64fad5f..a499db1a 100644 --- a/exchanges/bitfinex/bitfinex_test.go +++ b/exchanges/bitfinex/bitfinex_test.go @@ -29,7 +29,7 @@ func TestSetup(t *testing.T) { b.Setup(bfxConfig) if !b.Enabled || b.AuthenticatedAPISupport || b.RESTPollingDelay != time.Duration(10) || - b.Verbose || b.Websocket || len(b.BaseCurrencies) < 1 || + b.Verbose || b.Websocket.IsEnabled() || len(b.BaseCurrencies) < 1 || len(b.AvailablePairs) < 1 || len(b.EnabledPairs) < 1 { t.Error("Test Failed - Bitfinex Setup values not set correctly") } diff --git a/exchanges/bitfinex/bitfinex_websocket.go b/exchanges/bitfinex/bitfinex_websocket.go index 9a597251..e8e74bc7 100644 --- a/exchanges/bitfinex/bitfinex_websocket.go +++ b/exchanges/bitfinex/bitfinex_websocket.go @@ -1,14 +1,20 @@ package bitfinex import ( + "errors" + "fmt" "log" "net/http" + "net/url" "reflect" "strconv" "time" "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 ( @@ -36,26 +42,35 @@ const ( bitfinexWebsocketUnknownChannel = "10302" ) -// WebsocketPingHandler sends a ping request to the websocket server -func (b *Bitfinex) WebsocketPingHandler() error { +// WebsocketHandshake defines the communication between the websocket API for +// initial connection +type WebsocketHandshake struct { + Event string `json:"event"` + Code int64 `json:"code"` + Version float64 `json:"version"` +} + +var pongReceive chan struct{} + +// WsPingHandler sends a ping request to the websocket server +func (b *Bitfinex) WsPingHandler() error { request := make(map[string]string) request["event"] = "ping" - return b.WebsocketSend(request) + return b.WsSend(request) } -// WebsocketSend sends data to the websocket server -func (b *Bitfinex) WebsocketSend(data interface{}) error { +// WsSend sends data to the websocket server +func (b *Bitfinex) WsSend(data interface{}) error { json, err := common.JSONEncode(data) if err != nil { return err } - return b.WebsocketConn.WriteMessage(websocket.TextMessage, json) } -// WebsocketSubscribe subscribes to the websocket channel -func (b *Bitfinex) WebsocketSubscribe(channel string, params map[string]string) error { +// WsSubscribe subscribes to the websocket channel +func (b *Bitfinex) WsSubscribe(channel string, params map[string]string) error { request := make(map[string]string) request["event"] = "subscribe" request["channel"] = channel @@ -65,114 +80,169 @@ func (b *Bitfinex) WebsocketSubscribe(channel string, params map[string]string) request[k] = v } } - return b.WebsocketSend(request) + return b.WsSend(request) } -// WebsocketSendAuth sends a autheticated event payload -func (b *Bitfinex) WebsocketSendAuth() error { +// WsSendAuth sends a autheticated event payload +func (b *Bitfinex) WsSendAuth() error { request := make(map[string]interface{}) payload := "AUTH" + strconv.FormatInt(time.Now().UnixNano(), 10)[:13] request["event"] = "auth" request["apiKey"] = b.APIKey - request["authSig"] = common.HexEncodeToString(common.GetHMAC(common.HashSHA512_384, []byte(payload), []byte(b.APISecret))) + + request["authSig"] = common.HexEncodeToString( + common.GetHMAC( + common.HashSHA512_384, + []byte(payload), + []byte(b.APISecret))) + request["authPayload"] = payload - return b.WebsocketSend(request) + return b.WsSend(request) } -// WebsocketSendUnauth sends an unauthenticated payload -func (b *Bitfinex) WebsocketSendUnauth() error { +// WsSendUnauth sends an unauthenticated payload +func (b *Bitfinex) WsSendUnauth() error { request := make(map[string]string) request["event"] = "unauth" - return b.WebsocketSend(request) + return b.WsSend(request) } -// WebsocketAddSubscriptionChannel adds a new subscription channel to the +// WsAddSubscriptionChannel adds a new subscription channel to the // WebsocketSubdChannels map in bitfinex.go (Bitfinex struct) -func (b *Bitfinex) WebsocketAddSubscriptionChannel(chanID int, channel, pair string) { +func (b *Bitfinex) WsAddSubscriptionChannel(chanID int, channel, pair string) { chanInfo := WebsocketChanInfo{Pair: pair, Channel: channel} b.WebsocketSubdChannels[chanID] = chanInfo if b.Verbose { - log.Printf("%s Subscribed to Channel: %s Pair: %s ChannelID: %d\n", b.GetName(), channel, pair, chanID) + log.Printf("%s Subscribed to Channel: %s Pair: %s ChannelID: %d\n", + b.GetName(), + channel, + pair, + chanID) } } -// WebsocketClient makes a connection with the websocket server -func (b *Bitfinex) WebsocketClient() { - channels := []string{"book", "trades", "ticker"} - for b.Enabled && b.Websocket { - var Dialer websocket.Dialer - var err error - b.WebsocketConn, _, err = Dialer.Dial(bitfinexWebsocket, http.Header{}) +// WsConnect starts a new websocket connection +func (b *Bitfinex) WsConnect() error { + if !b.Websocket.IsEnabled() || !b.IsEnabled() { + return errors.New(exchange.WebsocketNotEnabled) + } + var channels = []string{"book", "trades", "ticker"} + var Dialer websocket.Dialer + var err error + + if b.Websocket.GetProxyAddress() != "" { + proxy, err := url.Parse(b.Websocket.GetProxyAddress()) if err != nil { - log.Printf("%s Unable to connect to Websocket. Error: %s\n", b.GetName(), err) - continue + return err } + Dialer.Proxy = http.ProxyURL(proxy) + } - msgType, resp, err := b.WebsocketConn.ReadMessage() - if err != nil { - log.Printf("%s Unable to read from Websocket. Error: %s\n", b.GetName(), err) - continue - } - if msgType != websocket.TextMessage { - continue - } + b.WebsocketConn, _, err = Dialer.Dial(b.Websocket.GetWebsocketURL(), http.Header{}) + if err != nil { + return fmt.Errorf("Unable to connect to Websocket. Error: %s", err) + } - type WebsocketHandshake struct { - Event string `json:"event"` - Code int64 `json:"code"` - Version float64 `json:"version"` - } + _, resp, err := b.WebsocketConn.ReadMessage() + if err != nil { + return fmt.Errorf("Unable to read from Websocket. Error: %s", err) + } - hs := WebsocketHandshake{} - err = common.JSONDecode(resp, &hs) - if err != nil { - log.Println(err) - continue - } + var hs WebsocketHandshake + err = common.JSONDecode(resp, &hs) + if err != nil { + return err + } - if hs.Event == "info" { - if b.Verbose { - log.Printf("%s Connected to Websocket.\n", b.GetName()) + if hs.Event == "info" { + if b.Verbose { + log.Printf("%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" } - } - - for _, x := range channels { - for _, y := range b.EnabledPairs { - params := make(map[string]string) - if x == "book" { - params["prec"] = "P0" - } - params["pair"] = y - b.WebsocketSubscribe(x, params) - } - } - - if b.AuthenticatedAPISupport { - err = b.WebsocketSendAuth() + params["pair"] = y + err := b.WsSubscribe(x, params) if err != nil { - log.Println(err) + return err } } + } - for b.Enabled && b.Websocket { + if b.AuthenticatedAPISupport { + err = b.WsSendAuth() + if err != nil { + return err + } + } + + pongReceive = make(chan struct{}, 1) + + go b.WsReadData() + go b.WsDataHandler() + + return nil +} + +// WsReadData reads and handles websocket stream data +func (b *Bitfinex) WsReadData() { + 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() + }() + + for { + select { + case <-b.Websocket.ShutdownC: + return + default: msgType, resp, err := b.WebsocketConn.ReadMessage() if err != nil { - log.Println(err) - break + b.Websocket.DataHandler <- err + return } - switch msgType { + b.Websocket.TrafficAlert <- struct{}{} + + b.Websocket.Intercomm <- exchange.WebsocketResponse{ + Type: msgType, + Raw: resp, + } + } + } +} + +// WsDataHandler handles data from WsReadData +func (b *Bitfinex) WsDataHandler() { + b.Websocket.Wg.Add(1) + defer b.Websocket.Wg.Done() + + for { + select { + case <-b.Websocket.ShutdownC: + return + + case stream := <-b.Websocket.Intercomm: + + switch stream.Type { case websocket.TextMessage: var result interface{} - err := common.JSONDecode(resp, &result) - if err != nil { - log.Println(err) - continue - } + common.JSONDecode(stream.Raw, &result) switch reflect.TypeOf(result).String() { case "map[string]interface {}": @@ -181,51 +251,99 @@ func (b *Bitfinex) WebsocketClient() { switch event { case "subscribed": - b.WebsocketAddSubscriptionChannel(int(eventData["chanId"].(float64)), eventData["channel"].(string), eventData["pair"].(string)) + b.WsAddSubscriptionChannel(int(eventData["chanId"].(float64)), + eventData["channel"].(string), + eventData["pair"].(string)) + case "auth": status := eventData["status"].(string) if status == "OK" { - b.WebsocketAddSubscriptionChannel(0, "account", "N/A") + b.WsAddSubscriptionChannel(0, "account", "N/A") + } else if status == "fail" { - log.Printf("%s Websocket unable to AUTH. Error code: %s\n", b.GetName(), eventData["code"].(string)) + b.Websocket.DataHandler <- fmt.Errorf("bitfinex.go error - Websocket unable to AUTH. Error code: %s", + eventData["code"].(string)) + b.AuthenticatedAPISupport = false } } + case "[]interface {}": chanData := result.([]interface{}) chanID := int(chanData[0].(float64)) - chanInfo, ok := b.WebsocketSubdChannels[chanID] + chanInfo, ok := b.WebsocketSubdChannels[chanID] if !ok { - log.Printf("Unable to locate chanID: %d\n", chanID) + b.Websocket.DataHandler <- fmt.Errorf("bitfinex.go error - Unable to locate chanID: %d", + chanID) + continue } else { if len(chanData) == 2 { if reflect.TypeOf(chanData[1]).String() == "string" { if chanData[1].(string) == bitfinexWebsocketHeartbeat { continue + } else if chanData[1].(string) == "pong" { + pongReceive <- struct{}{} + continue } } } + switch chanInfo.Channel { case "book": - orderbook := []WebsocketBook{} + newOrderbook := []WebsocketBook{} switch len(chanData) { case 2: data := chanData[1].([]interface{}) for _, x := range data { y := x.([]interface{}) - orderbook = append(orderbook, WebsocketBook{Price: y[0].(float64), Count: int(y[1].(float64)), Amount: y[2].(float64)}) + newOrderbook = append(newOrderbook, WebsocketBook{ + Price: y[0].(float64), + Count: int(y[1].(float64)), + Amount: y[2].(float64)}) } - case 4: - orderbook = append(orderbook, WebsocketBook{Price: chanData[1].(float64), Count: int(chanData[2].(float64)), Amount: chanData[3].(float64)}) - } - log.Println(orderbook) - case "ticker": - ticker := WebsocketTicker{Bid: chanData[1].(float64), BidSize: chanData[2].(float64), Ask: chanData[3].(float64), AskSize: chanData[4].(float64), - DailyChange: chanData[5].(float64), DialyChangePerc: chanData[6].(float64), LastPrice: chanData[7].(float64), Volume: chanData[8].(float64)} - log.Printf("Bitfinex %s Websocket Last %f Volume %f\n", chanInfo.Pair, ticker.LastPrice, ticker.Volume) + case 4: + newOrderbook = append(newOrderbook, WebsocketBook{ + Price: chanData[1].(float64), + Count: int(chanData[2].(float64)), + Amount: chanData[3].(float64)}) + } + + if len(newOrderbook) > 1 { + err := b.WsInsertSnapshot(pair.NewCurrencyPairFromString(chanInfo.Pair), + "SPOT", + newOrderbook) + + if err != nil { + b.Websocket.DataHandler <- fmt.Errorf("bitfinex_websocket.go inserting snapshot error: %s", + err) + } + + continue + } + + err := b.WsUpdateOrderbook(pair.NewCurrencyPairFromString(chanInfo.Pair), + "SPOT", + newOrderbook[0]) + + if err != nil { + b.Websocket.DataHandler <- fmt.Errorf("bitfinex_websocket.go updating orderbook error: %s", + err) + } + + case "ticker": + b.Websocket.DataHandler <- exchange.TickerData{ + Quantity: chanData[8].(float64), + ClosePrice: chanData[7].(float64), + HighPrice: chanData[9].(float64), + LowPrice: chanData[10].(float64), + Pair: pair.NewCurrencyPairFromString(chanInfo.Pair), + Exchange: b.GetName(), + AssetType: "SPOT", + } + case "account": switch chanData[1].(string) { case bitfinexWebsocketPositionSnapshot: @@ -233,47 +351,108 @@ func (b *Bitfinex) WebsocketClient() { data := chanData[2].([]interface{}) for _, x := range data { y := x.([]interface{}) - positionSnapshot = append(positionSnapshot, WebsocketPosition{Pair: y[0].(string), Status: y[1].(string), Amount: y[2].(float64), Price: y[3].(float64), - MarginFunding: y[4].(float64), MarginFundingType: int(y[5].(float64))}) + positionSnapshot = append(positionSnapshot, + WebsocketPosition{ + Pair: y[0].(string), + Status: y[1].(string), + Amount: y[2].(float64), + Price: y[3].(float64), + MarginFunding: y[4].(float64), + MarginFundingType: int(y[5].(float64))}) } - log.Println(positionSnapshot) + + if len(positionSnapshot) == 0 { + continue + } + + b.Websocket.DataHandler <- positionSnapshot + case bitfinexWebsocketPositionNew, bitfinexWebsocketPositionUpdate, bitfinexWebsocketPositionClose: data := chanData[2].([]interface{}) - position := WebsocketPosition{Pair: data[0].(string), Status: data[1].(string), Amount: data[2].(float64), Price: data[3].(float64), - MarginFunding: data[4].(float64), MarginFundingType: int(data[5].(float64))} - log.Println(position) + position := WebsocketPosition{ + Pair: data[0].(string), + Status: data[1].(string), + Amount: data[2].(float64), + Price: data[3].(float64), + MarginFunding: data[4].(float64), + MarginFundingType: int(data[5].(float64))} + + b.Websocket.DataHandler <- position + case bitfinexWebsocketWalletSnapshot: data := chanData[2].([]interface{}) walletSnapshot := []WebsocketWallet{} for _, x := range data { y := x.([]interface{}) - walletSnapshot = append(walletSnapshot, WebsocketWallet{Name: y[0].(string), Currency: y[1].(string), Balance: y[2].(float64), UnsettledInterest: y[3].(float64)}) + walletSnapshot = append(walletSnapshot, + WebsocketWallet{ + Name: y[0].(string), + Currency: y[1].(string), + Balance: y[2].(float64), + UnsettledInterest: y[3].(float64)}) } - log.Println(walletSnapshot) + + b.Websocket.DataHandler <- walletSnapshot + case bitfinexWebsocketWalletUpdate: data := chanData[2].([]interface{}) - wallet := WebsocketWallet{Name: data[0].(string), Currency: data[1].(string), Balance: data[2].(float64), UnsettledInterest: data[3].(float64)} - log.Println(wallet) + wallet := WebsocketWallet{ + Name: data[0].(string), + Currency: data[1].(string), + Balance: data[2].(float64), + UnsettledInterest: data[3].(float64)} + + b.Websocket.DataHandler <- wallet + case bitfinexWebsocketOrderSnapshot: orderSnapshot := []WebsocketOrder{} data := chanData[2].([]interface{}) for _, x := range data { y := x.([]interface{}) - orderSnapshot = append(orderSnapshot, WebsocketOrder{OrderID: int64(y[0].(float64)), Pair: y[1].(string), Amount: y[2].(float64), OrigAmount: y[3].(float64), - OrderType: y[4].(string), Status: y[5].(string), Price: y[6].(float64), PriceAvg: y[7].(float64), Timestamp: y[8].(string)}) + orderSnapshot = append(orderSnapshot, + WebsocketOrder{ + OrderID: int64(y[0].(float64)), + Pair: y[1].(string), + Amount: y[2].(float64), + OrigAmount: y[3].(float64), + OrderType: y[4].(string), + Status: y[5].(string), + Price: y[6].(float64), + PriceAvg: y[7].(float64), + Timestamp: y[8].(string)}) } - log.Println(orderSnapshot) + + b.Websocket.DataHandler <- orderSnapshot + case bitfinexWebsocketOrderNew, bitfinexWebsocketOrderUpdate, bitfinexWebsocketOrderCancel: data := chanData[2].([]interface{}) - order := WebsocketOrder{OrderID: int64(data[0].(float64)), Pair: data[1].(string), Amount: data[2].(float64), OrigAmount: data[3].(float64), - OrderType: data[4].(string), Status: data[5].(string), Price: data[6].(float64), PriceAvg: data[7].(float64), Timestamp: data[8].(string), Notify: int(data[9].(float64))} - log.Println(order) + order := WebsocketOrder{ + OrderID: int64(data[0].(float64)), + Pair: data[1].(string), + Amount: data[2].(float64), + OrigAmount: data[3].(float64), + OrderType: data[4].(string), + Status: data[5].(string), + Price: data[6].(float64), + PriceAvg: data[7].(float64), + Timestamp: data[8].(string), + Notify: int(data[9].(float64))} + + b.Websocket.DataHandler <- order + case bitfinexWebsocketTradeExecuted: data := chanData[2].([]interface{}) - trade := WebsocketTradeExecuted{TradeID: int64(data[0].(float64)), Pair: data[1].(string), Timestamp: int64(data[2].(float64)), OrderID: int64(data[3].(float64)), - AmountExecuted: data[4].(float64), PriceExecuted: data[5].(float64)} - log.Println(trade) + trade := WebsocketTradeExecuted{ + TradeID: int64(data[0].(float64)), + Pair: data[1].(string), + Timestamp: int64(data[2].(float64)), + OrderID: int64(data[3].(float64)), + AmountExecuted: data[4].(float64), + PriceExecuted: data[5].(float64)} + + b.Websocket.DataHandler <- trade } + case "trades": trades := []WebsocketTrade{} switch len(chanData) { @@ -284,23 +463,174 @@ func (b *Bitfinex) WebsocketClient() { if _, ok := y[0].(string); ok { continue } - trades = append(trades, WebsocketTrade{ID: int64(y[0].(float64)), Timestamp: int64(y[1].(float64)), Price: y[2].(float64), Amount: y[3].(float64)}) - } - case 7: - trade := WebsocketTrade{ID: int64(chanData[3].(float64)), Timestamp: int64(chanData[4].(float64)), Price: chanData[5].(float64), Amount: chanData[6].(float64)} - trades = append(trades, trade) - if b.Verbose { - log.Printf("Bitfinex %s Websocket Trade ID %d Timestamp %d Price %f Amount %f\n", chanInfo.Pair, trade.ID, trade.Timestamp, trade.Price, trade.Amount) + id, _ := y[0].(float64) + + trades = append(trades, + WebsocketTrade{ + ID: int64(id), + Timestamp: int64(y[1].(float64)), + Price: y[2].(float64), + Amount: y[3].(float64)}) + } + + case 7: + trade := WebsocketTrade{ + ID: int64(chanData[3].(float64)), + Timestamp: int64(chanData[4].(float64)), + Price: chanData[5].(float64), + Amount: chanData[6].(float64)} + trades = append(trades, trade) + } + + if len(trades) > 0 { + side := "BUY" + newAmount := trades[0].Amount + if newAmount < 0 { + side = "SELL" + newAmount = newAmount * -1 + } + + b.Websocket.DataHandler <- exchange.TradeData{ + CurrencyPair: pair.NewCurrencyPairFromString(chanInfo.Pair), + Timestamp: time.Unix(trades[0].Timestamp, 0), + Price: trades[0].Price, + Amount: newAmount, + Exchange: b.GetName(), + AssetType: "SPOT", + Side: side, } } - log.Println(trades) } } } } } - b.WebsocketConn.Close() - log.Printf("%s Websocket client disconnected.\n", b.GetName()) } } + +// WsInsertSnapshot add the initial orderbook snapshot when subscribed to a +// channel +func (b *Bitfinex) WsInsertSnapshot(p pair.CurrencyPair, assetType string, books []WebsocketBook) error { + if len(books) == 0 { + return errors.New("bitfinex.go error - no orderbooks submitted") + } + + var bid, ask []orderbook.Item + for _, book := range books { + if book.Amount >= 0 { + bid = append(bid, orderbook.Item{Amount: book.Amount, Price: book.Price}) + } else { + ask = append(ask, orderbook.Item{Amount: book.Amount * -1, Price: book.Price}) + } + } + + if len(bid) == 0 && len(ask) == 0 { + return errors.New("bitfinex.go error - no orderbooks in item lists") + } + + var newOrderbook orderbook.Base + newOrderbook.Asks = ask + newOrderbook.AssetType = assetType + newOrderbook.Bids = bid + newOrderbook.CurrencyPair = p.Pair().String() + newOrderbook.LastUpdated = time.Now() + newOrderbook.Pair = p + + err := b.Websocket.Orderbook.LoadSnapshot(newOrderbook, b.GetName()) + if err != nil { + return fmt.Errorf("bitfinex.go error - %s", err) + } + + b.Websocket.DataHandler <- exchange.WebsocketOrderbookUpdate{Pair: p, + Asset: assetType, + Exchange: b.GetName()} + return nil +} + +// WsUpdateOrderbook updates the orderbook list, removing and adding to the +// orderbook sides +func (b *Bitfinex) WsUpdateOrderbook(p pair.CurrencyPair, assetType string, book WebsocketBook) error { + + if book.Count > 0 { + if book.Amount > 0 { + // Update/add bid + newBidPrice := orderbook.Item{Price: book.Price, Amount: book.Amount} + err := b.Websocket.Orderbook.Update([]orderbook.Item{newBidPrice}, + nil, + p, + time.Now(), + b.GetName(), + assetType) + + if err != nil { + return err + } + + b.Websocket.DataHandler <- exchange.WebsocketOrderbookUpdate{Pair: p, + Asset: assetType, + Exchange: b.GetName()} + + return nil + } + + // Update/add ask + newAskPrice := orderbook.Item{Price: book.Price, Amount: book.Amount * -1} + err := b.Websocket.Orderbook.Update(nil, + []orderbook.Item{newAskPrice}, + p, + time.Now(), + b.GetName(), + assetType) + + if err != nil { + return err + } + + b.Websocket.DataHandler <- exchange.WebsocketOrderbookUpdate{Pair: p, + Asset: assetType, + Exchange: b.GetName()} + + return nil + } + + if book.Amount == 1 { + // Remove bid + bidPriceRemove := orderbook.Item{Price: book.Price, Amount: 0} + err := b.Websocket.Orderbook.Update([]orderbook.Item{bidPriceRemove}, + nil, + p, + time.Now(), + b.GetName(), + assetType) + + if err != nil { + return err + } + + b.Websocket.DataHandler <- exchange.WebsocketOrderbookUpdate{Pair: p, + Asset: assetType, + Exchange: b.GetName()} + + return nil + } + + // Remove from ask + askPriceRemove := orderbook.Item{Price: book.Price, Amount: 0} + err := b.Websocket.Orderbook.Update(nil, + []orderbook.Item{askPriceRemove}, + p, + time.Now(), + b.GetName(), + assetType) + + if err != nil { + return err + } + + b.Websocket.DataHandler <- exchange.WebsocketOrderbookUpdate{Pair: p, + Asset: assetType, + Exchange: b.GetName()} + + return nil +} diff --git a/exchanges/bitfinex/bitfinex_websocket_test.go b/exchanges/bitfinex/bitfinex_websocket_test.go deleted file mode 100644 index 65db4f0a..00000000 --- a/exchanges/bitfinex/bitfinex_websocket_test.go +++ /dev/null @@ -1,80 +0,0 @@ -package bitfinex - -// func TestWebsocketPingHandler(t *testing.T) { -// wsPingHandler := Bitfinex{} -// var Dialer websocket.Dialer -// var err error -// -// wsPingHandler.WebsocketConn, _, err = Dialer.Dial(bitfinexWebsocket, http.Header{}) -// if err != nil { -// t.Errorf("Test Failed - Bitfinex dialer error: %s", err) -// } -// err = wsPingHandler.WebsocketPingHandler() -// if err != nil { -// t.Errorf("Test Failed - Bitfinex WebsocketPingHandler() error: %s", err) -// } -// err = wsPingHandler.WebsocketConn.Close() -// if err != nil { -// t.Errorf("Test Failed - Bitfinex websocketConn.Close() error: %s", err) -// } -// } -// -// func TestWebsocketSubscribe(t *testing.T) { -// websocketSubcribe := Bitfinex{} -// var Dialer websocket.Dialer -// var err error -// params := make(map[string]string) -// params["pair"] = "BTCUSD" -// -// websocketSubcribe.WebsocketConn, _, err = Dialer.Dial(bitfinexWebsocket, http.Header{}) -// if err != nil { -// t.Errorf("Test Failed - Bitfinex Dialer error: %s", err) -// } -// err = websocketSubcribe.WebsocketSubscribe("ticker", params) -// if err != nil { -// t.Errorf("Test Failed - Bitfinex WebsocketSubscribe() error: %s", err) -// } -// -// err = websocketSubcribe.WebsocketConn.Close() -// if err != nil { -// t.Errorf("Test Failed - Bitfinex websocketConn.Close() error: %s", err) -// } -// } -// -// func TestWebsocketSendAuth(t *testing.T) { -// wsSendAuth := Bitfinex{} -// var Dialer websocket.Dialer -// var err error -// -// wsSendAuth.WebsocketConn, _, err = Dialer.Dial(bitfinexWebsocket, http.Header{}) -// if err != nil { -// t.Errorf("Test Failed - Bitfinex Dialer error: %s", err) -// } -// err = wsSendAuth.WebsocketSendAuth() -// if err != nil { -// t.Errorf("Test Failed - Bitfinex WebsocketSendAuth() error: %s", err) -// } -// } -// -// func TestWebsocketAddSubscriptionChannel(t *testing.T) { -// wsAddSubscriptionChannel := Bitfinex{} -// wsAddSubscriptionChannel.SetDefaults() -// var Dialer websocket.Dialer -// var err error -// -// wsAddSubscriptionChannel.WebsocketConn, _, err = Dialer.Dial(bitfinexWebsocket, http.Header{}) -// if err != nil { -// t.Errorf("Test Failed - Bitfinex Dialer error: %s", err) -// } -// -// wsAddSubscriptionChannel.WebsocketAddSubscriptionChannel(1337, "ticker", "BTCUSD") -// if len(wsAddSubscriptionChannel.WebsocketSubdChannels) == 0 { -// t.Errorf("Test Failed - Bitfinex WebsocketAddSubscriptionChannel() error: %s", err) -// } -// if wsAddSubscriptionChannel.WebsocketSubdChannels[1337].Channel != "ticker" { -// t.Errorf("Test Failed - Bitfinex WebsocketAddSubscriptionChannel() error: %s", err) -// } -// if wsAddSubscriptionChannel.WebsocketSubdChannels[1337].Pair != "BTCUSD" { -// t.Errorf("Test Failed - Bitfinex WebsocketAddSubscriptionChannel() error: %s", err) -// } -// } diff --git a/exchanges/bitfinex/bitfinex_wrapper.go b/exchanges/bitfinex/bitfinex_wrapper.go index e1873c49..38541954 100644 --- a/exchanges/bitfinex/bitfinex_wrapper.go +++ b/exchanges/bitfinex/bitfinex_wrapper.go @@ -25,15 +25,11 @@ func (b *Bitfinex) Start(wg *sync.WaitGroup) { // Run implements the Bitfinex wrapper func (b *Bitfinex) Run() { if b.Verbose { - log.Printf("%s Websocket: %s.", b.GetName(), common.IsEnabled(b.Websocket)) + log.Printf("%s Websocket: %s.", b.GetName(), common.IsEnabled(b.Websocket.IsEnabled())) log.Printf("%s polling delay: %ds.\n", b.GetName(), b.RESTPollingDelay) log.Printf("%s %d currencies enabled: %s.\n", b.GetName(), len(b.EnabledPairs), b.EnabledPairs) } - if b.Websocket { - go b.WebsocketClient() - } - exchangeProducts, err := b.GetSymbols() if err != nil { log.Printf("%s Failed to get available symbols.\n", b.GetName()) @@ -225,3 +221,8 @@ func (b *Bitfinex) WithdrawFiatExchangeFunds(currency pair.CurrencyItem, amount func (b *Bitfinex) WithdrawFiatExchangeFundsToInternationalBank(currency pair.CurrencyItem, amount float64) (string, error) { return "", errors.New("not yet implemented") } + +// GetWebsocket returns a pointer to the exchange websocket +func (b *Bitfinex) GetWebsocket() (*exchange.Websocket, error) { + return b.Websocket, nil +} diff --git a/exchanges/bitfinex/bitfinex_wrapper_test.go b/exchanges/bitfinex/bitfinex_wrapper_test.go deleted file mode 100644 index e2e2871c..00000000 --- a/exchanges/bitfinex/bitfinex_wrapper_test.go +++ /dev/null @@ -1,30 +0,0 @@ -package bitfinex - -// func TestStart(t *testing.T) { -// start := Bitfinex{} -// start.Start(wg *sync.WaitGroup) -// } -// -// func TestRun(t *testing.T) { -// run := Bitfinex{} -// run.Run() -// } -// -// func TestGetTickerPrice(t *testing.T) { -// getTickerPrice := Bitfinex{} -// getTickerPrice.EnabledPairs = []string{"BTCUSD", "LTCUSD"} -// _, err := getTickerPrice.GetTickerPrice(pair.NewCurrencyPair("BTC", "USD"), -// ticker.Spot) -// if err != nil { -// t.Errorf("Test Failed - Bitfinex GetTickerPrice() error: %s", err) -// } -// } -// -// func TestGetOrderbookEx(t *testing.T) { -// getOrderBookEx := Bitfinex{} -// _, err := getOrderBookEx.GetOrderbookEx(pair.NewCurrencyPair("BTC", "USD"), -// ticker.Spot) -// if err != nil { -// t.Errorf("Test Failed - Bitfinex GetOrderbookEx() error: %s", err) -// } -// } diff --git a/exchanges/bitflyer/bitflyer.go b/exchanges/bitflyer/bitflyer.go index 69308f98..039422c8 100644 --- a/exchanges/bitflyer/bitflyer.go +++ b/exchanges/bitflyer/bitflyer.go @@ -79,7 +79,6 @@ func (b *Bitflyer) SetDefaults() { b.Name = "Bitflyer" b.Enabled = false b.Verbose = false - b.Websocket = false b.RESTPollingDelay = 10 b.RequestCurrencyPairFormat.Delimiter = "_" b.RequestCurrencyPairFormat.Uppercase = true @@ -96,6 +95,7 @@ func (b *Bitflyer) SetDefaults() { b.APIUrl = b.APIUrlDefault b.APIUrlSecondaryDefault = chainAnalysis b.APIUrlSecondary = b.APIUrlSecondaryDefault + b.WebsocketInit() } // Setup takes in the supplied exchange configuration details and sets params @@ -110,7 +110,7 @@ func (b *Bitflyer) Setup(exch config.ExchangeConfig) { b.SetHTTPClientUserAgent(exch.HTTPUserAgent) b.RESTPollingDelay = exch.RESTPollingDelay b.Verbose = exch.Verbose - b.Websocket = exch.Websocket + b.Websocket.SetEnabled(exch.Websocket) b.BaseCurrencies = common.SplitStrings(exch.BaseCurrencies, ",") b.AvailablePairs = common.SplitStrings(exch.AvailablePairs, ",") b.EnabledPairs = common.SplitStrings(exch.EnabledPairs, ",") @@ -130,6 +130,10 @@ func (b *Bitflyer) Setup(exch config.ExchangeConfig) { if err != nil { log.Fatal(err) } + err = b.SetClientProxyAddress(exch.ProxyAddress) + if err != nil { + log.Fatal(err) + } } } diff --git a/exchanges/bitflyer/bitflyer_wrapper.go b/exchanges/bitflyer/bitflyer_wrapper.go index 80a1909e..45613deb 100644 --- a/exchanges/bitflyer/bitflyer_wrapper.go +++ b/exchanges/bitflyer/bitflyer_wrapper.go @@ -24,7 +24,7 @@ func (b *Bitflyer) Start(wg *sync.WaitGroup) { // Run implements the Bitflyer wrapper func (b *Bitflyer) Run() { if b.Verbose { - log.Printf("%s Websocket: %s.", b.GetName(), common.IsEnabled(b.Websocket)) + log.Printf("%s Websocket: %s.", b.GetName(), common.IsEnabled(b.Websocket.IsEnabled())) log.Printf("%s polling delay: %ds.\n", b.GetName(), b.RESTPollingDelay) log.Printf("%s %d currencies enabled: %s.\n", b.GetName(), len(b.EnabledPairs), b.EnabledPairs) } @@ -201,3 +201,8 @@ func (b *Bitflyer) WithdrawFiatExchangeFunds(currency pair.CurrencyItem, amount func (b *Bitflyer) WithdrawFiatExchangeFundsToInternationalBank(currency pair.CurrencyItem, amount float64) (string, error) { return "", errors.New("not yet implemented") } + +// GetWebsocket returns a pointer to the exchange websocket +func (b *Bitflyer) GetWebsocket() (*exchange.Websocket, error) { + return nil, errors.New("not yet implemented") +} diff --git a/exchanges/bithumb/bithumb.go b/exchanges/bithumb/bithumb.go index 8e700cdf..d0ea8c90 100644 --- a/exchanges/bithumb/bithumb.go +++ b/exchanges/bithumb/bithumb.go @@ -60,7 +60,6 @@ func (b *Bithumb) SetDefaults() { b.Name = "Bithumb" b.Enabled = false b.Verbose = false - b.Websocket = false b.RESTPollingDelay = 10 b.RequestCurrencyPairFormat.Delimiter = "" b.RequestCurrencyPairFormat.Uppercase = true @@ -76,6 +75,7 @@ func (b *Bithumb) SetDefaults() { common.NewHTTPClientWithTimeout(exchange.DefaultHTTPTimeout)) b.APIUrlDefault = apiURL b.APIUrl = b.APIUrlDefault + b.WebsocketInit() } // Setup takes in the supplied exchange configuration details and sets params @@ -90,7 +90,7 @@ func (b *Bithumb) Setup(exch config.ExchangeConfig) { b.SetHTTPClientUserAgent(exch.HTTPUserAgent) b.RESTPollingDelay = exch.RESTPollingDelay b.Verbose = exch.Verbose - b.Websocket = exch.Websocket + b.Websocket.SetEnabled(exch.Websocket) b.BaseCurrencies = common.SplitStrings(exch.BaseCurrencies, ",") b.AvailablePairs = common.SplitStrings(exch.AvailablePairs, ",") b.EnabledPairs = common.SplitStrings(exch.EnabledPairs, ",") @@ -110,6 +110,10 @@ func (b *Bithumb) Setup(exch config.ExchangeConfig) { if err != nil { log.Fatal(err) } + err = b.SetClientProxyAddress(exch.ProxyAddress) + if err != nil { + log.Fatal(err) + } } } diff --git a/exchanges/bithumb/bithumb_wrapper.go b/exchanges/bithumb/bithumb_wrapper.go index 1b0e72c5..8b801c71 100644 --- a/exchanges/bithumb/bithumb_wrapper.go +++ b/exchanges/bithumb/bithumb_wrapper.go @@ -24,7 +24,7 @@ func (b *Bithumb) Start(wg *sync.WaitGroup) { // Run implements the OKEX wrapper func (b *Bithumb) Run() { if b.Verbose { - log.Printf("%s Websocket: %s. (url: %s).\n", b.GetName(), common.IsEnabled(b.Websocket), b.WebsocketURL) + log.Printf("%s Websocket: %s. (url: %s).\n", b.GetName(), common.IsEnabled(b.Websocket.IsEnabled()), b.WebsocketURL) log.Printf("%s polling delay: %ds.\n", b.GetName(), b.RESTPollingDelay) log.Printf("%s %d currencies enabled: %s.\n", b.GetName(), len(b.EnabledPairs), b.EnabledPairs) } @@ -188,3 +188,8 @@ func (b *Bithumb) WithdrawFiatExchangeFunds(currency pair.CurrencyItem, amount f func (b *Bithumb) WithdrawFiatExchangeFundsToInternationalBank(currency pair.CurrencyItem, amount float64) (string, error) { return "", errors.New("not yet implemented") } + +// GetWebsocket returns a pointer to the exchange websocket +func (b *Bithumb) GetWebsocket() (*exchange.Websocket, error) { + return nil, errors.New("not yet implemented") +} diff --git a/exchanges/bitmex/bitmex.go b/exchanges/bitmex/bitmex.go index 2b44ad7b..104aa47b 100644 --- a/exchanges/bitmex/bitmex.go +++ b/exchanges/bitmex/bitmex.go @@ -20,7 +20,6 @@ import ( type Bitmex struct { exchange.Base WebsocketConn *websocket.Conn - shutdown *Shutdown } const ( @@ -114,7 +113,6 @@ func (b *Bitmex) SetDefaults() { b.Name = "Bitmex" b.Enabled = false b.Verbose = false - b.Websocket = false b.RESTPollingDelay = 10 b.RequestCurrencyPairFormat.Delimiter = "" b.RequestCurrencyPairFormat.Uppercase = true @@ -125,10 +123,10 @@ func (b *Bitmex) SetDefaults() { request.NewRateLimit(time.Second, bitmexAuthRate), request.NewRateLimit(time.Second, bitmexUnauthRate), common.NewHTTPClientWithTimeout(exchange.DefaultHTTPTimeout)) - b.shutdown = b.NewRoutineManagement() b.APIUrlDefault = bitmexAPIURL b.APIUrl = b.APIUrlDefault b.SupportsAutoPairUpdating = true + b.WebsocketInit() } // Setup takes in the supplied exchange configuration details and sets params @@ -141,7 +139,7 @@ func (b *Bitmex) Setup(exch config.ExchangeConfig) { b.SetAPIKeys(exch.APIKey, exch.APISecret, "", false) b.RESTPollingDelay = exch.RESTPollingDelay b.Verbose = exch.Verbose - b.Websocket = exch.Websocket + b.Websocket.SetEnabled(exch.Websocket) b.BaseCurrencies = common.SplitStrings(exch.BaseCurrencies, ",") b.AvailablePairs = common.SplitStrings(exch.AvailablePairs, ",") b.EnabledPairs = common.SplitStrings(exch.EnabledPairs, ",") @@ -161,6 +159,18 @@ func (b *Bitmex) Setup(exch config.ExchangeConfig) { if err != nil { log.Fatal(err) } + err = b.SetClientProxyAddress(exch.ProxyAddress) + if err != nil { + log.Fatal(err) + } + err = b.WebsocketSetup(b.WsConnector, + exch.Name, + exch.Websocket, + bitmexWSURL, + exch.WebsocketURL) + if err != nil { + log.Fatal(err) + } } } diff --git a/exchanges/bitmex/bitmex_websocket.go b/exchanges/bitmex/bitmex_websocket.go index 0d199816..e11f79e1 100644 --- a/exchanges/bitmex/bitmex_websocket.go +++ b/exchanges/bitmex/bitmex_websocket.go @@ -4,11 +4,17 @@ import ( "errors" "fmt" "log" + "net/http" + "net/url" "strconv" "time" + "github.com/thrasher-/gocryptotrader/exchanges/orderbook" + "github.com/gorilla/websocket" "github.com/thrasher-/gocryptotrader/common" + "github.com/thrasher-/gocryptotrader/currency/pair" + "github.com/thrasher-/gocryptotrader/exchanges" ) const ( @@ -55,15 +61,27 @@ const ( var ( pongChan = make(chan int, 1) - timer *time.Timer ) -// WebsocketConnect initiates a new websocket connection -func (b *Bitmex) WebsocketConnect() error { +// WsConnector initiates a new websocket connection +func (b *Bitmex) WsConnector() error { + if !b.Websocket.IsEnabled() || !b.IsEnabled() { + return errors.New(exchange.WebsocketNotEnabled) + } + var dialer websocket.Dialer var err error - b.WebsocketConn, _, err = dialer.Dial(bitmexWSURL, nil) + if b.Websocket.GetProxyAddress() != "" { + proxy, err := url.Parse(b.Websocket.GetProxyAddress()) + if err != nil { + return err + } + + dialer.Proxy = http.ProxyURL(proxy) + } + + b.WebsocketConn, _, err = dialer.Dial(b.Websocket.GetWebsocketURL(), nil) if err != nil { return err } @@ -79,8 +97,6 @@ func (b *Bitmex) WebsocketConnect() error { return err } - go b.connectionHandler() - if b.Verbose { log.Printf("Successfully connected to Bitmex %s at time: %s Limit: %d", welcomeResp.Info, @@ -88,309 +104,292 @@ func (b *Bitmex) WebsocketConnect() error { welcomeResp.Limit.Remaining) } - go b.handleIncomingData() + go b.wsHandleIncomingData() + go b.wsReadData() err = b.websocketSubscribe() if err != nil { - b.WebsocketConn.Close() + closeError := b.WebsocketConn.Close() + if closeError != nil { + return fmt.Errorf("bitmex_websocket.go error - Websocket connection could not close %s", + closeError) + } return err } if b.AuthenticatedAPISupport { err := b.websocketSendAuth() if err != nil { - log.Fatal(err) + return err } } return nil } -// Timer handles connection loss or failure -func (b *Bitmex) connectionHandler() { +func (b *Bitmex) wsReadData() { + b.Websocket.Wg.Add(1) + defer func() { - if b.Verbose { - log.Println("Bitmex websocket: Connection handler routine shutdown") + 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() }() - shutdown := b.shutdown.addRoutine() - - timer = time.NewTimer(5 * time.Second) for { select { - case <-timer.C: - timeout := time.After(5 * time.Second) - err := b.WebsocketConn.WriteJSON("ping") + case <-b.Websocket.ShutdownC: + return + + default: + _, resp, err := b.WebsocketConn.ReadMessage() if err != nil { - b.reconnect() + b.Websocket.DataHandler <- fmt.Errorf("bitmex_websocket.go - websocket connection Error: %s", + err) return } - for { - select { - case <-pongChan: - if b.Verbose { - log.Println("Bitmex websocket: PONG received") - } - break - case <-timeout: - log.Println("Bitmex websocket: Connection timed out - Closing connection....") - b.WebsocketConn.Close() - log.Println("Bitmex websocket: Connection timed out - Reconnecting...") - b.reconnect() - return - } + b.Websocket.TrafficAlert <- struct{}{} + + b.Websocket.Intercomm <- exchange.WebsocketResponse{ + Raw: resp, } - case <-shutdown: - log.Println("Bitmex websocket: shutdown requested - Closing connection....") - b.WebsocketConn.Close() - log.Println("Bitmex websocket: Sending shutdown message") - b.shutdown.routineShutdown() - return } } } -// Reconnect handles reconnections to websocket API -func (b *Bitmex) reconnect() { - for { - err := b.WebsocketConnect() - if err != nil { - log.Println("Bitmex websocket: Connection timed out - Failed to connect, sleeping...") - time.Sleep(time.Second * 2) - continue - } - return - } -} - -// handleIncomingData services incoming data from the websocket connection -func (b *Bitmex) handleIncomingData() { - defer func() { - if b.Verbose { - log.Println("Bitmex websocket: Response data handler routine shutdown") - } - }() +// wsHandleIncomingData services incoming data from the websocket connection +func (b *Bitmex) wsHandleIncomingData() { + b.Websocket.Wg.Add(1) + defer b.Websocket.Wg.Done() for { - _, resp, err := b.WebsocketConn.ReadMessage() - if err != nil { - if b.Verbose { - log.Println("Bitmex websocket: Connection error", err) - } + select { + case <-b.Websocket.ShutdownC: return - } - message := string(resp) - if common.StringContains(message, "pong") { - if b.Verbose { - log.Println("Bitmex websocket: PONG receieved") - } - pongChan <- 1 - continue - } - - if common.StringContains(message, "ping") { - err = b.WebsocketConn.WriteJSON("pong") - if err != nil { - if b.Verbose { - log.Println("Bitmex websocket error: ", err) - } - return - } - } - - if !timer.Reset(5 * time.Second) { - log.Fatal("Bitmex websocket: Timer failed to set") - } - - quickCapture := make(map[string]interface{}) - err = common.JSONDecode(resp, &quickCapture) - if err != nil { - log.Fatal(err) - } - - var respError WebsocketErrorResponse - if _, ok := quickCapture["status"]; ok { - err = common.JSONDecode(resp, &respError) - if err != nil { - log.Fatal(err) - } - log.Printf("Bitmex websocket error: %s", respError.Error) - continue - } - - if _, ok := quickCapture["success"]; ok { - var decodedResp WebsocketSubscribeResp - err := common.JSONDecode(resp, &decodedResp) - if err != nil { - log.Fatal(err) - } - - if decodedResp.Success { - if b.Verbose { - if len(quickCapture) == 3 { - log.Printf("Bitmex Websocket: Successfully subscribed to %s", - decodedResp.Subscribe) - } else { - log.Println("Bitmex Websocket: Successfully authenticated websocket connection") - } - } + case resp := <-b.Websocket.Intercomm: + message := string(resp.Raw) + if common.StringContains(message, "pong") { + pongChan <- 1 continue } - log.Printf("Bitmex websocket error: Unable to subscribe %s", - decodedResp.Subscribe) - } else if _, ok := quickCapture["table"]; ok { - var decodedResp WebsocketMainResponse - err := common.JSONDecode(resp, &decodedResp) + if common.StringContains(message, "ping") { + err := b.WebsocketConn.WriteJSON("pong") + if err != nil { + b.Websocket.DataHandler <- err + } + } + + quickCapture := make(map[string]interface{}) + err := common.JSONDecode(resp.Raw, &quickCapture) if err != nil { log.Fatal(err) } - switch decodedResp.Table { - case bitmexWSOrderbookL2: - var orderbooks OrderBookData - err = common.JSONDecode(resp, &orderbooks) + var respError WebsocketErrorResponse + if _, ok := quickCapture["status"]; ok { + err = common.JSONDecode(resp.Raw, &respError) if err != nil { log.Fatal(err) } - err = b.processOrderbook(orderbooks.Data, orderbooks.Action) + b.Websocket.DataHandler <- errors.New(respError.Error) + continue + } + + if _, ok := quickCapture["success"]; ok { + var decodedResp WebsocketSubscribeResp + err := common.JSONDecode(resp.Raw, &decodedResp) if err != nil { log.Fatal(err) } - case bitmexWSTrade: - var trades TradeData - err = common.JSONDecode(resp, &trades) + + if decodedResp.Success { + if b.Verbose { + if len(quickCapture) == 3 { + log.Printf("Bitmex Websocket: Successfully subscribed to %s", + decodedResp.Subscribe) + } else { + log.Println("Bitmex Websocket: Successfully authenticated websocket connection") + } + } + continue + } + + b.Websocket.DataHandler <- fmt.Errorf("Bitmex websocket error: Unable to subscribe %s", + decodedResp.Subscribe) + + } else if _, ok := quickCapture["table"]; ok { + var decodedResp WebsocketMainResponse + err := common.JSONDecode(resp.Raw, &decodedResp) if err != nil { log.Fatal(err) } - err = b.processTrades(trades.Data, trades.Action) - if err != nil { - log.Fatal(err) + + switch decodedResp.Table { + case bitmexWSOrderbookL2: + var orderbooks OrderBookData + err = common.JSONDecode(resp.Raw, &orderbooks) + if err != nil { + log.Fatal(err) + } + + p := pair.NewCurrencyPairFromString(orderbooks.Data[0].Symbol) + err = b.processOrderbook(orderbooks.Data, orderbooks.Action, p, "CONTRACT") + if err != nil { + log.Fatal(err) + } + + case bitmexWSTrade: + var trades TradeData + err = common.JSONDecode(resp.Raw, &trades) + if err != nil { + log.Fatal(err) + } + + if trades.Action == bitmexActionInitialData { + continue + } + + for _, trade := range trades.Data { + timestamp, err := time.Parse(time.RFC3339, trade.Timestamp) + if err != nil { + log.Fatal(err) + } + + b.Websocket.DataHandler <- exchange.TradeData{ + Timestamp: timestamp, + Price: trade.Price, + Amount: float64(trade.Size), + CurrencyPair: pair.NewCurrencyPairFromString(trade.Symbol), + Exchange: b.GetName(), + AssetType: "CONTRACT", + Side: trade.Side, + } + } + + case bitmexWSAnnouncement: + var announcement AnnouncementData + + err = common.JSONDecode(resp.Raw, &announcement) + if err != nil { + log.Fatal(err) + } + + if announcement.Action == bitmexActionInitialData { + continue + } + + b.Websocket.DataHandler <- announcement.Data + + default: + log.Fatal("Bitmex websocket error: Table unknown -", decodedResp.Table) } - case bitmexWSAnnouncement: - var announcement AnnouncementData - err = common.JSONDecode(resp, &announcement) - if err != nil { - log.Fatal(err) - } - err = b.processAnnouncement(announcement.Data, announcement.Action) - if err != nil { - log.Fatal(err) - } - default: - log.Fatal("Bitmex websocket error: Table unknown -", decodedResp.Table) } } } } -// Temporary local cache of Announcements -var localAnnouncements []Announcement -var partialLoadedAnnouncement bool - -// ProcessAnnouncement process announcements -func (b *Bitmex) processAnnouncement(data []Announcement, action string) error { - switch action { - case bitmexActionInitialData: - if !partialLoadedAnnouncement { - localAnnouncements = data - } - partialLoadedAnnouncement = true - default: - return fmt.Errorf("Bitmex websocket error: ProcessAnnouncement() unallocated action - %s", - action) - } - return nil -} - -// Temporary local cache of orderbooks -var localOb []OrderBookL2 -var partialLoaded bool +var snapshotloaded = make(map[pair.CurrencyPair]map[string]bool) // ProcessOrderbook processes orderbook updates -func (b *Bitmex) processOrderbook(data []OrderBookL2, action string) error { - switch action { - case bitmexActionInitialData: - if !partialLoaded { - localOb = data - } - partialLoaded = true - case bitmexActionUpdateData: - if partialLoaded { - updated := len(data) - for _, elem := range data { - for i := range localOb { - if localOb[i].ID == elem.ID && localOb[i].Symbol == elem.Symbol { - localOb[i].Side = elem.Side - localOb[i].Size = elem.Size - updated-- - break - } - } - } - if updated != 0 { - return errors.New("Bitmex websocket error: Elements not updated correctly") - } - } - case bitmexActionInsertData: - if partialLoaded { - updated := len(data) - for _, elem := range data { - localOb = append(localOb, OrderBookL2{ - Symbol: elem.Symbol, - ID: elem.ID, - Side: elem.Side, - Size: elem.Size, - Price: elem.Price, - }) - updated-- - } - if updated != 0 { - return errors.New("Bitmex websocket error: Elements not updated correctly") - } - } - case bitmexActionDeleteData: - if partialLoaded { - updated := len(data) - for _, elem := range data { - for i := range localOb { - if localOb[i].ID == elem.ID && localOb[i].Symbol == elem.Symbol { - localOb[i] = localOb[len(localOb)-1] - localOb = localOb[:len(localOb)-1] - updated-- - break - } - } - } - if updated != 0 { - return errors.New("Bitmex websocket error: Elements not updated correctly") - } - } +func (b *Bitmex) processOrderbook(data []OrderBookL2, action string, currencyPair pair.CurrencyPair, assetType string) error { + if len(data) < 1 { + return errors.New("bitmex_websocket.go error - no orderbook data") } - return nil -} -// Temporary local cache of orderbooks -var localTrades []Trade -var partialLoadedTrades bool + _, ok := snapshotloaded[currencyPair] + if !ok { + snapshotloaded[currencyPair] = make(map[string]bool) + } + + _, ok = snapshotloaded[currencyPair][assetType] + if !ok { + snapshotloaded[currencyPair][assetType] = false + } -// ProcessTrades processes new trades that have occured -func (b *Bitmex) processTrades(data []Trade, action string) error { switch action { case bitmexActionInitialData: - if !partialLoadedTrades { - localTrades = data - } - partialLoadedTrades = true - case bitmexActionInsertData: - if partialLoadedTrades { - localTrades = append(localTrades, data...) + if !snapshotloaded[currencyPair][assetType] { + var newOrderbook orderbook.Base + var bids, asks []orderbook.Item + + for _, orderbookItem := range data { + if orderbookItem.Side == "Sell" { + asks = append(asks, orderbook.Item{ + Price: orderbookItem.Price, + Amount: float64(orderbookItem.Size), + }) + continue + } + bids = append(bids, orderbook.Item{ + Price: orderbookItem.Price, + Amount: float64(orderbookItem.Size), + }) + } + + if len(bids) == 0 || len(asks) == 0 { + return errors.New("bitmex_websocket.go error - snapshot not initialised correctly") + } + + newOrderbook.Asks = asks + newOrderbook.Bids = bids + newOrderbook.AssetType = assetType + newOrderbook.CurrencyPair = currencyPair.Pair().String() + newOrderbook.LastUpdated = time.Now() + newOrderbook.Pair = currencyPair + + err := b.Websocket.Orderbook.LoadSnapshot(newOrderbook, b.GetName()) + if err != nil { + return fmt.Errorf("bitmex_websocket.go process orderbook error - %s", + err) + } + snapshotloaded[currencyPair][assetType] = true + b.Websocket.DataHandler <- exchange.WebsocketOrderbookUpdate{ + Pair: currencyPair, + Asset: assetType, + Exchange: b.GetName(), + } } + default: - return fmt.Errorf("Bitmex websocket error: ProcessTrades() unallocated action - %s", - action) + if snapshotloaded[currencyPair][assetType] { + var asks, bids []orderbook.Item + for _, orderbookItem := range data { + if orderbookItem.Side == "Sell" { + asks = append(asks, orderbook.Item{ + Price: orderbookItem.Price, + Amount: float64(orderbookItem.Size), + }) + continue + } + bids = append(bids, orderbook.Item{ + Price: orderbookItem.Price, + Amount: float64(orderbookItem.Size), + }) + } + + err := b.Websocket.Orderbook.UpdateUsingID(bids, + asks, + currencyPair, + time.Now(), + b.GetName(), + assetType, + action) + + if err != nil { + return err + } + + b.Websocket.DataHandler <- exchange.WebsocketOrderbookUpdate{ + Pair: currencyPair, + Asset: assetType, + Exchange: b.GetName(), + } + } } return nil } @@ -444,51 +443,3 @@ func (b *Bitmex) websocketSendAuth() error { return b.WebsocketConn.WriteJSON(sendAuth) } - -// Shutdown to monitor and shut down routines package specific -type Shutdown struct { - c chan int - routineCount int - finishC chan int -} - -// NewRoutineManagement returns an new initial routine management system -func (b *Bitmex) NewRoutineManagement() *Shutdown { - return &Shutdown{ - c: make(chan int, 1), - finishC: make(chan int, 1), - } -} - -// AddRoutine adds a routine to the monitor and returns a channel -func (r *Shutdown) addRoutine() chan int { - log.Println("Bitmex Websocket: Routine added to monitor") - r.routineCount++ - return r.c -} - -// RoutineShutdown sends a message to the finisher channel -func (r *Shutdown) routineShutdown() { - log.Println("Bitmex Websocket: Routine is shutting down") - r.finishC <- 1 -} - -// SignalShutdown signals a shutdown across routines -func (r *Shutdown) SignalShutdown() { - log.Println("Bitmex Websocket: Shutdown signal sending..") - for i := 0; i < r.routineCount; i++ { - log.Printf("Bitmex Websocket: Shutdown signal sent to routine %d", i+1) - r.c <- 1 - } - - for { - <-r.finishC - r.routineCount-- - if r.routineCount <= 0 { - close(r.c) - close(r.finishC) - log.Println("Bitmex Websocket: All routines stopped") - return - } - } -} diff --git a/exchanges/bitmex/bitmex_wrapper.go b/exchanges/bitmex/bitmex_wrapper.go index f9141b64..2d306e36 100644 --- a/exchanges/bitmex/bitmex_wrapper.go +++ b/exchanges/bitmex/bitmex_wrapper.go @@ -25,7 +25,7 @@ func (b *Bitmex) Start(wg *sync.WaitGroup) { // Run implements the Bitmex wrapper func (b *Bitmex) Run() { if b.Verbose { - log.Printf("%s Websocket: %s. (url: %s).\n", b.GetName(), common.IsEnabled(b.Websocket), b.WebsocketURL) + log.Printf("%s Websocket: %s. (url: %s).\n", b.GetName(), common.IsEnabled(b.Websocket.IsEnabled()), b.WebsocketURL) log.Printf("%s polling delay: %ds.\n", b.GetName(), b.RESTPollingDelay) log.Printf("%s %d currencies enabled: %s.\n", b.GetName(), len(b.EnabledPairs), b.EnabledPairs) } @@ -33,10 +33,9 @@ func (b *Bitmex) Run() { marketInfo, err := b.GetActiveInstruments(GenericRequestParams{}) if err != nil { log.Printf("%s Failed to get available symbols.\n", b.GetName()) + } else { - var exchangeProducts []string - for _, info := range marketInfo { exchangeProducts = append(exchangeProducts, info.Symbol) } @@ -193,3 +192,8 @@ func (b *Bitmex) WithdrawFiatExchangeFunds(currency pair.CurrencyItem, amount fl func (b *Bitmex) WithdrawExchangeFiatFundsToInternationalBank(currency pair.CurrencyItem, amount float64) (string, error) { return "", errors.New("not yet implemented") } + +// GetWebsocket returns a pointer to the exchange websocket +func (b *Bitmex) GetWebsocket() (*exchange.Websocket, error) { + return b.Websocket, nil +} diff --git a/exchanges/bitstamp/bitstamp.go b/exchanges/bitstamp/bitstamp.go index 3e37dbfb..b96c4cac 100644 --- a/exchanges/bitstamp/bitstamp.go +++ b/exchanges/bitstamp/bitstamp.go @@ -56,7 +56,8 @@ const ( // Bitstamp is the overarching type across the bitstamp package type Bitstamp struct { exchange.Base - Balance Balances + Balance Balances + WebsocketConn WebsocketConn } // SetDefaults sets default for Bitstamp @@ -64,7 +65,6 @@ func (b *Bitstamp) SetDefaults() { b.Name = "Bitstamp" b.Enabled = false b.Verbose = false - b.Websocket = false b.RESTPollingDelay = 10 b.RequestCurrencyPairFormat.Delimiter = "" b.RequestCurrencyPairFormat.Uppercase = true @@ -79,6 +79,7 @@ func (b *Bitstamp) SetDefaults() { common.NewHTTPClientWithTimeout(exchange.DefaultHTTPTimeout)) b.APIUrlDefault = bitstampAPIURL b.APIUrl = b.APIUrlDefault + b.WebsocketInit() } // Setup sets configuration values to bitstamp @@ -93,7 +94,7 @@ func (b *Bitstamp) Setup(exch config.ExchangeConfig) { b.SetHTTPClientUserAgent(exch.HTTPUserAgent) b.RESTPollingDelay = exch.RESTPollingDelay b.Verbose = exch.Verbose - b.Websocket = exch.Websocket + b.Websocket.SetEnabled(exch.Websocket) b.BaseCurrencies = common.SplitStrings(exch.BaseCurrencies, ",") b.AvailablePairs = common.SplitStrings(exch.AvailablePairs, ",") b.EnabledPairs = common.SplitStrings(exch.EnabledPairs, ",") @@ -113,6 +114,18 @@ func (b *Bitstamp) Setup(exch config.ExchangeConfig) { if err != nil { log.Fatal(err) } + err = b.SetClientProxyAddress(exch.ProxyAddress) + if err != nil { + log.Fatal(err) + } + err = b.WebsocketSetup(b.WsConnect, + exch.Name, + exch.Websocket, + BitstampPusherKey, + exch.WebsocketURL) + if err != nil { + log.Fatal(err) + } } } diff --git a/exchanges/bitstamp/bitstamp_test.go b/exchanges/bitstamp/bitstamp_test.go index 0252d6cb..3bb78466 100644 --- a/exchanges/bitstamp/bitstamp_test.go +++ b/exchanges/bitstamp/bitstamp_test.go @@ -29,7 +29,7 @@ func TestSetDefaults(t *testing.T) { if b.Verbose != false { t.Error("Test Failed - SetDefaults() error") } - if b.Websocket != false { + if b.Websocket.IsEnabled() != false { t.Error("Test Failed - SetDefaults() error") } if b.RESTPollingDelay != 10 { @@ -47,7 +47,7 @@ func TestSetup(t *testing.T) { b.Setup(bConfig) if !b.IsEnabled() || b.AuthenticatedAPISupport || b.RESTPollingDelay != time.Duration(10) || - b.Verbose || b.Websocket || len(b.BaseCurrencies) < 1 || + b.Verbose || b.Websocket.IsEnabled() || len(b.BaseCurrencies) < 1 || len(b.AvailablePairs) < 1 || len(b.EnabledPairs) < 1 { t.Error("Test Failed - Bitstamp Setup values not set correctly") } diff --git a/exchanges/bitstamp/bitstamp_websocket.go b/exchanges/bitstamp/bitstamp_websocket.go index fdd0fa38..4a473a34 100644 --- a/exchanges/bitstamp/bitstamp_websocket.go +++ b/exchanges/bitstamp/bitstamp_websocket.go @@ -4,23 +4,47 @@ import ( "errors" "fmt" "log" + "strconv" "strings" + "time" "github.com/thrasher-/gocryptotrader/common" + "github.com/thrasher-/gocryptotrader/currency/pair" + "github.com/thrasher-/gocryptotrader/exchanges" + "github.com/thrasher-/gocryptotrader/exchanges/orderbook" "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"` + 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"` + 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 ( @@ -28,6 +52,8 @@ const ( BitstampPusherKey = "de504dc5763aeef9ff52" ) +var tradingPairs map[string]string + // findPairFromChannel extracts the capitalized trading pair from the channel and returns it only if enabled in the config func (b *Bitstamp) findPairFromChannel(channelName string) (string, error) { split := strings.Split(channelName, "_") @@ -39,92 +65,214 @@ func (b *Bitstamp) findPairFromChannel(channelName string) (string, error) { } } - return "", errors.New("Could not find trading pair") + return "", errors.New("bistamp_websocket.go error - could not find trading pair") } -// PusherClient starts the push mechanism -func (b *Bitstamp) PusherClient() { - for b.Enabled && b.Websocket { - // hold the mapping of channel:tradingPair in order not to always compute it - seenTradingPairs := map[string]string{} +// WsConnect connects to a websocket feed +func (b *Bitstamp) WsConnect() error { + if !b.Websocket.IsEnabled() || !b.IsEnabled() { + return errors.New(exchange.WebsocketNotEnabled) + } - pusherClient, err := pusher.NewClient(BitstampPusherKey) + tradingPairs = make(map[string]string) + var err error + + if b.Websocket.GetProxyAddress() != "" { + log.Println("bistamp_websocket.go warning - set proxy address error: proxy not supported") + } + + b.WebsocketConn.Client, err = pusher.NewClient(BitstampPusherKey) + if err != nil { + return fmt.Errorf("%s Unable to connect to Websocket. Error: %s", + b.GetName(), + err) + } + + b.WebsocketConn.Data, err = b.WebsocketConn.Client.Bind("data") + if err != nil { + return fmt.Errorf("%s Websocket Bind error: %s", b.GetName(), err) + + } + + b.WebsocketConn.Trade, err = b.WebsocketConn.Client.Bind("trade") + if err != nil { + return fmt.Errorf("%s Websocket Bind error: %s", b.GetName(), err) + } + + go b.WsReadData() + + for _, p := range b.GetEnabledCurrencies() { + orderbookSeed, err := b.GetOrderbook(p.Pair().String()) if err != nil { - log.Printf("%s Unable to connect to Websocket. Error: %s\n", b.GetName(), err) - continue + return err } - for _, pair := range b.EnabledPairs { - err = pusherClient.Subscribe(fmt.Sprintf("live_trades_%s", strings.ToLower(pair))) + var newOrderbook orderbook.Base + + var asks []orderbook.Item + for _, ask := range orderbookSeed.Asks { + var item orderbook.Item + item.Amount = ask.Amount + item.Price = ask.Price + asks = append(asks, item) + } + + var bids []orderbook.Item + for _, bid := range orderbookSeed.Bids { + var item orderbook.Item + item.Amount = bid.Amount + item.Price = bid.Price + bids = append(bids, item) + } + + newOrderbook.Asks = asks + newOrderbook.Bids = bids + newOrderbook.CurrencyPair = p.Pair().String() + newOrderbook.Pair = p + newOrderbook.LastUpdated = time.Unix(0, orderbookSeed.Timestamp) + newOrderbook.AssetType = "SPOT" + + err = b.Websocket.Orderbook.LoadSnapshot(newOrderbook, b.GetName()) + if err != nil { + return err + } + + b.Websocket.DataHandler <- exchange.WebsocketOrderbookUpdate{ + Pair: p, + Asset: "SPOT", + Exchange: b.GetName(), + } + + err = b.WebsocketConn.Client.Subscribe(fmt.Sprintf("live_trades_%s", + strings.ToLower(p.Pair().String()))) + + if err != nil { + log.Println(err) + return fmt.Errorf("%s Websocket Trade subscription error: %s", + b.GetName(), + err) + } + + err = b.WebsocketConn.Client.Subscribe(fmt.Sprintf("diff_order_book_%s", + strings.ToLower(p.Pair().String()))) + + if err != nil { + log.Println(err) + return fmt.Errorf("%s Websocket Trade subscription error: %s", + b.GetName(), + err) + } + + } + return nil +} + +// 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 { + b.Websocket.DataHandler <- fmt.Errorf("bitstamp_websocket.go - Unable to to close Websocket connection. Error: %s", + err) + } + b.Websocket.Wg.Done() + }() + + for { + select { + case <-b.Websocket.ShutdownC: + return + + case data := <-b.WebsocketConn.Data: + b.Websocket.TrafficAlert <- struct{}{} + + result := PusherOrderbook{} + err := common.JSONDecode([]byte(data.Data), &result) if err != nil { - log.Printf("%s Websocket Trade subscription error: %s\n", b.GetName(), err) + log.Fatal(err) } - err = pusherClient.Subscribe(fmt.Sprintf("order_book_%s", strings.ToLower(pair))) + currencyPair := common.SplitStrings(data.Channel, "_") + p := pair.NewCurrencyPairFromString(common.StringToUpper(currencyPair[3])) + + err = b.WsUpdateOrderbook(result, p, "SPOT") if err != nil { - log.Printf("%s Websocket Trade subscription error: %s\n", b.GetName(), err) + b.Websocket.DataHandler <- err } - } - dataChannelTrade, err := pusherClient.Bind("data") - if err != nil { - log.Printf("%s Websocket Bind error: %s\n", b.GetName(), err) - continue - } + case trade := <-b.WebsocketConn.Trade: + b.Websocket.TrafficAlert <- struct{}{} - tradeChannelTrade, err := pusherClient.Bind("trade") - if err != nil { - log.Printf("%s Websocket Bind error: %s\n", b.GetName(), err) - continue - } + result := PusherTrade{} + err := common.JSONDecode([]byte(trade.Data), &result) + if err != nil { + log.Fatal(err) + } - log.Printf("%s Pusher client connected.\n", b.GetName()) + currencyPair := common.SplitStrings(trade.Channel, "_") - for b.Websocket { - select { - case data := <-dataChannelTrade: - result := PusherOrderbook{} - err := common.JSONDecode([]byte(data.Data), &result) - var channelTradingPair string - var ok bool - - if channelTradingPair, ok = seenTradingPairs[data.Channel]; !ok { - if foundTradingPair, noPair := b.findPairFromChannel(data.Channel); noPair == nil { - seenTradingPairs[data.Channel] = foundTradingPair - } else { - log.Printf("%s Pair from Channel: %s does not seem to be enabled or found", b.GetName(), data.Channel) - continue - } - } - - log.Printf("%s Pusher: received ticker for Pair: %s\n", b.GetName(), channelTradingPair) - - if err != nil { - log.Println(err) - } - case trade := <-tradeChannelTrade: - result := PusherTrade{} - err := common.JSONDecode([]byte(trade.Data), &result) - - if err != nil { - log.Println(err) - } - - var channelTradingPair string - var ok bool - - if channelTradingPair, ok = seenTradingPairs[trade.Channel]; !ok { - if foundTradingPair, noPair := b.findPairFromChannel(trade.Channel); noPair == nil { - seenTradingPairs[trade.Channel] = foundTradingPair - } else { - log.Printf("%s LiveTrade Pair from Channel: %s does not seem to be enabled or found", b.GetName(), trade.Channel) - continue - } - } - - log.Println(trade.Channel) - log.Printf("%s Pusher trade: Pair: %s Price: %f Amount: %f\n", b.GetName(), channelTradingPair, result.Price, result.Amount) + b.Websocket.DataHandler <- exchange.TradeData{ + Price: result.Price, + Amount: result.Amount, + CurrencyPair: pair.NewCurrencyPairFromString(currencyPair[2]), + Exchange: b.GetName(), + AssetType: "SPOT", } } } } + +// WsUpdateOrderbook updates local cache of orderbook information +func (b *Bitstamp) WsUpdateOrderbook(ob PusherOrderbook, p pair.CurrencyPair, assetType string) error { + if len(ob.Asks) == 0 && len(ob.Bids) == 0 { + return errors.New("bitstamp_websocket.go error - no orderbook data") + } + + var asks, bids []orderbook.Item + if len(ob.Asks) > 0 { + for _, ask := range ob.Asks { + target, err := strconv.ParseFloat(ask[0], 64) + if err != nil { + log.Fatal(err) + } + + amount, err := strconv.ParseFloat(ask[1], 64) + if err != nil { + log.Fatal(err) + } + + asks = append(asks, orderbook.Item{Price: target, Amount: amount}) + } + } + + if len(ob.Bids) > 0 { + for _, bid := range ob.Bids { + target, err := strconv.ParseFloat(bid[0], 64) + if err != nil { + log.Fatal(err) + } + + amount, err := strconv.ParseFloat(bid[1], 64) + if err != nil { + log.Fatal(err) + } + + bids = append(bids, orderbook.Item{Price: target, Amount: amount}) + } + } + + err := b.Websocket.Orderbook.Update(bids, asks, p, time.Now(), b.GetName(), assetType) + if err != nil { + return err + } + + b.Websocket.DataHandler <- exchange.WebsocketOrderbookUpdate{ + Pair: p, + Asset: assetType, + Exchange: b.GetName(), + } + + return nil +} diff --git a/exchanges/bitstamp/bitstamp_wrapper.go b/exchanges/bitstamp/bitstamp_wrapper.go index 4e8b7009..4953f241 100644 --- a/exchanges/bitstamp/bitstamp_wrapper.go +++ b/exchanges/bitstamp/bitstamp_wrapper.go @@ -25,15 +25,11 @@ func (b *Bitstamp) Start(wg *sync.WaitGroup) { // Run implements the Bitstamp wrapper func (b *Bitstamp) Run() { if b.Verbose { - log.Printf("%s Websocket: %s.", b.GetName(), common.IsEnabled(b.Websocket)) + log.Printf("%s Websocket: %s.", b.GetName(), common.IsEnabled(b.Websocket.IsEnabled())) log.Printf("%s polling delay: %ds.\n", b.GetName(), b.RESTPollingDelay) log.Printf("%s %d currencies enabled: %s.\n", b.GetName(), len(b.EnabledPairs), b.EnabledPairs) } - if b.Websocket { - go b.PusherClient() - } - pairs, err := b.GetTradingPairs() if err != nil { log.Printf("%s failed to get trading pairs. Err: %s", b.Name, err) @@ -211,3 +207,8 @@ func (b *Bitstamp) WithdrawFiatExchangeFunds(currency pair.CurrencyItem, amount func (b *Bitstamp) WithdrawFiatExchangeFundsToInternationalBank(currency pair.CurrencyItem, amount float64) (string, error) { return "", errors.New("not yet implemented") } + +// GetWebsocket returns a pointer to the exchange websocket +func (b *Bitstamp) GetWebsocket() (*exchange.Websocket, error) { + return b.Websocket, nil +} diff --git a/exchanges/bittrex/bittrex.go b/exchanges/bittrex/bittrex.go index f99563c1..9ec38ad0 100644 --- a/exchanges/bittrex/bittrex.go +++ b/exchanges/bittrex/bittrex.go @@ -67,7 +67,6 @@ func (b *Bittrex) SetDefaults() { b.Name = "Bittrex" b.Enabled = false b.Verbose = false - b.Websocket = false b.RESTPollingDelay = 10 b.RequestCurrencyPairFormat.Delimiter = "-" b.RequestCurrencyPairFormat.Uppercase = true @@ -82,6 +81,7 @@ func (b *Bittrex) SetDefaults() { common.NewHTTPClientWithTimeout(exchange.DefaultHTTPTimeout)) b.APIUrlDefault = bittrexAPIURL b.APIUrl = b.APIUrlDefault + b.WebsocketInit() } // Setup method sets current configuration details if enabled @@ -96,7 +96,6 @@ func (b *Bittrex) Setup(exch config.ExchangeConfig) { b.SetHTTPClientUserAgent(exch.HTTPUserAgent) b.RESTPollingDelay = exch.RESTPollingDelay b.Verbose = exch.Verbose - b.Websocket = exch.Websocket b.BaseCurrencies = common.SplitStrings(exch.BaseCurrencies, ",") b.AvailablePairs = common.SplitStrings(exch.AvailablePairs, ",") b.EnabledPairs = common.SplitStrings(exch.EnabledPairs, ",") @@ -116,6 +115,10 @@ func (b *Bittrex) Setup(exch config.ExchangeConfig) { if err != nil { log.Fatal(err) } + err = b.SetClientProxyAddress(exch.ProxyAddress) + if err != nil { + log.Fatal(err) + } } } diff --git a/exchanges/bittrex/bittrex_test.go b/exchanges/bittrex/bittrex_test.go index 05469375..6501a0ae 100644 --- a/exchanges/bittrex/bittrex_test.go +++ b/exchanges/bittrex/bittrex_test.go @@ -32,8 +32,9 @@ func TestSetup(t *testing.T) { b.Setup(bConfig) - if !b.IsEnabled() || b.AuthenticatedAPISupport || b.RESTPollingDelay != time.Duration(10) || - b.Verbose || b.Websocket || len(b.BaseCurrencies) < 1 || + if !b.IsEnabled() || b.AuthenticatedAPISupport || + b.RESTPollingDelay != time.Duration(10) || b.Verbose || + b.Websocket.IsEnabled() || len(b.BaseCurrencies) < 1 || len(b.AvailablePairs) < 1 || len(b.EnabledPairs) < 1 { t.Error("Test Failed - Bittrex Setup values not set correctly") } diff --git a/exchanges/bittrex/bittrex_wrapper.go b/exchanges/bittrex/bittrex_wrapper.go index 56ef5bc5..cee54302 100644 --- a/exchanges/bittrex/bittrex_wrapper.go +++ b/exchanges/bittrex/bittrex_wrapper.go @@ -217,3 +217,8 @@ func (b *Bittrex) WithdrawFiatExchangeFunds(currency pair.CurrencyItem, amount f func (b *Bittrex) WithdrawFiatExchangeFundsToInternationalBank(currency pair.CurrencyItem, amount float64) (string, error) { return "", errors.New("not yet implemented") } + +// GetWebsocket returns a pointer to the exchange websocket +func (b *Bittrex) GetWebsocket() (*exchange.Websocket, error) { + return nil, errors.New("not yet implemented") +} diff --git a/exchanges/btcc/btcc.go b/exchanges/btcc/btcc.go index 739aad0c..be55dbbc 100644 --- a/exchanges/btcc/btcc.go +++ b/exchanges/btcc/btcc.go @@ -1,14 +1,10 @@ package btcc import ( - "errors" - "fmt" "log" - "net/url" - "strconv" - "strings" "time" + "github.com/gorilla/websocket" "github.com/thrasher-/gocryptotrader/common" "github.com/thrasher-/gocryptotrader/config" "github.com/thrasher-/gocryptotrader/exchanges" @@ -17,39 +13,16 @@ import ( ) const ( - btccAPIUrl = "https://spotusd-data.btcc.com" - btccAPIAuthenticatedMethod = "api_trade_v1.php" - btccAPIVersion = "2.0.1.3" - btccOrderBuy = "buyOrder2" - btccOrderSell = "sellOrder2" - btccOrderCancel = "cancelOrder" - btccIcebergBuy = "buyIcebergOrder" - btccIcebergSell = "sellIcebergOrder" - btccIcebergOrder = "getIcebergOrder" - btccIcebergOrders = "getIcebergOrders" - btccIcebergCancel = "cancelIcebergOrder" - btccAccountInfo = "getAccountInfo" - btccDeposits = "getDeposits" - btccMarketdepth = "getMarketDepth2" - btccOrder = "getOrder" - btccOrders = "getOrders" - btccTransactions = "getTransactions" - btccWithdrawal = "getWithdrawal" - btccWithdrawals = "getWithdrawals" - btccWithdrawalRequest = "requestWithdrawal" - btccStoporderBuy = "buyStopOrder" - btccStoporderSell = "sellStopOrder" - btccStoporderCancel = "cancelStopOrder" - btccStoporder = "getStopOrder" - btccStoporders = "getStopOrders" - btccAuthRate = 0 btccUnauthRate = 0 ) // BTCC is the main overaching type across the BTCC package +// NOTE this package is websocket connection dependant, the REST endpoints have +// been dropped type BTCC struct { exchange.Base + Conn *websocket.Conn } // SetDefaults sets default values for the exchange @@ -58,10 +31,9 @@ func (b *BTCC) SetDefaults() { b.Enabled = false b.Fee = 0 b.Verbose = false - b.Websocket = false b.RESTPollingDelay = 10 b.RequestCurrencyPairFormat.Delimiter = "" - b.RequestCurrencyPairFormat.Uppercase = false + b.RequestCurrencyPairFormat.Uppercase = true b.ConfigCurrencyPairFormat.Delimiter = "" b.ConfigCurrencyPairFormat.Uppercase = true b.AssetTypes = []string{ticker.Spot} @@ -71,8 +43,7 @@ func (b *BTCC) SetDefaults() { request.NewRateLimit(time.Second, btccAuthRate), request.NewRateLimit(time.Second, btccUnauthRate), common.NewHTTPClientWithTimeout(exchange.DefaultHTTPTimeout)) - b.APIUrlDefault = btccAPIUrl - b.APIUrl = b.APIUrlDefault + b.WebsocketInit() } // Setup is run on startup to setup exchange with config values @@ -87,7 +58,7 @@ func (b *BTCC) Setup(exch config.ExchangeConfig) { b.SetHTTPClientUserAgent(exch.HTTPUserAgent) b.RESTPollingDelay = exch.RESTPollingDelay b.Verbose = exch.Verbose - b.Websocket = exch.Websocket + b.Websocket.SetEnabled(exch.Websocket) b.BaseCurrencies = common.SplitStrings(exch.BaseCurrencies, ",") b.AvailablePairs = common.SplitStrings(exch.AvailablePairs, ",") b.EnabledPairs = common.SplitStrings(exch.EnabledPairs, ",") @@ -107,6 +78,18 @@ func (b *BTCC) Setup(exch config.ExchangeConfig) { if err != nil { log.Fatal(err) } + err = b.SetClientProxyAddress(exch.ProxyAddress) + if err != nil { + log.Fatal(err) + } + err = b.WebsocketSetup(b.WsConnect, + exch.Name, + exch.Websocket, + btccSocketioAddress, + exch.WebsocketURL) + if err != nil { + log.Fatal(err) + } } } @@ -114,528 +97,3 @@ func (b *BTCC) Setup(exch config.ExchangeConfig) { func (b *BTCC) GetFee() float64 { return b.Fee } - -// GetTicker returns ticker information -// currencyPair - Example "btccny", "ltccny" or "ltcbtc" -func (b *BTCC) GetTicker(currencyPair string) (Ticker, error) { - resp := Response{} - path := fmt.Sprintf("%s/data/pro/ticker?symbol=%s", b.APIUrl, currencyPair) - return resp.Ticker, b.SendHTTPRequest(path, &resp) -} - -// GetTradeHistory returns trade history data -// currencyPair - Example "btccny", "ltccny" or "ltcbtc" -// limit - limits the returned trades example "10" -// sinceTid - returns trade records starting from id supplied example "5000" -// time - returns trade records starting from unix time 1406794449 -func (b *BTCC) GetTradeHistory(currencyPair string, limit, sinceTid int64, time time.Time) ([]Trade, error) { - trades := []Trade{} - path := fmt.Sprintf("%s/data/pro/historydata?symbol=%s", b.APIUrl, currencyPair) - v := url.Values{} - - if limit > 0 { - v.Set("limit", strconv.FormatInt(limit, 10)) - } - if sinceTid > 0 { - v.Set("since", strconv.FormatInt(sinceTid, 10)) - } - if !time.IsZero() { - v.Set("sincetype", strconv.FormatInt(time.Unix(), 10)) - } - - path = common.EncodeURLValues(path, v) - return trades, b.SendHTTPRequest(path, &trades) -} - -// GetOrderBook returns current symbol order book -// currencyPair - Example "btccny", "ltccny" or "ltcbtc" -// limit - limits the returned trades example "10" if 0 will return full -// orderbook -func (b *BTCC) GetOrderBook(currencyPair string, limit int) (Orderbook, error) { - result := Orderbook{} - path := fmt.Sprintf("%s/data/pro/orderbook?symbol=%s&limit=%d", b.APIUrl, currencyPair, limit) - if limit == 0 { - path = fmt.Sprintf("%s/data/pro/orderbook?symbol=%s", b.APIUrl, currencyPair) - } - - return result, b.SendHTTPRequest(path, &result) -} - -// GetAccountInfo returns account information -func (b *BTCC) GetAccountInfo(infoType string) error { - params := make([]interface{}, 0) - - if len(infoType) > 0 { - params = append(params, infoType) - } - - return b.SendAuthenticatedHTTPRequest(btccAccountInfo, params) -} - -// PlaceOrder places a new order -func (b *BTCC) PlaceOrder(buyOrder bool, price, amount float64, symbol string) { - params := make([]interface{}, 0) - params = append(params, strconv.FormatFloat(price, 'f', -1, 64)) - params = append(params, strconv.FormatFloat(amount, 'f', -1, 64)) - - if len(symbol) > 0 { - params = append(params, symbol) - } - - req := btccOrderBuy - if !buyOrder { - req = btccOrderSell - } - - err := b.SendAuthenticatedHTTPRequest(req, params) - - if err != nil { - log.Println(err) - } -} - -// CancelOrder cancels an order -func (b *BTCC) CancelOrder(orderID int64, symbol string) { - params := make([]interface{}, 0) - params = append(params, orderID) - - if len(symbol) > 0 { - params = append(params, symbol) - } - - err := b.SendAuthenticatedHTTPRequest(btccOrderCancel, params) - - if err != nil { - log.Println(err) - } -} - -// GetDeposits returns deposit information -func (b *BTCC) GetDeposits(currency string, pending bool) { - params := make([]interface{}, 0) - params = append(params, currency) - - if pending { - params = append(params, pending) - } - - err := b.SendAuthenticatedHTTPRequest(btccDeposits, params) - - if err != nil { - log.Println(err) - } -} - -// GetMarketDepth returns market depth at limit -func (b *BTCC) GetMarketDepth(symbol string, limit int64) { - params := make([]interface{}, 0) - - if limit > 0 { - params = append(params, limit) - } - - if len(symbol) > 0 { - params = append(params, symbol) - } - - err := b.SendAuthenticatedHTTPRequest(btccMarketdepth, params) - - if err != nil { - log.Println(err) - } -} - -// GetOrder returns information about a specific order -func (b *BTCC) GetOrder(orderID int64, symbol string, detailed bool) { - params := make([]interface{}, 0) - params = append(params, orderID) - - if len(symbol) > 0 { - params = append(params, symbol) - } - - if detailed { - params = append(params, detailed) - } - - err := b.SendAuthenticatedHTTPRequest(btccOrder, params) - - if err != nil { - log.Println(err) - } -} - -// GetOrders returns information of a range of orders -func (b *BTCC) GetOrders(openonly bool, symbol string, limit, offset, since int64, detailed bool) { - params := make([]interface{}, 0) - - if openonly { - params = append(params, openonly) - } - - if len(symbol) > 0 { - params = append(params, symbol) - } - - if limit > 0 { - params = append(params, limit) - } - - if offset > 0 { - params = append(params, offset) - } - - if since > 0 { - params = append(params, since) - } - - if detailed { - params = append(params, detailed) - } - - err := b.SendAuthenticatedHTTPRequest(btccOrders, params) - - if err != nil { - log.Println(err) - } -} - -// GetTransactions returns transaction lists -func (b *BTCC) GetTransactions(transType string, limit, offset, since int64, sinceType string) { - params := make([]interface{}, 0) - - if len(transType) > 0 { - params = append(params, transType) - } - - if limit > 0 { - params = append(params, limit) - } - - if offset > 0 { - params = append(params, offset) - } - - if since > 0 { - params = append(params, since) - } - - if len(sinceType) > 0 { - params = append(params, sinceType) - } - - err := b.SendAuthenticatedHTTPRequest(btccTransactions, params) - - if err != nil { - log.Println(err) - } -} - -// GetWithdrawal returns information about a withdrawal process -func (b *BTCC) GetWithdrawal(withdrawalID int64, currency string) { - params := make([]interface{}, 0) - params = append(params, withdrawalID) - - if len(currency) > 0 { - params = append(params, currency) - } - - err := b.SendAuthenticatedHTTPRequest(btccWithdrawal, params) - - if err != nil { - log.Println(err) - } -} - -// GetWithdrawals gets information about all withdrawals -func (b *BTCC) GetWithdrawals(currency string, pending bool) { - params := make([]interface{}, 0) - params = append(params, currency) - - if pending { - params = append(params, pending) - } - - err := b.SendAuthenticatedHTTPRequest(btccWithdrawals, params) - - if err != nil { - log.Println(err) - } -} - -// RequestWithdrawal requests a new withdrawal -func (b *BTCC) RequestWithdrawal(currency string, amount float64) { - params := make([]interface{}, 0) - params = append(params, currency) - params = append(params, amount) - - err := b.SendAuthenticatedHTTPRequest(btccWithdrawalRequest, params) - - if err != nil { - log.Println(err) - } -} - -// IcebergOrder intiates a large order but at intervals to preserve orderbook -// integrity -func (b *BTCC) IcebergOrder(buyOrder bool, price, amount, discAmount, variance float64, symbol string) { - params := make([]interface{}, 0) - params = append(params, strconv.FormatFloat(price, 'f', -1, 64)) - params = append(params, strconv.FormatFloat(amount, 'f', -1, 64)) - params = append(params, strconv.FormatFloat(discAmount, 'f', -1, 64)) - params = append(params, strconv.FormatFloat(variance, 'f', -1, 64)) - - if len(symbol) > 0 { - params = append(params, symbol) - } - - req := btccIcebergBuy - if !buyOrder { - req = btccIcebergSell - } - - err := b.SendAuthenticatedHTTPRequest(req, params) - - if err != nil { - log.Println(err) - } -} - -// GetIcebergOrder returns information on your iceberg order -func (b *BTCC) GetIcebergOrder(orderID int64, symbol string) { - params := make([]interface{}, 0) - params = append(params, orderID) - - if len(symbol) > 0 { - params = append(params, symbol) - } - - err := b.SendAuthenticatedHTTPRequest(btccIcebergOrder, params) - - if err != nil { - log.Println(err) - } -} - -// GetIcebergOrders returns information on all iceberg orders -func (b *BTCC) GetIcebergOrders(limit, offset int64, symbol string) { - params := make([]interface{}, 0) - - if limit > 0 { - params = append(params, limit) - } - - if offset > 0 { - params = append(params, offset) - } - - if len(symbol) > 0 { - params = append(params, symbol) - } - - err := b.SendAuthenticatedHTTPRequest(btccIcebergOrders, params) - - if err != nil { - log.Println(err) - } -} - -// CancelIcebergOrder cancels iceberg order -func (b *BTCC) CancelIcebergOrder(orderID int64, symbol string) { - params := make([]interface{}, 0) - params = append(params, orderID) - - if len(symbol) > 0 { - params = append(params, symbol) - } - - err := b.SendAuthenticatedHTTPRequest(btccIcebergCancel, params) - - if err != nil { - log.Println(err) - } -} - -// PlaceStopOrder inserts a stop loss order -func (b *BTCC) PlaceStopOrder(buyOder bool, stopPrice, price, amount, trailingAmt, trailingPct float64, symbol string) { - params := make([]interface{}, 0) - - if stopPrice > 0 { - params = append(params, stopPrice) - } - - params = append(params, strconv.FormatFloat(price, 'f', -1, 64)) - params = append(params, strconv.FormatFloat(amount, 'f', -1, 64)) - - if trailingAmt > 0 { - params = append(params, strconv.FormatFloat(trailingAmt, 'f', -1, 64)) - } - - if trailingPct > 0 { - params = append(params, strconv.FormatFloat(trailingPct, 'f', -1, 64)) - } - - if len(symbol) > 0 { - params = append(params, symbol) - } - - req := btccStoporderBuy - if !buyOder { - req = btccStoporderSell - } - - err := b.SendAuthenticatedHTTPRequest(req, params) - - if err != nil { - log.Println(err) - } -} - -// GetStopOrder returns a stop order -func (b *BTCC) GetStopOrder(orderID int64, symbol string) { - params := make([]interface{}, 0) - params = append(params, orderID) - - if len(symbol) > 0 { - params = append(params, symbol) - } - - err := b.SendAuthenticatedHTTPRequest(btccStoporder, params) - - if err != nil { - log.Println(err) - } -} - -// GetStopOrders returns all stop orders -func (b *BTCC) GetStopOrders(status, orderType string, stopPrice float64, limit, offset int64, symbol string) { - params := make([]interface{}, 0) - - if len(status) > 0 { - params = append(params, status) - } - - if len(orderType) > 0 { - params = append(params, orderType) - } - - if stopPrice > 0 { - params = append(params, stopPrice) - } - - if limit > 0 { - params = append(params, limit) - } - - if offset > 0 { - params = append(params, limit) - } - - if len(symbol) > 0 { - params = append(params, symbol) - } - - err := b.SendAuthenticatedHTTPRequest(btccStoporders, params) - - if err != nil { - log.Println(err) - } -} - -// CancelStopOrder cancels a stop order -func (b *BTCC) CancelStopOrder(orderID int64, symbol string) { - params := make([]interface{}, 0) - params = append(params, orderID) - - if len(symbol) > 0 { - params = append(params, symbol) - } - - err := b.SendAuthenticatedHTTPRequest(btccStoporderCancel, params) - - if err != nil { - log.Println(err) - } -} - -// SendHTTPRequest sends an unauthenticated HTTP request -func (b *BTCC) SendHTTPRequest(path string, result interface{}) error { - return b.SendPayload("GET", path, nil, nil, result, false, b.Verbose) -} - -// SendAuthenticatedHTTPRequest sends a valid authenticated HTTP request -func (b *BTCC) SendAuthenticatedHTTPRequest(method string, params []interface{}) (err error) { - if !b.AuthenticatedAPISupport { - return fmt.Errorf(exchange.WarningAuthenticatedRequestWithoutCredentialsSet, b.Name) - } - - if b.Nonce.Get() == 0 { - b.Nonce.Set(time.Now().UnixNano()) - } else { - b.Nonce.Inc() - } - encoded := fmt.Sprintf("tonce=%s&accesskey=%s&requestmethod=post&id=%d&method=%s¶ms=", b.Nonce.String()[0:16], b.APIKey, 1, method) - - if len(params) == 0 { - params = make([]interface{}, 0) - } else { - items := make([]string, 0) - for _, x := range params { - xType := fmt.Sprintf("%T", x) - switch xType { - case "int64", "int": - { - items = append(items, fmt.Sprintf("%d", x)) - } - case "string": - { - items = append(items, fmt.Sprintf("%s", x)) - } - case "float64": - { - items = append(items, fmt.Sprintf("%f", x)) - } - case "bool": - { - if x == true { - items = append(items, "1") - } else { - items = append(items, "") - } - } - default: - { - items = append(items, fmt.Sprintf("%v", x)) - } - } - } - encoded += common.JoinStrings(items, ",") - } - if b.Verbose { - log.Println(encoded) - } - - hmac := common.GetHMAC(common.HashSHA1, []byte(encoded), []byte(b.APISecret)) - postData := make(map[string]interface{}) - postData["method"] = method - postData["params"] = params - postData["id"] = 1 - - apiURL := fmt.Sprintf("%s/%s", b.APIUrl, btccAPIAuthenticatedMethod) - - data, err := common.JSONEncode(postData) - if err != nil { - return errors.New("Unable to JSON Marshal POST data") - } - - if b.Verbose { - log.Printf("Sending POST request to %s calling method %s with params %s\n", apiURL, method, data) - } - - headers := make(map[string]string) - headers["Content-type"] = "application/json-rpc" - headers["Authorization"] = "Basic " + common.Base64Encode([]byte(b.APIKey+":"+common.HexEncodeToString(hmac))) - headers["Json-Rpc-Tonce"] = b.Nonce.String() - - return b.SendPayload("POST", apiURL, headers, strings.NewReader(string(data)), nil, true, b.Verbose) -} diff --git a/exchanges/btcc/btcc_test.go b/exchanges/btcc/btcc_test.go index b5660368..9c5886e9 100644 --- a/exchanges/btcc/btcc_test.go +++ b/exchanges/btcc/btcc_test.go @@ -28,8 +28,9 @@ func TestSetup(t *testing.T) { } b.Setup(bConfig) - if !b.IsEnabled() || b.AuthenticatedAPISupport || b.RESTPollingDelay != time.Duration(10) || - b.Verbose || b.Websocket || len(b.BaseCurrencies) < 1 || + if !b.IsEnabled() || b.AuthenticatedAPISupport || + b.RESTPollingDelay != time.Duration(10) || b.Verbose || + b.Websocket.IsEnabled() || len(b.BaseCurrencies) < 1 || len(b.AvailablePairs) < 1 || len(b.EnabledPairs) < 1 { t.Error("Test Failed - BTCC Setup values not set correctly") } @@ -41,38 +42,38 @@ func TestGetFee(t *testing.T) { } } -func TestGetTicker(t *testing.T) { - t.Skip() - _, err := b.GetTicker("BTCUSD") - if err != nil { - t.Error("Test failed - GetTicker() error", err) - } -} +// func TestGetTicker(t *testing.T) { +// t.Skip() +// _, err := b.GetTicker("BTCUSD") +// if err != nil { +// t.Error("Test failed - GetTicker() error", err) +// } +// } -func TestGetTradeHistory(t *testing.T) { - t.Skip() - _, err := b.GetTradeHistory("BTCUSD", 0, 0, time.Time{}) - if err != nil { - t.Error("Test failed - GetTradeHistory() error", err) - } -} +// func TestGetTradeHistory(t *testing.T) { +// t.Skip() +// _, err := b.GetTradeHistory("BTCUSD", 0, 0, time.Time{}) +// if err != nil { +// t.Error("Test failed - GetTradeHistory() error", err) +// } +// } -func TestGetOrderBook(t *testing.T) { - t.Skip() - _, err := b.GetOrderBook("BTCUSD", 100) - if err != nil { - t.Error("Test failed - GetOrderBook() error", err) - } - _, err = b.GetOrderBook("BTCUSD", 0) - if err != nil { - t.Error("Test failed - GetOrderBook() error", err) - } -} +// func TestGetOrderBook(t *testing.T) { +// t.Skip() +// _, err := b.GetOrderBook("BTCUSD", 100) +// if err != nil { +// t.Error("Test failed - GetOrderBook() error", err) +// } +// _, err = b.GetOrderBook("BTCUSD", 0) +// if err != nil { +// t.Error("Test failed - GetOrderBook() error", err) +// } +// } -func TestGetAccountInfo(t *testing.T) { - t.Skip() - err := b.GetAccountInfo("") - if err == nil { - t.Error("Test failed - GetAccountInfo() error", err) - } -} +// func TestGetAccountInfo(t *testing.T) { +// t.Skip() +// err := b.GetAccountInfo("") +// if err == nil { +// t.Error("Test failed - GetAccountInfo() error", err) +// } +// } diff --git a/exchanges/btcc/btcc_types.go b/exchanges/btcc/btcc_types.go index f329885a..1feffd6b 100644 --- a/exchanges/btcc/btcc_types.go +++ b/exchanges/btcc/btcc_types.go @@ -1,12 +1,70 @@ package btcc -// Response is the generalized response type -type Response struct { - Ticker Ticker `json:"ticker"` +import "encoding/json" + +// WsAllTickerData defines multiple ticker data +type WsAllTickerData []WsTicker + +// WsOutgoing defines outgoing JSON +type WsOutgoing struct { + Action string `json:"action"` + Symbol string `json:"symbol,omitempty"` + Count int `json:"count,omitempty"` + Len int `json:"len,omitempty"` } -// Ticker holds basic ticker information -type Ticker struct { +// WsResponseMain defines the main websocket response +type WsResponseMain struct { + MsgType string `json:"MsgType"` + CRID string `json:"CRID"` + RC interface{} `json:"RC"` + Reason string `json:"Reason"` + Data json.RawMessage `json:"data"` +} + +// WsOrderbookSnapshot defines an orderbook from the websocket +type WsOrderbookSnapshot struct { + Timestamp int64 `json:"Timestamp"` + Symbol string `json:"Symbol"` + Version int64 `json:"Version"` + Type string `json:"Type"` + Content string `json:"Content"` + List []struct { + Side string `json:"Side"` + Size interface{} `json:"Size"` + Price float64 `json:"Price"` + } `json:"List"` + MsgType string `json:"MsgType"` +} + +// WsOrderbookSnapshotOld defines an old orderbook from the websocket connection +type WsOrderbookSnapshotOld struct { + MsgType string `json:"MsgType"` + Symbol string `json:"Symbol"` + Data map[string][]interface{} `json:"Data"` + Timestamp int64 `json:"Timestamp"` +} + +// WsTrades defines trading data from the websocket +type WsTrades struct { + Trades []struct { + TID int64 `json:"TID"` + Timestamp int64 `json:"Timestamp"` + Symbol string `json:"Symbol"` + Side string `json:"Side"` + Size float64 `json:"Size"` + Price float64 `json:"Price"` + MsgType string `json:"MsgType"` + } `json:"Trades"` + RC int64 `json:"RC"` + CRID string `json:"CRID"` + Reason string `json:"Reason"` + MsgType string `json:"MsgType"` +} + +// WsTicker defines ticker data from the websocket +type WsTicker struct { + Symbol string `json:"Symbol"` BidPrice float64 `json:"BidPrice"` AskPrice float64 `json:"AskPrice"` Open float64 `json:"Open"` @@ -20,177 +78,5 @@ type Ticker struct { Timestamp int64 `json:"Timestamp"` ExecutionLimitDown float64 `json:"ExecutionLimitDown"` ExecutionLimitUp float64 `json:"ExecutionLimitUp"` -} - -// Trade holds executed trade data -type Trade struct { - ID int64 `json:"Id"` - Timestamp int64 `json:"Timestamp"` - Price float64 `json:"Price"` - Quantity float64 `json:"Quantity"` - Side string `json:"Side"` -} - -// Orderbook holds orderbook data -type Orderbook struct { - Bids [][]float64 `json:"bids"` - Asks [][]float64 `json:"asks"` - Date int64 `json:"date"` -} - -// Profile holds profile information -type Profile struct { - Username string - TradePasswordEnabled bool `json:"trade_password_enabled,bool"` - OTPEnabled bool `json:"otp_enabled,bool"` - TradeFee float64 `json:"trade_fee"` - TradeFeeCNYLTC float64 `json:"trade_fee_cnyltc"` - TradeFeeBTCLTC float64 `json:"trade_fee_btcltc"` - DailyBTCLimit float64 `json:"daily_btc_limit"` - DailyLTCLimit float64 `json:"daily_ltc_limit"` - BTCDespoitAddress string `json:"btc_despoit_address"` - BTCWithdrawalAddress string `json:"btc_withdrawal_address"` - LTCDepositAddress string `json:"ltc_deposit_address"` - LTCWithdrawalAddress string `json:"ltc_withdrawal_request"` - APIKeyPermission int64 `json:"api_key_permission"` -} - -// CurrencyGeneric holds currency information -type CurrencyGeneric struct { - Currency string - Symbol string - Amount string - AmountInt int64 `json:"amount_integer"` - AmountDecimal float64 `json:"amount_decimal"` -} - -// Order holds order information -type Order struct { - ID int64 - Type string - Price float64 - Currency string - Amount float64 - AmountOrig float64 `json:"amount_original"` - Date int64 - Status string - Detail OrderDetail -} - -// OrderDetail holds order detail information -type OrderDetail struct { - Dateline int64 - Price float64 - Amount float64 -} - -// Withdrawal holds withdrawal transaction information -type Withdrawal struct { - ID int64 - Address string - Currency string - Amount float64 - Date int64 - Transaction string - Status string -} - -// Deposit holds deposit address information -type Deposit struct { - ID int64 - Address string - Currency string - Amount float64 - Date int64 - Status string -} - -// BidAsk holds bid and ask information -type BidAsk struct { - Price float64 - Amount float64 -} - -// Depth holds order book depth -type Depth struct { - Bid []BidAsk - Ask []BidAsk -} - -// Transaction holds transaction information -type Transaction struct { - ID int64 - Type string - BTCAmount float64 `json:"btc_amount"` - LTCAmount float64 `json:"ltc_amount"` - CNYAmount float64 `json:"cny_amount"` - Date int64 -} - -// IcebergOrder holds iceberg lettuce -type IcebergOrder struct { - ID int64 - Type string - Price float64 - Market string - Amount float64 - AmountOrig float64 `json:"amount_original"` - DisclosedAmount float64 `json:"disclosed_amount"` - Variance float64 - Date int64 - Status string -} - -// StopOrder holds stop order information -type StopOrder struct { - ID int64 - Type string - StopPrice float64 `json:"stop_price"` - TrailingAmt float64 `json:"trailing_amount"` - TrailingPct float64 `json:"trailing_percentage"` - Price float64 - Market string - Amount float64 - Date int64 - Status string - OrderID int64 `json:"order_id"` -} - -// WebsocketOrder holds websocket order information -type WebsocketOrder struct { - Price float64 `json:"price"` - TotalAmount float64 `json:"totalamount"` - Type string `json:"type"` -} - -// WebsocketGroupOrder holds websocket group order book information -type WebsocketGroupOrder struct { - Asks []WebsocketOrder `json:"ask"` - Bids []WebsocketOrder `json:"bid"` - Market string `json:"market"` -} - -// WebsocketTrade holds websocket trade information -type WebsocketTrade struct { - Amount float64 `json:"amount"` - Date float64 `json:"date"` - Market string `json:"market"` - Price float64 `json:"price"` - TradeID float64 `json:"trade_id"` - Type string `json:"type"` -} - -// WebsocketTicker holds websocket ticker information -type WebsocketTicker struct { - Buy float64 `json:"buy"` - Date float64 `json:"date"` - High float64 `json:"high"` - Last float64 `json:"last"` - Low float64 `json:"low"` - Market string `json:"market"` - Open float64 `json:"open"` - PrevClose float64 `json:"prev_close"` - Sell float64 `json:"sell"` - Volume float64 `json:"vol"` - Vwap float64 `json:"vwap"` + MsgType string `json:"MsgType"` } diff --git a/exchanges/btcc/btcc_websocket.go b/exchanges/btcc/btcc_websocket.go index bac59e5c..3ebd8f84 100644 --- a/exchanges/btcc/btcc_websocket.go +++ b/exchanges/btcc/btcc_websocket.go @@ -1,125 +1,569 @@ package btcc import ( + "errors" "fmt" "log" + "net/http" + "net/url" + "strconv" + "sync" + "time" + "github.com/gorilla/websocket" "github.com/thrasher-/gocryptotrader/common" - "github.com/thrasher-/socketio" + "github.com/thrasher-/gocryptotrader/currency/pair" + exchange "github.com/thrasher-/gocryptotrader/exchanges" + "github.com/thrasher-/gocryptotrader/exchanges/orderbook" ) const ( - btccSocketioAddress = "https://websocket.btcc.com" + btccSocketioAddress = "wss://ws.btcc.com" + + msgTypeHeartBeat = "Heartbeat" + msgTypeGetActiveContracts = "GetActiveContractsResponse" + msgTypeQuote = "QuoteResponse" + msgTypeLogin = "LoginResponse" + msgTypeAccountInfo = "AccountInfo" + msgTypeExecReport = "ExecReport" + msgTypePlaceOrder = "PlaceOrderResponse" + msgTypeCancelAllOrders = "CancelAllOrdersResponse" + msgTypeCancelOrder = "CancelOrderResponse" + msgTypeCancelReplaceOrder = "CancelReplaceOrderResponse" + msgTypeGetAccountInfo = "GetAccountInfoResponse" + msgTypeRetrieveOrder = "RetrieveOrderResponse" + msgTypeGetTrades = "GetTradesResponse" + + msgTypeAllTickers = "AllTickersResponse" ) -// BTCCSocket is a pointer to a IO socket -var BTCCSocket *socketio.SocketIO +var ( + mtx sync.Mutex +) -// OnConnect gets information from the server when its connected -func (b *BTCC) OnConnect(output chan socketio.Message) { - if b.Verbose { - log.Printf("%s Connected to Websocket.", b.GetName()) +// WsConnect initiates a websocket client connection +func (b *BTCC) WsConnect() error { + if !b.Websocket.IsEnabled() || !b.IsEnabled() { + return errors.New(exchange.WebsocketNotEnabled) } - currencies := []string{} - for _, x := range b.EnabledPairs { - currency := common.StringToLower(x[3:] + x[0:3]) - currencies = append(currencies, currency) - } - endpoints := []string{"marketdata", "grouporder"} + var dialer websocket.Dialer + var err error - for _, x := range endpoints { - for _, y := range currencies { - channel := fmt.Sprintf(`"%s_%s"`, x, y) - if b.Verbose { - log.Printf("%s Websocket subscribing to channel: %s.", b.GetName(), channel) + if b.Websocket.GetProxyAddress() != "" { + prxy, err := url.Parse(b.Websocket.GetProxyAddress()) + if err != nil { + return err + } + dialer.Proxy = http.ProxyURL(prxy) + } + + b.Conn, _, err = dialer.Dial(b.Websocket.GetWebsocketURL(), http.Header{}) + if err != nil { + return err + } + + err = b.WsUpdateCurrencyPairs() + if err != nil { + return err + } + + go b.WsReadData() + go b.WsHandleData() + + err = b.WsSubscribeToOrderbook() + if err != nil { + return err + } + + err = b.WsSubcribeToTicker() + if err != nil { + return err + } + + return b.WsSubcribeToTrades() +} + +// WsReadData reads data from the websocket connection +func (b *BTCC) WsReadData() { + b.Websocket.Wg.Add(1) + defer b.Websocket.Wg.Done() + + for { + select { + case <-b.Websocket.ShutdownC: + return + + default: + mtx.Lock() + _, resp, err := b.Conn.ReadMessage() + mtx.Unlock() + if err != nil { + b.Websocket.DataHandler <- err + } + + b.Websocket.TrafficAlert <- struct{}{} + + b.Websocket.Intercomm <- exchange.WebsocketResponse{ + Raw: resp, } - output <- socketio.CreateMessageEvent("subscribe", channel, b.OnMessage, BTCCSocket.Version) } } } -// OnDisconnect alerts when disconnection occurs -func (b *BTCC) OnDisconnect(output chan socketio.Message) { - log.Printf("%s Disconnected from websocket server.. Reconnecting.\n", b.GetName()) - b.WebsocketClient() -} +// WsHandleData handles read data +func (b *BTCC) WsHandleData() { + b.Websocket.Wg.Add(1) + defer b.Websocket.Wg.Done() -// OnError alerts when error occurs -func (b *BTCC) OnError() { - log.Printf("%s Error with Websocket connection.. Reconnecting.\n", b.GetName()) - b.WebsocketClient() -} + for { + select { + case <-b.Websocket.ShutdownC: + return -// OnMessage if message received and verbose it is printed out -func (b *BTCC) OnMessage(message []byte, output chan socketio.Message) { - if b.Verbose { - log.Printf("%s Websocket message received which isn't handled by default.\n", b.GetName()) - log.Println(string(message)) + case resp := <-b.Websocket.Intercomm: + var Result WsResponseMain + err := common.JSONDecode(resp.Raw, &Result) + if err != nil { + log.Fatal(err) + } + + switch Result.MsgType { + case msgTypeHeartBeat: + + case msgTypeGetActiveContracts: + log.Println("Active Contracts") + log.Fatal(string(resp.Raw)) + + case msgTypeQuote: + log.Println("Quotes") + log.Fatal(string(resp.Raw)) + + case msgTypeLogin: + log.Println("Login") + log.Fatal(string(resp.Raw)) + + case msgTypeAccountInfo: + log.Println("Account info") + log.Fatal(string(resp.Raw)) + + case msgTypeExecReport: + log.Println("Exec Report") + log.Fatal(string(resp.Raw)) + + case msgTypePlaceOrder: + log.Println("Place order") + log.Fatal(string(resp.Raw)) + + case msgTypeCancelAllOrders: + log.Println("Cancel All orders") + log.Fatal(string(resp.Raw)) + + case msgTypeCancelOrder: + log.Println("Cancel order") + log.Fatal(string(resp.Raw)) + + case msgTypeCancelReplaceOrder: + log.Println("Replace order") + log.Fatal(string(resp.Raw)) + + case msgTypeGetAccountInfo: + log.Println("Account info") + log.Fatal(string(resp.Raw)) + + case msgTypeRetrieveOrder: + log.Println("Retrieve order") + log.Fatal(string(resp.Raw)) + + case msgTypeGetTrades: + var trades WsTrades + + err := common.JSONDecode(resp.Raw, &trades) + if err != nil { + b.Websocket.DataHandler <- err + continue + } + + case "OrderBook": + // NOTE: This seems to be a websocket update not reflected in + // current API docs, this comes in conjunction with the other + // orderbook feeds + var orderbook WsOrderbookSnapshot + + err := common.JSONDecode(resp.Raw, &orderbook) + if err != nil { + b.Websocket.DataHandler <- err + continue + } + + switch orderbook.Type { + case "F": + err = b.WsProcessOrderbookSnapshot(orderbook) + if err != nil { + b.Websocket.DataHandler <- err + } + + case "I": + err = b.WsProcessOrderbookUpdate(orderbook) + if err != nil { + b.Websocket.DataHandler <- err + } + } + + case "SubOrderBookResponse": + + case "Ticker": + var ticker WsTicker + + err = common.JSONDecode(resp.Raw, &ticker) + if err != nil { + b.Websocket.DataHandler <- err + continue + } + + tick := exchange.TickerData{} + tick.AssetType = "SPOT" + tick.ClosePrice = ticker.PrevCls + tick.Exchange = b.GetName() + tick.HighPrice = ticker.High + tick.LowPrice = ticker.Low + tick.OpenPrice = ticker.Open + tick.Pair = pair.NewCurrencyPairFromString(ticker.Symbol) + tick.Quantity = ticker.Volume + timestamp := time.Unix(ticker.Timestamp, 0) + tick.Timestamp = timestamp + + b.Websocket.DataHandler <- tick + + default: + + if common.StringContains(Result.MsgType, "OrderBook") { + var oldOrderbookType WsOrderbookSnapshotOld + err = common.JSONDecode(resp.Raw, &oldOrderbookType) + if err != nil { + b.Websocket.DataHandler <- err + continue + } + + symbol := common.SplitStrings(Result.MsgType, ".") + err = b.WsProcessOldOrderbookSnapshot(oldOrderbookType, symbol[1]) + if err != nil { + b.Websocket.DataHandler <- err + continue + } + continue + } + } + } } } -// OnTicker handles ticker information -func (b *BTCC) OnTicker(message []byte, output chan socketio.Message) { - type Response struct { - Ticker WebsocketTicker `json:"ticker"` - } - var resp Response - err := common.JSONDecode(message, &resp) +// 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 { - log.Println(err) - return - } -} - -// OnGroupOrder handles group order information -func (b *BTCC) OnGroupOrder(message []byte, output chan socketio.Message) { - type Response struct { - GroupOrder WebsocketGroupOrder `json:"grouporder"` - } - var resp Response - err := common.JSONDecode(message, &resp) - - if err != nil { - log.Println(err) - return - } -} - -// OnTrade handles group trade information -func (b *BTCC) OnTrade(message []byte, output chan socketio.Message) { - trade := WebsocketTrade{} - err := common.JSONDecode(message, &trade) - - if err != nil { - log.Println(err) - return - } -} - -// WebsocketClient initiates a websocket client -func (b *BTCC) WebsocketClient() { - events := make(map[string]func(message []byte, output chan socketio.Message)) - events["grouporder"] = b.OnGroupOrder - events["ticker"] = b.OnTicker - events["trade"] = b.OnTrade - - BTCCSocket = &socketio.SocketIO{ - Version: 1, - OnConnect: b.OnConnect, - OnEvent: events, - OnError: b.OnError, - OnMessage: b.OnMessage, - OnDisconnect: b.OnDisconnect, + return err } - for b.Enabled && b.Websocket { - err := socketio.ConnectToSocket(btccSocketioAddress, BTCCSocket) + var currencyResponse WsResponseMain + for { + _, resp, err := b.Conn.ReadMessage() if err != nil { - log.Printf("%s Unable to connect to Websocket. Err: %s\n", b.GetName(), err) + return err + } + + b.Websocket.TrafficAlert <- struct{}{} + + err = common.JSONDecode(resp, ¤cyResponse) + if err != nil { + return err + } + + switch currencyResponse.MsgType { + case msgTypeAllTickers: + var tickers WsAllTickerData + err := common.JSONDecode(currencyResponse.Data, &tickers) + if err != nil { + return err + } + + var availableTickers []string + for _, tickerData := range tickers { + availableTickers = append(availableTickers, tickerData.Symbol) + } + + err = b.UpdateCurrencies(availableTickers, false, true) + if err != nil { + return fmt.Errorf("%s failed to update available currencies. %s", + b.Name, + err) + } + + return b.WsUnSubscribeAllTickers() + + case "Heartbeat": + + default: + return fmt.Errorf("btcc_websocket.go error - Updating currency pairs resp incorrect: %s", + string(resp)) + } + } +} + +// 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 + for _, data := range ob.List { + var newSize float64 + switch data.Size.(type) { + case float64: + newSize = data.Size.(float64) + case string: + var err error + newSize, err = strconv.ParseFloat(data.Size.(string), 64) + if err != nil { + return err + } + } + + if data.Side == "1" { + asks = append(asks, orderbook.Item{Price: data.Price, Amount: newSize}) continue } - log.Printf("%s Disconnected from Websocket.\n", b.GetName()) + + bids = append(bids, orderbook.Item{Price: data.Price, Amount: newSize}) } + + var newOrderbook orderbook.Base + + newOrderbook.Asks = asks + newOrderbook.AssetType = "SPOT" + newOrderbook.Bids = bids + newOrderbook.CurrencyPair = ob.Symbol + newOrderbook.LastUpdated = time.Now() + newOrderbook.Pair = pair.NewCurrencyPairFromString(ob.Symbol) + + err := b.Websocket.Orderbook.LoadSnapshot(newOrderbook, b.GetName()) + if err != nil { + return err + } + + b.Websocket.DataHandler <- exchange.WebsocketOrderbookUpdate{ + Exchange: b.GetName(), + Asset: "SPOT", + Pair: pair.NewCurrencyPairFromString(ob.Symbol), + } + + return nil +} + +// WsProcessOrderbookUpdate processes an orderbook update +func (b *BTCC) WsProcessOrderbookUpdate(ob WsOrderbookSnapshot) error { + var asks, bids []orderbook.Item + for _, data := range ob.List { + var newSize float64 + switch data.Size.(type) { + case float64: + newSize = data.Size.(float64) + case string: + var err error + newSize, err = strconv.ParseFloat(data.Size.(string), 64) + if err != nil { + return err + } + } + + if data.Side == "1" { + if newSize < 0 { + asks = append(asks, orderbook.Item{Price: data.Price, Amount: 0}) + continue + } + asks = append(asks, orderbook.Item{Price: data.Price, Amount: newSize}) + continue + } + + if newSize < 0 { + bids = append(bids, orderbook.Item{Price: data.Price, Amount: 0}) + continue + } + + bids = append(bids, orderbook.Item{Price: data.Price, Amount: newSize}) + } + + p := pair.NewCurrencyPairFromString(ob.Symbol) + + err := b.Websocket.Orderbook.Update(bids, asks, p, time.Now(), b.GetName(), "SPOT") + if err != nil { + return err + } + + b.Websocket.DataHandler <- exchange.WebsocketOrderbookUpdate{ + Exchange: b.GetName(), + Asset: "SPOT", + Pair: pair.NewCurrencyPairFromString(ob.Symbol), + } + + return nil +} + +// WsProcessOldOrderbookSnapshot processes an old orderbook snapshot +func (b *BTCC) WsProcessOldOrderbookSnapshot(ob WsOrderbookSnapshotOld, symbol string) error { + var asks, bids []orderbook.Item + + askData, _ := ob.Data["Asks"] + bidData, _ := ob.Data["Bids"] + + for _, ask := range askData { + data := ask.([]interface{}) + var price, amount float64 + + switch data[0].(type) { + case string: + var err error + price, err = strconv.ParseFloat(data[0].(string), 64) + if err != nil { + return err + } + case float64: + price = data[0].(float64) + } + + switch data[0].(type) { + case string: + var err error + amount, err = strconv.ParseFloat(data[0].(string), 64) + if err != nil { + return err + } + case float64: + amount = data[0].(float64) + } + + asks = append(asks, orderbook.Item{ + Price: price, + Amount: amount, + }) + } + + for _, bid := range bidData { + data := bid.([]interface{}) + var price, amount float64 + + switch data[1].(type) { + case string: + var err error + price, err = strconv.ParseFloat(data[1].(string), 64) + if err != nil { + return err + } + case float64: + price = data[1].(float64) + } + + switch data[1].(type) { + case string: + var err error + amount, err = strconv.ParseFloat(data[1].(string), 64) + if err != nil { + return err + } + case float64: + amount = data[1].(float64) + } + + bids = append(bids, orderbook.Item{ + Price: price, + Amount: amount, + }) + } + + p := pair.NewCurrencyPairFromString(symbol) + + err := b.Websocket.Orderbook.Update(bids, asks, p, time.Now(), b.GetName(), "SPOT") + if err != nil { + return err + } + + b.Websocket.DataHandler <- exchange.WebsocketOrderbookUpdate{ + Exchange: b.GetName(), + Pair: p, + Asset: "SPOT", + } + + return nil } diff --git a/exchanges/btcc/btcc_wrapper.go b/exchanges/btcc/btcc_wrapper.go index bb2b6c54..ffcb5bcc 100644 --- a/exchanges/btcc/btcc_wrapper.go +++ b/exchanges/btcc/btcc_wrapper.go @@ -25,15 +25,11 @@ func (b *BTCC) Start(wg *sync.WaitGroup) { // Run implements the BTCC wrapper func (b *BTCC) Run() { if b.Verbose { - log.Printf("%s Websocket: %s.", b.GetName(), common.IsEnabled(b.Websocket)) + log.Printf("%s Websocket: %s.", b.GetName(), common.IsEnabled(b.Websocket.IsEnabled())) log.Printf("%s polling delay: %ds.\n", b.GetName(), b.RESTPollingDelay) log.Printf("%s %d currencies enabled: %s.\n", b.GetName(), len(b.EnabledPairs), b.EnabledPairs) } - if b.Websocket { - go b.WebsocketClient() - } - if common.StringDataContains(b.EnabledPairs, "CNY") || common.StringDataContains(b.AvailablePairs, "CNY") || common.StringDataContains(b.BaseCurrencies, "CNY") { log.Println("WARNING: BTCC only supports BTCUSD now, upgrading available, enabled and base currencies to BTCUSD/USD") pairs := []string{"BTCUSD"} @@ -69,82 +65,89 @@ func (b *BTCC) Run() { // UpdateTicker updates and returns the ticker for a currency pair func (b *BTCC) UpdateTicker(p pair.CurrencyPair, assetType string) (ticker.Price, error) { - var tickerPrice ticker.Price - tick, err := b.GetTicker(exchange.FormatExchangeCurrency(b.GetName(), p).String()) - if err != nil { - return tickerPrice, err - } - tickerPrice.Pair = p - tickerPrice.Ask = tick.AskPrice - tickerPrice.Bid = tick.BidPrice - tickerPrice.Low = tick.Low - tickerPrice.Last = tick.Last - tickerPrice.Volume = tick.Volume24H - tickerPrice.High = tick.High - ticker.ProcessTicker(b.GetName(), p, tickerPrice, assetType) - return ticker.GetTicker(b.Name, p, assetType) + // var tickerPrice ticker.Price + // tick, err := b.GetTicker(exchange.FormatExchangeCurrency(b.GetName(), p).String()) + // if err != nil { + // return tickerPrice, err + // } + // tickerPrice.Pair = p + // tickerPrice.Ask = tick.AskPrice + // tickerPrice.Bid = tick.BidPrice + // tickerPrice.Low = tick.Low + // tickerPrice.Last = tick.Last + // tickerPrice.Volume = tick.Volume24H + // tickerPrice.High = tick.High + // ticker.ProcessTicker(b.GetName(), p, tickerPrice, assetType) + // return ticker.GetTicker(b.Name, p, assetType) + return ticker.Price{}, errors.New("REST NOT SUPPORTED") } // GetTickerPrice returns the ticker for a currency pair func (b *BTCC) GetTickerPrice(p pair.CurrencyPair, assetType string) (ticker.Price, error) { - tickerNew, err := ticker.GetTicker(b.GetName(), p, assetType) - if err != nil { - return b.UpdateTicker(p, assetType) - } - return tickerNew, nil + // tickerNew, err := ticker.GetTicker(b.GetName(), p, assetType) + // if err != nil { + // return b.UpdateTicker(p, assetType) + // } + // return tickerNew, nil + return ticker.Price{}, errors.New("REST NOT SUPPORTED") } // GetOrderbookEx returns the orderbook for a currency pair func (b *BTCC) GetOrderbookEx(p pair.CurrencyPair, assetType string) (orderbook.Base, error) { - ob, err := orderbook.GetOrderbook(b.GetName(), p, assetType) - if err != nil { - return b.UpdateOrderbook(p, assetType) - } - return ob, nil + // ob, err := orderbook.GetOrderbook(b.GetName(), p, assetType) + // if err != nil { + // return b.UpdateOrderbook(p, assetType) + // } + // return ob, nil + return orderbook.Base{}, errors.New("REST NOT SUPPORTED") } // UpdateOrderbook updates and returns the orderbook for a currency pair func (b *BTCC) UpdateOrderbook(p pair.CurrencyPair, assetType string) (orderbook.Base, error) { - var orderBook orderbook.Base - orderbookNew, err := b.GetOrderBook(exchange.FormatExchangeCurrency(b.GetName(), p).String(), 100) - if err != nil { - return orderBook, err - } + // var orderBook orderbook.Base + // orderbookNew, err := b.GetOrderBook(exchange.FormatExchangeCurrency(b.GetName(), p).String(), 100) + // if err != nil { + // return orderBook, err + // } - for x := range orderbookNew.Bids { - data := orderbookNew.Bids[x] - orderBook.Bids = append(orderBook.Bids, orderbook.Item{Price: data[0], Amount: data[1]}) - } + // for x := range orderbookNew.Bids { + // data := orderbookNew.Bids[x] + // orderBook.Bids = append(orderBook.Bids, orderbook.Item{Price: data[0], Amount: data[1]}) + // } - for x := range orderbookNew.Asks { - data := orderbookNew.Asks[x] - orderBook.Asks = append(orderBook.Asks, orderbook.Item{Price: data[0], Amount: data[1]}) - } + // for x := range orderbookNew.Asks { + // data := orderbookNew.Asks[x] + // orderBook.Asks = append(orderBook.Asks, orderbook.Item{Price: data[0], Amount: data[1]}) + // } - orderbook.ProcessOrderbook(b.GetName(), p, orderBook, assetType) - return orderbook.GetOrderbook(b.Name, p, assetType) + // orderbook.ProcessOrderbook(b.GetName(), p, orderBook, assetType) + // return orderbook.GetOrderbook(b.Name, p, assetType) + return orderbook.Base{}, errors.New("REST NOT SUPPORTED") } // GetExchangeAccountInfo : Retrieves balances for all enabled currencies for // the Kraken exchange - TODO func (b *BTCC) GetExchangeAccountInfo() (exchange.AccountInfo, error) { - var response exchange.AccountInfo - response.ExchangeName = b.GetName() - return response, nil + // var response exchange.AccountInfo + // response.ExchangeName = b.GetName() + // return response, nil + return exchange.AccountInfo{}, errors.New("REST NOT SUPPORTED") } // GetExchangeFundTransferHistory returns funding history, deposits and // withdrawals func (b *BTCC) GetExchangeFundTransferHistory() ([]exchange.FundHistory, error) { - var fundHistory []exchange.FundHistory - return fundHistory, errors.New("not supported on exchange") + // var fundHistory []exchange.FundHistory + // return fundHistory, errors.New("not supported on exchange") + return nil, errors.New("REST NOT SUPPORTED") } // GetExchangeHistory returns historic trade data since exchange opening. func (b *BTCC) GetExchangeHistory(p pair.CurrencyPair, assetType string) ([]exchange.TradeHistory, error) { - var resp []exchange.TradeHistory + // var resp []exchange.TradeHistory - return resp, errors.New("trade history not yet implemented") + // return resp, errors.New("trade history not yet implemented") + return nil, errors.New("REST NOT SUPPORTED") } // SubmitExchangeOrder submits a new order @@ -196,3 +199,8 @@ func (b *BTCC) WithdrawFiatExchangeFunds(currency pair.CurrencyItem, amount floa func (b *BTCC) WithdrawFiatExchangeFundsToInternationalBank(currency pair.CurrencyItem, amount float64) (string, error) { return "", errors.New("not yet implemented") } + +// GetWebsocket returns a pointer to the exchange websocket +func (b *BTCC) GetWebsocket() (*exchange.Websocket, error) { + return b.Websocket, nil +} diff --git a/exchanges/btcmarkets/btcmarkets.go b/exchanges/btcmarkets/btcmarkets.go index 655610ea..a36d3f27 100644 --- a/exchanges/btcmarkets/btcmarkets.go +++ b/exchanges/btcmarkets/btcmarkets.go @@ -54,7 +54,6 @@ func (b *BTCMarkets) SetDefaults() { b.Enabled = false b.Fee = 0.85 b.Verbose = false - b.Websocket = false b.RESTPollingDelay = 10 b.Ticker = make(map[string]Ticker) b.RequestCurrencyPairFormat.Delimiter = "" @@ -70,6 +69,7 @@ func (b *BTCMarkets) SetDefaults() { common.NewHTTPClientWithTimeout(exchange.DefaultHTTPTimeout)) b.APIUrlDefault = btcMarketsAPIURL b.APIUrl = b.APIUrlDefault + b.WebsocketInit() } // Setup takes in an exchange configuration and sets all parameters @@ -84,7 +84,6 @@ func (b *BTCMarkets) Setup(exch config.ExchangeConfig) { b.SetHTTPClientUserAgent(exch.HTTPUserAgent) b.RESTPollingDelay = exch.RESTPollingDelay b.Verbose = exch.Verbose - b.Websocket = exch.Websocket b.BaseCurrencies = common.SplitStrings(exch.BaseCurrencies, ",") b.AvailablePairs = common.SplitStrings(exch.AvailablePairs, ",") b.EnabledPairs = common.SplitStrings(exch.EnabledPairs, ",") @@ -104,6 +103,10 @@ func (b *BTCMarkets) Setup(exch config.ExchangeConfig) { if err != nil { log.Fatal(err) } + err = b.SetClientProxyAddress(exch.ProxyAddress) + if err != nil { + log.Fatal(err) + } } } diff --git a/exchanges/btcmarkets/btcmarkets_wrapper.go b/exchanges/btcmarkets/btcmarkets_wrapper.go index 13960c53..0339ffd4 100644 --- a/exchanges/btcmarkets/btcmarkets_wrapper.go +++ b/exchanges/btcmarkets/btcmarkets_wrapper.go @@ -250,3 +250,8 @@ func (b *BTCMarkets) WithdrawFiatExchangeFunds(currency pair.CurrencyItem, amoun func (b *BTCMarkets) WithdrawFiatExchangeFundsToInternationalBank(currency pair.CurrencyItem, amount float64) (string, error) { return "", errors.New("not yet implemented") } + +// GetWebsocket returns a pointer to the exchange websocket +func (b *BTCMarkets) GetWebsocket() (*exchange.Websocket, error) { + return nil, errors.New("not yet implemented") +} diff --git a/exchanges/coinbasepro/coinbasepro.go b/exchanges/coinbasepro/coinbasepro.go index 3eff5585..d1dee89b 100644 --- a/exchanges/coinbasepro/coinbasepro.go +++ b/exchanges/coinbasepro/coinbasepro.go @@ -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" @@ -56,6 +57,7 @@ const ( // CoinbasePro is the overarching type across the coinbasepro package type CoinbasePro struct { exchange.Base + WebsocketConn *websocket.Conn } // SetDefaults sets default values for the exchange @@ -65,7 +67,6 @@ func (c *CoinbasePro) SetDefaults() { c.Verbose = false c.TakerFee = 0.25 c.MakerFee = 0 - c.Websocket = false c.RESTPollingDelay = 10 c.RequestCurrencyPairFormat.Delimiter = "-" c.RequestCurrencyPairFormat.Uppercase = true @@ -80,6 +81,7 @@ func (c *CoinbasePro) SetDefaults() { common.NewHTTPClientWithTimeout(exchange.DefaultHTTPTimeout)) c.APIUrlDefault = coinbaseproAPIURL c.APIUrl = c.APIUrlDefault + c.WebsocketInit() } // Setup initialises the exchange parameters with the current configuration @@ -94,7 +96,7 @@ func (c *CoinbasePro) Setup(exch config.ExchangeConfig) { c.SetHTTPClientUserAgent(exch.HTTPUserAgent) c.RESTPollingDelay = exch.RESTPollingDelay c.Verbose = exch.Verbose - c.Websocket = exch.Websocket + c.Websocket.SetEnabled(exch.Websocket) c.BaseCurrencies = common.SplitStrings(exch.BaseCurrencies, ",") c.AvailablePairs = common.SplitStrings(exch.AvailablePairs, ",") c.EnabledPairs = common.SplitStrings(exch.EnabledPairs, ",") @@ -117,6 +119,18 @@ func (c *CoinbasePro) Setup(exch config.ExchangeConfig) { if err != nil { log.Fatal(err) } + err = c.SetClientProxyAddress(exch.ProxyAddress) + if err != nil { + log.Fatal(err) + } + err = c.WebsocketSetup(c.WsConnect, + exch.Name, + exch.Websocket, + coinbaseproWebsocketURL, + exch.WebsocketURL) + if err != nil { + log.Fatal(err) + } } } diff --git a/exchanges/coinbasepro/coinbasepro_types.go b/exchanges/coinbasepro/coinbasepro_types.go index 23074876..c6900a31 100644 --- a/exchanges/coinbasepro/coinbasepro_types.go +++ b/exchanges/coinbasepro/coinbasepro_types.go @@ -341,55 +341,68 @@ type FillResponse struct { // WebsocketSubscribe takes in subscription information type WebsocketSubscribe struct { - Type string `json:"type"` - ProductID string `json:"product_id"` + Type string `json:"type"` + ProductID string `json:"product_id,omitempty"` + Channels []WsChannels `json:"channels,omitempty"` +} + +// WsChannels defines outgoing channels for subscription purposes +type WsChannels struct { + Name string `json:"name"` + ProductIDs []string `json:"product_ids"` } // WebsocketReceived holds websocket received values type WebsocketReceived struct { - Type string `json:"type"` - Time string `json:"time"` - Sequence int `json:"sequence"` - OrderID string `json:"order_id"` - Size float64 `json:"size,string"` - Price float64 `json:"price,string"` - Side string `json:"side"` + Type string `json:"type"` + OrderID string `json:"order_id"` + OrderType string `json:"order_type"` + Size float64 `json:"size,string"` + Price float64 `json:"price,string"` + Side string `json:"side"` + ClientOID string `json:"client_oid"` + ProductID string `json:"product_id"` + Sequence int64 `json:"sequence"` + Time string `json:"time"` } // WebsocketOpen collates open orders type WebsocketOpen struct { Type string `json:"type"` - Time string `json:"time"` - Sequence int `json:"sequence"` - OrderID string `json:"order_id"` - Price float64 `json:"price,string"` - RemainingSize float64 `json:"remaining_size,string"` Side string `json:"side"` + Price float64 `json:"price,string"` + OrderID string `json:"order_id"` + RemainingSize float64 `json:"remaining_size,string"` + ProductID string `json:"product_id"` + Sequence int64 `json:"sequence"` + Time string `json:"time"` } // WebsocketDone holds finished order information type WebsocketDone struct { Type string `json:"type"` - Time string `json:"time"` - Sequence int `json:"sequence"` - Price float64 `json:"price,string"` + Side string `json:"side"` OrderID string `json:"order_id"` Reason string `json:"reason"` - Side string `json:"side"` + ProductID string `json:"product_id"` + Price float64 `json:"price,string"` RemainingSize float64 `json:"remaining_size,string"` + Sequence int64 `json:"sequence"` + Time string `json:"time"` } // WebsocketMatch holds match information type WebsocketMatch struct { Type string `json:"type"` TradeID int `json:"trade_id"` - Sequence int `json:"sequence"` MakerOrderID string `json:"maker_order_id"` TakerOrderID string `json:"taker_order_id"` - Time string `json:"time"` + Side string `json:"side"` Size float64 `json:"size,string"` Price float64 `json:"price,string"` - Side string `json:"side"` + ProductID string `json:"product_id"` + Sequence int64 `json:"sequence"` + Time string `json:"time"` } // WebsocketChange holds change information @@ -403,3 +416,43 @@ type WebsocketChange struct { Price float64 `json:"price,string"` Side string `json:"side"` } + +// WebsocketHeartBeat defines JSON response for a heart beat message +type WebsocketHeartBeat struct { + Type string `json:"type"` + Sequence int64 `json:"sequence"` + LastTradeID int64 `json:"last_trade_id"` + ProductID string `json:"product_id"` + Time string `json:"time"` +} + +// WebsocketTicker defines ticker websocket response +type WebsocketTicker struct { + Type string `json:"type"` + Sequence int64 `json:"sequence"` + ProductID string `json:"product_id"` + Price float64 `json:"price,string"` + Open24H float64 `json:"open_24h,string"` + Volume24H float64 `json:"volumen_24h,string"` + Low24H float64 `json:"low_24h,string"` + High24H float64 `json:"high_24h,string"` + Volume30D float64 `json:"volume_30d,string"` + BestBid float64 `json:"best_bid,string"` + BestAsk float64 `json:"best_ask,string"` +} + +// WebsocketOrderbookSnapshot defines a snapshot reponse +type WebsocketOrderbookSnapshot struct { + ProductID string `json:"product_id"` + Type string `json:"type"` + Bids [][]interface{} `json:"bids"` + Asks [][]interface{} `json:"asks"` +} + +// WebsocketL2Update defines an update on the L2 orderbooks +type WebsocketL2Update struct { + Type string `json:"type"` + ProductID string `json:"product_id"` + Time string `json:"time"` + Changes [][]interface{} `json:"changes"` +} diff --git a/exchanges/coinbasepro/coinbasepro_websocket.go b/exchanges/coinbasepro/coinbasepro_websocket.go index 46f5bdc8..407752e0 100644 --- a/exchanges/coinbasepro/coinbasepro_websocket.go +++ b/exchanges/coinbasepro/coinbasepro_websocket.go @@ -1,127 +1,293 @@ package coinbasepro import ( + "errors" + "fmt" "log" "net/http" + "net/url" + "strconv" + "time" "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 ( coinbaseproWebsocketURL = "wss://ws-feed.pro.coinbase.com" ) -// WebsocketSubscribe subscribes to a websocket connection -func (c *CoinbasePro) WebsocketSubscribe(product string, conn *websocket.Conn) error { - subscribe := WebsocketSubscribe{"subscribe", product} +// WebsocketSubscriber subscribes to websocket channels with respect to enabled +// currencies +func (c *CoinbasePro) WebsocketSubscriber() error { + currencies := []string{} + for _, x := range c.EnabledPairs { + currency := x[0:3] + "-" + x[3:] + currencies = append(currencies, currency) + } + + var channels []WsChannels + channels = append(channels, WsChannels{ + Name: "heartbeat", + ProductIDs: currencies, + }) + + channels = append(channels, WsChannels{ + Name: "ticker", + ProductIDs: currencies, + }) + + channels = append(channels, WsChannels{ + Name: "level2", + ProductIDs: currencies, + }) + + subscribe := WebsocketSubscribe{Type: "subscribe", Channels: channels} + json, err := common.JSONEncode(subscribe) if err != nil { return err } - err = conn.WriteMessage(websocket.TextMessage, json) + return c.WebsocketConn.WriteMessage(websocket.TextMessage, json) +} +// WsConnect initiates a websocket connection +func (c *CoinbasePro) WsConnect() error { + if !c.Websocket.IsEnabled() || !c.IsEnabled() { + return errors.New(exchange.WebsocketNotEnabled) + } + + var dialer websocket.Dialer + + if c.Websocket.GetProxyAddress() != "" { + proxy, err := url.Parse(c.Websocket.GetProxyAddress()) + if err != nil { + return fmt.Errorf("coinbasepro_websocket.go error - proxy address %s", + err) + } + + dialer.Proxy = http.ProxyURL(proxy) + } + + var err error + c.WebsocketConn, _, err = dialer.Dial(c.Websocket.GetWebsocketURL(), + http.Header{}) + if err != nil { + return fmt.Errorf("coinbasepro_websocket.go error - unable to connect to websocket %s", + err) + } + + err = c.WebsocketSubscriber() if err != nil { return err } + + go c.WsReadData() + go c.WsHandleData() + return nil } -// WebsocketClient initiates a websocket client -func (c *CoinbasePro) WebsocketClient() { - for c.Enabled && c.Websocket { - var Dialer websocket.Dialer - conn, _, err := Dialer.Dial(coinbaseproWebsocketURL, http.Header{}) +// WsReadData reads data from the websocket connection +func (c *CoinbasePro) WsReadData() { + c.Websocket.Wg.Add(1) + defer func() { + err := c.WebsocketConn.Close() if err != nil { - log.Printf("%s Unable to connect to Websocket. Error: %s\n", c.GetName(), err) - continue + c.Websocket.DataHandler <- fmt.Errorf("coinbasepro_websocket.go - Unable to to close Websocket connection. Error: %s", + err) } + c.Websocket.Wg.Done() + }() - log.Printf("%s Connected to Websocket.\n", c.GetName()) + for { + select { + case <-c.Websocket.ShutdownC: + return - currencies := []string{} - for _, x := range c.EnabledPairs { - currency := x[0:3] + "-" + x[3:] - currencies = append(currencies, currency) - } - - for _, x := range currencies { - err = c.WebsocketSubscribe(x, conn) + default: + _, resp, err := c.WebsocketConn.ReadMessage() if err != nil { - log.Printf("%s Websocket subscription error: %s\n", c.GetName(), err) - continue - } - } - - if c.Verbose { - log.Printf("%s Subscribed to product messages.", c.GetName()) - } - - for c.Enabled && c.Websocket { - msgType, resp, err := conn.ReadMessage() - if err != nil { - log.Println(err) - break + c.Websocket.DataHandler <- err + return } - switch msgType { - case websocket.TextMessage: - type MsgType struct { - Type string `json:"type"` - } - - msgType := MsgType{} - err := common.JSONDecode(resp, &msgType) - if err != nil { - log.Println(err) - continue - } - - switch msgType.Type { - case "error": - log.Println(string(resp)) - break - case "received": - received := WebsocketReceived{} - err := common.JSONDecode(resp, &received) - if err != nil { - log.Println(err) - continue - } - case "open": - open := WebsocketOpen{} - err := common.JSONDecode(resp, &open) - if err != nil { - log.Println(err) - continue - } - case "done": - done := WebsocketDone{} - err := common.JSONDecode(resp, &done) - if err != nil { - log.Println(err) - continue - } - case "match": - match := WebsocketMatch{} - err := common.JSONDecode(resp, &match) - if err != nil { - log.Println(err) - continue - } - case "change": - change := WebsocketChange{} - err := common.JSONDecode(resp, &change) - if err != nil { - log.Println(err) - continue - } - } - } + c.Websocket.TrafficAlert <- struct{}{} + c.Websocket.Intercomm <- exchange.WebsocketResponse{Raw: resp} } - conn.Close() - log.Printf("%s Websocket client disconnected.", c.GetName()) } } + +// WsHandleData handles read data from websocket connection +func (c *CoinbasePro) WsHandleData() { + c.Websocket.Wg.Add(1) + defer c.Websocket.Wg.Done() + + for { + select { + case <-c.Websocket.ShutdownC: + return + + case resp := <-c.Websocket.Intercomm: + type MsgType struct { + Type string `json:"type"` + Sequence int64 `json:"sequence"` + ProductID string `json:"product_id"` + } + + msgType := MsgType{} + err := common.JSONDecode(resp.Raw, &msgType) + if err != nil { + log.Fatal(err) + } + + if msgType.Type == "subscriptions" || msgType.Type == "heartbeat" { + continue + } + + switch msgType.Type { + case "error": + c.Websocket.DataHandler <- errors.New(string(resp.Raw)) + + case "ticker": + ticker := WebsocketTicker{} + err := common.JSONDecode(resp.Raw, &ticker) + if err != nil { + log.Fatal(err) + } + + c.Websocket.DataHandler <- exchange.TickerData{ + Timestamp: time.Now(), + Pair: pair.NewCurrencyPairFromString(ticker.ProductID), + AssetType: "SPOT", + Exchange: c.GetName(), + OpenPrice: ticker.Price, + HighPrice: ticker.High24H, + LowPrice: ticker.Low24H, + Quantity: ticker.Volume24H, + } + + case "snapshot": + snapshot := WebsocketOrderbookSnapshot{} + err := common.JSONDecode(resp.Raw, &snapshot) + if err != nil { + log.Fatal(err) + } + + err = c.ProcessSnapshot(snapshot) + if err != nil { + log.Fatal(err) + } + + case "l2update": + update := WebsocketL2Update{} + err := common.JSONDecode(resp.Raw, &update) + if err != nil { + log.Fatal(err) + } + + err = c.ProcessUpdate(update) + if err != nil { + log.Fatal(err) + } + + default: + log.Fatal("Edge test", string(resp.Raw)) + } + } + } +} + +// ProcessSnapshot processes the intial orderbook snap shot +func (c *CoinbasePro) ProcessSnapshot(snapshot WebsocketOrderbookSnapshot) error { + var base orderbook.Base + for _, bid := range snapshot.Bids { + price, err := strconv.ParseFloat(bid[0].(string), 64) + if err != nil { + return err + } + + amount, err := strconv.ParseFloat(bid[1].(string), 64) + if err != nil { + return err + } + + base.Bids = append(base.Bids, + orderbook.Item{Price: price, Amount: amount}) + } + + for _, ask := range snapshot.Asks { + price, err := strconv.ParseFloat(ask[0].(string), 64) + if err != nil { + return err + } + + amount, err := strconv.ParseFloat(ask[1].(string), 64) + if err != nil { + return err + } + + base.Asks = append(base.Asks, + orderbook.Item{Price: price, Amount: amount}) + } + + p := pair.NewCurrencyPairFromString(snapshot.ProductID) + + base.AssetType = "SPOT" + base.Pair = p + base.CurrencyPair = snapshot.ProductID + base.LastUpdated = time.Now() + + err := c.Websocket.Orderbook.LoadSnapshot(base, c.GetName()) + if err != nil { + return err + } + + c.Websocket.DataHandler <- exchange.WebsocketOrderbookUpdate{ + Pair: p, + Asset: "SPOT", + Exchange: c.GetName(), + } + + return nil +} + +// ProcessUpdate updates the orderbook local cache +func (c *CoinbasePro) ProcessUpdate(update WebsocketL2Update) error { + var Asks, Bids []orderbook.Item + + for _, data := range update.Changes { + price, _ := strconv.ParseFloat(data[1].(string), 64) + volume, _ := strconv.ParseFloat(data[2].(string), 64) + + if data[0].(string) == "buy" { + Bids = append(Bids, orderbook.Item{Price: price, Amount: volume}) + } else { + Asks = append(Asks, orderbook.Item{Price: price, Amount: volume}) + } + } + + if len(Asks) == 0 && len(Bids) == 0 { + return errors.New("coibasepro_websocket.go error - no data in websocket update") + } + + p := pair.NewCurrencyPairFromString(update.ProductID) + + err := c.Websocket.Orderbook.Update(Bids, Asks, p, time.Now(), c.GetName(), "SPOT") + if err != nil { + return err + } + + c.Websocket.DataHandler <- exchange.WebsocketOrderbookUpdate{ + Pair: p, + Asset: "SPOT", + Exchange: c.GetName(), + } + + return nil +} diff --git a/exchanges/coinbasepro/coinbasepro_wrapper.go b/exchanges/coinbasepro/coinbasepro_wrapper.go index fbe32f31..e5a2790d 100644 --- a/exchanges/coinbasepro/coinbasepro_wrapper.go +++ b/exchanges/coinbasepro/coinbasepro_wrapper.go @@ -24,15 +24,11 @@ func (c *CoinbasePro) Start(wg *sync.WaitGroup) { // Run implements the coinbasepro wrapper func (c *CoinbasePro) Run() { if c.Verbose { - log.Printf("%s Websocket: %s. (url: %s).\n", c.GetName(), common.IsEnabled(c.Websocket), coinbaseproWebsocketURL) + log.Printf("%s Websocket: %s. (url: %s).\n", c.GetName(), common.IsEnabled(c.Websocket.IsEnabled()), coinbaseproWebsocketURL) log.Printf("%s polling delay: %ds.\n", c.GetName(), c.RESTPollingDelay) log.Printf("%s %d currencies enabled: %s.\n", c.GetName(), len(c.EnabledPairs), c.EnabledPairs) } - if c.Websocket { - go c.WebsocketClient() - } - exchangeProducts, err := c.GetProducts() if err != nil { log.Printf("%s Failed to get available products.\n", c.GetName()) @@ -190,3 +186,8 @@ func (c *CoinbasePro) WithdrawCryptoExchangeFunds(address string, cryptocurrency func (c *CoinbasePro) WithdrawFiatExchangeFunds(cryptocurrency pair.CurrencyItem, amount float64) (string, error) { return "", errors.New("not yet implemented") } + +// GetWebsocket returns a pointer to the exchange websocket +func (c *CoinbasePro) GetWebsocket() (*exchange.Websocket, error) { + return c.Websocket, nil +} diff --git a/exchanges/coinut/coinut.go b/exchanges/coinut/coinut.go index e9960c37..2c0525e5 100644 --- a/exchanges/coinut/coinut.go +++ b/exchanges/coinut/coinut.go @@ -53,7 +53,6 @@ func (c *COINUT) SetDefaults() { c.TakerFee = 0.1 //spot c.MakerFee = 0 c.Verbose = false - c.Websocket = false c.RESTPollingDelay = 10 c.RequestCurrencyPairFormat.Delimiter = "" c.RequestCurrencyPairFormat.Uppercase = true @@ -68,6 +67,7 @@ func (c *COINUT) SetDefaults() { common.NewHTTPClientWithTimeout(exchange.DefaultHTTPTimeout)) c.APIUrlDefault = coinutAPIURL c.APIUrl = c.APIUrlDefault + c.WebsocketInit() } // Setup sets the current exchange configuration @@ -82,7 +82,7 @@ func (c *COINUT) Setup(exch config.ExchangeConfig) { c.SetHTTPClientUserAgent(exch.HTTPUserAgent) c.RESTPollingDelay = exch.RESTPollingDelay c.Verbose = exch.Verbose - c.Websocket = exch.Websocket + c.Websocket.SetEnabled(exch.Websocket) c.BaseCurrencies = common.SplitStrings(exch.BaseCurrencies, ",") c.AvailablePairs = common.SplitStrings(exch.AvailablePairs, ",") c.EnabledPairs = common.SplitStrings(exch.EnabledPairs, ",") @@ -102,6 +102,18 @@ func (c *COINUT) Setup(exch config.ExchangeConfig) { if err != nil { log.Fatal(err) } + err = c.SetClientProxyAddress(exch.ProxyAddress) + if err != nil { + log.Fatal(err) + } + err = c.WebsocketSetup(c.WsConnect, + exch.Name, + exch.Websocket, + coinutWebsocketURL, + exch.WebsocketURL) + if err != nil { + log.Fatal(err) + } } } diff --git a/exchanges/coinut/coinut_test.go b/exchanges/coinut/coinut_test.go index d973ba11..0a70b888 100644 --- a/exchanges/coinut/coinut_test.go +++ b/exchanges/coinut/coinut_test.go @@ -28,8 +28,9 @@ func TestSetup(t *testing.T) { } c.Setup(bConfig) - if !c.IsEnabled() || c.AuthenticatedAPISupport || c.RESTPollingDelay != time.Duration(10) || - c.Verbose || c.Websocket || len(c.BaseCurrencies) < 1 || + if !c.IsEnabled() || c.AuthenticatedAPISupport || + c.RESTPollingDelay != time.Duration(10) || c.Verbose || + c.Websocket.IsEnabled() || len(c.BaseCurrencies) < 1 || len(c.AvailablePairs) < 1 || len(c.EnabledPairs) < 1 { t.Error("Test Failed - Coinut Setup values not set correctly") } diff --git a/exchanges/coinut/coinut_types.go b/exchanges/coinut/coinut_types.go index 5a0f20c2..410f03c1 100644 --- a/exchanges/coinut/coinut_types.go +++ b/exchanges/coinut/coinut_types.go @@ -236,3 +236,113 @@ type OpenPosition struct { OpenTimestamp int64 `json:"open_timestamp"` InstrumentID int `json:"inst_id"` } + +type wsRequest struct { + Request string `json:"request"` + SecType string `json:"sec_type,omitempty"` + InstID int64 `json:"inst_id,omitempty"` + TopN int64 `json:"top_n,omitempty"` + Subscribe bool `json:"subscribe"` + Nonce int64 `json:"nonce"` +} + +type wsResponse struct { + Reply string `json:"reply"` +} + +type wsHeartbeatResp struct { + Nonce int64 `json:"nonce"` + Reply string `json:"reply"` + Status []interface{} `json:"status"` +} + +// WsTicker defines the resp for ticker updates from the websocket connection +type WsTicker struct { + HighestBuy float64 `json:"highest_buy,string"` + InstID int64 `json:"inst_id"` + Last float64 `json:"last,string"` + LowestSell float64 `json:"lowest_sell,string"` + OpenInterest float64 `json:"open_interest,string"` + Reply string `json:"reply"` + Timestamp int64 `json:"timestamp"` + TransID int64 `json:"trans_id"` + Volume float64 `json:"volume,string"` + Volume24H float64 `json:"volume24,string"` +} + +// WsOrderbookSnapshot defines the resp for orderbook snapshot updates from +// the websocket connection +type WsOrderbookSnapshot struct { + Buy []WsOrderbookData `json:"buy"` + Sell []WsOrderbookData `json:"sell"` + InstID int64 `json:"inst_id"` + Nonce int64 `json:"nonce"` + TotalBuy float64 `json:"total_buy,string"` + TotalSell float64 `json:"total_sell,string"` + Reply string `json:"reply"` + Status []interface{} `json:"status"` +} + +// WsOrderbookData defines singular orderbook data +type WsOrderbookData struct { + Count int64 `json:"count"` + Price float64 `json:"price,string"` + Volume float64 `json:"qty,string"` +} + +// WsOrderbookUpdate defines orderbook update response from the websocket +// connection +type WsOrderbookUpdate struct { + Count int64 `json:"count"` + InstID int64 `json:"inst_id"` + Price float64 `json:"price,string"` + Volume float64 `json:"qty,string"` + TotalBuy float64 `json:"total_buy,string"` + Reply string `json:"reply"` + Side string `json:"side"` + TransID int64 `json:"trans_id"` +} + +// WsTradeSnapshot defines Market trade response from the websocket +// connection +type WsTradeSnapshot struct { + Nonce int64 `json:"nonce"` + Reply string `json:"reply"` + Status []interface{} `json:"status"` + Trades []WsTradeData `json:"trades"` +} + +// WsTradeData defines market trade data +type WsTradeData struct { + Price float64 `json:"price,string"` + Volume float64 `json:"qty,string"` + Side string `json:"side"` + Timestamp int64 `json:"timestamp"` + TransID int64 `json:"trans_id"` +} + +// WsTradeUpdate defines trade update response from the websocket connection +type WsTradeUpdate struct { + InstID int64 `json:"inst_id"` + Price float64 `json:"price,string"` + Reply string `json:"reply"` + Side string `json:"side"` + Timestamp int64 `json:"timestamp"` + TransID int64 `json:"trans_id"` +} + +// WsInstrumentList defines instrument list +type WsInstrumentList struct { + Spot map[string][]WsSupportedCurrency `json:"SPOT"` + Nonce int64 `json:"nonce"` + Reply string `json:"inst_list"` + Status []interface{} `json:"status"` +} + +// WsSupportedCurrency defines supported currency on the exchange +type WsSupportedCurrency struct { + Base string `json:"base"` + InstID int64 `json:"inst_id"` + DecimalPlaces int64 `json:"decimal_places"` + Quote string `json:"quote"` +} diff --git a/exchanges/coinut/coinut_websocket.go b/exchanges/coinut/coinut_websocket.go index b1d350eb..ba829d8e 100644 --- a/exchanges/coinut/coinut_websocket.go +++ b/exchanges/coinut/coinut_websocket.go @@ -1,61 +1,365 @@ package coinut import ( + "errors" + "fmt" "log" "net/http" + "net/url" + "time" "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 coinutWebsocketURL = "wss://wsapi.coinut.com" -// WebsocketClient initiates a websocket client -func (c *COINUT) WebsocketClient() { - for c.Enabled && c.Websocket { - var Dialer websocket.Dialer - var err error - c.WebsocketConn, _, err = Dialer.Dial(c.WebsocketURL, http.Header{}) +var nNonce map[int64]string +var channels map[string]chan []byte +var instrumentListByString map[string]int64 +var instrumentListByCode map[int64]string +var populatedList bool +// NOTE for speed considerations +// wss://wsapi-as.coinut.com +// wss://wsapi-na.coinut.com +// wss://wsapi-eu.coinut.com + +// WsReadData reads data from the websocket conection +func (c *COINUT) WsReadData() { + c.Websocket.Wg.Add(1) + + defer func() { + err := c.WebsocketConn.Close() if err != nil { - log.Printf("%s Unable to connect to Websocket. Error: %s\n", c.Name, err) - continue + c.Websocket.DataHandler <- fmt.Errorf("coinut_websocket.go - Unable to to close Websocket connection. Error: %s", + err) } + c.Websocket.Wg.Done() + }() - if c.Verbose { - log.Printf("%s Connected to Websocket.\n", c.Name) - } - - err = c.WebsocketConn.WriteMessage(websocket.TextMessage, []byte(`{"messageType": "hello_world"}`)) - - if err != nil { - log.Println(err) + for { + select { + case <-c.Websocket.ShutdownC: return - } - for c.Enabled && c.Websocket { - msgType, resp, err := c.WebsocketConn.ReadMessage() + default: + _, resp, err := c.WebsocketConn.ReadMessage() if err != nil { - log.Println(err) - break + c.Websocket.DataHandler <- err + return } - switch msgType { - case websocket.TextMessage: - type MsgType struct { - MessageType string `json:"messageType"` - } - - msgType := MsgType{} - err := common.JSONDecode(resp, &msgType) - if err != nil { - log.Println(err) - continue - } - log.Println(string(resp)) - } + c.Websocket.TrafficAlert <- struct{}{} + c.Websocket.Intercomm <- exchange.WebsocketResponse{Raw: resp} } - c.WebsocketConn.Close() - log.Printf("%s Websocket client disconnected.", c.Name) } } + +// WsHandleData handles read data +func (c *COINUT) WsHandleData() { + c.Websocket.Wg.Add(1) + defer c.Websocket.Wg.Done() + + for { + select { + case <-c.Websocket.ShutdownC: + return + + case resp := <-c.Websocket.Intercomm: + var incoming wsResponse + err := common.JSONDecode(resp.Raw, &incoming) + if err != nil { + log.Fatal(err) + } + + switch incoming.Reply { + case "hb": + channels["hb"] <- resp.Raw + + case "inst_tick": + var ticker WsTicker + err := common.JSONDecode(resp.Raw, &ticker) + if err != nil { + log.Fatal(err) + } + + c.Websocket.DataHandler <- exchange.TickerData{ + Timestamp: time.Unix(0, ticker.Timestamp), + Exchange: c.GetName(), + AssetType: "SPOT", + HighPrice: ticker.HighestBuy, + LowPrice: ticker.LowestSell, + ClosePrice: ticker.Last, + Quantity: ticker.Volume, + } + + case "inst_order_book": + var orderbooksnapshot WsOrderbookSnapshot + err := common.JSONDecode(resp.Raw, &orderbooksnapshot) + if err != nil { + log.Fatal(err) + } + + err = c.WsProcessOrderbookSnapshot(orderbooksnapshot) + if err != nil { + log.Fatal(err) + } + + currencyPair := instrumentListByCode[orderbooksnapshot.InstID] + + c.Websocket.DataHandler <- exchange.WebsocketOrderbookUpdate{ + Exchange: c.GetName(), + Asset: "SPOT", + Pair: pair.NewCurrencyPairFromString(currencyPair), + } + + case "inst_order_book_update": + var orderbookUpdate WsOrderbookUpdate + err := common.JSONDecode(resp.Raw, &orderbookUpdate) + if err != nil { + log.Fatal(err) + } + + err = c.WsProcessOrderbookUpdate(orderbookUpdate) + if err != nil { + log.Fatal(err) + } + + currencyPair := instrumentListByCode[orderbookUpdate.InstID] + + c.Websocket.DataHandler <- exchange.WebsocketOrderbookUpdate{ + Exchange: c.GetName(), + Asset: "SPOT", + Pair: pair.NewCurrencyPairFromString(currencyPair), + } + + case "inst_trade": + var tradeSnap WsTradeSnapshot + err := common.JSONDecode(resp.Raw, &tradeSnap) + if err != nil { + log.Fatal(err) + } + + case "inst_trade_update": + var tradeUpdate WsTradeUpdate + err := common.JSONDecode(resp.Raw, &tradeUpdate) + if err != nil { + log.Fatal(err) + } + + currencyPair := instrumentListByCode[tradeUpdate.InstID] + + c.Websocket.DataHandler <- exchange.TradeData{ + Timestamp: time.Unix(tradeUpdate.Timestamp, 0), + CurrencyPair: pair.NewCurrencyPairFromString(currencyPair), + AssetType: "SPOT", + Exchange: c.GetName(), + Price: tradeUpdate.Price, + Side: tradeUpdate.Side, + } + } + } + } +} + +// WsConnect intiates a websocket connection +func (c *COINUT) WsConnect() error { + if !c.Websocket.IsEnabled() || !c.IsEnabled() { + return errors.New(exchange.WebsocketNotEnabled) + } + + var Dialer websocket.Dialer + + if c.Websocket.GetProxyAddress() != "" { + proxy, err := url.Parse(c.Websocket.GetProxyAddress()) + if err != nil { + return err + } + + Dialer.Proxy = http.ProxyURL(proxy) + } + + var err error + c.WebsocketConn, _, err = Dialer.Dial(c.Websocket.GetWebsocketURL(), + http.Header{}) + + if err != nil { + return err + } + + if !populatedList { + instrumentListByString = make(map[string]int64) + instrumentListByCode = make(map[int64]string) + + err = c.WsSetInstrumentList() + if err != nil { + return err + } + populatedList = true + } + + err = c.WsSubscribe() + if err != nil { + return err + } + + // define bi-directional communication + channels = make(map[string]chan []byte) + channels["hb"] = make(chan []byte, 1) + + go c.WsReadData() + go c.WsHandleData() + + return nil +} + +// GetNonce returns a nonce for a required request +func (c *COINUT) GetNonce() int64 { + if c.Nonce.Get() == 0 { + c.Nonce.Set(time.Now().Unix()) + } else { + c.Nonce.Inc() + } + + return c.Nonce.Get() +} + +// WsSetInstrumentList fetches instrument list and propagates a local cache +func (c *COINUT) WsSetInstrumentList() error { + request, err := common.JSONEncode(wsRequest{ + Request: "inst_list", + SecType: "SPOT", + Nonce: c.GetNonce(), + }) + + if err != nil { + return err + } + + err = c.WebsocketConn.WriteMessage(websocket.TextMessage, request) + if err != nil { + return err + } + + _, resp, err := c.WebsocketConn.ReadMessage() + if err != nil { + return err + } + + c.Websocket.TrafficAlert <- struct{}{} + + var list WsInstrumentList + err = common.JSONDecode(resp, &list) + if err != nil { + return err + } + + for currency, data := range list.Spot { + instrumentListByString[currency] = data[0].InstID + instrumentListByCode[data[0].InstID] = currency + } + + if len(instrumentListByString) == 0 || len(instrumentListByCode) == 0 { + return errors.New("instrument lists failed to populate") + } + + 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.Pair().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 + } + + orderbook := wsRequest{ + Request: "inst_order_book", + InstID: instrumentListByString[p.Pair().String()], + Subscribe: true, + Nonce: c.GetNonce(), + } + + objson, err := common.JSONEncode(orderbook) + 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 + for _, bid := range ob.Buy { + bids = append(bids, orderbook.Item{ + Amount: bid.Volume, + Price: bid.Price, + }) + } + + var asks []orderbook.Item + for _, ask := range ob.Sell { + asks = append(asks, orderbook.Item{ + Amount: ask.Volume, + Price: ask.Price, + }) + } + + var newOrderbook orderbook.Base + newOrderbook.Asks = asks + newOrderbook.Bids = bids + newOrderbook.CurrencyPair = instrumentListByCode[ob.InstID] + newOrderbook.Pair = pair.NewCurrencyPairFromString(instrumentListByCode[ob.InstID]) + newOrderbook.AssetType = "SPOT" + newOrderbook.LastUpdated = time.Now() + + return c.Websocket.Orderbook.LoadSnapshot(newOrderbook, c.GetName()) +} + +// WsProcessOrderbookUpdate process an orderbook update +func (c *COINUT) WsProcessOrderbookUpdate(ob WsOrderbookUpdate) error { + p := pair.NewCurrencyPairFromString(instrumentListByCode[ob.InstID]) + + if ob.Side == "buy" { + return c.Websocket.Orderbook.Update([]orderbook.Item{ + orderbook.Item{Price: ob.Price, Amount: ob.Volume}}, + nil, + p, + time.Now(), + c.GetName(), + "SPOT") + } + + return c.Websocket.Orderbook.Update([]orderbook.Item{ + orderbook.Item{Price: ob.Price, Amount: ob.Volume}}, + nil, + p, + time.Now(), + c.GetName(), + "SPOT") +} diff --git a/exchanges/coinut/coinut_wrapper.go b/exchanges/coinut/coinut_wrapper.go index 28ed2c56..238f46f4 100644 --- a/exchanges/coinut/coinut_wrapper.go +++ b/exchanges/coinut/coinut_wrapper.go @@ -24,15 +24,11 @@ func (c *COINUT) Start(wg *sync.WaitGroup) { // Run implements the COINUT wrapper func (c *COINUT) Run() { if c.Verbose { - log.Printf("%s Websocket: %s. (url: %s).\n", c.GetName(), common.IsEnabled(c.Websocket), coinutWebsocketURL) + log.Printf("%s Websocket: %s. (url: %s).\n", c.GetName(), common.IsEnabled(c.Websocket.IsEnabled()), coinutWebsocketURL) log.Printf("%s polling delay: %ds.\n", c.GetName(), c.RESTPollingDelay) log.Printf("%s %d currencies enabled: %s.\n", c.GetName(), len(c.EnabledPairs), c.EnabledPairs) } - if c.Websocket { - go c.WebsocketClient() - } - exchangeProducts, err := c.GetInstruments() if err != nil { log.Printf("%s Failed to get available products.\n", c.GetName()) @@ -193,3 +189,8 @@ func (c *COINUT) WithdrawFiatExchangeFunds(currency pair.CurrencyItem, amount fl func (c *COINUT) WithdrawFiatExchangeFundsToInternationalBank(currency pair.CurrencyItem, amount float64) (string, error) { return "", errors.New("not yet implemented") } + +// GetWebsocket returns a pointer to the exchange websocket +func (c *COINUT) GetWebsocket() (*exchange.Websocket, error) { + return c.Websocket, nil +} diff --git a/exchanges/exchange.go b/exchanges/exchange.go index 443bf7f7..f19d46f9 100644 --- a/exchanges/exchange.go +++ b/exchanges/exchange.go @@ -5,6 +5,7 @@ import ( "fmt" "log" "net/http" + "net/url" "sync" "time" @@ -90,7 +91,6 @@ type Base struct { Name string Enabled bool Verbose bool - Websocket bool RESTPollingDelay time.Duration AuthenticatedAPISupport bool APIAuthPEMKeySupport bool @@ -113,6 +113,7 @@ type Base struct { APIUrlSecondaryDefault string RequestCurrencyPairFormat config.CurrencyPairFormatConfig ConfigCurrencyPairFormat config.CurrencyPairFormatConfig + Websocket *Websocket *request.Requester } @@ -149,6 +150,8 @@ type IBotExchange interface { WithdrawCryptoExchangeFunds(address string, cryptocurrency pair.CurrencyItem, amount float64) (string, error) WithdrawFiatExchangeFunds(currency pair.CurrencyItem, amount float64) (string, error) + + GetWebsocket() (*Websocket, error) } // SupportsRESTTickerBatchUpdates returns whether or not the @@ -161,7 +164,10 @@ func (e *Base) SupportsRESTTickerBatchUpdates() bool { // HTTP Client func (e *Base) SetHTTPClientTimeout(t time.Duration) { if e.Requester == nil { - e.Requester = request.New(e.Name, request.NewRateLimit(time.Second, 0), request.NewRateLimit(time.Second, 0), new(http.Client)) + e.Requester = request.New(e.Name, + request.NewRateLimit(time.Second, 0), + request.NewRateLimit(time.Second, 0), + new(http.Client)) } e.Requester.HTTPClient.Timeout = t } @@ -169,7 +175,10 @@ func (e *Base) SetHTTPClientTimeout(t time.Duration) { // SetHTTPClient sets exchanges HTTP client func (e *Base) SetHTTPClient(h *http.Client) { if e.Requester == nil { - e.Requester = request.New(e.Name, request.NewRateLimit(time.Second, 0), request.NewRateLimit(time.Second, 0), new(http.Client)) + e.Requester = request.New(e.Name, + request.NewRateLimit(time.Second, 0), + request.NewRateLimit(time.Second, 0), + new(http.Client)) } e.Requester.HTTPClient = h } @@ -177,7 +186,10 @@ func (e *Base) SetHTTPClient(h *http.Client) { // GetHTTPClient gets the exchanges HTTP client func (e *Base) GetHTTPClient() *http.Client { if e.Requester == nil { - e.Requester = request.New(e.Name, request.NewRateLimit(time.Second, 0), request.NewRateLimit(time.Second, 0), new(http.Client)) + e.Requester = request.New(e.Name, + request.NewRateLimit(time.Second, 0), + request.NewRateLimit(time.Second, 0), + new(http.Client)) } return e.Requester.HTTPClient } @@ -185,7 +197,10 @@ func (e *Base) GetHTTPClient() *http.Client { // SetHTTPClientUserAgent sets the exchanges HTTP user agent func (e *Base) SetHTTPClientUserAgent(ua string) { if e.Requester == nil { - e.Requester = request.New(e.Name, request.NewRateLimit(time.Second, 0), request.NewRateLimit(time.Second, 0), new(http.Client)) + e.Requester = request.New(e.Name, + request.NewRateLimit(time.Second, 0), + request.NewRateLimit(time.Second, 0), + new(http.Client)) } e.Requester.UserAgent = ua e.HTTPUserAgent = ua @@ -196,6 +211,31 @@ func (e *Base) GetHTTPClientUserAgent() string { return e.HTTPUserAgent } +// SetClientProxyAddress sets a proxy address for REST and websocket requests +func (e *Base) SetClientProxyAddress(addr string) error { + if addr != "" { + proxy, err := url.Parse(addr) + if err != nil { + return fmt.Errorf("exchange.go - setting proxy address error %s", + err) + } + + err = e.Requester.SetProxy(proxy) + if err != nil { + return fmt.Errorf("exchange.go - setting proxy address error %s", + err) + } + + if e.Websocket != nil { + err = e.Websocket.SetProxyAddress(addr) + if err != nil { + return err + } + } + } + return nil +} + // SetAutoPairDefaults sets the default values for whether or not the exchange // supports auto pair updating or not func (e *Base) SetAutoPairDefaults() error { @@ -645,10 +685,10 @@ func (e *Base) SetAPIURL(ec config.ExchangeConfig) error { if ec.APIURL == "" || ec.APIURLSecondary == "" { return errors.New("SetAPIURL error variable zero value") } - if ec.APIURL != config.APIURLDefaultMessage { + if ec.APIURL != config.APIURLNonDefaultMessage { e.APIUrl = ec.APIURL } - if ec.APIURLSecondary != config.APIURLDefaultMessage { + if ec.APIURLSecondary != config.APIURLNonDefaultMessage { e.APIUrlSecondary = ec.APIURLSecondary } return nil diff --git a/exchanges/exchange_test.go b/exchanges/exchange_test.go index b48e9466..9d08f4c6 100644 --- a/exchanges/exchange_test.go +++ b/exchanges/exchange_test.go @@ -46,7 +46,10 @@ func TestHTTPClient(t *testing.T) { } b := Base{Name: "RAWR"} - b.Requester = request.New(b.Name, request.NewRateLimit(time.Second, 1), request.NewRateLimit(time.Second, 1), new(http.Client)) + b.Requester = request.New(b.Name, + request.NewRateLimit(time.Second, 1), + request.NewRateLimit(time.Second, 1), + new(http.Client)) b.SetHTTPClientTimeout(time.Second * 5) if b.GetHTTPClient().Timeout != time.Second*5 { @@ -61,6 +64,36 @@ func TestHTTPClient(t *testing.T) { t.Fatalf("Test failed. TestHTTPClient unexpected value") } } + +func TestSetClientProxyAddress(t *testing.T) { + requester := request.New("testicles", + &request.RateLimit{}, + &request.RateLimit{}, + &http.Client{}) + + newBase := Base{Name: "Testicles", Requester: requester} + + newBase.WebsocketInit() + + err := newBase.SetClientProxyAddress(":invalid") + if err == nil { + t.Error("Test failed. SetClientProxyAddress parsed invalid URL") + } + + if newBase.Websocket.GetProxyAddress() != "" { + t.Error("Test failed. SetClientProxyAddress error", err) + } + + err = newBase.SetClientProxyAddress("www.valid.com") + if err != nil { + t.Error("Test failed. SetClientProxyAddress error", err) + } + + if newBase.Websocket.GetProxyAddress() != "www.valid.com" { + t.Error("Test failed. SetClientProxyAddress error", err) + } +} + func TestSetAutoPairDefaults(t *testing.T) { cfg := config.GetConfig() err := cfg.LoadConfig(config.ConfigTestFile) diff --git a/exchanges/exchange_websocket.go b/exchanges/exchange_websocket.go new file mode 100644 index 00000000..70b441d0 --- /dev/null +++ b/exchanges/exchange_websocket.go @@ -0,0 +1,618 @@ +package exchange + +import ( + "errors" + "fmt" + "sync" + "time" + + "github.com/thrasher-/gocryptotrader/config" + "github.com/thrasher-/gocryptotrader/currency/pair" + "github.com/thrasher-/gocryptotrader/exchanges/orderbook" +) + +const ( + // 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 +) + +// WebsocketInit initialises the websocket struct +func (e *Base) WebsocketInit() { + e.Websocket = &Websocket{ + defaultURL: "", + enabled: false, + proxyAddr: "", + runningURL: "", + init: true, + } +} + +// WebsocketSetup sets main variables for websocket connection +func (e *Base) WebsocketSetup(connector func() error, + exchangeName string, + wsEnabled bool, + defaultURL, + runningURL string) error { + + e.Websocket.DataHandler = make(chan interface{}, 1) + e.Websocket.Connected = make(chan struct{}, 1) + e.Websocket.Disconnected = make(chan struct{}, 1) + e.Websocket.Intercomm = make(chan WebsocketResponse, 1) + e.Websocket.TrafficAlert = make(chan struct{}, 1) + + err := e.Websocket.SetEnabled(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 + + 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{} + + // Intercomm denotes a channel from read data routine to handle data routine + Intercomm chan WebsocketResponse + + // 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{} +} + +// 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 { + w.m.Lock() + defer w.m.Unlock() + + if !w.IsEnabled() { + return fmt.Errorf("exchange_websocket.go %s error - websocket disabled", + w.GetName()) + } + + if w.connected { + return errors.New("exchange_websocket.go error - already connected, cannot connect again") + } + + w.ShutdownC = make(chan struct{}, 1) + + var anotherWG sync.WaitGroup + anotherWG.Add(1) + go w.trafficMonitor(&anotherWG) + anotherWG.Wait() + + err := w.connector() + if err != nil { + return fmt.Errorf("exchange_websocket.go connection error %s", + err) + } + + // Divert for incoming websocket traffic + w.Connected <- struct{}{} + w.connected = true + + return nil +} + +// 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") + } + + timer := time.NewTimer(5 * time.Second) + c := make(chan struct{}, 1) + + go func(c chan struct{}) { + close(w.ShutdownC) + w.Wg.Wait() + c <- struct{}{} + }(c) + + select { + case <-c: + w.connected = false + return nil + case <-timer.C: + return fmt.Errorf("%s - Websocket routines failed to shutdown", + w.GetName()) + } +} + +// SetWebsocketURL sets websocket URL +func (w *Websocket) SetWebsocketURL(URL string) { + if URL == "" || URL == config.WebsocketURLNonDefaultMessage { + w.runningURL = w.defaultURL + return + } + w.runningURL = URL +} + +// GetWebsocketURL returns the running websocket URL +func (w *Websocket) GetWebsocketURL() string { + return w.runningURL +} + +// SetEnabled sets if websocket is enabled +func (w *Websocket) SetEnabled(enabled bool) error { + if w.enabled == enabled { + if w.init { + return nil + } + return fmt.Errorf("exchange_websocket.go error - already set as %t", + enabled) + } + + w.enabled = enabled + + if !w.init { + if enabled { + if w.connected { + return nil + } + return w.Connect() + } + + if !w.connected { + return nil + } + return w.Shutdown() + } + return nil +} + +// IsEnabled returns bool +func (w *Websocket) IsEnabled() bool { + return w.enabled +} + +// SetProxyAddress sets websocket proxy address +func (w *Websocket) SetProxyAddress(URL string) error { + if w.proxyAddr == URL { + return errors.New("exchange_websocket.go error - Setting proxy address - same address") + } + + w.proxyAddr = URL + + if !w.init && w.enabled { + if w.connected { + err := w.Shutdown() + if err != nil { + return err + } + return w.Connect() + } + return w.Connect() + } + return nil +} + +// GetProxyAddress returns the current websocket proxy +func (w *Websocket) GetProxyAddress() string { + return w.proxyAddr +} + +// SetDefaultURL sets default websocket URL +func (w *Websocket) SetDefaultURL(defaultURL string) { + w.defaultURL = defaultURL +} + +// GetDefaultURL returns the default websocket URL +func (w *Websocket) GetDefaultURL() string { + return w.defaultURL +} + +// SetConnector sets connection function +func (w *Websocket) SetConnector(connector func() error) { + w.connector = connector +} + +// SetExchangeName sets exchange name +func (w *Websocket) SetExchangeName(exchName string) { + w.exchangeName = exchName +} + +// GetName returns exchange name +func (w *Websocket) GetName() string { + return w.exchangeName +} + +// WebsocketOrderbookLocal defines a local cache of orderbooks for ammending, +// 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 +// Price target not found; append of price target +// Price target found; ammend volume of price target +func (w *WebsocketOrderbookLocal) Update(bidTargets, askTargets []orderbook.Item, + p pair.CurrencyPair, + updated time.Time, + exchName, assetType string) error { + if bidTargets == nil && askTargets == nil { + return errors.New("exchange.go websocket orderbook cache Update() error - cannot have bids and ask targets both nil") + } + + if w.lastUpdated.After(updated) { + return errors.New("exchange.go WebsocketOrderbookLocal Update() - update is before last update time") + } + + w.m.Lock() + defer w.m.Unlock() + + var orderbookAddress *orderbook.Base + for i := range w.ob { + if w.ob[i].Pair == p && w.ob[i].AssetType == assetType { + orderbookAddress = &w.ob[i] + } + } + + if orderbookAddress == nil { + return fmt.Errorf("exchange.go WebsocketOrderbookLocal Update() - orderbook.Base could not be found for Exchange %s CurrencyPair: %s AssetType: %s", + exchName, + p.Pair().String(), + assetType) + } + + if len(orderbookAddress.Asks) == 0 || len(orderbookAddress.Bids) == 0 { + return errors.New("exchange.go websocket orderbook cache Update() error - snapshot incorrectly loaded") + } + + if orderbookAddress.Pair == (pair.CurrencyPair{}) { + return fmt.Errorf("exchange.go websocket orderbook cache Update() error - snapshot not found %v", + p) + } + + for x := range bidTargets { + // bid targets + func() { + for y := range orderbookAddress.Bids { + if orderbookAddress.Bids[y].Price == bidTargets[x].Price { + if bidTargets[x].Amount == 0 { + // Delete + orderbookAddress.Asks = append(orderbookAddress.Bids[:y], + orderbookAddress.Bids[y+1:]...) + return + } + // Ammend + orderbookAddress.Bids[y].Amount = bidTargets[x].Amount + return + } + } + + if bidTargets[x].Amount == 0 { + // Makes sure we dont append things we missed + return + } + + // Append + orderbookAddress.Bids = append(orderbookAddress.Bids, orderbook.Item{ + Price: bidTargets[x].Price, + Amount: bidTargets[x].Amount, + }) + }() + // bid targets + } + + for x := range askTargets { + func() { + for y := range orderbookAddress.Asks { + if orderbookAddress.Asks[y].Price == askTargets[x].Price { + if askTargets[x].Amount == 0 { + // Delete + orderbookAddress.Asks = append(orderbookAddress.Asks[:y], + orderbookAddress.Asks[y+1:]...) + return + } + // Ammend + orderbookAddress.Asks[y].Amount = askTargets[x].Amount + return + } + } + + if askTargets[x].Amount == 0 { + // Makes sure we dont append things we missed + return + } + + // Append + orderbookAddress.Asks = append(orderbookAddress.Asks, orderbook.Item{ + Price: askTargets[x].Price, + Amount: askTargets[x].Amount, + }) + }() + } + + orderbook.ProcessOrderbook(exchName, p, *orderbookAddress, assetType) + return nil +} + +// LoadSnapshot loads initial snapshot of orderbook data +func (w *WebsocketOrderbookLocal) LoadSnapshot(newOrderbook orderbook.Base, exchName string) error { + if len(newOrderbook.Asks) == 0 || len(newOrderbook.Bids) == 0 { + return errors.New("exchange.go websocket orderbook cache LoadSnapshot() error - snapshot ask and bids are nil") + } + + w.m.Lock() + defer w.m.Unlock() + + for i := range w.ob { + if w.ob[i].Pair == newOrderbook.Pair && w.ob[i].AssetType == newOrderbook.AssetType { + return errors.New("exchange.go websocket orderbook cache LoadSnapshot() error - Snapshot instance already found") + } + } + + w.ob = append(w.ob, newOrderbook) + w.lastUpdated = newOrderbook.LastUpdated + + orderbook.ProcessOrderbook(exchName, + newOrderbook.Pair, + newOrderbook, + newOrderbook.AssetType) + + return nil +} + +// UpdateUsingID updates orderbooks using specified ID +func (w *WebsocketOrderbookLocal) UpdateUsingID(bidTargets, askTargets []orderbook.Item, + p pair.CurrencyPair, + updated time.Time, + exchName, assetType, action string) error { + w.m.Lock() + defer w.m.Unlock() + + var orderbookAddress *orderbook.Base + for i := range w.ob { + if w.ob[i].Pair == p && w.ob[i].AssetType == assetType { + orderbookAddress = &w.ob[i] + } + } + + if orderbookAddress == nil { + return fmt.Errorf("exchange.go WebsocketOrderbookLocal Update() - orderbook.Base could not be found for Exchange %s CurrencyPair: %s AssetType: %s", + exchName, + assetType, + p.Pair().String()) + } + + switch action { + case "update": + for _, target := range bidTargets { + for i := range orderbookAddress.Bids { + if orderbookAddress.Bids[i].ID == target.ID { + orderbookAddress.Bids[i].Amount = target.Amount + break + } + } + } + + for _, target := range askTargets { + for i := range orderbookAddress.Asks { + if orderbookAddress.Asks[i].ID == target.ID { + orderbookAddress.Asks[i].Amount = target.Amount + break + } + } + } + + case "delete": + for _, target := range bidTargets { + for i := range orderbookAddress.Bids { + if orderbookAddress.Bids[i].ID == target.ID { + orderbookAddress.Bids = append(orderbookAddress.Bids[:i], + orderbookAddress.Bids[i+1:]...) + break + } + } + } + + for _, target := range askTargets { + for i := range orderbookAddress.Asks { + if orderbookAddress.Asks[i].ID == target.ID { + orderbookAddress.Asks = append(orderbookAddress.Asks[:i], + orderbookAddress.Asks[i+1:]...) + break + } + } + } + + case "insert": + for _, target := range bidTargets { + orderbookAddress.Bids = append(orderbookAddress.Bids, target) + } + + for _, target := range askTargets { + orderbookAddress.Asks = append(orderbookAddress.Asks, target) + } + } + + orderbook.ProcessOrderbook(exchName, p, *orderbookAddress, assetType) + + return nil +} + +// FlushCache flushes w.ob data to be garbage collected and refreshed when a +// connection is lost and reconnected +func (w *WebsocketOrderbookLocal) FlushCache() { + w.m.Lock() + w.ob = nil + 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 pair.CurrencyPair + Asset string + Exchange string +} + +// TradeData defines trade data +type TradeData struct { + Timestamp time.Time + CurrencyPair pair.CurrencyPair + 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 pair.CurrencyPair + 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 pair.CurrencyPair + 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 pair.CurrencyPair + AssetType string + Exchange string +} diff --git a/exchanges/exchange_websocket_test.go b/exchanges/exchange_websocket_test.go new file mode 100644 index 00000000..c06b3022 --- /dev/null +++ b/exchanges/exchange_websocket_test.go @@ -0,0 +1,311 @@ +package exchange + +import ( + "testing" + "time" + + "github.com/thrasher-/gocryptotrader/currency/pair" + "github.com/thrasher-/gocryptotrader/exchanges/orderbook" +) + +var wsTest Base + +func TestWebsocketInit(t *testing.T) { + if wsTest.Websocket != nil { + t.Error("test failed - WebsocketInit() error") + } + + wsTest.WebsocketInit() + + if wsTest.Websocket == nil { + t.Error("test failed - WebsocketInit() error") + } +} + +func TestWebsocket(t *testing.T) { + if err := wsTest.Websocket.SetProxyAddress("testProxy"); err != nil { + t.Error("test failed - SetProxyAddress", err) + } + + wsTest.WebsocketSetup(func() error { return nil }, + "testName", + true, + "testDefaultURL", + "testRunningURL") + + // Test variable setting and retreival + if wsTest.Websocket.GetName() != "testName" { + t.Error("test failed - WebsocketSetup") + } + + if !wsTest.Websocket.IsEnabled() { + t.Error("test failed - WebsocketSetup") + } + + if wsTest.Websocket.GetProxyAddress() != "testProxy" { + t.Error("test failed - WebsocketSetup") + } + + if wsTest.Websocket.GetDefaultURL() != "testDefaultURL" { + t.Error("test failed - WebsocketSetup") + } + + if wsTest.Websocket.GetWebsocketURL() != "testRunningURL" { + t.Error("test failed - WebsocketSetup") + } + + // Test websocket connect and shutdown functions + comms := make(chan struct{}, 1) + go func() { + var count int + for { + if count == 4 { + close(comms) + return + } + select { + case <-wsTest.Websocket.Connected: + count++ + case <-wsTest.Websocket.Disconnected: + count++ + } + } + }() + + // -- Not connected shutdown + err := wsTest.Websocket.Shutdown() + if err == nil { + t.Fatal("test failed - should not be connected to able to shut down") + } + + // -- Normal connect + err = wsTest.Websocket.Connect() + if err != nil { + t.Fatal("test failed - WebsocketSetup", err) + } + + // -- Already connected connect + err = wsTest.Websocket.Connect() + if err == nil { + t.Fatal("test failed - should not connect, already connected") + } + + wsTest.Websocket.SetWebsocketURL("") + + // -- Set true when already true + err = wsTest.Websocket.SetEnabled(true) + if err == nil { + t.Fatal("test failed - setting enabled should not work") + } + + // -- Set false normal + err = wsTest.Websocket.SetEnabled(false) + if err != nil { + t.Fatal("test failed - setting enabled should not work") + } + + // -- Set true normal + err = wsTest.Websocket.SetEnabled(true) + if err != nil { + t.Fatal("test failed - setting enabled should not work") + } + + // -- Normal shutdown + err = wsTest.Websocket.Shutdown() + if err != nil { + t.Fatal("test failed - WebsocketSetup", err) + } + + timer := time.NewTimer(5 * time.Second) + select { + case <-comms: + case <-timer.C: + t.Fatal("test failed - WebsocketSetup - timeout") + } +} + +func TestInsertingSnapShots(t *testing.T) { + var snapShot1 orderbook.Base + asks := []orderbook.Item{ + orderbook.Item{Price: 6000, Amount: 1, ID: 1}, + orderbook.Item{Price: 6001, Amount: 0.5, ID: 2}, + orderbook.Item{Price: 6002, Amount: 2, ID: 3}, + orderbook.Item{Price: 6003, Amount: 3, ID: 4}, + orderbook.Item{Price: 6004, Amount: 5, ID: 5}, + orderbook.Item{Price: 6005, Amount: 2, ID: 6}, + orderbook.Item{Price: 6006, Amount: 1.5, ID: 7}, + orderbook.Item{Price: 6007, Amount: 0.5, ID: 8}, + orderbook.Item{Price: 6008, Amount: 23, ID: 9}, + orderbook.Item{Price: 6009, Amount: 9, ID: 10}, + orderbook.Item{Price: 6010, Amount: 7, ID: 11}, + } + + bids := []orderbook.Item{ + orderbook.Item{Price: 5999, Amount: 1, ID: 12}, + orderbook.Item{Price: 5998, Amount: 0.5, ID: 13}, + orderbook.Item{Price: 5997, Amount: 2, ID: 14}, + orderbook.Item{Price: 5996, Amount: 3, ID: 15}, + orderbook.Item{Price: 5995, Amount: 5, ID: 16}, + orderbook.Item{Price: 5994, Amount: 2, ID: 17}, + orderbook.Item{Price: 5993, Amount: 1.5, ID: 18}, + orderbook.Item{Price: 5992, Amount: 0.5, ID: 19}, + orderbook.Item{Price: 5991, Amount: 23, ID: 20}, + orderbook.Item{Price: 5990, Amount: 9, ID: 21}, + orderbook.Item{Price: 5989, Amount: 7, ID: 22}, + } + + snapShot1.Asks = asks + snapShot1.Bids = bids + snapShot1.AssetType = "SPOT" + snapShot1.CurrencyPair = "BTCUSD" + snapShot1.LastUpdated = time.Now() + snapShot1.Pair = pair.NewCurrencyPairFromString("BTCUSD") + + wsTest.Websocket.Orderbook.LoadSnapshot(snapShot1, "ExchangeTest") + + var snapShot2 orderbook.Base + asks = []orderbook.Item{ + orderbook.Item{Price: 51, Amount: 1, ID: 1}, + orderbook.Item{Price: 52, Amount: 0.5, ID: 2}, + orderbook.Item{Price: 53, Amount: 2, ID: 3}, + orderbook.Item{Price: 54, Amount: 3, ID: 4}, + orderbook.Item{Price: 55, Amount: 5, ID: 5}, + orderbook.Item{Price: 56, Amount: 2, ID: 6}, + orderbook.Item{Price: 57, Amount: 1.5, ID: 7}, + orderbook.Item{Price: 58, Amount: 0.5, ID: 8}, + orderbook.Item{Price: 59, Amount: 23, ID: 9}, + orderbook.Item{Price: 50, Amount: 9, ID: 10}, + orderbook.Item{Price: 60, Amount: 7, ID: 11}, + } + + bids = []orderbook.Item{ + orderbook.Item{Price: 49, Amount: 1, ID: 12}, + orderbook.Item{Price: 48, Amount: 0.5, ID: 13}, + orderbook.Item{Price: 47, Amount: 2, ID: 14}, + orderbook.Item{Price: 46, Amount: 3, ID: 15}, + orderbook.Item{Price: 45, Amount: 5, ID: 16}, + orderbook.Item{Price: 44, Amount: 2, ID: 17}, + orderbook.Item{Price: 43, Amount: 1.5, ID: 18}, + orderbook.Item{Price: 42, Amount: 0.5, ID: 19}, + orderbook.Item{Price: 41, Amount: 23, ID: 20}, + orderbook.Item{Price: 40, Amount: 9, ID: 21}, + orderbook.Item{Price: 39, Amount: 7, ID: 22}, + } + + snapShot2.Asks = asks + snapShot2.Bids = bids + snapShot2.AssetType = "SPOT" + snapShot2.CurrencyPair = "LTCUSD" + snapShot2.LastUpdated = time.Now() + snapShot2.Pair = pair.NewCurrencyPairFromString("LTCUSD") + + wsTest.Websocket.Orderbook.LoadSnapshot(snapShot2, "ExchangeTest") + + var snapShot3 orderbook.Base + asks = []orderbook.Item{ + orderbook.Item{Price: 51, Amount: 1, ID: 1}, + orderbook.Item{Price: 52, Amount: 0.5, ID: 2}, + orderbook.Item{Price: 53, Amount: 2, ID: 3}, + orderbook.Item{Price: 54, Amount: 3, ID: 4}, + orderbook.Item{Price: 55, Amount: 5, ID: 5}, + orderbook.Item{Price: 56, Amount: 2, ID: 6}, + orderbook.Item{Price: 57, Amount: 1.5, ID: 7}, + orderbook.Item{Price: 58, Amount: 0.5, ID: 8}, + orderbook.Item{Price: 59, Amount: 23, ID: 9}, + orderbook.Item{Price: 50, Amount: 9, ID: 10}, + orderbook.Item{Price: 60, Amount: 7, ID: 11}, + } + + bids = []orderbook.Item{ + orderbook.Item{Price: 49, Amount: 1, ID: 12}, + orderbook.Item{Price: 48, Amount: 0.5, ID: 13}, + orderbook.Item{Price: 47, Amount: 2, ID: 14}, + orderbook.Item{Price: 46, Amount: 3, ID: 15}, + orderbook.Item{Price: 45, Amount: 5, ID: 16}, + orderbook.Item{Price: 44, Amount: 2, ID: 17}, + orderbook.Item{Price: 43, Amount: 1.5, ID: 18}, + orderbook.Item{Price: 42, Amount: 0.5, ID: 19}, + orderbook.Item{Price: 41, Amount: 23, ID: 20}, + orderbook.Item{Price: 40, Amount: 9, ID: 21}, + orderbook.Item{Price: 39, Amount: 7, ID: 22}, + } + + snapShot3.Asks = asks + snapShot3.Bids = bids + snapShot3.AssetType = "FUTURES" + snapShot3.CurrencyPair = "LTCUSD" + snapShot3.LastUpdated = time.Now() + snapShot3.Pair = pair.NewCurrencyPairFromString("LTCUSD") + + wsTest.Websocket.Orderbook.LoadSnapshot(snapShot3, "ExchangeTest") + + if len(wsTest.Websocket.Orderbook.ob) != 3 { + t.Error("test failed - inserting orderbook data") + } +} + +func TestUpdate(t *testing.T) { + LTCUSDPAIR := pair.NewCurrencyPairFromString("LTCUSD") + BTCUSDPAIR := pair.NewCurrencyPairFromString("BTCUSD") + + bidTargets := []orderbook.Item{ + orderbook.Item{Price: 49, Amount: 24}, // Ammend + orderbook.Item{Price: 48, Amount: 0}, // Delete + orderbook.Item{Price: 1337, Amount: 100}, // Append + orderbook.Item{Price: 1336, Amount: 0}, // Ghost delete + } + + askTargets := []orderbook.Item{ + orderbook.Item{Price: 51, Amount: 24}, // Ammend + orderbook.Item{Price: 52, Amount: 0}, // Delete + orderbook.Item{Price: 1337, Amount: 100}, // Append + orderbook.Item{Price: 1336, Amount: 0}, // Ghost delete + } + + err := wsTest.Websocket.Orderbook.Update(bidTargets, + askTargets, + LTCUSDPAIR, + time.Now(), + "ExchangeTest", + "SPOT") + + if err != nil { + t.Error("test failed - OrderbookUpdate error", err) + } + + err = wsTest.Websocket.Orderbook.Update(bidTargets, + askTargets, + LTCUSDPAIR, + time.Now(), + "ExchangeTest", + "FUTURES") + + if err != nil { + t.Error("test failed - OrderbookUpdate error", err) + } + + bidTargets = []orderbook.Item{ + orderbook.Item{Price: 5999, Amount: 24}, // Ammend + orderbook.Item{Price: 5998, Amount: 0}, // Delete + orderbook.Item{Price: 1337, Amount: 100}, // Append + orderbook.Item{Price: 1336, Amount: 0}, // Ghost delete + } + + askTargets = []orderbook.Item{ + orderbook.Item{Price: 6000, Amount: 24}, // Ammend + orderbook.Item{Price: 6001, Amount: 0}, // Delete + orderbook.Item{Price: 1337, Amount: 100}, // Append + orderbook.Item{Price: 1336, Amount: 0}, // Ghost delete + } + + err = wsTest.Websocket.Orderbook.Update(bidTargets, + askTargets, + BTCUSDPAIR, + time.Now(), + "ExchangeTest", + "SPOT") + + if err != nil { + t.Error("test failed - OrderbookUpdate error", err) + } +} diff --git a/exchanges/exmo/exmo.go b/exchanges/exmo/exmo.go index 23d7f811..dd6be332 100644 --- a/exchanges/exmo/exmo.go +++ b/exchanges/exmo/exmo.go @@ -55,7 +55,6 @@ func (e *EXMO) SetDefaults() { e.Name = "EXMO" e.Enabled = false e.Verbose = false - e.Websocket = false e.RESTPollingDelay = 10 e.RequestCurrencyPairFormat.Delimiter = "_" e.RequestCurrencyPairFormat.Uppercase = true @@ -71,6 +70,7 @@ func (e *EXMO) SetDefaults() { common.NewHTTPClientWithTimeout(exchange.DefaultHTTPTimeout)) e.APIUrlDefault = exmoAPIURL e.APIUrl = e.APIUrlDefault + e.WebsocketInit() } // Setup takes in the supplied exchange configuration details and sets params @@ -85,7 +85,6 @@ func (e *EXMO) Setup(exch config.ExchangeConfig) { e.SetHTTPClientUserAgent(exch.HTTPUserAgent) e.RESTPollingDelay = exch.RESTPollingDelay e.Verbose = exch.Verbose - e.Websocket = exch.Websocket e.BaseCurrencies = common.SplitStrings(exch.BaseCurrencies, ",") e.AvailablePairs = common.SplitStrings(exch.AvailablePairs, ",") e.EnabledPairs = common.SplitStrings(exch.EnabledPairs, ",") @@ -105,6 +104,10 @@ func (e *EXMO) Setup(exch config.ExchangeConfig) { if err != nil { log.Fatal(err) } + err = e.SetClientProxyAddress(exch.ProxyAddress) + if err != nil { + log.Fatal(err) + } } } diff --git a/exchanges/exmo/exmo_wrapper.go b/exchanges/exmo/exmo_wrapper.go index 81dbad12..72d7f546 100644 --- a/exchanges/exmo/exmo_wrapper.go +++ b/exchanges/exmo/exmo_wrapper.go @@ -224,3 +224,8 @@ func (e *EXMO) WithdrawFiatExchangeFunds(currency pair.CurrencyItem, amount floa func (e *EXMO) WithdrawFiatExchangeFundsToInternationalBank(currency pair.CurrencyItem, amount float64) (string, error) { return "", errors.New("not yet implemented") } + +// GetWebsocket returns a pointer to the exchange websocket +func (e *EXMO) GetWebsocket() (*exchange.Websocket, error) { + return nil, errors.New("not yet implemented") +} diff --git a/exchanges/gateio/gateio.go b/exchanges/gateio/gateio.go index 08631495..e80f69c5 100644 --- a/exchanges/gateio/gateio.go +++ b/exchanges/gateio/gateio.go @@ -45,7 +45,6 @@ func (g *Gateio) SetDefaults() { g.Name = "GateIO" g.Enabled = false g.Verbose = false - g.Websocket = false g.RESTPollingDelay = 10 g.RequestCurrencyPairFormat.Delimiter = "_" g.RequestCurrencyPairFormat.Uppercase = false @@ -62,6 +61,7 @@ func (g *Gateio) SetDefaults() { g.APIUrl = g.APIUrlDefault g.APIUrlSecondaryDefault = gateioMarketURL g.APIUrlSecondary = g.APIUrlSecondaryDefault + g.WebsocketInit() } // Setup sets user configuration @@ -77,7 +77,6 @@ func (g *Gateio) Setup(exch config.ExchangeConfig) { g.SetHTTPClientUserAgent(exch.HTTPUserAgent) g.RESTPollingDelay = exch.RESTPollingDelay g.Verbose = exch.Verbose - g.Websocket = exch.Websocket g.BaseCurrencies = common.SplitStrings(exch.BaseCurrencies, ",") g.AvailablePairs = common.SplitStrings(exch.AvailablePairs, ",") g.EnabledPairs = common.SplitStrings(exch.EnabledPairs, ",") @@ -97,6 +96,10 @@ func (g *Gateio) Setup(exch config.ExchangeConfig) { if err != nil { log.Fatal(err) } + err = g.SetClientProxyAddress(exch.ProxyAddress) + if err != nil { + log.Fatal(err) + } } } diff --git a/exchanges/gateio/gateio_wrapper.go b/exchanges/gateio/gateio_wrapper.go index dbe6564f..db1852e4 100644 --- a/exchanges/gateio/gateio_wrapper.go +++ b/exchanges/gateio/gateio_wrapper.go @@ -24,7 +24,7 @@ func (g *Gateio) Start(wg *sync.WaitGroup) { // Run implements the GateIO wrapper func (g *Gateio) Run() { if g.Verbose { - log.Printf("%s Websocket: %s. (url: %s).\n", g.GetName(), common.IsEnabled(g.Websocket), g.WebsocketURL) + log.Printf("%s Websocket: %s. (url: %s).\n", g.GetName(), common.IsEnabled(g.Websocket.IsEnabled()), g.WebsocketURL) log.Printf("%s polling delay: %ds.\n", g.GetName(), g.RESTPollingDelay) log.Printf("%s %d currencies enabled: %s.\n", g.GetName(), len(g.EnabledPairs), g.EnabledPairs) } @@ -175,3 +175,8 @@ func (g *Gateio) WithdrawFiatExchangeFunds(currency pair.CurrencyItem, amount fl func (g *Gateio) WithdrawFiatExchangeFundsToInternationalBank(currency pair.CurrencyItem, amount float64) (string, error) { return "", errors.New("not yet implemented") } + +// GetWebsocket returns a pointer to the exchange websocket +func (g *Gateio) GetWebsocket() (*exchange.Websocket, error) { + return nil, errors.New("not yet implemented") +} diff --git a/exchanges/gemini/gemini.go b/exchanges/gemini/gemini.go index 82fe98f0..4a3e5627 100644 --- a/exchanges/gemini/gemini.go +++ b/exchanges/gemini/gemini.go @@ -100,7 +100,6 @@ func (g *Gemini) SetDefaults() { g.Name = "Gemini" g.Enabled = false g.Verbose = false - g.Websocket = false g.RESTPollingDelay = 10 g.RequestCurrencyPairFormat.Delimiter = "" g.RequestCurrencyPairFormat.Uppercase = true @@ -115,6 +114,7 @@ func (g *Gemini) SetDefaults() { common.NewHTTPClientWithTimeout(exchange.DefaultHTTPTimeout)) g.APIUrlDefault = geminiAPIURL g.APIUrl = g.APIUrlDefault + g.WebsocketInit() } // Setup sets exchange configuration parameters @@ -129,7 +129,6 @@ func (g *Gemini) Setup(exch config.ExchangeConfig) { g.SetHTTPClientUserAgent(exch.HTTPUserAgent) g.RESTPollingDelay = exch.RESTPollingDelay g.Verbose = exch.Verbose - g.Websocket = exch.Websocket g.BaseCurrencies = common.SplitStrings(exch.BaseCurrencies, ",") g.AvailablePairs = common.SplitStrings(exch.AvailablePairs, ",") g.EnabledPairs = common.SplitStrings(exch.EnabledPairs, ",") @@ -153,6 +152,10 @@ func (g *Gemini) Setup(exch config.ExchangeConfig) { if exch.UseSandbox { g.APIUrl = geminiSandboxAPIURL } + err = g.SetClientProxyAddress(exch.ProxyAddress) + if err != nil { + log.Fatal(err) + } } } diff --git a/exchanges/gemini/gemini_wrapper.go b/exchanges/gemini/gemini_wrapper.go index f971268d..a20a55bb 100644 --- a/exchanges/gemini/gemini_wrapper.go +++ b/exchanges/gemini/gemini_wrapper.go @@ -175,3 +175,8 @@ func (g *Gemini) WithdrawFiatExchangeFunds(currency pair.CurrencyItem, amount fl func (g *Gemini) WithdrawFiatExchangeFundsToInternationalBank(currency pair.CurrencyItem, amount float64) (string, error) { return "", errors.New("not yet implemented") } + +// GetWebsocket returns a pointer to the exchange websocket +func (g *Gemini) GetWebsocket() (*exchange.Websocket, error) { + return nil, errors.New("not yet implemented") +} diff --git a/exchanges/hitbtc/hitbtc.go b/exchanges/hitbtc/hitbtc.go index 6800e973..8dc1af9f 100644 --- a/exchanges/hitbtc/hitbtc.go +++ b/exchanges/hitbtc/hitbtc.go @@ -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) + } } } diff --git a/exchanges/hitbtc/hitbtc_websocket.go b/exchanges/hitbtc/hitbtc_websocket.go index 623429fb..b1161edf 100644 --- a/exchanges/hitbtc/hitbtc_websocket.go +++ b/exchanges/hitbtc/hitbtc_websocket.go @@ -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"` +} diff --git a/exchanges/hitbtc/hitbtc_wrapper.go b/exchanges/hitbtc/hitbtc_wrapper.go index 2db604ca..64f385b6 100644 --- a/exchanges/hitbtc/hitbtc_wrapper.go +++ b/exchanges/hitbtc/hitbtc_wrapper.go @@ -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 +} diff --git a/exchanges/huobi/huobi.go b/exchanges/huobi/huobi.go index a33cc11c..b7f3ca90 100644 --- a/exchanges/huobi/huobi.go +++ b/exchanges/huobi/huobi.go @@ -17,6 +17,7 @@ import ( "strings" "time" + "github.com/gorilla/websocket" "github.com/thrasher-/gocryptotrader/common" "github.com/thrasher-/gocryptotrader/config" exchange "github.com/thrasher-/gocryptotrader/exchanges" @@ -62,6 +63,7 @@ const ( // HUOBI is the overarching type across this package type HUOBI struct { exchange.Base + WebsocketConn *websocket.Conn } // SetDefaults sets default values for the exchange @@ -70,7 +72,6 @@ func (h *HUOBI) SetDefaults() { h.Enabled = false h.Fee = 0 h.Verbose = false - h.Websocket = false h.RESTPollingDelay = 10 h.RequestCurrencyPairFormat.Delimiter = "" h.RequestCurrencyPairFormat.Uppercase = false @@ -85,6 +86,7 @@ func (h *HUOBI) SetDefaults() { common.NewHTTPClientWithTimeout(exchange.DefaultHTTPTimeout)) h.APIUrlDefault = huobiAPIURL h.APIUrl = h.APIUrlDefault + h.WebsocketInit() } // Setup sets user configuration @@ -101,7 +103,7 @@ func (h *HUOBI) Setup(exch config.ExchangeConfig) { h.SetHTTPClientUserAgent(exch.HTTPUserAgent) h.RESTPollingDelay = exch.RESTPollingDelay h.Verbose = exch.Verbose - h.Websocket = exch.Websocket + h.Websocket.SetEnabled(exch.Websocket) h.BaseCurrencies = common.SplitStrings(exch.BaseCurrencies, ",") h.AvailablePairs = common.SplitStrings(exch.AvailablePairs, ",") h.EnabledPairs = common.SplitStrings(exch.EnabledPairs, ",") @@ -121,6 +123,18 @@ func (h *HUOBI) Setup(exch config.ExchangeConfig) { if err != nil { log.Fatal(err) } + err = h.SetClientProxyAddress(exch.ProxyAddress) + if err != nil { + log.Fatal(err) + } + err = h.WebsocketSetup(h.WsConnect, + exch.Name, + exch.Websocket, + huobiSocketIOAddress, + exch.WebsocketURL) + if err != nil { + log.Fatal(err) + } } } diff --git a/exchanges/huobi/huobi_websocket.go b/exchanges/huobi/huobi_websocket.go index affe3825..5dee620c 100644 --- a/exchanges/huobi/huobi_websocket.go +++ b/exchanges/huobi/huobi_websocket.go @@ -1,237 +1,343 @@ package huobi import ( + "bytes" + "compress/gzip" + "errors" + "fmt" + "io/ioutil" "log" + "math/big" + "net/http" + "net/url" + "time" + "github.com/gorilla/websocket" "github.com/thrasher-/gocryptotrader/common" - "github.com/thrasher-/socketio" + "github.com/thrasher-/gocryptotrader/currency/pair" + "github.com/thrasher-/gocryptotrader/exchanges" + "github.com/thrasher-/gocryptotrader/exchanges/orderbook" ) const ( - huobiSocketIOAddress = "https://hq.huobi.com:443" - - //Service API - huobiSocketReqSymbolList = "reqSymbolList" - huobiSocketReqSymbolDetail = "reqSymbolDetail" - huobiSocketReqSubscribe = "reqMsgSubscribe" - huobiSocketReqUnsubscribe = "reqMsgUnsubscribe" - - // Market data API - huobiSocketMarketDetail = "marketDetail" - huobiSocketTradeDetail = "tradeDetail" - huobiSocketMarketDepthTop = "marketDepthTop" - huobiSocketMarketDepthTopShort = "marketDepthTopShort" - huobiSocketMarketDepth = "marketDepth" - huobiSocketMarketDepthTopDiff = "marketDepthTopDiff" - huobiSocketMarketDepthDiff = "marketDepthDiff" - huobiSocketMarketLastKline = "lastKLine" - huobiSocketMarketLastTimeline = "lastTimeLine" - huobiSocketMarketOverview = "marketOverview" - huobiSocketMarketStatic = "marketStatic" - - // History data API - huobiSocketReqTimeline = "reqTimeLine" - huobiSocketReqKline = "reqKLine" - huobiSocketReqDepthTop = "reqMarketDepthTop" - huobiSocketReqDepth = "reqMarketDepth" - huobiSocketReqTradeDetailTop = "reqTradeDetailTop" - huobiSocketReqMarketDetail = "reqMarketDetail" + huobiSocketIOAddress = "wss://api.huobi.pro/ws" + wsMarketKline = "market.%s.kline.1min" + wsMarketDepth = "market.%s.depth.step0" + wsMarketTrade = "market.%s.trade.detail" ) -// HuobiSocket is a pointer to a IO Socket -var HuobiSocket *socketio.SocketIO - -// Depth holds depth information -type Depth struct { - SymbolID string `json:"symbolId"` - Time float64 `json:"time"` - Version float64 `json:"version"` - BidName string `json:"bidName"` - BidPrice []float64 `json:"bidPrice"` - BidTotal []float64 `json:"bidTotal"` - BidAmount []float64 `json:"bidAmount"` - AskName string `json:"askName"` - AskPrice []float64 `json:"askPrice"` - AskTotal []float64 `json:"askTotal"` - AskAmount []float64 `json:"askAmount"` -} - -// WebsocketTrade holds full trade data -type WebsocketTrade struct { - Price []float64 `json:"price"` - Level []float64 `json:"level"` - Amount []float64 `json:"amount"` - AccuAmount []float64 `json:"accuAmount"` -} - -// WebsocketTradeDetail holds specific trade details -type WebsocketTradeDetail struct { - SymbolID string `json:"symbolId"` - TradeID []int64 `json:"tradeId"` - Price []float64 `json:"price"` - Time []int64 `json:"time"` - Amount []float64 `json:"amount"` - TopBids []WebsocketTrade `json:"topBids"` - TopAsks []WebsocketTrade `json:"topAsks"` -} - -// WebsocketMarketOverview holds market overview data -type WebsocketMarketOverview struct { - SymbolID string `json:"symbolId"` - Last float64 `json:"priceNew"` - Open float64 `json:"priceOpen"` - High float64 `json:"priceHigh"` - Low float64 `json:"priceLow"` - Ask float64 `json:"priceAsk"` - Bid float64 `json:"priceBid"` - Volume float64 `json:"totalVolume"` - TotalAmount float64 `json:"totalAmount"` -} - -// WebsocketLastTimeline holds timeline data -type WebsocketLastTimeline struct { - ID int64 `json:"_id"` - SymbolID string `json:"symbolId"` - Time int64 `json:"time"` - LastPrice float64 `json:"priceLast"` - Amount float64 `json:"amount"` - Volume float64 `json:"volume"` - Count int64 `json:"count"` -} - -// WebsocketResponse is a general response type for websocket -type WebsocketResponse struct { - Version int `json:"version"` - MsgType string `json:"msgType"` - RequestIndex int64 `json:"requestIndex"` - RetCode int64 `json:"retCode"` - RetMessage string `json:"retMsg"` - Payload map[string]interface{} `json:"payload"` -} - -// BuildHuobiWebsocketRequest packages a new request -func (h *HUOBI) BuildHuobiWebsocketRequest(msgType string, requestIndex int64, symbolRequest []string) map[string]interface{} { - request := map[string]interface{}{} - request["version"] = 1 - request["msgType"] = msgType - - if requestIndex != 0 { - request["requestIndex"] = requestIndex +// WsConnect initiates a new websocket connection +func (h *HUOBI) WsConnect() error { + if !h.Websocket.IsEnabled() || !h.IsEnabled() { + return errors.New(exchange.WebsocketNotEnabled) } - if len(symbolRequest) != 0 { - request["symbolIdList"] = symbolRequest - } + var dialer websocket.Dialer - return request -} - -// BuildHuobiWebsocketRequestExtra packages an extra request -func (h *HUOBI) BuildHuobiWebsocketRequestExtra(msgType string, requestIndex int64, symbolIDList interface{}) interface{} { - request := map[string]interface{}{} - request["version"] = 1 - request["msgType"] = msgType - - if requestIndex != 0 { - request["requestIndex"] = requestIndex - } - - request["symbolList"] = symbolIDList - return request -} - -// BuildHuobiWebsocketParamsList packages a parameter list -func (h *HUOBI) BuildHuobiWebsocketParamsList(objectName, currency, pushType, period, count, from, to, percentage string) interface{} { - list := map[string]interface{}{} - list["symbolId"] = currency - list["pushType"] = pushType - - if period != "" { - list["period"] = period - } - if percentage != "" { - list["percent"] = percentage - } - if count != "" { - list["count"] = count - } - if from != "" { - list["from"] = from - } - if to != "" { - list["to"] = to - } - - listArray := []map[string]interface{}{} - listArray = append(listArray, list) - - listCompleted := make(map[string][]map[string]interface{}) - listCompleted[objectName] = listArray - return listCompleted -} - -// OnConnect handles connection establishment -func (h *HUOBI) OnConnect(output chan socketio.Message) { - if h.Verbose { - log.Printf("%s Connected to Websocket.", h.GetName()) - } - - for _, x := range h.EnabledPairs { - currency := common.StringToLower(x) - msg := h.BuildHuobiWebsocketRequestExtra(huobiSocketReqSubscribe, 100, h.BuildHuobiWebsocketParamsList(huobiSocketMarketOverview, currency, "pushLong", "", "", "", "", "")) - result, err := common.JSONEncode(msg) + if h.Websocket.GetProxyAddress() != "" { + proxy, err := url.Parse(h.Websocket.GetProxyAddress()) if err != nil { - log.Println(err) + return err } - output <- socketio.CreateMessageEvent("request", string(result), nil, HuobiSocket.Version) + + dialer.Proxy = http.ProxyURL(proxy) } -} -// OnDisconnect handles disconnection -func (h *HUOBI) OnDisconnect(output chan socketio.Message) { - log.Printf("%s Disconnected from websocket server.. Reconnecting.\n", h.GetName()) - h.WebsocketClient() -} - -// OnError handles error issues -func (h *HUOBI) OnError() { - log.Printf("%s Error with Websocket connection.. Reconnecting.\n", h.GetName()) - h.WebsocketClient() -} - -// OnMessage handles messages from the exchange -func (h *HUOBI) OnMessage(message []byte, output chan socketio.Message) { -} - -// OnRequest handles requests -func (h *HUOBI) OnRequest(message []byte, output chan socketio.Message) { - response := WebsocketResponse{} - err := common.JSONDecode(message, &response) + var err error + h.WebsocketConn, _, err = dialer.Dial(h.Websocket.GetWebsocketURL(), http.Header{}) if err != nil { - log.Println(err) + return err } + + go h.WsHandleData() + go h.WsReadData() + + err = h.WsSubscribe() + if err != nil { + return err + } + + return nil } -// WebsocketClient creates a new websocket client -func (h *HUOBI) WebsocketClient() { - events := make(map[string]func(message []byte, output chan socketio.Message)) - events["request"] = h.OnRequest - events["message"] = h.OnMessage +// WsReadData reads data from the websocket connection +func (h *HUOBI) WsReadData() { + h.Websocket.Wg.Add(1) - HuobiSocket = &socketio.SocketIO{ - Version: 0.9, - OnConnect: h.OnConnect, - OnEvent: events, - OnError: h.OnError, - OnDisconnect: h.OnDisconnect, - } - - for h.Enabled && h.Websocket { - err := socketio.ConnectToSocket(huobiSocketIOAddress, HuobiSocket) + defer func() { + err := h.WebsocketConn.Close() if err != nil { - log.Printf("%s Unable to connect to Websocket. Err: %s\n", h.GetName(), err) - continue + h.Websocket.DataHandler <- fmt.Errorf("huobi_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 { + log.Fatal(err) + } + + h.Websocket.TrafficAlert <- struct{}{} + + b := bytes.NewReader(resp) + gReader, err := gzip.NewReader(b) + if err != nil { + log.Fatal(err) + } + + unzipped, err := ioutil.ReadAll(gReader) + if err != nil { + log.Fatal(err) + } + gReader.Close() + + h.Websocket.Intercomm <- exchange.WebsocketResponse{Raw: unzipped} } - log.Printf("%s Disconnected from Websocket.\n", h.GetName()) + } +} + +// WsHandleData handles data read from the websocket connection +func (h *HUOBI) WsHandleData() { + h.Websocket.Wg.Add(1) + defer h.Websocket.Wg.Done() + + for { + select { + case <-h.Websocket.ShutdownC: + case resp := <-h.Websocket.Intercomm: + var init WsResponse + err := common.JSONDecode(resp.Raw, &init) + if err != nil { + log.Fatal(err) + } + + if init.Status == "error" { + h.Websocket.DataHandler <- fmt.Errorf("huobi.go Websocker error %s %s", + init.ErrorCode, + init.ErrorMessage) + continue + } + + if init.Subscribed != "" { + continue + } + + if init.Ping != 0 { + err = h.WebsocketConn.WriteJSON(`{"pong":1337}`) + if err != nil { + log.Fatal(err) + } + continue + } + + switch { + case common.StringContains(init.Channel, "depth"): + var depth WsDepth + err := common.JSONDecode(resp.Raw, &depth) + if err != nil { + log.Fatal(err) + } + + data := common.SplitStrings(depth.Channel, ".") + + h.WsProcessOrderbook(depth, data[1]) + + case common.StringContains(init.Channel, "kline"): + var kline WsKline + err := common.JSONDecode(resp.Raw, &kline) + if err != nil { + log.Fatal(err) + } + + data := common.SplitStrings(kline.Channel, ".") + + h.Websocket.DataHandler <- exchange.KlineData{ + Timestamp: time.Unix(0, kline.Timestamp), + Exchange: h.GetName(), + AssetType: "SPOT", + Pair: pair.NewCurrencyPairFromString(data[1]), + OpenPrice: kline.Tick.Open, + ClosePrice: kline.Tick.Close, + HighPrice: kline.Tick.High, + LowPrice: kline.Tick.Low, + Volume: kline.Tick.Volume, + } + + case common.StringContains(init.Channel, "trade"): + var trade WsTrade + err := common.JSONDecode(resp.Raw, &trade) + if err != nil { + log.Fatal(err) + } + + data := common.SplitStrings(trade.Channel, ".") + + h.Websocket.DataHandler <- exchange.TradeData{ + Exchange: h.GetName(), + AssetType: "SPOT", + CurrencyPair: pair.NewCurrencyPairFromString(data[1]), + Timestamp: time.Unix(0, trade.Tick.Timestamp), + } + } + } + } +} + +// WsProcessOrderbook processes new orderbook data +func (h *HUOBI) WsProcessOrderbook(ob WsDepth, symbol string) error { + var bids []orderbook.Item + for _, data := range ob.Tick.Bids { + bidLevel := data.([]interface{}) + bids = append(bids, orderbook.Item{Price: bidLevel[0].(float64), + Amount: bidLevel[0].(float64)}) + } + + var asks []orderbook.Item + for _, data := range ob.Tick.Asks { + askLevel := data.([]interface{}) + asks = append(asks, orderbook.Item{Price: askLevel[0].(float64), + Amount: askLevel[0].(float64)}) + } + + p := pair.NewCurrencyPairFromString(symbol) + + var newOrderbook orderbook.Base + newOrderbook.Asks = asks + newOrderbook.Bids = bids + newOrderbook.CurrencyPair = 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{ + Pair: p, + Exchange: h.GetName(), + Asset: "SPOT", + } + + 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 + } + } + return nil +} + +// 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"` + } +} + +// 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"` } } diff --git a/exchanges/huobi/huobi_wrapper.go b/exchanges/huobi/huobi_wrapper.go index 129abf6f..e875df5c 100644 --- a/exchanges/huobi/huobi_wrapper.go +++ b/exchanges/huobi/huobi_wrapper.go @@ -25,15 +25,11 @@ func (h *HUOBI) Start(wg *sync.WaitGroup) { // Run implements the HUOBI wrapper func (h *HUOBI) Run() { if h.Verbose { - log.Printf("%s Websocket: %s (url: %s).\n", h.GetName(), common.IsEnabled(h.Websocket), huobiSocketIOAddress) + log.Printf("%s Websocket: %s (url: %s).\n", h.GetName(), common.IsEnabled(h.Websocket.IsEnabled()), huobiSocketIOAddress) 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.GetSymbols() if err != nil { log.Printf("%s Failed to get available symbols.\n", h.GetName()) @@ -222,3 +218,8 @@ func (h *HUOBI) WithdrawFiatExchangeFunds(currency pair.CurrencyItem, amount flo func (h *HUOBI) WithdrawFiatExchangeFundsToInternationalBank(currency pair.CurrencyItem, amount float64) (string, error) { return "", errors.New("not yet implemented") } + +// GetWebsocket returns a pointer to the exchange websocket +func (h *HUOBI) GetWebsocket() (*exchange.Websocket, error) { + return h.Websocket, nil +} diff --git a/exchanges/huobihadax/huobihadax.go b/exchanges/huobihadax/huobihadax.go index 85306d07..911eb42c 100644 --- a/exchanges/huobihadax/huobihadax.go +++ b/exchanges/huobihadax/huobihadax.go @@ -64,7 +64,6 @@ func (h *HUOBIHADAX) SetDefaults() { h.Enabled = false h.Fee = 0 h.Verbose = false - h.Websocket = false h.RESTPollingDelay = 10 h.RequestCurrencyPairFormat.Delimiter = "" h.RequestCurrencyPairFormat.Uppercase = false @@ -79,6 +78,7 @@ func (h *HUOBIHADAX) SetDefaults() { common.NewHTTPClientWithTimeout(exchange.DefaultHTTPTimeout)) h.APIUrlDefault = huobihadaxAPIURL h.APIUrl = h.APIUrlDefault + h.WebsocketInit() } // Setup sets user configuration @@ -95,7 +95,6 @@ func (h *HUOBIHADAX) Setup(exch config.ExchangeConfig) { h.SetHTTPClientUserAgent(exch.HTTPUserAgent) h.RESTPollingDelay = exch.RESTPollingDelay h.Verbose = exch.Verbose - h.Websocket = exch.Websocket h.BaseCurrencies = common.SplitStrings(exch.BaseCurrencies, ",") h.AvailablePairs = common.SplitStrings(exch.AvailablePairs, ",") h.EnabledPairs = common.SplitStrings(exch.EnabledPairs, ",") @@ -115,6 +114,10 @@ func (h *HUOBIHADAX) Setup(exch config.ExchangeConfig) { if err != nil { log.Fatal(err) } + err = h.SetClientProxyAddress(exch.ProxyAddress) + if err != nil { + log.Fatal(err) + } } } diff --git a/exchanges/huobihadax/huobihadax_wrapper.go b/exchanges/huobihadax/huobihadax_wrapper.go index 4cf3b6f9..68bc2eb0 100644 --- a/exchanges/huobihadax/huobihadax_wrapper.go +++ b/exchanges/huobihadax/huobihadax_wrapper.go @@ -24,7 +24,7 @@ func (h *HUOBIHADAX) Start(wg *sync.WaitGroup) { // Run implements the OKEX wrapper func (h *HUOBIHADAX) Run() { if h.Verbose { - log.Printf("%s Websocket: %s. (url: %s).\n", h.GetName(), common.IsEnabled(h.Websocket), h.WebsocketURL) + log.Printf("%s Websocket: %s. (url: %s).\n", h.GetName(), common.IsEnabled(h.Websocket.IsEnabled()), h.WebsocketURL) 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) } @@ -183,3 +183,8 @@ func (h *HUOBIHADAX) WithdrawFiatExchangeFunds(currency pair.CurrencyItem, amoun func (h *HUOBIHADAX) WithdrawFiatExchangeFundsToInternationalBank(currency pair.CurrencyItem, amount float64) (string, error) { return "", errors.New("not yet implemented") } + +// GetWebsocket returns a pointer to the exchange websocket +func (h *HUOBIHADAX) GetWebsocket() (*exchange.Websocket, error) { + return nil, errors.New("not yet implemented") +} diff --git a/exchanges/itbit/itbit.go b/exchanges/itbit/itbit.go index 75c7da57..b394a992 100644 --- a/exchanges/itbit/itbit.go +++ b/exchanges/itbit/itbit.go @@ -46,7 +46,6 @@ func (i *ItBit) SetDefaults() { i.MakerFee = -0.10 i.TakerFee = 0.50 i.Verbose = false - i.Websocket = false i.RESTPollingDelay = 10 i.RequestCurrencyPairFormat.Delimiter = "" i.RequestCurrencyPairFormat.Uppercase = true @@ -61,6 +60,7 @@ func (i *ItBit) SetDefaults() { common.NewHTTPClientWithTimeout(exchange.DefaultHTTPTimeout)) i.APIUrlDefault = itbitAPIURL i.APIUrl = i.APIUrlDefault + i.WebsocketInit() } // Setup sets the exchange parameters from exchange config @@ -75,7 +75,6 @@ func (i *ItBit) Setup(exch config.ExchangeConfig) { i.SetHTTPClientUserAgent(exch.HTTPUserAgent) i.RESTPollingDelay = exch.RESTPollingDelay i.Verbose = exch.Verbose - i.Websocket = exch.Websocket i.BaseCurrencies = common.SplitStrings(exch.BaseCurrencies, ",") i.AvailablePairs = common.SplitStrings(exch.AvailablePairs, ",") i.EnabledPairs = common.SplitStrings(exch.EnabledPairs, ",") @@ -95,6 +94,10 @@ func (i *ItBit) Setup(exch config.ExchangeConfig) { if err != nil { log.Fatal(err) } + err = i.SetClientProxyAddress(exch.ProxyAddress) + if err != nil { + log.Fatal(err) + } } } diff --git a/exchanges/itbit/itbit_wrapper.go b/exchanges/itbit/itbit_wrapper.go index f27ef026..eda3a346 100644 --- a/exchanges/itbit/itbit_wrapper.go +++ b/exchanges/itbit/itbit_wrapper.go @@ -177,3 +177,8 @@ func (i *ItBit) WithdrawFiatExchangeFunds(currency pair.CurrencyItem, amount flo func (i *ItBit) WithdrawFiatExchangeFundsToInternationalBank(currency pair.CurrencyItem, amount float64) (string, error) { return "", errors.New("not yet implemented") } + +// GetWebsocket returns a pointer to the exchange websocket +func (i *ItBit) GetWebsocket() (*exchange.Websocket, error) { + return nil, errors.New("not yet implemented") +} diff --git a/exchanges/kraken/kraken.go b/exchanges/kraken/kraken.go index 7b4e981e..624abbc7 100644 --- a/exchanges/kraken/kraken.go +++ b/exchanges/kraken/kraken.go @@ -58,7 +58,6 @@ func (k *Kraken) SetDefaults() { k.FiatFee = 0.35 k.CryptoFee = 0.10 k.Verbose = false - k.Websocket = false k.RESTPollingDelay = 10 k.Ticker = make(map[string]Ticker) k.RequestCurrencyPairFormat.Delimiter = "" @@ -75,6 +74,7 @@ func (k *Kraken) SetDefaults() { common.NewHTTPClientWithTimeout(exchange.DefaultHTTPTimeout)) k.APIUrlDefault = krakenAPIURL k.APIUrl = k.APIUrlDefault + k.WebsocketInit() } // Setup sets current exchange configuration @@ -89,7 +89,6 @@ func (k *Kraken) Setup(exch config.ExchangeConfig) { k.SetHTTPClientUserAgent(exch.HTTPUserAgent) k.RESTPollingDelay = exch.RESTPollingDelay k.Verbose = exch.Verbose - k.Websocket = exch.Websocket k.BaseCurrencies = common.SplitStrings(exch.BaseCurrencies, ",") k.AvailablePairs = common.SplitStrings(exch.AvailablePairs, ",") k.EnabledPairs = common.SplitStrings(exch.EnabledPairs, ",") @@ -109,6 +108,10 @@ func (k *Kraken) Setup(exch config.ExchangeConfig) { if err != nil { log.Fatal(err) } + err = k.SetClientProxyAddress(exch.ProxyAddress) + if err != nil { + log.Fatal(err) + } } } diff --git a/exchanges/kraken/kraken_wrapper.go b/exchanges/kraken/kraken_wrapper.go index 67a017da..402f0ced 100644 --- a/exchanges/kraken/kraken_wrapper.go +++ b/exchanges/kraken/kraken_wrapper.go @@ -250,3 +250,8 @@ func (k *Kraken) WithdrawFiatExchangeFunds(currency pair.CurrencyItem, amount fl func (k *Kraken) WithdrawFiatExchangeFundsToInternationalBank(currency pair.CurrencyItem, amount float64) (string, error) { return "", errors.New("not yet implemented") } + +// GetWebsocket returns a pointer to the exchange websocket +func (k *Kraken) GetWebsocket() (*exchange.Websocket, error) { + return nil, errors.New("not yet implemented") +} diff --git a/exchanges/lakebtc/lakebtc.go b/exchanges/lakebtc/lakebtc.go index 87ad3507..552b9b0e 100644 --- a/exchanges/lakebtc/lakebtc.go +++ b/exchanges/lakebtc/lakebtc.go @@ -47,7 +47,6 @@ func (l *LakeBTC) SetDefaults() { l.TakerFee = 0.2 l.MakerFee = 0.15 l.Verbose = false - l.Websocket = false l.RESTPollingDelay = 10 l.RequestCurrencyPairFormat.Delimiter = "" l.RequestCurrencyPairFormat.Uppercase = true @@ -62,6 +61,7 @@ func (l *LakeBTC) SetDefaults() { common.NewHTTPClientWithTimeout(exchange.DefaultHTTPTimeout)) l.APIUrlDefault = lakeBTCAPIURL l.APIUrl = l.APIUrlDefault + l.WebsocketInit() } // Setup sets exchange configuration profile @@ -76,7 +76,6 @@ func (l *LakeBTC) Setup(exch config.ExchangeConfig) { l.SetHTTPClientUserAgent(exch.HTTPUserAgent) l.RESTPollingDelay = exch.RESTPollingDelay l.Verbose = exch.Verbose - l.Websocket = exch.Websocket l.BaseCurrencies = common.SplitStrings(exch.BaseCurrencies, ",") l.AvailablePairs = common.SplitStrings(exch.AvailablePairs, ",") l.EnabledPairs = common.SplitStrings(exch.EnabledPairs, ",") @@ -96,6 +95,10 @@ func (l *LakeBTC) Setup(exch config.ExchangeConfig) { if err != nil { log.Fatal(err) } + err = l.SetClientProxyAddress(exch.ProxyAddress) + if err != nil { + log.Fatal(err) + } } } diff --git a/exchanges/lakebtc/lakebtc_wrapper.go b/exchanges/lakebtc/lakebtc_wrapper.go index ae624d9d..45859d49 100644 --- a/exchanges/lakebtc/lakebtc_wrapper.go +++ b/exchanges/lakebtc/lakebtc_wrapper.go @@ -187,3 +187,8 @@ func (l *LakeBTC) WithdrawFiatExchangeFunds(currency pair.CurrencyItem, amount f func (l *LakeBTC) WithdrawFiatExchangeFundsToInternationalBank(currency pair.CurrencyItem, amount float64) (string, error) { return "", errors.New("not yet implemented") } + +// GetWebsocket returns a pointer to the exchange websocket +func (l *LakeBTC) GetWebsocket() (*exchange.Websocket, error) { + return nil, errors.New("not yet implemented") +} diff --git a/exchanges/liqui/liqui.go b/exchanges/liqui/liqui.go index a6bdb5c7..d764ee64 100644 --- a/exchanges/liqui/liqui.go +++ b/exchanges/liqui/liqui.go @@ -50,7 +50,6 @@ func (l *Liqui) SetDefaults() { l.Enabled = false l.Fee = 0.25 l.Verbose = false - l.Websocket = false l.RESTPollingDelay = 10 l.Ticker = make(map[string]Ticker) l.RequestCurrencyPairFormat.Delimiter = "_" @@ -69,6 +68,7 @@ func (l *Liqui) SetDefaults() { l.APIUrl = l.APIUrlDefault l.APIUrlSecondaryDefault = liquiAPIPrivateURL l.APIUrlSecondary = l.APIUrlSecondaryDefault + l.WebsocketInit() } // Setup sets exchange configuration parameters for liqui @@ -83,7 +83,6 @@ func (l *Liqui) Setup(exch config.ExchangeConfig) { l.SetHTTPClientUserAgent(exch.HTTPUserAgent) l.RESTPollingDelay = exch.RESTPollingDelay l.Verbose = exch.Verbose - l.Websocket = exch.Websocket l.BaseCurrencies = common.SplitStrings(exch.BaseCurrencies, ",") l.AvailablePairs = common.SplitStrings(exch.AvailablePairs, ",") l.EnabledPairs = common.SplitStrings(exch.EnabledPairs, ",") @@ -103,6 +102,10 @@ func (l *Liqui) Setup(exch config.ExchangeConfig) { if err != nil { log.Fatal(err) } + err = l.SetClientProxyAddress(exch.ProxyAddress) + if err != nil { + log.Fatal(err) + } } } diff --git a/exchanges/liqui/liqui_wrapper.go b/exchanges/liqui/liqui_wrapper.go index 32b10903..1a369f86 100644 --- a/exchanges/liqui/liqui_wrapper.go +++ b/exchanges/liqui/liqui_wrapper.go @@ -196,3 +196,8 @@ func (l *Liqui) WithdrawFiatExchangeFunds(currency pair.CurrencyItem, amount flo func (l *Liqui) WithdrawFiatExchangeFundsToInternationalBank(currency pair.CurrencyItem, amount float64) (string, error) { return "", errors.New("not yet implemented") } + +// GetWebsocket returns a pointer to the exchange websocket +func (l *Liqui) GetWebsocket() (*exchange.Websocket, error) { + return nil, errors.New("not yet implemented") +} diff --git a/exchanges/localbitcoins/localbitcoins.go b/exchanges/localbitcoins/localbitcoins.go index bcd7eae1..f1a79cd9 100644 --- a/exchanges/localbitcoins/localbitcoins.go +++ b/exchanges/localbitcoins/localbitcoins.go @@ -116,7 +116,6 @@ func (l *LocalBitcoins) SetDefaults() { l.Enabled = false l.Verbose = false l.Verbose = false - l.Websocket = false l.RESTPollingDelay = 10 l.RequestCurrencyPairFormat.Delimiter = "" l.RequestCurrencyPairFormat.Uppercase = true @@ -130,6 +129,7 @@ func (l *LocalBitcoins) SetDefaults() { common.NewHTTPClientWithTimeout(exchange.DefaultHTTPTimeout)) l.APIUrlDefault = localbitcoinsAPIURL l.APIUrl = l.APIUrlDefault + l.WebsocketInit() } // Setup sets exchange configuration parameters @@ -144,7 +144,6 @@ func (l *LocalBitcoins) Setup(exch config.ExchangeConfig) { l.SetHTTPClientUserAgent(exch.HTTPUserAgent) l.RESTPollingDelay = exch.RESTPollingDelay l.Verbose = exch.Verbose - l.Websocket = exch.Websocket l.BaseCurrencies = common.SplitStrings(exch.BaseCurrencies, ",") l.AvailablePairs = common.SplitStrings(exch.AvailablePairs, ",") l.EnabledPairs = common.SplitStrings(exch.EnabledPairs, ",") @@ -160,6 +159,10 @@ func (l *LocalBitcoins) Setup(exch config.ExchangeConfig) { if err != nil { log.Fatal(err) } + err = l.SetClientProxyAddress(exch.ProxyAddress) + if err != nil { + log.Fatal(err) + } } } diff --git a/exchanges/localbitcoins/localbitcoins_wrapper.go b/exchanges/localbitcoins/localbitcoins_wrapper.go index 605e9f8f..308d574c 100644 --- a/exchanges/localbitcoins/localbitcoins_wrapper.go +++ b/exchanges/localbitcoins/localbitcoins_wrapper.go @@ -168,3 +168,8 @@ func (l *LocalBitcoins) WithdrawFiatExchangeFunds(currency pair.CurrencyItem, am func (l *LocalBitcoins) WithdrawFiatExchangeFundsToInternationalBank(currency pair.CurrencyItem, amount float64) (string, error) { return "", errors.New("not yet implemented") } + +// GetWebsocket returns a pointer to the exchange websocket +func (l *LocalBitcoins) GetWebsocket() (*exchange.Websocket, error) { + return nil, errors.New("not yet implemented") +} diff --git a/exchanges/okcoin/okcoin.go b/exchanges/okcoin/okcoin.go index 9974af00..0ee533f9 100644 --- a/exchanges/okcoin/okcoin.go +++ b/exchanges/okcoin/okcoin.go @@ -19,7 +19,7 @@ import ( const ( okcoinAPIURL = "https://www.okcoin.com/api/v1/" - okcoinAPIURLChina = "https://www.okcoin.cn/api/v1/" + okcoinAPIURLChina = "https://www.okcoin.com/api/v1/" okcoinAPIVersion = "1" okcoinWebsocketURL = "wss://real.okcoin.com:10440/websocket/okcoinapi" okcoinWebsocketURLChina = "wss://real.okcoin.cn:10440/websocket/okcoinapi" @@ -72,10 +72,6 @@ const ( okcoinUnauthRate = 0 ) -var ( - okcoinDefaultsSet = false -) - // OKCoin is the overarching type across this package type OKCoin struct { exchange.Base @@ -99,37 +95,11 @@ func (o *OKCoin) SetDefaults() { o.SetWebsocketErrorDefaults() o.Enabled = false o.Verbose = false - o.Websocket = false o.RESTPollingDelay = 10 o.AssetTypes = []string{ticker.Spot} o.SupportsAutoPairUpdating = false o.SupportsRESTTickerBatching = false - - if okcoinDefaultsSet { - o.APIUrlDefault = okcoinAPIURL - o.APIUrl = o.APIUrlDefault - o.Name = "OKCOIN International" - o.WebsocketURL = okcoinWebsocketURL - o.RequestCurrencyPairFormat.Delimiter = "_" - o.RequestCurrencyPairFormat.Uppercase = false - o.ConfigCurrencyPairFormat.Delimiter = "_" - o.ConfigCurrencyPairFormat.Uppercase = true - o.Requester = request.New(o.Name, - request.NewRateLimit(time.Second, okcoinAuthRate), - request.NewRateLimit(time.Second, okcoinUnauthRate), - common.NewHTTPClientWithTimeout(exchange.DefaultHTTPTimeout)) - } else { - o.APIUrlDefault = okcoinAPIURLChina - o.APIUrl = o.APIUrlDefault - o.Name = "OKCOIN China" - o.WebsocketURL = okcoinWebsocketURLChina - okcoinDefaultsSet = true - o.setCurrencyPairFormats() - o.Requester = request.New(o.Name, - request.NewRateLimit(time.Second, okcoinAuthRate), - request.NewRateLimit(time.Second, okcoinUnauthRate), - common.NewHTTPClientWithTimeout(exchange.DefaultHTTPTimeout)) - } + o.WebsocketInit() } // Setup sets exchange configuration parameters @@ -137,6 +107,37 @@ func (o *OKCoin) Setup(exch config.ExchangeConfig) { if !exch.Enabled { o.SetEnabled(false) } else { + if exch.Name == "OKCOIN International" { + o.AssetTypes = append(o.AssetTypes, o.FuturesValues...) + o.APIUrlDefault = okcoinAPIURL + o.APIUrl = o.APIUrlDefault + o.Name = "OKCOIN International" + o.WebsocketURL = okcoinWebsocketURL + o.setCurrencyPairFormats() + o.Requester = request.New(o.Name, + request.NewRateLimit(time.Second, okcoinAuthRate), + request.NewRateLimit(time.Second, okcoinUnauthRate), + common.NewHTTPClientWithTimeout(exchange.DefaultHTTPTimeout)) + o.ConfigCurrencyPairFormat.Delimiter = "_" + o.ConfigCurrencyPairFormat.Uppercase = true + o.RequestCurrencyPairFormat.Uppercase = false + o.RequestCurrencyPairFormat.Delimiter = "_" + } else { + o.APIUrlDefault = okcoinAPIURLChina + o.APIUrl = o.APIUrlDefault + o.Name = "OKCOIN China" + o.WebsocketURL = okcoinWebsocketURLChina + o.setCurrencyPairFormats() + o.Requester = request.New(o.Name, + request.NewRateLimit(time.Second, okcoinAuthRate), + request.NewRateLimit(time.Second, okcoinUnauthRate), + common.NewHTTPClientWithTimeout(exchange.DefaultHTTPTimeout)) + o.ConfigCurrencyPairFormat.Delimiter = "" + o.ConfigCurrencyPairFormat.Uppercase = true + o.RequestCurrencyPairFormat.Uppercase = false + o.RequestCurrencyPairFormat.Delimiter = "" + } + o.Enabled = true o.AuthenticatedAPISupport = exch.AuthenticatedAPISupport o.SetAPIKeys(exch.APIKey, exch.APISecret, "", false) @@ -144,7 +145,7 @@ func (o *OKCoin) Setup(exch config.ExchangeConfig) { o.SetHTTPClientUserAgent(exch.HTTPUserAgent) o.RESTPollingDelay = exch.RESTPollingDelay o.Verbose = exch.Verbose - o.Websocket = exch.Websocket + o.Websocket.SetEnabled(exch.Websocket) o.BaseCurrencies = common.SplitStrings(exch.BaseCurrencies, ",") o.AvailablePairs = common.SplitStrings(exch.AvailablePairs, ",") o.EnabledPairs = common.SplitStrings(exch.EnabledPairs, ",") @@ -164,6 +165,18 @@ func (o *OKCoin) Setup(exch config.ExchangeConfig) { if err != nil { log.Fatal(err) } + err = o.SetClientProxyAddress(exch.ProxyAddress) + if err != nil { + log.Fatal(err) + } + err = o.WebsocketSetup(o.WsConnect, + exch.Name, + exch.Websocket, + okcoinWebsocketURL, + o.WebsocketURL) + if err != nil { + log.Fatal(err) + } } } diff --git a/exchanges/okcoin/okcoin_types.go b/exchanges/okcoin/okcoin_types.go index d78e79f4..8b595d7b 100644 --- a/exchanges/okcoin/okcoin_types.go +++ b/exchanges/okcoin/okcoin_types.go @@ -251,17 +251,6 @@ type WebsocketFutureIndex struct { Timestamp int64 `json:"timestamp,string"` } -// WebsocketTicker holds ticker data for websocket -type WebsocketTicker struct { - Timestamp float64 - Vol string - Buy float64 - High float64 - Last float64 - Low float64 - Sell float64 -} - // WebsocketFuturesTicker holds futures ticker data for websocket type WebsocketFuturesTicker struct { Buy float64 `json:"buy"` @@ -275,13 +264,6 @@ type WebsocketFuturesTicker struct { Volume float64 `json:"vol,string"` } -// WebsocketOrderbook holds orderbook data for websocket -type WebsocketOrderbook struct { - Asks [][]float64 `json:"asks"` - Bids [][]float64 `json:"bids"` - Timestamp int64 `json:"timestamp,string"` -} - // WebsocketUserinfo holds user info for websocket type WebsocketUserinfo struct { Info struct { diff --git a/exchanges/okcoin/okcoin_websocket.go b/exchanges/okcoin/okcoin_websocket.go index 0d63b89d..536db691 100644 --- a/exchanges/okcoin/okcoin_websocket.go +++ b/exchanges/okcoin/okcoin_websocket.go @@ -1,524 +1,258 @@ package okcoin import ( + "encoding/json" + "errors" "fmt" "log" "net/http" "net/url" - "reflect" "strconv" - "strings" "time" "github.com/gorilla/websocket" "github.com/thrasher-/gocryptotrader/common" + "github.com/thrasher-/gocryptotrader/currency/pair" + "github.com/thrasher-/gocryptotrader/exchanges" ) const ( - okcoinWebsocketUSDRealTrades = "ok_usd_realtrades" - okcoinWebsocketCNYRealTrades = "ok_cny_realtrades" - okcoinWebsocketSpotUSDTrade = "ok_spotusd_trade" - okcoinWebsocketSpotCNYTrade = "ok_spotcny_trade" - okcoinWebsocketSpotUSDCancelOrder = "ok_spotusd_cancel_order" - okcoinWebsocketSpotCNYCancelOrder = "ok_spotcny_cancel_order" - okcoinWebsocketSpotUSDUserInfo = "ok_spotusd_userinfo" - okcoinWebsocketSpotCNYUserInfo = "ok_spotcny_userinfo" - okcoinWebsocketSpotUSDOrderInfo = "ok_spotusd_order_info" - okcoinWebsocketSpotCNYOrderInfo = "ok_spotcny_order_info" - okcoinWebsocketFuturesTrade = "ok_futuresusd_trade" - okcoinWebsocketFuturesCancelOrder = "ok_futuresusd_cancel_order" - okcoinWebsocketFuturesRealTrades = "ok_usd_future_realtrades" - okcoinWebsocketFuturesUserInfo = "ok_futureusd_userinfo" - okcoinWebsocketFuturesOrderInfo = "ok_futureusd_order_info" + wsSubTicker = "ok_sub_spot_%s_ticker" + wsSubDepthIncrement = "ok_sub_spot_%s_depth" + wsSubDepthFull = "ok_sub_spot_%s_depth_%s" + wsSubTrades = "ok_sub_spot_%s_deals" + wsSubKline = "ok_sub_spot_%s_kline_%s" ) // PingHandler handles the keep alive func (o *OKCoin) PingHandler(message string) error { - err := o.WebsocketConn.WriteControl(websocket.PingMessage, []byte("{'event':'ping'}"), time.Now().Add(time.Second)) - - if err != nil { - log.Println(err) - return err - } - return nil + return o.WebsocketConn.WriteControl(websocket.PingMessage, + []byte("{'event':'ping'}"), + time.Now().Add(time.Second)) } // AddChannel adds a new channel on the websocket client -func (o *OKCoin) AddChannel(channel string) { +func (o *OKCoin) AddChannel(channel string) error { event := WebsocketEvent{"addChannel", channel} json, err := common.JSONEncode(event) if err != nil { - log.Println(err) - return - } - err = o.WebsocketConn.WriteMessage(websocket.TextMessage, json) - - if err != nil { - log.Println(err) - return + return err } - if o.Verbose { - log.Printf("%s Adding channel: %s\n", o.GetName(), channel) - } + return o.WebsocketConn.WriteMessage(websocket.TextMessage, json) } -// RemoveChannel removes a channel on the websocket client -func (o *OKCoin) RemoveChannel(channel string) { - event := WebsocketEvent{"removeChannel", channel} - json, err := common.JSONEncode(event) - if err != nil { - log.Println(err) - return - } - err = o.WebsocketConn.WriteMessage(websocket.TextMessage, json) - - if err != nil { - log.Println(err) - return +// WsConnect initiates a websocket connection +func (o *OKCoin) WsConnect() error { + if !o.Websocket.IsEnabled() || !o.IsEnabled() { + return errors.New(exchange.WebsocketNotEnabled) } - if o.Verbose { - log.Printf("%s Removing channel: %s\n", o.GetName(), channel) - } -} + klineValues := []string{"1min", "3min", "5min", "15min", "30min", "1hour", + "2hour", "4hour", "6hour", "12hour", "day", "3day", "week"} -// WebsocketSpotTrade handles spot trade request on the websocket client -func (o *OKCoin) WebsocketSpotTrade(symbol, orderType string, price, amount float64) { - values := make(map[string]string) - values["symbol"] = symbol - values["type"] = orderType - values["price"] = strconv.FormatFloat(price, 'f', -1, 64) - values["amount"] = strconv.FormatFloat(amount, 'f', -1, 64) - - channel := okcoinWebsocketSpotUSDTrade - if o.WebsocketURL == okcoinWebsocketURLChina { - channel = okcoinWebsocketSpotCNYTrade - } - - o.AddChannelAuthenticated(channel, values) -} - -// WebsocketFuturesTrade handles a futures trade on the websocket client -func (o *OKCoin) WebsocketFuturesTrade(symbol, contractType string, price, amount float64, orderType, matchPrice, leverage int) { - values := make(map[string]string) - values["symbol"] = symbol - values["contract_type"] = contractType - values["price"] = strconv.FormatFloat(price, 'f', -1, 64) - values["amount"] = strconv.FormatFloat(amount, 'f', -1, 64) - values["type"] = strconv.Itoa(orderType) - values["match_price"] = strconv.Itoa(matchPrice) - values["lever_rate"] = strconv.Itoa(orderType) - o.AddChannelAuthenticated(okcoinWebsocketFuturesTrade, values) -} - -// WebsocketSpotCancel cancels a spot trade on the websocket client -func (o *OKCoin) WebsocketSpotCancel(symbol string, orderID int64) { - values := make(map[string]string) - values["symbol"] = symbol - values["order_id"] = strconv.FormatInt(orderID, 10) - - channel := okcoinWebsocketSpotUSDCancelOrder - if o.WebsocketURL == okcoinWebsocketURLChina { - channel = okcoinWebsocketSpotCNYCancelOrder - } - - o.AddChannelAuthenticated(channel, values) -} - -// WebsocketFuturesCancel cancels a futures contract on the websocket client -func (o *OKCoin) WebsocketFuturesCancel(symbol, contractType string, orderID int64) { - values := make(map[string]string) - values["symbol"] = symbol - values["order_id"] = strconv.FormatInt(orderID, 10) - values["contract_type"] = contractType - o.AddChannelAuthenticated(okcoinWebsocketFuturesCancelOrder, values) -} - -// WebsocketSpotOrderInfo request information on an order on the websocket -// client -func (o *OKCoin) WebsocketSpotOrderInfo(symbol string, orderID int64) { - values := make(map[string]string) - values["symbol"] = symbol - values["order_id"] = strconv.FormatInt(orderID, 10) - - channel := okcoinWebsocketSpotUSDOrderInfo - if o.WebsocketURL == okcoinWebsocketURLChina { - channel = okcoinWebsocketSpotCNYOrderInfo - } - - o.AddChannelAuthenticated(channel, values) -} - -// WebsocketFuturesOrderInfo requests futures order info on the websocket client -func (o *OKCoin) WebsocketFuturesOrderInfo(symbol, contractType string, orderID int64, orderStatus, currentPage, pageLength int) { - values := make(map[string]string) - values["symbol"] = symbol - values["order_id"] = strconv.FormatInt(orderID, 10) - values["contract_type"] = contractType - values["status"] = strconv.Itoa(orderStatus) - values["current_page"] = strconv.Itoa(currentPage) - values["page_length"] = strconv.Itoa(pageLength) - o.AddChannelAuthenticated(okcoinWebsocketFuturesOrderInfo, values) -} - -// ConvertToURLValues converts values to url.Values -func (o *OKCoin) ConvertToURLValues(values map[string]string) url.Values { - urlVals := url.Values{} - for i, x := range values { - urlVals.Set(i, x) - } - return urlVals -} - -// WebsocketSign signs values on the webcoket client -func (o *OKCoin) WebsocketSign(values map[string]string) string { - values["api_key"] = o.APIKey - urlVals := o.ConvertToURLValues(values) - return strings.ToUpper(common.HexEncodeToString(common.GetMD5([]byte(urlVals.Encode() + "&secret_key=" + o.APISecret)))) -} - -// AddChannelAuthenticated adds an authenticated channel on the websocket client -func (o *OKCoin) AddChannelAuthenticated(channel string, values map[string]string) { - values["sign"] = o.WebsocketSign(values) - event := WebsocketEventAuth{"addChannel", channel, values} - json, err := common.JSONEncode(event) - if err != nil { - log.Println(err) - return - } - err = o.WebsocketConn.WriteMessage(websocket.TextMessage, json) - - if err != nil { - log.Println(err) - return - } - - if o.Verbose { - log.Printf("%s Adding authenticated channel: %s\n", o.GetName(), channel) - } -} - -// RemoveChannelAuthenticated removes the added authenticated channel on the -// websocket client -func (o *OKCoin) RemoveChannelAuthenticated(conn *websocket.Conn, channel string, values map[string]string) { - values["sign"] = o.WebsocketSign(values) - event := WebsocketEventAuthRemove{"removeChannel", channel, values} - json, err := common.JSONEncode(event) - if err != nil { - log.Println(err) - return - } - err = o.WebsocketConn.WriteMessage(websocket.TextMessage, json) - - if err != nil { - log.Println(err) - return - } - - if o.Verbose { - log.Printf("%s Removing authenticated channel: %s\n", o.GetName(), channel) - } -} - -// WebsocketClient starts a websocket client -func (o *OKCoin) WebsocketClient() { - klineValues := []string{"1min", "3min", "5min", "15min", "30min", "1hour", "2hour", "4hour", "6hour", "12hour", "day", "3day", "week"} - var currencyChan, userinfoChan string - - if o.WebsocketURL == okcoinWebsocketURLChina { - currencyChan = okcoinWebsocketCNYRealTrades - userinfoChan = okcoinWebsocketSpotCNYUserInfo - } else { - currencyChan = okcoinWebsocketUSDRealTrades - userinfoChan = okcoinWebsocketSpotUSDUserInfo - } - - for o.Enabled && o.Websocket { - var Dialer websocket.Dialer - var err error - o.WebsocketConn, _, err = Dialer.Dial(o.WebsocketURL, http.Header{}) + var dialer websocket.Dialer + if o.Websocket.GetProxyAddress() != "" { + proxy, err := url.Parse(o.Websocket.GetProxyAddress()) if err != nil { - log.Printf("%s Unable to connect to Websocket. Error: %s\n", o.GetName(), err) - continue + return err } - if o.Verbose { - log.Printf("%s Connected to Websocket.\n", o.GetName()) + dialer.Proxy = http.ProxyURL(proxy) + } + + var err error + o.WebsocketConn, _, err = dialer.Dial(o.Websocket.GetWebsocketURL(), + http.Header{}) + if err != nil { + return err + } + + o.WebsocketConn.SetPingHandler(o.PingHandler) + + go o.WsReadData() + go o.WsHandleData() + + for _, p := range o.GetEnabledCurrencies() { + fPair := exchange.FormatExchangeCurrency(o.GetName(), p) + + o.AddChannel(fmt.Sprintf(wsSubDepthFull, fPair.String(), "20")) + o.AddChannel(fmt.Sprintf(wsSubKline, fPair.String(), klineValues[0])) + o.AddChannel(fmt.Sprintf(wsSubTicker, fPair.String())) + o.AddChannel(fmt.Sprintf(wsSubTrades, fPair.String())) + } + + return nil +} + +// WsReadData reads from the websocket connection +func (o *OKCoin) WsReadData() { + o.Websocket.Wg.Add(1) + + defer func() { + err := o.WebsocketConn.Close() + if err != nil { + o.Websocket.DataHandler <- fmt.Errorf("okcoin_websocket.go - Unable to to close Websocket connection. Error: %s", + err) } + o.Websocket.Wg.Done() + }() - o.WebsocketConn.SetPingHandler(o.PingHandler) + for { + select { + case <-o.Websocket.ShutdownC: + return - if o.AuthenticatedAPISupport { - if o.WebsocketURL == okcoinWebsocketURL { - o.AddChannelAuthenticated(okcoinWebsocketFuturesRealTrades, map[string]string{}) - o.AddChannelAuthenticated(okcoinWebsocketFuturesUserInfo, map[string]string{}) - } - o.AddChannelAuthenticated(currencyChan, map[string]string{}) - o.AddChannelAuthenticated(userinfoChan, map[string]string{}) - } - - for _, x := range o.EnabledPairs { - currency := common.StringToLower(x) - currencyUL := currency[0:3] + "_" + currency[3:] - if o.AuthenticatedAPISupport { - o.WebsocketSpotOrderInfo(currencyUL, -1) - } - if o.WebsocketURL == okcoinWebsocketURL { - o.AddChannel(fmt.Sprintf("ok_%s_future_index", currency)) - for _, y := range o.FuturesValues { - if o.AuthenticatedAPISupport { - o.WebsocketFuturesOrderInfo(currencyUL, y, -1, 1, 1, 50) - } - o.AddChannel(fmt.Sprintf("ok_%s_future_ticker_%s", currency, y)) - o.AddChannel(fmt.Sprintf("ok_%s_future_depth_%s_60", currency, y)) - o.AddChannel(fmt.Sprintf("ok_%s_future_trade_v1_%s", currency, y)) - for _, z := range klineValues { - o.AddChannel(fmt.Sprintf("ok_future_%s_kline_%s_%s", currency, y, z)) - } - } - } else { - o.AddChannel(fmt.Sprintf("ok_%s_ticker", currency)) - o.AddChannel(fmt.Sprintf("ok_%s_depth60", currency)) - o.AddChannel(fmt.Sprintf("ok_%s_trades_v1", currency)) - - for _, y := range klineValues { - o.AddChannel(fmt.Sprintf("ok_%s_kline_%s", currency, y)) - } - } - } - - for o.Enabled && o.Websocket { - msgType, resp, err := o.WebsocketConn.ReadMessage() + default: + _, resp, err := o.WebsocketConn.ReadMessage() if err != nil { - log.Println(err) - break + o.Websocket.DataHandler <- err + return } - switch msgType { - case websocket.TextMessage: - response := []interface{}{} - err = common.JSONDecode(resp, &response) - if err != nil { - log.Println(err) + o.Websocket.TrafficAlert <- struct{}{} + o.Websocket.Intercomm <- exchange.WebsocketResponse{Raw: resp} + } + } + +} + +// WsHandleData handles stream data from the websocket connection +func (o *OKCoin) WsHandleData() { + o.Websocket.Wg.Add(1) + defer o.Websocket.Wg.Done() + + for { + select { + case <-o.Websocket.ShutdownC: + return + + case resp := <-o.Websocket.Intercomm: + var init []WsResponse + err := common.JSONDecode(resp.Raw, &init) + if err != nil { + log.Fatal(err) + } + + if init[0].ErrorCode != "" { + log.Fatal(o.WebsocketErrors[init[0].ErrorCode]) + } + + if init[0].Success { + if init[0].Data == nil { continue } + } - for _, y := range response { - z := y.(map[string]interface{}) - channel := z["channel"] - data := z["data"] - success := z["success"] - errorcode := z["errorcode"] - channelStr, ok := channel.(string) + if init[0].Channel == "addChannel" { + continue + } - if !ok { - log.Println("Unable to convert channel to string") - continue + var currencyPairSlice []string + splitChar := common.SplitStrings(init[0].Channel, "_") + currencyPairSlice = append(currencyPairSlice, + common.StringToUpper(splitChar[3])) + currencyPairSlice = append(currencyPairSlice, + common.StringToUpper(splitChar[4])) + currencyPair := common.JoinStrings(currencyPairSlice, "-") + + assetType := common.StringToUpper(splitChar[2]) + + switch { + case common.StringContains(init[0].Channel, "ticker") && + common.StringContains(init[0].Channel, "spot"): + var ticker WsTicker + + err = common.JSONDecode(init[0].Data, &ticker) + if err != nil { + log.Fatal(err) + + } + + o.Websocket.DataHandler <- exchange.TickerData{ + Timestamp: time.Unix(0, ticker.Timestamp), + Pair: pair.NewCurrencyPairFromString(currencyPair), + AssetType: assetType, + Exchange: o.GetName(), + ClosePrice: ticker.Close, + OpenPrice: ticker.Open, + HighPrice: ticker.Last, + LowPrice: ticker.Low, + Quantity: ticker.Volume, + } + + case common.StringContains(init[0].Channel, "depth"): + var orderbook WsOrderbook + + err = common.JSONDecode(init[0].Data, &orderbook) + if err != nil { + log.Fatal(err) + } + + o.Websocket.DataHandler <- exchange.WebsocketOrderbookUpdate{ + Pair: pair.NewCurrencyPairFromString(currencyPair), + Exchange: o.GetName(), + Asset: assetType, + } + + case common.StringContains(init[0].Channel, "kline"): + var klineData [][]interface{} + + err = common.JSONDecode(init[0].Data, &klineData) + if err != nil { + log.Fatal(err) + } + + var klines []WsKlines + for _, data := range klineData { + var newKline WsKlines + + newKline.Timestamp, _ = strconv.ParseInt(data[0].(string), 10, 64) + newKline.Open, _ = strconv.ParseFloat(data[1].(string), 64) + newKline.High, _ = strconv.ParseFloat(data[1].(string), 64) + newKline.Low, _ = strconv.ParseFloat(data[1].(string), 64) + newKline.Close, _ = strconv.ParseFloat(data[1].(string), 64) + newKline.Volume, _ = strconv.ParseFloat(data[1].(string), 64) + + klines = append(klines, newKline) + } + + for _, data := range klines { + o.Websocket.DataHandler <- exchange.KlineData{ + Timestamp: time.Unix(0, data.Timestamp), + Pair: pair.NewCurrencyPairFromString(currencyPair), + AssetType: assetType, + Exchange: o.GetName(), + OpenPrice: data.Open, + ClosePrice: data.Close, + HighPrice: data.High, + LowPrice: data.Low, + Volume: data.Volume, } + } - if success != "true" && success != nil { - errorCodeStr, ok := errorcode.(string) - if !ok { - log.Printf("%s Websocket: Unable to convert errorcode to string.\n", o.GetName()) - log.Printf("%s Websocket: channel %s error code: %s.\n", o.GetName(), channelStr, errorcode) - } else { - log.Printf("%s Websocket: channel %s error: %s.\n", o.GetName(), channelStr, o.WebsocketErrors[errorCodeStr]) - } - continue - } + case common.StringContains(init[0].Channel, "spot") && + common.StringContains(init[0].Channel, "deals"): + var dealsData [][]interface{} + err = common.JSONDecode(init[0].Data, &dealsData) + if err != nil { + log.Fatal(err) + } - if success == "true" { - if data == nil { - continue - } - } + var deals []WsDeals + for _, data := range dealsData { + var newDeal WsDeals + newDeal.TID, _ = strconv.ParseInt(data[0].(string), 10, 64) + newDeal.Price, _ = strconv.ParseFloat(data[1].(string), 64) + newDeal.Amount, _ = strconv.ParseFloat(data[2].(string), 64) + newDeal.Timestamp, _ = data[3].(string) + newDeal.Type, _ = data[4].(string) - dataJSON, err := common.JSONEncode(data) - - if err != nil { - log.Println(err) - continue - } - - switch true { - case common.StringContains(channelStr, "ticker") && !common.StringContains(channelStr, "future"): - tickerValues := []string{"buy", "high", "last", "low", "sell", "timestamp"} - tickerMap := data.(map[string]interface{}) - ticker := WebsocketTicker{} - ticker.Vol = tickerMap["vol"].(string) - - for _, z := range tickerValues { - result := reflect.TypeOf(tickerMap[z]).String() - if result == "string" { - value, errTickVals := strconv.ParseFloat(tickerMap[z].(string), 64) - if errTickVals != nil { - log.Println(errTickVals) - continue - } - - switch z { - case "buy": - ticker.Buy = value - case "high": - ticker.High = value - case "last": - ticker.Last = value - case "low": - ticker.Low = value - case "sell": - ticker.Sell = value - case "timestamp": - ticker.Timestamp = value - } - - } else if result == "float64" { - switch z { - case "buy": - ticker.Buy = tickerMap[z].(float64) - case "high": - ticker.High = tickerMap[z].(float64) - case "last": - ticker.Last = tickerMap[z].(float64) - case "low": - ticker.Low = tickerMap[z].(float64) - case "sell": - ticker.Sell = tickerMap[z].(float64) - case "timestamp": - ticker.Timestamp = tickerMap[z].(float64) - } - } - } - case common.StringContains(channelStr, "ticker") && common.StringContains(channelStr, "future"): - ticker := WebsocketFuturesTicker{} - err = common.JSONDecode(dataJSON, &ticker) - - if err != nil { - log.Println(err) - continue - } - case common.StringContains(channelStr, "depth"): - orderbook := WebsocketOrderbook{} - err = common.JSONDecode(dataJSON, &orderbook) - - if err != nil { - log.Println(err) - continue - } - case common.StringContains(channelStr, "trades_v1") || common.StringContains(channelStr, "trade_v1"): - type TradeResponse struct { - Data [][]string - } - - trades := TradeResponse{} - err = common.JSONDecode(dataJSON, &trades.Data) - - if err != nil { - log.Println(err) - continue - } - // to-do: convert from string array to trade struct - case common.StringContains(channelStr, "kline"): - klines := []interface{}{} - - err = common.JSONDecode(dataJSON, &klines) - if err != nil { - log.Println(err) - continue - } - case common.StringContains(channelStr, "spot") && common.StringContains(channelStr, "realtrades"): - if string(dataJSON) == "null" { - continue - } - realtrades := WebsocketRealtrades{} - - err = common.JSONDecode(dataJSON, &realtrades) - if err != nil { - log.Println(err) - continue - } - case common.StringContains(channelStr, "future") && common.StringContains(channelStr, "realtrades"): - if string(dataJSON) == "null" { - continue - } - realtrades := WebsocketFuturesRealtrades{} - - err = common.JSONDecode(dataJSON, &realtrades) - if err != nil { - log.Println(err) - continue - } - case common.StringContains(channelStr, "spot") && common.StringContains(channelStr, "trade") || common.StringContains(channelStr, "futures") && common.StringContains(channelStr, "trade"): - tradeOrder := WebsocketTradeOrderResponse{} - - err = common.JSONDecode(dataJSON, &tradeOrder) - if err != nil { - log.Println(err) - continue - } - case common.StringContains(channelStr, "cancel_order"): - cancelOrder := WebsocketTradeOrderResponse{} - - err = common.JSONDecode(dataJSON, &cancelOrder) - if err != nil { - log.Println(err) - continue - } - case common.StringContains(channelStr, "spot") && common.StringContains(channelStr, "userinfo"): - userinfo := WebsocketUserinfo{} - - err = common.JSONDecode(dataJSON, &userinfo) - if err != nil { - log.Println(err) - continue - } - case common.StringContains(channelStr, "futureusd_userinfo"): - userinfo := WebsocketFuturesUserInfo{} - - err = common.JSONDecode(dataJSON, &userinfo) - if err != nil { - log.Println(err) - continue - } - case common.StringContains(channelStr, "spot") && common.StringContains(channelStr, "order_info"): - type OrderInfoResponse struct { - Result bool `json:"result"` - Orders []WebsocketOrder `json:"orders"` - } - var orders OrderInfoResponse - - err = common.JSONDecode(dataJSON, &orders) - if err != nil { - log.Println(err) - continue - } - case common.StringContains(channelStr, "futureusd_order_info"): - type OrderInfoResponse struct { - Result bool `json:"result"` - Orders []WebsocketFuturesOrder `json:"orders"` - } - var orders OrderInfoResponse - - err = common.JSONDecode(dataJSON, &orders) - if err != nil { - log.Println(err) - continue - } - case common.StringContains(channelStr, "future_index"): - index := WebsocketFutureIndex{} - - err = common.JSONDecode(dataJSON, &index) - if err != nil { - log.Println(err) - continue - } - } + deals = append(deals, newDeal) } } } - o.WebsocketConn.Close() - log.Printf("%s Websocket client disconnected.", o.GetName()) } } @@ -567,3 +301,54 @@ func (o *OKCoin) SetWebsocketErrorDefaults() { "20025": "Leverage rate error", } } + +// WsOrderbook defines orderbook data from websocket connection +type WsOrderbook struct { + Asks [][]string `json:"asks"` + Bids [][]string `json:"bids"` + Timestamp int64 `json:"timestamp"` +} + +// WsResponse defines initial response stream +type WsResponse struct { + Channel string `json:"channel"` + Result bool `json:"result"` + Success bool `json:"success"` + ErrorCode string `json:"errorcode"` + Data json.RawMessage `json:"data"` +} + +// WsKlines defines a Kline response data from the websocket connection +type WsKlines struct { + Timestamp int64 + Open float64 + High float64 + Low float64 + Close float64 + Volume float64 +} + +// WsTicker holds ticker data for websocket +type WsTicker struct { + High float64 `json:"high,string"` + Volume float64 `json:"vol,string"` + Last float64 `json:"last,string"` + Low float64 `json:"low,string"` + Buy float64 `json:"buy,string"` + Change float64 `json:"change,string"` + Sell float64 `json:"sell,string"` + DayLow float64 `json:"dayLow,string"` + Close float64 `json:"close,string"` + DayHigh float64 `json:"dayHigh,string"` + Open float64 `json:"open,string"` + Timestamp int64 `json:"timestamp"` +} + +// WsDeals defines a deal response from the websocket connection +type WsDeals struct { + TID int64 + Price float64 + Amount float64 + Timestamp string + Type string +} diff --git a/exchanges/okcoin/okcoin_wrapper.go b/exchanges/okcoin/okcoin_wrapper.go index 38692263..255b6039 100644 --- a/exchanges/okcoin/okcoin_wrapper.go +++ b/exchanges/okcoin/okcoin_wrapper.go @@ -24,7 +24,7 @@ func (o *OKCoin) Start(wg *sync.WaitGroup) { // Run implements the OKCoin wrapper func (o *OKCoin) Run() { if o.Verbose { - log.Printf("%s Websocket: %s. (url: %s).\n", o.GetName(), common.IsEnabled(o.Websocket), o.WebsocketURL) + log.Printf("%s Websocket: %s. (url: %s).\n", o.GetName(), common.IsEnabled(o.Websocket.IsEnabled()), o.WebsocketURL) log.Printf("%s polling delay: %ds.\n", o.GetName(), o.RESTPollingDelay) log.Printf("%s %d currencies enabled: %s.\n", o.GetName(), len(o.EnabledPairs), o.EnabledPairs) } @@ -56,10 +56,6 @@ func (o *OKCoin) Run() { } } } - - if o.Websocket { - go o.WebsocketClient() - } } // UpdateTicker updates and returns the ticker for a currency pair @@ -238,3 +234,8 @@ func (o *OKCoin) WithdrawFiatExchangeFunds(currency pair.CurrencyItem, amount fl func (o *OKCoin) WithdrawFiatExchangeFundsToInternationalBank(currency pair.CurrencyItem, amount float64) (string, error) { return "", errors.New("not yet implemented") } + +// GetWebsocket returns a pointer to the exchange websocket +func (o *OKCoin) GetWebsocket() (*exchange.Websocket, error) { + return o.Websocket, nil +} diff --git a/exchanges/okex/okex.go b/exchanges/okex/okex.go index 7ab2a07c..8dbd976a 100644 --- a/exchanges/okex/okex.go +++ b/exchanges/okex/okex.go @@ -101,7 +101,6 @@ func (o *OKEX) SetDefaults() { o.Name = "OKEX" o.Enabled = false o.Verbose = false - o.Websocket = false o.RESTPollingDelay = 10 o.RequestCurrencyPairFormat.Delimiter = "_" o.RequestCurrencyPairFormat.Uppercase = false @@ -116,6 +115,7 @@ func (o *OKEX) SetDefaults() { o.APIUrlDefault = apiURL o.APIUrl = o.APIUrlDefault o.AssetTypes = []string{ticker.Spot} + o.WebsocketInit() } // Setup method sets current configuration details if enabled @@ -130,7 +130,7 @@ func (o *OKEX) Setup(exch config.ExchangeConfig) { o.SetHTTPClientUserAgent(exch.HTTPUserAgent) o.RESTPollingDelay = exch.RESTPollingDelay o.Verbose = exch.Verbose - o.Websocket = exch.Websocket + o.Websocket.SetEnabled(exch.Websocket) o.BaseCurrencies = common.SplitStrings(exch.BaseCurrencies, ",") o.AvailablePairs = common.SplitStrings(exch.AvailablePairs, ",") o.EnabledPairs = common.SplitStrings(exch.EnabledPairs, ",") @@ -150,6 +150,18 @@ func (o *OKEX) Setup(exch config.ExchangeConfig) { if err != nil { log.Fatal(err) } + err = o.SetClientProxyAddress(exch.ProxyAddress) + if err != nil { + log.Fatal(err) + } + err = o.WebsocketSetup(o.WsConnect, + exch.Name, + exch.Websocket, + okexDefaultWebsocketURL, + exch.WebsocketURL) + if err != nil { + log.Fatal(err) + } } } @@ -588,7 +600,11 @@ func (o *OKEX) PlaceContractOrders(symbol, contractType, position string, levera return 0, o.GetErrorCode(code) } - return contractMap["order_id"].(float64), nil + if orderID, ok := contractMap["order_id"]; ok { + return orderID.(float64), nil + } + + return 0, errors.New("orderID returned nil") } // GetContractFuturesTradeHistory returns OKEX Contract Trade History (Not for Personal) diff --git a/exchanges/okex/okex_types.go b/exchanges/okex/okex_types.go index dbcb792b..0144798c 100644 --- a/exchanges/okex/okex_types.go +++ b/exchanges/okex/okex_types.go @@ -19,11 +19,13 @@ type ContractPrice struct { Error interface{} `json:"error_code"` } +// MultiStreamData contains raw data from okex type MultiStreamData struct { Channel string `json:"channel"` Data json.RawMessage `json:"data"` } +// TickerStreamData contains ticker stream data from okex type TickerStreamData struct { Buy string `json:"buy"` Change string `json:"change"` @@ -37,9 +39,13 @@ type TickerStreamData struct { Vol string `json:"vol"` } +// DealsStreamData defines Deals data type DealsStreamData = [][]string + +// KlineStreamData defines kline data type KlineStreamData = [][]string +// DepthStreamData defines orderbook depth type DepthStreamData struct { Asks [][]string `json:"asks"` Bids [][]string `json:"bids"` diff --git a/exchanges/okex/okex_websocket.go b/exchanges/okex/okex_websocket.go index 90cea0d6..5d396ea0 100644 --- a/exchanges/okex/okex_websocket.go +++ b/exchanges/okex/okex_websocket.go @@ -1,14 +1,19 @@ package okex import ( + "errors" "fmt" "log" "net/http" + "net/url" + "strconv" "strings" "time" "github.com/gorilla/websocket" "github.com/thrasher-/gocryptotrader/common" + "github.com/thrasher-/gocryptotrader/currency/pair" + "github.com/thrasher-/gocryptotrader/exchanges" ) const ( @@ -22,129 +27,282 @@ func (o *OKEX) writeToWebsocket(message string) error { return o.WebsocketConn.WriteMessage(websocket.TextMessage, []byte(message)) } -func (o *OKEX) websocketConnect() { - var Dialer websocket.Dialer +// WsConnect initiates a websocket connection +func (o *OKEX) WsConnect() error { + if !o.Websocket.IsEnabled() || !o.IsEnabled() { + return errors.New(exchange.WebsocketNotEnabled) + } + + 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) + } + var err error + o.WebsocketConn, _, err = dialer.Dial(o.Websocket.GetWebsocketURL(), + http.Header{}) + if err != nil { + return fmt.Errorf("%s Unable to connect to Websocket. Error: %s", + o.Name, + err) + } + + go o.WsHandleData() + go o.WsReadData() + go o.wsPingHandler() + + err = o.WsSubscribe() + if err != nil { + return fmt.Errorf("Error: Could not subscribe to the OKEX websocket %s", + err) + } + + return nil +} + +// WsSubscribe subscribes to the websocket channels +func (o *OKEX) WsSubscribe() error { myEnabledSubscriptionChannels := []string{} for _, pair := range o.EnabledPairs { - myEnabledSubscriptionChannels = append(myEnabledSubscriptionChannels, fmt.Sprintf("{'event':'addChannel','channel':'ok_sub_spot_%s_ticker'}", pair)) - myEnabledSubscriptionChannels = append(myEnabledSubscriptionChannels, fmt.Sprintf("{'event':'addChannel','channel':'ok_sub_spot_%s_depth'}", pair)) - myEnabledSubscriptionChannels = append(myEnabledSubscriptionChannels, fmt.Sprintf("{'event':'addChannel','channel':'ok_sub_spot_%s_deals'}", pair)) - myEnabledSubscriptionChannels = append(myEnabledSubscriptionChannels, fmt.Sprintf("{'event':'addChannel','channel':'ok_sub_spot_%s_kline_1min'}", pair)) + + // ----------- deprecate when usd pairs are upgraded to usdt ---------- + checkSymbol := common.SplitStrings(pair, "_") + for i := range checkSymbol { + if common.StringContains(checkSymbol[i], "usdt") { + break + } + if common.StringContains(checkSymbol[i], "usd") { + checkSymbol[i] = "usdt" + } + } + + symbolRedone := common.JoinStrings(checkSymbol, "_") + // ----------- deprecate when usd pairs are upgraded to usdt ---------- + + myEnabledSubscriptionChannels = append(myEnabledSubscriptionChannels, + fmt.Sprintf("{'event':'addChannel','channel':'ok_sub_spot_%s_ticker'}", + symbolRedone)) + + myEnabledSubscriptionChannels = append(myEnabledSubscriptionChannels, + fmt.Sprintf("{'event':'addChannel','channel':'ok_sub_spot_%s_depth'}", + symbolRedone)) + + myEnabledSubscriptionChannels = append(myEnabledSubscriptionChannels, + fmt.Sprintf("{'event':'addChannel','channel':'ok_sub_spot_%s_deals'}", + symbolRedone)) + + myEnabledSubscriptionChannels = append(myEnabledSubscriptionChannels, + fmt.Sprintf("{'event':'addChannel','channel':'ok_sub_spot_%s_kline_1min'}", + symbolRedone)) } - mySubscriptionString := "[" + strings.Join(myEnabledSubscriptionChannels, ",") + "]" - - o.WebsocketConn, _, err = Dialer.Dial(okexDefaultWebsocketURL, http.Header{}) - - if err != nil { - log.Printf("%s Unable to connect to Websocket. Error: %s\n", o.Name, err) - return + for _, outgoing := range myEnabledSubscriptionChannels { + err := o.writeToWebsocket(outgoing) + if err != nil { + return err + } } - if o.Verbose { - log.Printf("%s Connected to Websocket.\n", o.Name) - log.Printf("Subscription String is %s\n", mySubscriptionString) - } + return nil +} - log.Printf("Subscription String is %s\n", mySubscriptionString) +// WsReadData reads data from the websocket connection +func (o *OKEX) WsReadData() { + o.Websocket.Wg.Add(1) - // subscribe to all the desired subscriptions - err = o.writeToWebsocket(mySubscriptionString) + 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() + }() - if err != nil { - log.Printf("Error: Could not subscribe to the OKEX websocket %s", err) - return + for { + select { + case <-o.Websocket.ShutdownC: + return + + default: + _, resp, err := o.WebsocketConn.ReadMessage() + if err != nil { + o.Websocket.DataHandler <- err + return + } + o.Websocket.TrafficAlert <- struct{}{} + o.Websocket.Intercomm <- exchange.WebsocketResponse{Raw: resp} + } } } -// WebsocketClient the main function handling the OKEX websocket -// Documentation URL: https://github.com/okcoin-okex/API-docs-OKEx.com/blob/master/API-For-Spot-EN/WEBSOCKET%20API%20for%20SPOT.md -func (o *OKEX) WebsocketClient() { - for o.Enabled && o.Websocket { - o.websocketConnect() +func (o *OKEX) wsPingHandler() { + o.Websocket.Wg.Add(1) + defer o.Websocket.Wg.Done() - go func() { - for { - time.Sleep(time.Second * 27) - o.writeToWebsocket("{'event':'ping'}") - log.Printf("%s sent Ping message\n", o.GetName()) - } - }() + ticker := time.NewTicker(time.Second * 27) - for o.Enabled && o.Websocket { - msgType, resp, err := o.WebsocketConn.ReadMessage() + for { + select { + case <-o.Websocket.ShutdownC: + return + case <-ticker.C: + err := o.writeToWebsocket("{'event':'ping'}") if err != nil { - log.Printf("Error: Could not read from the OKEX websocket %s", err) - o.websocketConnect() - continue + o.Websocket.DataHandler <- err + return + } + } + } +} + +// WsHandleData handles the read data from the websocket connection +func (o *OKEX) WsHandleData() { + o.Websocket.Wg.Add(1) + defer o.Websocket.Wg.Done() + + for { + select { + case <-o.Websocket.ShutdownC: + return + + case resp := <-o.Websocket.Intercomm: + multiStreamDataArr := []MultiStreamData{} + + err := common.JSONDecode(resp.Raw, &multiStreamDataArr) + if err != nil { + if strings.Contains(string(resp.Raw), "pong") { + continue + } else { + log.Fatal("okex.go error -", err) + } } - switch msgType { - case websocket.TextMessage: - multiStreamDataArr := []MultiStreamData{} - - err = common.JSONDecode(resp, &multiStreamDataArr) - - if err != nil { - if strings.Contains(string(resp), "pong") { - log.Printf("%s received Pong message\n", o.GetName()) - } else { - log.Printf("%s some other error happened: %s", o.GetName(), err) - continue + for _, multiStreamData := range multiStreamDataArr { + var errResponse ErrorResponse + if common.StringContains(string(resp.Raw), "error_msg") { + err = common.JSONDecode(resp.Raw, &errResponse) + if err != nil { + log.Fatal(err) } + o.Websocket.DataHandler <- fmt.Errorf("okex.go error - %s resp: %s ", + errResponse.ErrorMsg, + string(resp.Raw)) + continue } - for _, multiStreamData := range multiStreamDataArr { - if strings.Contains(multiStreamData.Channel, "ticker") { - // ticker data - ticker := TickerStreamData{} - tickerDecodeError := common.JSONDecode(multiStreamData.Data, &ticker) + var newPair string + var assetType string + currencyPairSlice := common.SplitStrings(multiStreamData.Channel, "_") + if len(currencyPairSlice) > 5 { + newPair = currencyPairSlice[3] + "_" + currencyPairSlice[4] + assetType = currencyPairSlice[2] + } - if tickerDecodeError != nil { - log.Printf("OKEX Ticker Decode Error: %s", tickerDecodeError) - continue + if strings.Contains(multiStreamData.Channel, "ticker") { + var ticker TickerStreamData + + err = common.JSONDecode(multiStreamData.Data, &ticker) + if err != nil { + log.Fatal("OKEX Ticker Decode Error:", err) + } + + o.Websocket.DataHandler <- exchange.TickerData{ + Timestamp: time.Unix(0, int64(ticker.Timestamp)), + Exchange: o.GetName(), + AssetType: assetType, + } + + } else if strings.Contains(multiStreamData.Channel, "deals") { + var deals DealsStreamData + + err = common.JSONDecode(multiStreamData.Data, &deals) + if err != nil { + log.Fatal("OKEX Deals Decode Error:", err) + } + + for _, trade := range deals { + price, _ := strconv.ParseFloat(trade[1], 64) + amount, _ := strconv.ParseFloat(trade[2], 64) + time, _ := time.Parse(time.RFC3339, trade[3]) + + o.Websocket.DataHandler <- exchange.TradeData{ + Timestamp: time, + Exchange: o.GetName(), + AssetType: assetType, + CurrencyPair: pair.NewCurrencyPairFromString(newPair), + Price: price, + Amount: amount, + EventType: trade[4], } + } - log.Printf("OKEX Channel: %s\tData: %s\n", multiStreamData.Channel, multiStreamData.Data) - } else if strings.Contains(multiStreamData.Channel, "deals") { - // orderbook data - deals := DealsStreamData{} - decodeError := common.JSONDecode(multiStreamData.Data, &deals) + } else if strings.Contains(multiStreamData.Channel, "kline") { + var klines KlineStreamData - if decodeError != nil { - log.Printf("OKEX Deals Decode Error: %s", decodeError) - continue + err := common.JSONDecode(multiStreamData.Data, &klines) + if err != nil { + log.Fatal("OKEX Klines Decode Error:", err) + } + + for _, kline := range klines { + ntime, _ := strconv.ParseInt(kline[0], 10, 64) + open, _ := strconv.ParseFloat(kline[1], 64) + high, _ := strconv.ParseFloat(kline[2], 64) + low, _ := strconv.ParseFloat(kline[3], 64) + close, _ := strconv.ParseFloat(kline[4], 64) + volume, _ := strconv.ParseFloat(kline[5], 64) + + o.Websocket.DataHandler <- exchange.KlineData{ + Timestamp: time.Unix(ntime, 0), + Pair: pair.NewCurrencyPairFromString(newPair), + AssetType: assetType, + Exchange: o.GetName(), + OpenPrice: open, + HighPrice: high, + LowPrice: low, + ClosePrice: close, + Volume: volume, } + } - log.Printf("OKEX Channel: %s\tData: %s\n", multiStreamData.Channel, multiStreamData.Data) - } else if strings.Contains(multiStreamData.Channel, "kline") { - // 1 min kline data - klines := KlineStreamData{} - decodeError := common.JSONDecode(multiStreamData.Data, &klines) + } else if strings.Contains(multiStreamData.Channel, "depth") { + var depth DepthStreamData - if decodeError != nil { - log.Printf("OKEX Klines Decode Error: %s", decodeError) - continue - } + err := common.JSONDecode(multiStreamData.Data, &depth) + if err != nil { + log.Fatal("OKEX Depth Decode Error:", err) + } - log.Printf("OKEX Channel: %s\tData: %s\n", multiStreamData.Channel, multiStreamData.Data) - } else if strings.Contains(multiStreamData.Channel, "depth") { - // market depth data - depth := DepthStreamData{} - decodeError := common.JSONDecode(multiStreamData.Data, &depth) - - if decodeError != nil { - log.Printf("OKEX Depth Decode Error: %s", decodeError) - continue - } - - log.Printf("OKEX Channel: %s\tData: %s\n", multiStreamData.Channel, multiStreamData.Data) + o.Websocket.DataHandler <- exchange.WebsocketOrderbookUpdate{ + Exchange: o.GetName(), + Asset: assetType, + Pair: pair.NewCurrencyPairFromString(newPair), } } } } } } + +// ErrorResponse defines an error response type from the websocket connection +type ErrorResponse struct { + Result bool `json:"result"` + ErrorMsg string `json:"error_msg"` + ErrorCode int64 `json:"error_code"` +} + +// Request defines the JSON request structure to the websocket server +type Request struct { + Event string `json:"event"` + Channel string `json:"channel"` + Parameters string `json:"parameters,omitempty"` +} diff --git a/exchanges/okex/okex_wrapper.go b/exchanges/okex/okex_wrapper.go index 40f06b44..7f2cbba2 100644 --- a/exchanges/okex/okex_wrapper.go +++ b/exchanges/okex/okex_wrapper.go @@ -24,14 +24,10 @@ func (o *OKEX) Start(wg *sync.WaitGroup) { // Run implements the OKEX wrapper func (o *OKEX) Run() { if o.Verbose { - log.Printf("%s Websocket: %s. (url: %s).\n", o.GetName(), common.IsEnabled(o.Websocket), o.WebsocketURL) + log.Printf("%s Websocket: %s. (url: %s).\n", o.GetName(), common.IsEnabled(o.Websocket.IsEnabled()), o.WebsocketURL) log.Printf("%s polling delay: %ds.\n", o.GetName(), o.RESTPollingDelay) log.Printf("%s %d currencies enabled: %s.\n", o.GetName(), len(o.EnabledPairs), o.EnabledPairs) } - - if o.Websocket { - go o.WebsocketClient() - } } // UpdateTicker updates and returns the ticker for a currency pair @@ -204,3 +200,8 @@ func (o *OKEX) WithdrawFiatExchangeFunds(currency pair.CurrencyItem, amount floa func (o *OKEX) WithdrawFiatExchangeFundsToInternationalBank(currency pair.CurrencyItem, amount float64) (string, error) { return "", errors.New("not yet implemented") } + +// GetWebsocket returns a pointer to the exchange websocket +func (o *OKEX) GetWebsocket() (*exchange.Websocket, error) { + return o.Websocket, nil +} diff --git a/exchanges/orderbook/orderbook.go b/exchanges/orderbook/orderbook.go index 107ffbe6..b0ebd527 100644 --- a/exchanges/orderbook/orderbook.go +++ b/exchanges/orderbook/orderbook.go @@ -27,6 +27,7 @@ var ( type Item struct { Amount float64 Price float64 + ID int64 } // Base holds the fields for the orderbook base @@ -36,6 +37,7 @@ type Base struct { Bids []Item `json:"bids"` Asks []Item `json:"asks"` LastUpdated time.Time `json:"last_updated"` + AssetType string } // Orderbook holds the orderbook information for a currency pair and type diff --git a/exchanges/poloniex/poloniex.go b/exchanges/poloniex/poloniex.go index 6e17dfc9..d7f5ee7c 100644 --- a/exchanges/poloniex/poloniex.go +++ b/exchanges/poloniex/poloniex.go @@ -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" @@ -55,6 +56,7 @@ const ( // Poloniex is the overarching type across the poloniex package type Poloniex struct { exchange.Base + WebsocketConn *websocket.Conn } // SetDefaults sets default settings for poloniex @@ -63,7 +65,6 @@ func (p *Poloniex) SetDefaults() { p.Enabled = false p.Fee = 0 p.Verbose = false - p.Websocket = false p.RESTPollingDelay = 10 p.RequestCurrencyPairFormat.Delimiter = "_" p.RequestCurrencyPairFormat.Uppercase = true @@ -78,6 +79,7 @@ func (p *Poloniex) SetDefaults() { common.NewHTTPClientWithTimeout(exchange.DefaultHTTPTimeout)) p.APIUrlDefault = poloniexAPIURL p.APIUrl = p.APIUrlDefault + p.WebsocketInit() } // Setup sets user exchange configuration settings @@ -92,7 +94,7 @@ func (p *Poloniex) Setup(exch config.ExchangeConfig) { p.SetHTTPClientUserAgent(exch.HTTPUserAgent) p.RESTPollingDelay = exch.RESTPollingDelay 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, ",") @@ -112,6 +114,18 @@ func (p *Poloniex) 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, + poloniexWebsocketAddress, + exch.WebsocketURL) + if err != nil { + log.Fatal(err) + } } } diff --git a/exchanges/poloniex/poloniex_types.go b/exchanges/poloniex/poloniex_types.go index 9e0ad507..484d36b9 100644 --- a/exchanges/poloniex/poloniex_types.go +++ b/exchanges/poloniex/poloniex_types.go @@ -301,3 +301,36 @@ type WebsocketTrollboxMessage struct { Message string Reputation float64 } + +// WsCommand defines the request params after a websocket connection has been +// established +type WsCommand struct { + Command string `json:"command"` + Channel interface{} `json:"channel"` + APIKey string `json:"key,omitempty"` + Payload string `json:"payload,omitempty"` + Sign string `json:"sign,omitempty"` +} + +// WsTicker defines the websocket ticker response +type WsTicker struct { + LastPrice float64 + LowestAsk float64 + HighestBid float64 + PercentageChange float64 + BaseCurrencyVolume24H float64 + QuoteCurrencyVolume24H float64 + IsFrozen bool + HighestTradeIn24H float64 + LowestTradePrice24H float64 +} + +// WsTrade defines the websocket trade response +type WsTrade struct { + Symbol string + TradeID int64 + Side string + Volume float64 + Price float64 + Timestamp int64 +} diff --git a/exchanges/poloniex/poloniex_websocket.go b/exchanges/poloniex/poloniex_websocket.go index 1db5eeee..1142f7e6 100644 --- a/exchanges/poloniex/poloniex_websocket.go +++ b/exchanges/poloniex/poloniex_websocket.go @@ -1,166 +1,764 @@ package poloniex import ( + "errors" + "fmt" "log" + "net/http" + "net/url" "strconv" + "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 ( - poloniexWebsocketAddress = "wss://api.poloniex.com" - poloniexWebsocketRealm = "realm1" - poloniexWebsocketTicker = "ticker" - poloniexWebsocketTrollbox = "trollbox" + poloniexWebsocketAddress = "wss://api2.poloniex.com" + wsAccountNotificationID = 1000 + wsTickerDataID = 1002 + ws24HourExchangeVolumeID = 1003 + wsHeartbeat = 1010 ) -// OnTicker converts ticker data to a websocketTicker -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 initiates a websocket connection +func (p *Poloniex) WsConnect() error { + if !p.Websocket.IsEnabled() || !p.IsEnabled() { + return errors.New(exchange.WebsocketNotEnabled) } - ticker.High, _ = strconv.ParseFloat(args[8].(string), 64) - ticker.Low, _ = strconv.ParseFloat(args[9].(string), 64) -} - -// OnTrollbox handles 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 handles orderbook depth and trade events -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 PoloniexWebsocketOrderbookModify struct { - Type string - Rate float64 - Amount float64 - } - - orderModify := PoloniexWebsocketOrderbookModify{} - 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 PoloniexWebsocketOrderbookRemove struct { - Type string - Rate float64 - } - - orderRemoval := PoloniexWebsocketOrderbookRemove{} - orderRemoval.Type = msgData["type"].(string) - - rateStr := msgData["rate"].(string) - orderRemoval.Rate, _ = strconv.ParseFloat(rateStr, 64) - } - case "newTrade": - { - type PoloniexWebsocketNewTrade struct { - Type string - TradeID int64 - Rate float64 - Amount float64 - Date string - Total float64 - } - - trade := PoloniexWebsocketNewTrade{} - 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 creates a new websocket client -func (p *Poloniex) WebsocketClient() { - for p.Enabled && p.Websocket { - c, err := turnpike.NewWebsocketClient(turnpike.JSON, poloniexWebsocketAddress, nil) + var dialer websocket.Dialer + if p.Websocket.GetProxyAddress() != "" { + proxy, err := url.Parse(p.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(poloniexWebsocketRealm, nil) + var err error + p.WebsocketConn, _, err = dialer.Dial(p.Websocket.GetWebsocketURL(), + http.Header{}) + if err != nil { + return err + } + + go p.WsReadData() + go p.WsHandleData() + + 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(), + }) + + err = p.WebsocketConn.WriteMessage(websocket.TextMessage, orderbookJSON) if err != nil { - log.Printf("%s Unable to join realm. Error: %s\n", p.GetName(), err) - continue + return err } + } + return nil +} - if p.Verbose { - log.Printf("%s Joined Websocket realm.\n", p.GetName()) +// WsReadData reads data from the websocket connection +func (p *Poloniex) WsReadData() { + 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() + }() - c.ReceiveDone = make(chan bool) + for { + select { + case <-p.Websocket.ShutdownC: + return - if err := c.Subscribe(poloniexWebsocketTicker, OnTicker); err != nil { - log.Printf("%s Error subscribing to ticker channel: %s\n", p.GetName(), err) - } - - if err := c.Subscribe(poloniexWebsocketTrollbox, OnTrollbox); err != nil { - log.Printf("%s Error subscribing to trollbox channel: %s\n", p.GetName(), err) - } - - 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) + default: + _, resp, err := p.WebsocketConn.ReadMessage() + if err != nil { + p.Websocket.DataHandler <- err + return } - } - if p.Verbose { - log.Printf("%s Subscribed to websocket channels.\n", p.GetName()) + p.Websocket.TrafficAlert <- struct{}{} + p.Websocket.Intercomm <- exchange.WebsocketResponse{Raw: resp} } - - <-c.ReceiveDone - log.Printf("%s Websocket client disconnected.\n", p.GetName()) } } + +// WsHandleData handles data from the websocket connection +func (p *Poloniex) WsHandleData() { + p.Websocket.Wg.Add(1) + defer p.Websocket.Wg.Done() + + for { + select { + case <-p.Websocket.ShutdownC: + return + + case resp := <-p.Websocket.Intercomm: + var check []interface{} + err := common.JSONDecode(resp.Raw, &check) + if err != nil { + log.Fatal("poloniex_websocket.go - ", err) + } + + switch len(check) { + case 1: + if check[0].(float64) == wsHeartbeat { + continue + } + + case 2: + switch check[0].(type) { + case float64: + subscriptionID := check[0].(float64) + if subscriptionID == ws24HourExchangeVolumeID || + subscriptionID == wsAccountNotificationID || + subscriptionID == wsTickerDataID { + if check[1].(float64) != 1 { + p.Websocket.DataHandler <- errors.New("poloniex.go error - Subcription failed") + continue + } + continue + } + + case string: + orderbookSubscriptionID := check[0].(string) + if check[1].(float64) != 1 { + p.Websocket.DataHandler <- fmt.Errorf("poloniex.go error - orderbook subscription failed with symbol %s", + orderbookSubscriptionID) + continue + } + } + + case 3: + switch len(check[2].([]interface{})) { + case 1: + // Snapshot + datalevel1 := check[2].([]interface{}) + datalevel2 := datalevel1[0].([]interface{}) + + switch datalevel2[1].(type) { + case float64: + err := p.WsProcessOrderbookUpdate(datalevel2, + CurrencyPairID[int64(check[0].(float64))]) + if err != nil { + log.Fatal(err) + } + + p.Websocket.DataHandler <- exchange.WebsocketOrderbookUpdate{ + Exchange: p.GetName(), + Asset: "SPOT", + // Pair: pair.NewCurrencyPairFromString(currencyPair), + } + + case map[string]interface{}: + datalevel3 := datalevel2[1].(map[string]interface{}) + currencyPair, ok := datalevel3["currencyPair"].(string) + if !ok { + log.Fatal("poloniex.go error - could not find currency pair in map") + } + + orderbookData, ok := datalevel3["orderBook"].([]interface{}) + if !ok { + log.Fatal("poloniex.go error - could not find orderbook data in map") + } + + err := p.WsProcessOrderbookSnapshot(orderbookData, currencyPair) + if err != nil { + log.Fatal(err) + } + + p.Websocket.DataHandler <- exchange.WebsocketOrderbookUpdate{ + Exchange: p.GetName(), + Asset: "SPOT", + Pair: pair.NewCurrencyPairFromString(currencyPair), + } + continue + } + + case 10: + tickerData := check[2].([]interface{}) + var ticker WsTicker + + ticker.LastPrice, _ = tickerData[0].(float64) + // ticker.LowestAsk, _ = strconv.ParseFloat(tickerData[1].(string), 64) + ticker.HighestBid, _ = strconv.ParseFloat(tickerData[2].(string), 64) + ticker.PercentageChange, _ = strconv.ParseFloat(tickerData[3].(string), 64) + ticker.BaseCurrencyVolume24H, _ = strconv.ParseFloat(tickerData[4].(string), 64) + ticker.QuoteCurrencyVolume24H, _ = strconv.ParseFloat(tickerData[5].(string), 64) + frozen, _ := strconv.ParseInt(tickerData[6].(string), 10, 64) + if frozen == 1 { + ticker.IsFrozen = true + } + ticker.HighestTradeIn24H, _ = tickerData[7].(float64) + ticker.LowestTradePrice24H, _ = strconv.ParseFloat(tickerData[8].(string), 64) + + p.Websocket.DataHandler <- exchange.TickerData{ + Timestamp: time.Now(), + Exchange: p.GetName(), + AssetType: "SPOT", + LowPrice: ticker.LowestAsk, + HighPrice: ticker.HighestBid, + } + + default: + for _, element := range check[2].([]interface{}) { + switch element.(type) { + case []interface{}: + data := element.([]interface{}) + if data[0].(string) == "o" { + p.WsProcessOrderbookUpdate(data, CurrencyPairID[int64(check[0].(float64))]) + continue + } + + var trade WsTrade + + id, _ := strconv.ParseInt(data[0].(string), 10, 64) + trade.Symbol = CurrencyPairID[id] + trade.TradeID, _ = data[0].(int64) + trade.Side, _ = data[0].(string) + trade.Volume, _ = data[0].(float64) + trade.Price, _ = data[0].(float64) + trade.Timestamp, _ = data[0].(int64) + + p.Websocket.DataHandler <- exchange.TradeData{ + Timestamp: time.Unix(trade.Timestamp, 0), + // CurrencyPair: pair.NewCurrencyPairFromString(trade.Symbol), + Side: trade.Side, + Amount: trade.Volume, + Price: trade.Price, + } + } + } + } + } + } + } +} + +// WsProcessOrderbookSnapshot processes a new orderbook snapshot into a local +// of orderbooks +func (p *Poloniex) WsProcessOrderbookSnapshot(ob []interface{}, symbol string) error { + askdata := ob[0].(map[string]interface{}) + var asks []orderbook.Item + for price, volume := range askdata { + assetPrice, err := strconv.ParseFloat(price, 64) + if err != nil { + return err + } + + assetVolume, err := strconv.ParseFloat(volume.(string), 64) + if err != nil { + return err + } + + asks = append(asks, orderbook.Item{ + Price: assetPrice, + Amount: assetVolume, + }) + } + + bidData := ob[1].(map[string]interface{}) + var bids []orderbook.Item + for price, volume := range bidData { + assetPrice, err := strconv.ParseFloat(price, 64) + if err != nil { + return err + } + + assetVolume, err := strconv.ParseFloat(volume.(string), 64) + if err != nil { + return err + } + + bids = append(bids, orderbook.Item{ + Price: assetPrice, + Amount: assetVolume, + }) + } + + var newOrderbook orderbook.Base + newOrderbook.Asks = asks + newOrderbook.Bids = bids + newOrderbook.AssetType = "SPOT" + newOrderbook.CurrencyPair = symbol + newOrderbook.LastUpdated = time.Now() + newOrderbook.Pair = pair.NewCurrencyPairFromString(symbol) + + return p.Websocket.Orderbook.LoadSnapshot(newOrderbook, p.GetName()) +} + +// WsProcessOrderbookUpdate processses new orderbook updates +func (p *Poloniex) WsProcessOrderbookUpdate(target []interface{}, symbol string) error { + sideCheck := target[1].(float64) + + cP := pair.NewCurrencyPairFromString(symbol) + + price, err := strconv.ParseFloat(target[2].(string), 64) + if err != nil { + return err + } + + volume, err := strconv.ParseFloat(target[3].(string), 64) + if err != nil { + return err + } + + if sideCheck == 0 { + return p.Websocket.Orderbook.Update(nil, + []orderbook.Item{orderbook.Item{Price: price, Amount: volume}}, + cP, + time.Now(), + p.GetName(), + "SPOT") + } + + return p.Websocket.Orderbook.Update([]orderbook.Item{orderbook.Item{Price: price, Amount: volume}}, + nil, + cP, + time.Now(), + p.GetName(), + "SPOT") +} + +// CurrencyPairID contains a list of IDS for currency pairs. +var CurrencyPairID = map[int64]string{ + 7: "BTC_BCN", + 14: "BTC_BTS", + 15: "BTC_BURST", + 20: "BTC_CLAM", + 25: "BTC_DGB", + 27: "BTC_DOGE", + 24: "BTC_DASH", + 38: "BTC_GAME", + 43: "BTC_HUC", + 50: "BTC_LTC", + 51: "BTC_MAID", + 58: "BTC_OMNI", + 61: "BTC_NAV", + 64: "BTC_NMC", + 69: "BTC_NXT", + 75: "BTC_PPC", + 89: "BTC_STR", + 92: "BTC_SYS", + 97: "BTC_VIA", + 100: "BTC_VTC", + 108: "BTC_XCP", + 114: "BTC_XMR", + 116: "BTC_XPM", + 117: "BTC_XRP", + 112: "BTC_XEM", + 148: "BTC_ETH", + 150: "BTC_SC", + 153: "BTC_EXP", + 155: "BTC_FCT", + 160: "BTC_AMP", + 162: "BTC_DCR", + 163: "BTC_LSK", + 167: "BTC_LBC", + 168: "BTC_STEEM", + 170: "BTC_SBD", + 171: "BTC_ETC", + 174: "BTC_REP", + 177: "BTC_ARDR", + 178: "BTC_ZEC", + 182: "BTC_STRAT", + 184: "BTC_PASC", + 185: "BTC_GNT", + 187: "BTC_GNO", + 189: "BTC_BCH", + 192: "BTC_ZRX", + 194: "BTC_CVC", + 196: "BTC_OMG", + 198: "BTC_GAS", + 200: "BTC_STORJ", + 201: "BTC_EOS", + 204: "BTC_SNT", + 207: "BTC_KNC", + 210: "BTC_BAT", + 213: "BTC_LOOM", + 221: "BTC_QTUM", + 121: "USDT_BTC", + 216: "USDT_DOGE", + 122: "USDT_DASH", + 123: "USDT_LTC", + 124: "USDT_NXT", + 125: "USDT_STR", + 126: "USDT_XMR", + 127: "USDT_XRP", + 149: "USDT_ETH", + 219: "USDT_SC", + 218: "USDT_LSK", + 173: "USDT_ETC", + 175: "USDT_REP", + 180: "USDT_ZEC", + 217: "USDT_GNT", + 191: "USDT_BCH", + 220: "USDT_ZRX", + 203: "USDT_EOS", + 206: "USDT_SNT", + 209: "USDT_KNC", + 212: "USDT_BAT", + 215: "USDT_LOOM", + 223: "USDT_QTUM", + 129: "XMR_BCN", + 132: "XMR_DASH", + 137: "XMR_LTC", + 138: "XMR_MAID", + 140: "XMR_NXT", + 181: "XMR_ZEC", + 166: "ETH_LSK", + 169: "ETH_STEEM", + 172: "ETH_ETC", + 176: "ETH_REP", + 179: "ETH_ZEC", + 186: "ETH_GNT", + 188: "ETH_GNO", + 190: "ETH_BCH", + 193: "ETH_ZRX", + 195: "ETH_CVC", + 197: "ETH_OMG", + 199: "ETH_GAS", + 202: "ETH_EOS", + 205: "ETH_SNT", + 208: "ETH_KNC", + 211: "ETH_BAT", + 214: "ETH_LOOM", + 222: "ETH_QTUM", + 224: "USDC_BTC", + 226: "USDC_USDT", + 225: "USDC_ETH", +} + +// CurrencyID defines IDs to a currency supported by the exchange +var CurrencyID = map[int64]string{ + 1: "1CR", + 2: "ABY", + 3: "AC", + 4: "ACH", + 5: "ADN", + 6: "AEON", + 7: "AERO", + 8: "AIR", + 9: "APH", + 10: "AUR", + 11: "AXIS", + 12: "BALLS", + 13: "BANK", + 14: "BBL", + 15: "BBR", + 16: "BCC", + 17: "BCN", + 18: "BDC", + 19: "BDG", + 20: "BELA", + 21: "BITS", + 22: "BLK", + 23: "BLOCK", + 24: "BLU", + 25: "BNS", + 26: "BONES", + 27: "BOST", + 28: "BTC", + 29: "BTCD", + 30: "BTCS", + 31: "BTM", + 32: "BTS", + 33: "BURN", + 34: "BURST", + 35: "C2", + 36: "CACH", + 37: "CAI", + 38: "CC", + 39: "CCN", + 40: "CGA", + 41: "CHA", + 42: "CINNI", + 43: "CLAM", + 44: "CNL", + 45: "CNMT", + 46: "CNOTE", + 47: "COMM", + 48: "CON", + 49: "CORG", + 50: "CRYPT", + 51: "CURE", + 52: "CYC", + 53: "DGB", + 54: "DICE", + 55: "DIEM", + 56: "DIME", + 57: "DIS", + 58: "DNS", + 59: "DOGE", + 60: "DASH", + 61: "DRKC", + 62: "DRM", + 63: "DSH", + 64: "DVK", + 65: "EAC", + 66: "EBT", + 67: "ECC", + 68: "EFL", + 69: "EMC2", + 70: "EMO", + 71: "ENC", + 72: "eTOK", + 73: "EXE", + 74: "FAC", + 75: "FCN", + 76: "FIBRE", + 77: "FLAP", + 78: "FLDC", + 79: "FLT", + 80: "FOX", + 81: "FRAC", + 82: "FRK", + 83: "FRQ", + 84: "FVZ", + 85: "FZ", + 86: "FZN", + 87: "GAP", + 88: "GDN", + 89: "GEMZ", + 90: "GEO", + 91: "GIAR", + 92: "GLB", + 93: "GAME", + 94: "GML", + 95: "GNS", + 96: "GOLD", + 97: "GPC", + 98: "GPUC", + 99: "GRCX", + 100: "GRS", + 101: "GUE", + 102: "H2O", + 103: "HIRO", + 104: "HOT", + 105: "HUC", + 106: "HVC", + 107: "HYP", + 108: "HZ", + 109: "IFC", + 110: "ITC", + 111: "IXC", + 112: "JLH", + 113: "JPC", + 114: "JUG", + 115: "KDC", + 116: "KEY", + 117: "LC", + 118: "LCL", + 119: "LEAF", + 120: "LGC", + 121: "LOL", + 122: "LOVE", + 123: "LQD", + 124: "LTBC", + 125: "LTC", + 126: "LTCX", + 127: "MAID", + 128: "MAST", + 129: "MAX", + 130: "MCN", + 131: "MEC", + 132: "METH", + 133: "MIL", + 134: "MIN", + 135: "MINT", + 136: "MMC", + 137: "MMNXT", + 138: "MMXIV", + 139: "MNTA", + 140: "MON", + 141: "MRC", + 142: "MRS", + 143: "OMNI", + 144: "MTS", + 145: "MUN", + 146: "MYR", + 147: "MZC", + 148: "N5X", + 149: "NAS", + 150: "NAUT", + 151: "NAV", + 152: "NBT", + 153: "NEOS", + 154: "NL", + 155: "NMC", + 156: "NOBL", + 157: "NOTE", + 158: "NOXT", + 159: "NRS", + 160: "NSR", + 161: "NTX", + 162: "NXT", + 163: "NXTI", + 164: "OPAL", + 165: "PAND", + 166: "PAWN", + 167: "PIGGY", + 168: "PINK", + 169: "PLX", + 170: "PMC", + 171: "POT", + 172: "PPC", + 173: "PRC", + 174: "PRT", + 175: "PTS", + 176: "Q2C", + 177: "QBK", + 178: "QCN", + 179: "QORA", + 180: "QTL", + 181: "RBY", + 182: "RDD", + 183: "RIC", + 184: "RZR", + 185: "SDC", + 186: "SHIBE", + 187: "SHOPX", + 188: "SILK", + 189: "SJCX", + 190: "SLR", + 191: "SMC", + 192: "SOC", + 193: "SPA", + 194: "SQL", + 195: "SRCC", + 196: "SRG", + 197: "SSD", + 198: "STR", + 199: "SUM", + 200: "SUN", + 201: "SWARM", + 202: "SXC", + 203: "SYNC", + 204: "SYS", + 205: "TAC", + 206: "TOR", + 207: "TRUST", + 208: "TWE", + 209: "UIS", + 210: "ULTC", + 211: "UNITY", + 212: "URO", + 213: "USDE", + 214: "USDT", + 215: "UTC", + 216: "UTIL", + 217: "UVC", + 218: "VIA", + 219: "VOOT", + 220: "VRC", + 221: "VTC", + 222: "WC", + 223: "WDC", + 224: "WIKI", + 225: "WOLF", + 226: "X13", + 227: "XAI", + 228: "XAP", + 229: "XBC", + 230: "XC", + 231: "XCH", + 232: "XCN", + 233: "XCP", + 234: "XCR", + 235: "XDN", + 236: "XDP", + 237: "XHC", + 238: "XLB", + 239: "XMG", + 240: "XMR", + 241: "XPB", + 242: "XPM", + 243: "XRP", + 244: "XSI", + 245: "XST", + 246: "XSV", + 247: "XUSD", + 248: "XXC", + 249: "YACC", + 250: "YANG", + 251: "YC", + 252: "YIN", + 253: "XVC", + 254: "FLO", + 256: "XEM", + 258: "ARCH", + 260: "HUGE", + 261: "GRC", + 263: "IOC", + 265: "INDEX", + 267: "ETH", + 268: "SC", + 269: "BCY", + 270: "EXP", + 271: "FCT", + 272: "BITUSD", + 273: "BITCNY", + 274: "RADS", + 275: "AMP", + 276: "VOX", + 277: "DCR", + 278: "LSK", + 279: "DAO", + 280: "LBC", + 281: "STEEM", + 282: "SBD", + 283: "ETC", + 284: "REP", + 285: "ARDR", + 286: "ZEC", + 287: "STRAT", + 288: "NXC", + 289: "PASC", + 290: "GNT", + 291: "GNO", + 292: "BCH", + 293: "ZRX", + 294: "CVC", + 295: "OMG", + 296: "GAS", + 297: "STORJ", + 298: "EOS", + 299: "USDC", + 300: "SNT", + 301: "KNC", + 302: "BAT", + 303: "LOOM", + 304: "QTUM", +} diff --git a/exchanges/poloniex/poloniex_wrapper.go b/exchanges/poloniex/poloniex_wrapper.go index 6e4eb393..da204c12 100644 --- a/exchanges/poloniex/poloniex_wrapper.go +++ b/exchanges/poloniex/poloniex_wrapper.go @@ -13,54 +13,50 @@ import ( ) // Start starts the Poloniex go routine -func (po *Poloniex) Start(wg *sync.WaitGroup) { +func (p *Poloniex) Start(wg *sync.WaitGroup) { wg.Add(1) go func() { - po.Run() + p.Run() wg.Done() }() } // Run implements the Poloniex wrapper -func (po *Poloniex) Run() { - if po.Verbose { - log.Printf("%s Websocket: %s (url: %s).\n", po.GetName(), common.IsEnabled(po.Websocket), poloniexWebsocketAddress) - log.Printf("%s polling delay: %ds.\n", po.GetName(), po.RESTPollingDelay) - log.Printf("%s %d currencies enabled: %s.\n", po.GetName(), len(po.EnabledPairs), po.EnabledPairs) +func (p *Poloniex) Run() { + if p.Verbose { + log.Printf("%s Websocket: %s (url: %s).\n", p.GetName(), common.IsEnabled(p.Websocket.IsEnabled()), poloniexWebsocketAddress) + log.Printf("%s polling delay: %ds.\n", p.GetName(), p.RESTPollingDelay) + log.Printf("%s %d currencies enabled: %s.\n", p.GetName(), len(p.EnabledPairs), p.EnabledPairs) } - if po.Websocket { - go po.WebsocketClient() - } - - exchangeCurrencies, err := po.GetExchangeCurrencies() + exchangeCurrencies, err := p.GetExchangeCurrencies() if err != nil { - log.Printf("%s Failed to get available symbols.\n", po.GetName()) + log.Printf("%s Failed to get available symbols.\n", p.GetName()) } else { forceUpdate := false - if common.StringDataCompare(po.AvailablePairs, "BTC_USDT") { + if common.StringDataCompare(p.AvailablePairs, "BTC_USDT") { log.Printf("%s contains invalid pair, forcing upgrade of available currencies.\n", - po.GetName()) + p.GetName()) forceUpdate = true } - err = po.UpdateCurrencies(exchangeCurrencies, false, forceUpdate) + err = p.UpdateCurrencies(exchangeCurrencies, false, forceUpdate) if err != nil { - log.Printf("%s Failed to update available currencies %s.\n", po.GetName(), err) + log.Printf("%s Failed to update available currencies %s.\n", p.GetName(), err) } } } // UpdateTicker updates and returns the ticker for a currency pair -func (po *Poloniex) UpdateTicker(currencyPair pair.CurrencyPair, assetType string) (ticker.Price, error) { +func (p *Poloniex) UpdateTicker(currencyPair pair.CurrencyPair, assetType string) (ticker.Price, error) { var tickerPrice ticker.Price - tick, err := po.GetTicker() + tick, err := p.GetTicker() if err != nil { return tickerPrice, err } - for _, x := range po.GetEnabledCurrencies() { + for _, x := range p.GetEnabledCurrencies() { var tp ticker.Price - curr := exchange.FormatExchangeCurrency(po.GetName(), x).String() + curr := exchange.FormatExchangeCurrency(p.GetName(), x).String() tp.Pair = x tp.Ask = tick[curr].LowestAsk tp.Bid = tick[curr].HighestBid @@ -68,39 +64,39 @@ func (po *Poloniex) UpdateTicker(currencyPair pair.CurrencyPair, assetType strin tp.Last = tick[curr].Last tp.Low = tick[curr].Low24Hr tp.Volume = tick[curr].BaseVolume - ticker.ProcessTicker(po.GetName(), x, tp, assetType) + ticker.ProcessTicker(p.GetName(), x, tp, assetType) } - return ticker.GetTicker(po.Name, currencyPair, assetType) + return ticker.GetTicker(p.Name, currencyPair, assetType) } // GetTickerPrice returns the ticker for a currency pair -func (po *Poloniex) GetTickerPrice(currencyPair pair.CurrencyPair, assetType string) (ticker.Price, error) { - tickerNew, err := ticker.GetTicker(po.GetName(), currencyPair, assetType) +func (p *Poloniex) GetTickerPrice(currencyPair pair.CurrencyPair, assetType string) (ticker.Price, error) { + tickerNew, err := ticker.GetTicker(p.GetName(), currencyPair, assetType) if err != nil { - return po.UpdateTicker(currencyPair, assetType) + return p.UpdateTicker(currencyPair, assetType) } return tickerNew, nil } // GetOrderbookEx returns orderbook base on the currency pair -func (po *Poloniex) GetOrderbookEx(currencyPair pair.CurrencyPair, assetType string) (orderbook.Base, error) { - ob, err := orderbook.GetOrderbook(po.GetName(), currencyPair, assetType) +func (p *Poloniex) GetOrderbookEx(currencyPair pair.CurrencyPair, assetType string) (orderbook.Base, error) { + ob, err := orderbook.GetOrderbook(p.GetName(), currencyPair, assetType) if err != nil { - return po.UpdateOrderbook(currencyPair, assetType) + return p.UpdateOrderbook(currencyPair, assetType) } return ob, nil } // UpdateOrderbook updates and returns the orderbook for a currency pair -func (po *Poloniex) UpdateOrderbook(currencyPair pair.CurrencyPair, assetType string) (orderbook.Base, error) { +func (p *Poloniex) UpdateOrderbook(currencyPair pair.CurrencyPair, assetType string) (orderbook.Base, error) { var orderBook orderbook.Base - orderbookNew, err := po.GetOrderbook("", 1000) + orderbookNew, err := p.GetOrderbook("", 1000) if err != nil { return orderBook, err } - for _, x := range po.GetEnabledCurrencies() { - currency := exchange.FormatExchangeCurrency(po.Name, x).String() + for _, x := range p.GetEnabledCurrencies() { + currency := exchange.FormatExchangeCurrency(p.Name, x).String() data, ok := orderbookNew.Data[currency] if !ok { continue @@ -120,17 +116,17 @@ func (po *Poloniex) UpdateOrderbook(currencyPair pair.CurrencyPair, assetType st obItems = append(obItems, orderbook.Item{Amount: obData.Amount, Price: obData.Price}) } orderBook.Asks = obItems - orderbook.ProcessOrderbook(po.Name, x, orderBook, assetType) + orderbook.ProcessOrderbook(p.Name, x, orderBook, assetType) } - return orderbook.GetOrderbook(po.Name, currencyPair, assetType) + return orderbook.GetOrderbook(p.Name, currencyPair, assetType) } // GetExchangeAccountInfo retrieves balances for all enabled currencies for the // Poloniex exchange -func (po *Poloniex) GetExchangeAccountInfo() (exchange.AccountInfo, error) { +func (p *Poloniex) GetExchangeAccountInfo() (exchange.AccountInfo, error) { var response exchange.AccountInfo - response.ExchangeName = po.GetName() - accountBalance, err := po.GetBalances() + response.ExchangeName = p.GetName() + accountBalance, err := p.GetBalances() if err != nil { return response, err } @@ -146,64 +142,69 @@ func (po *Poloniex) GetExchangeAccountInfo() (exchange.AccountInfo, error) { // GetExchangeFundTransferHistory returns funding history, deposits and // withdrawals -func (po *Poloniex) GetExchangeFundTransferHistory() ([]exchange.FundHistory, error) { +func (p *Poloniex) GetExchangeFundTransferHistory() ([]exchange.FundHistory, error) { var fundHistory []exchange.FundHistory return fundHistory, errors.New("not supported on exchange") } // GetExchangeHistory returns historic trade data since exchange opening. -func (po *Poloniex) GetExchangeHistory(p pair.CurrencyPair, assetType string) ([]exchange.TradeHistory, error) { +func (p *Poloniex) GetExchangeHistory(cP pair.CurrencyPair, assetType string) ([]exchange.TradeHistory, error) { var resp []exchange.TradeHistory return resp, errors.New("trade history not yet implemented") } // SubmitExchangeOrder submits a new order -func (po *Poloniex) SubmitExchangeOrder(p pair.CurrencyPair, side exchange.OrderSide, orderType exchange.OrderType, amount, price float64, clientID string) (int64, error) { +func (p *Poloniex) SubmitExchangeOrder(cP pair.CurrencyPair, side exchange.OrderSide, orderType exchange.OrderType, amount, price float64, clientID string) (int64, error) { return 0, errors.New("not yet implemented") } // ModifyExchangeOrder will allow of changing orderbook placement and limit to // market conversion -func (po *Poloniex) ModifyExchangeOrder(orderID int64, action exchange.ModifyOrder) (int64, error) { +func (p *Poloniex) ModifyExchangeOrder(orderID int64, action exchange.ModifyOrder) (int64, error) { return 0, errors.New("not yet implemented") } // CancelExchangeOrder cancels an order by its corresponding ID number -func (po *Poloniex) CancelExchangeOrder(orderID int64) error { +func (p *Poloniex) CancelExchangeOrder(orderID int64) error { return errors.New("not yet implemented") } // CancelAllExchangeOrders cancels all orders associated with a currency pair -func (po *Poloniex) CancelAllExchangeOrders() error { +func (p *Poloniex) CancelAllExchangeOrders() error { return errors.New("not yet implemented") } // GetExchangeOrderInfo returns information on a current open order -func (po *Poloniex) GetExchangeOrderInfo(orderID int64) (exchange.OrderDetail, error) { +func (p *Poloniex) GetExchangeOrderInfo(orderID int64) (exchange.OrderDetail, error) { var orderDetail exchange.OrderDetail return orderDetail, errors.New("not yet implemented") } // GetExchangeDepositAddress returns a deposit address for a specified currency -func (po *Poloniex) GetExchangeDepositAddress(cryptocurrency pair.CurrencyItem) (string, error) { +func (p *Poloniex) GetExchangeDepositAddress(cryptocurrency pair.CurrencyItem) (string, error) { return "", errors.New("not yet implemented") } // WithdrawCryptoExchangeFunds returns a withdrawal ID when a withdrawal is // submitted -func (po *Poloniex) WithdrawCryptoExchangeFunds(address string, cryptocurrency pair.CurrencyItem, amount float64) (string, error) { +func (p *Poloniex) WithdrawCryptoExchangeFunds(address string, cryptocurrency pair.CurrencyItem, amount float64) (string, error) { return "", errors.New("not yet implemented") } // WithdrawFiatExchangeFunds returns a withdrawal ID when a // withdrawal is submitted -func (po *Poloniex) WithdrawFiatExchangeFunds(currency pair.CurrencyItem, amount float64) (string, error) { +func (p *Poloniex) WithdrawFiatExchangeFunds(currency pair.CurrencyItem, amount float64) (string, error) { return "", errors.New("not yet implemented") } // WithdrawFiatExchangeFundsToInternationalBank returns a withdrawal ID when a // withdrawal is submitted -func (po *Poloniex) WithdrawFiatExchangeFundsToInternationalBank(currency pair.CurrencyItem, amount float64) (string, error) { +func (p *Poloniex) WithdrawFiatExchangeFundsToInternationalBank(currency pair.CurrencyItem, amount float64) (string, error) { return "", errors.New("not yet implemented") } + +// GetWebsocket returns a pointer to the exchange websocket +func (p *Poloniex) GetWebsocket() (*exchange.Websocket, error) { + return p.Websocket, nil +} diff --git a/exchanges/request/request.go b/exchanges/request/request.go index 63ba3c24..fbd5c192 100644 --- a/exchanges/request/request.go +++ b/exchanges/request/request.go @@ -7,6 +7,7 @@ import ( "io/ioutil" "log" "net/http" + "net/url" "sync" "time" @@ -16,7 +17,8 @@ import ( var supportedMethods = []string{"GET", "POST", "HEAD", "PUT", "DELETE", "OPTIONS", "CONNECT"} const ( - maxRequestJobs = 50 + maxRequestJobs = 50 + proxyTLSTimeout = 15 * time.Second ) // Requester struct for the request client @@ -191,6 +193,7 @@ func (r *Requester) GetRateLimit(auth bool) *RateLimit { // New returns a new Requester func New(name string, authLimit, unauthLimit *RateLimit, httpRequester *http.Client) *Requester { + return &Requester{ HTTPClient: httpRequester, UnauthLimit: unauthLimit, @@ -380,3 +383,16 @@ func (r *Requester) SendPayload(method, path string, headers map[string]string, } return resp.Error } + +// SetProxy sets a proxy address to the client transport +func (r *Requester) SetProxy(p *url.URL) error { + if p.String() == "" { + return errors.New("No proxy URL supplied") + } + + r.HTTPClient.Transport = &http.Transport{ + Proxy: http.ProxyURL(p), + TLSHandshakeTimeout: proxyTLSTimeout, + } + return nil +} diff --git a/exchanges/wex/wex.go b/exchanges/wex/wex.go index 95c44748..00e004d4 100644 --- a/exchanges/wex/wex.go +++ b/exchanges/wex/wex.go @@ -53,7 +53,6 @@ func (w *WEX) SetDefaults() { w.Enabled = false w.Fee = 0.2 w.Verbose = false - w.Websocket = false w.RESTPollingDelay = 10 w.Ticker = make(map[string]Ticker) w.RequestCurrencyPairFormat.Delimiter = "_" @@ -72,6 +71,7 @@ func (w *WEX) SetDefaults() { w.APIUrl = w.APIUrlDefault w.APIUrlSecondaryDefault = wexAPIPrivateURL w.APIUrlSecondary = w.APIUrlSecondaryDefault + w.WebsocketInit() } // Setup sets exchange configuration parameters for WEX @@ -86,7 +86,6 @@ func (w *WEX) Setup(exch config.ExchangeConfig) { w.SetHTTPClientUserAgent(exch.HTTPUserAgent) w.RESTPollingDelay = exch.RESTPollingDelay w.Verbose = exch.Verbose - w.Websocket = exch.Websocket w.BaseCurrencies = common.SplitStrings(exch.BaseCurrencies, ",") w.AvailablePairs = common.SplitStrings(exch.AvailablePairs, ",") w.EnabledPairs = common.SplitStrings(exch.EnabledPairs, ",") @@ -106,6 +105,10 @@ func (w *WEX) Setup(exch config.ExchangeConfig) { if err != nil { log.Fatal(err) } + err = w.SetClientProxyAddress(exch.ProxyAddress) + if err != nil { + log.Fatal(err) + } } } diff --git a/exchanges/wex/wex_wrapper.go b/exchanges/wex/wex_wrapper.go index 4b1555cb..f4de5188 100644 --- a/exchanges/wex/wex_wrapper.go +++ b/exchanges/wex/wex_wrapper.go @@ -24,7 +24,7 @@ func (w *WEX) Start(wg *sync.WaitGroup) { // Run implements the WEX wrapper func (w *WEX) Run() { if w.Verbose { - log.Printf("%s Websocket: %s.", w.GetName(), common.IsEnabled(w.Websocket)) + log.Printf("%s Websocket: %s.", w.GetName(), common.IsEnabled(w.Websocket.IsEnabled())) log.Printf("%s polling delay: %ds.\n", w.GetName(), w.RESTPollingDelay) log.Printf("%s %d currencies enabled: %s.\n", w.GetName(), len(w.EnabledPairs), w.EnabledPairs) } @@ -206,3 +206,8 @@ func (w *WEX) WithdrawFiatExchangeFunds(currency pair.CurrencyItem, amount float func (w *WEX) WithdrawFiatExchangeFundsToInternationalBank(currency pair.CurrencyItem, amount float64) (string, error) { return "", errors.New("not yet implemented") } + +// GetWebsocket returns a pointer to the exchange websocket +func (w *WEX) GetWebsocket() (*exchange.Websocket, error) { + return nil, errors.New("not yet implemented") +} diff --git a/exchanges/yobit/yobit.go b/exchanges/yobit/yobit.go index dda12e36..a490bdf5 100644 --- a/exchanges/yobit/yobit.go +++ b/exchanges/yobit/yobit.go @@ -51,7 +51,6 @@ func (y *Yobit) SetDefaults() { y.Enabled = true y.Fee = 0.2 y.Verbose = false - y.Websocket = false y.RESTPollingDelay = 10 y.AuthenticatedAPISupport = true y.Ticker = make(map[string]Ticker) @@ -71,6 +70,7 @@ func (y *Yobit) SetDefaults() { y.APIUrl = y.APIUrlDefault y.APIUrlSecondaryDefault = apiPrivateURL y.APIUrlSecondary = y.APIUrlSecondaryDefault + y.WebsocketInit() } // Setup sets exchange configuration parameters for Yobit @@ -83,7 +83,7 @@ func (y *Yobit) Setup(exch config.ExchangeConfig) { y.SetAPIKeys(exch.APIKey, exch.APISecret, "", false) y.RESTPollingDelay = exch.RESTPollingDelay y.Verbose = exch.Verbose - y.Websocket = exch.Websocket + y.Websocket.SetEnabled(exch.Websocket) y.BaseCurrencies = common.SplitStrings(exch.BaseCurrencies, ",") y.AvailablePairs = common.SplitStrings(exch.AvailablePairs, ",") y.EnabledPairs = common.SplitStrings(exch.EnabledPairs, ",") @@ -105,6 +105,10 @@ func (y *Yobit) Setup(exch config.ExchangeConfig) { if err != nil { log.Fatal(err) } + err = y.SetClientProxyAddress(exch.ProxyAddress) + if err != nil { + log.Fatal(err) + } } } diff --git a/exchanges/yobit/yobit_wrapper.go b/exchanges/yobit/yobit_wrapper.go index d95967c5..6ee2dbc9 100644 --- a/exchanges/yobit/yobit_wrapper.go +++ b/exchanges/yobit/yobit_wrapper.go @@ -24,7 +24,7 @@ func (y *Yobit) Start(wg *sync.WaitGroup) { // Run implements the Yobit wrapper func (y *Yobit) Run() { if y.Verbose { - log.Printf("%s Websocket: %s.", y.GetName(), common.IsEnabled(y.Websocket)) + log.Printf("%s Websocket: %s.", y.GetName(), common.IsEnabled(y.Websocket.IsEnabled())) log.Printf("%s polling delay: %ds.\n", y.GetName(), y.RESTPollingDelay) log.Printf("%s %d currencies enabled: %s.\n", y.GetName(), len(y.EnabledPairs), y.EnabledPairs) } @@ -188,3 +188,8 @@ func (y *Yobit) WithdrawFiatExchangeFunds(currency pair.CurrencyItem, amount flo func (y *Yobit) WithdrawFiatExchangeFundsToInternationalBank(currency pair.CurrencyItem, amount float64) (string, error) { return "", errors.New("not yet implemented") } + +// GetWebsocket returns a pointer to the exchange websocket +func (y *Yobit) GetWebsocket() (*exchange.Websocket, error) { + return nil, errors.New("not yet implemented") +} diff --git a/exchanges/zb/zb.go b/exchanges/zb/zb.go index a1bb56ff..47b389af 100644 --- a/exchanges/zb/zb.go +++ b/exchanges/zb/zb.go @@ -48,7 +48,6 @@ func (z *ZB) SetDefaults() { z.Enabled = false z.Fee = 0 z.Verbose = false - z.Websocket = false z.RESTPollingDelay = 10 z.RequestCurrencyPairFormat.Delimiter = "_" z.RequestCurrencyPairFormat.Uppercase = false @@ -65,6 +64,7 @@ func (z *ZB) SetDefaults() { z.APIUrl = z.APIUrlDefault z.APIUrlSecondaryDefault = zbMarketURL z.APIUrlSecondary = z.APIUrlSecondaryDefault + z.WebsocketInit() } // Setup sets user configuration @@ -80,7 +80,7 @@ func (z *ZB) Setup(exch config.ExchangeConfig) { z.SetHTTPClientUserAgent(exch.HTTPUserAgent) z.RESTPollingDelay = exch.RESTPollingDelay z.Verbose = exch.Verbose - z.Websocket = exch.Websocket + z.Websocket.SetEnabled(exch.Websocket) z.BaseCurrencies = common.SplitStrings(exch.BaseCurrencies, ",") z.AvailablePairs = common.SplitStrings(exch.AvailablePairs, ",") z.EnabledPairs = common.SplitStrings(exch.EnabledPairs, ",") @@ -100,6 +100,10 @@ func (z *ZB) Setup(exch config.ExchangeConfig) { if err != nil { log.Fatal(err) } + err = z.SetClientProxyAddress(exch.ProxyAddress) + if err != nil { + log.Fatal(err) + } } } diff --git a/exchanges/zb/zb_wrapper.go b/exchanges/zb/zb_wrapper.go index ac4f1593..d67ffbd1 100644 --- a/exchanges/zb/zb_wrapper.go +++ b/exchanges/zb/zb_wrapper.go @@ -24,7 +24,7 @@ func (z *ZB) Start(wg *sync.WaitGroup) { // Run implements the OKEX wrapper func (z *ZB) Run() { if z.Verbose { - log.Printf("%s Websocket: %s. (url: %s).\n", z.GetName(), common.IsEnabled(z.Websocket), z.WebsocketURL) + log.Printf("%s Websocket: %s. (url: %s).\n", z.GetName(), common.IsEnabled(z.Websocket.IsEnabled()), z.WebsocketURL) log.Printf("%s polling delay: %ds.\n", z.GetName(), z.RESTPollingDelay) log.Printf("%s %d currencies enabled: %s.\n", z.GetName(), len(z.EnabledPairs), z.EnabledPairs) } @@ -184,3 +184,8 @@ func (z *ZB) WithdrawFiatExchangeFunds(currency pair.CurrencyItem, amount float6 func (z *ZB) WithdrawFiatExchangeFundsToInternationalBank(currency pair.CurrencyItem, amount float64) (string, error) { return "", errors.New("not yet implemented") } + +// GetWebsocket returns a pointer to the exchange websocket +func (z *ZB) GetWebsocket() (*exchange.Websocket, error) { + return nil, errors.New("not yet implemented") +} diff --git a/main.go b/main.go index 545462c3..4ecf6763 100644 --- a/main.go +++ b/main.go @@ -59,6 +59,8 @@ func main() { flag.StringVar(&bot.dataDir, "datadir", common.GetDefaultDataDir(runtime.GOOS), "default data directory for GoCryptoTrader files") dryrun := flag.Bool("dryrun", false, "dry runs bot, doesn't save config file") version := flag.Bool("version", false, "retrieves current GoCryptoTrader version") + verbosity := flag.Bool("verbose", false, "-verbose increases logging verbosity for GoCryptoTrader") + flag.Parse() if *version { @@ -133,10 +135,6 @@ func main() { bot.portfolio.SeedPortfolio(bot.config.Portfolio) SeedExchangeAccountInfo(GetAllEnabledExchangeAccountInfo().Data) - go portfolio.StartPortfolioWatcher() - go TickerUpdaterRoutine() - go OrderbookUpdaterRoutine() - if bot.config.Webserver.Enabled { listenAddr := bot.config.Webserver.ListenAddress log.Printf( @@ -159,6 +157,12 @@ func main() { log.Println("HTTP RESTful Webserver support disabled.") } + go portfolio.StartPortfolioWatcher() + + go TickerUpdaterRoutine(*verbosity) + go OrderbookUpdaterRoutine(*verbosity) + go WebsocketRoutine(*verbosity) + <-bot.shutdown Shutdown() } diff --git a/routines.go b/routines.go index 9c80a35e..d7adcd0d 100644 --- a/routines.go +++ b/routines.go @@ -1,11 +1,13 @@ package main import ( + "errors" "fmt" "log" "sync" "time" + "github.com/thrasher-/gocryptotrader/common" "github.com/thrasher-/gocryptotrader/currency" "github.com/thrasher-/gocryptotrader/currency/pair" "github.com/thrasher-/gocryptotrader/currency/symbol" @@ -51,7 +53,7 @@ func printConvertCurrencyFormat(origCurrency string, origPrice float64) string { ) } -func printTickerSummary(result ticker.Price, p pair.CurrencyPair, assetType, exchangeName string, err error) { +func printTickerSummary(result ticker.Price, p pair.CurrencyPair, assetType, exchangeName string, err error, verbose bool) { if err != nil { log.Printf("Failed to get %s %s ticker. Error: %s", p.Pair().String(), @@ -60,6 +62,10 @@ func printTickerSummary(result ticker.Price, p pair.CurrencyPair, assetType, exc return } + if !verbose { + return + } + stats.Add(exchangeName, p, assetType, result.Last, result.Volume) if currency.IsFiatCurrency(p.SecondCurrency.String()) && p.SecondCurrency.String() != bot.config.Currency.FiatDisplayCurrency { origCurrency := p.SecondCurrency.Upper().String() @@ -100,7 +106,7 @@ func printTickerSummary(result ticker.Price, p pair.CurrencyPair, assetType, exc } } -func printOrderbookSummary(result orderbook.Base, p pair.CurrencyPair, assetType, exchangeName string, err error) { +func printOrderbookSummary(result orderbook.Base, p pair.CurrencyPair, assetType, exchangeName string, err error, verbose bool) { if err != nil { log.Printf("Failed to get %s %s orderbook. Error: %s", p.Pair().String(), @@ -108,6 +114,11 @@ func printOrderbookSummary(result orderbook.Base, p pair.CurrencyPair, assetType err) return } + + if !verbose { + return + } + bidsAmount, bidsValue := result.CalculateTotalBids() asksAmount, asksValue := result.CalculateTotalAsks() @@ -157,10 +168,9 @@ func printOrderbookSummary(result orderbook.Base, p pair.CurrencyPair, assetType ) } } - } -func relayWebsocketEvent(result interface{}, event, assetType, exchangeName string) { +func relayWebsocketEvent(result interface{}, event, assetType, exchangeName string, verbose bool) { evt := WebsocketEvent{ Data: result, Event: event, @@ -169,14 +179,16 @@ func relayWebsocketEvent(result interface{}, event, assetType, exchangeName stri } err := BroadcastWebsocketMessage(evt) if err != nil { - log.Println(fmt.Errorf("Failed to broadcast websocket event. Error: %s", - err)) + if verbose { + log.Println(fmt.Errorf("Failed to broadcast websocket event. Error: %s", + err)) + } } } // TickerUpdaterRoutine fetches and updates the ticker for all enabled // currency pairs and exchanges -func TickerUpdaterRoutine() { +func TickerUpdaterRoutine(verbose bool) { log.Println("Starting ticker updater routine.") var wg sync.WaitGroup for { @@ -205,11 +217,11 @@ func TickerUpdaterRoutine() { } else { result, err = exch.GetTickerPrice(c, assetType) } - printTickerSummary(result, c, assetType, exchangeName, err) + printTickerSummary(result, c, assetType, exchangeName, err, verbose) if err == nil { bot.comms.StageTickerData(exchangeName, assetType, result) if bot.config.Webserver.Enabled { - relayWebsocketEvent(result, "ticker_update", assetType, exchangeName) + relayWebsocketEvent(result, "ticker_update", assetType, exchangeName, verbose) } } } @@ -226,14 +238,16 @@ func TickerUpdaterRoutine() { }(x, &wg) } wg.Wait() - log.Println("All enabled currency tickers fetched.") + if verbose { + log.Println("All enabled currency tickers fetched.") + } time.Sleep(time.Second * 10) } } // OrderbookUpdaterRoutine fetches and updates the orderbooks for all enabled // currency pairs and exchanges -func OrderbookUpdaterRoutine() { +func OrderbookUpdaterRoutine(verbose bool) { log.Println("Starting orderbook updater routine.") var wg sync.WaitGroup for { @@ -256,11 +270,11 @@ func OrderbookUpdaterRoutine() { processOrderbook := func(exch exchange.IBotExchange, c pair.CurrencyPair, assetType string) { result, err := exch.UpdateOrderbook(c, assetType) - printOrderbookSummary(result, c, assetType, exchangeName, err) + printOrderbookSummary(result, c, assetType, exchangeName, err, verbose) if err == nil { bot.comms.StageOrderbookData(exchangeName, assetType, result) if bot.config.Webserver.Enabled { - relayWebsocketEvent(result, "orderbook_update", assetType, exchangeName) + relayWebsocketEvent(result, "orderbook_update", assetType, exchangeName, verbose) } } } @@ -273,7 +287,190 @@ func OrderbookUpdaterRoutine() { }(x, &wg) } wg.Wait() - log.Println("All enabled currency orderbooks fetched.") + if verbose { + log.Println("All enabled currency orderbooks fetched.") + } time.Sleep(time.Second * 10) } } + +// WebsocketRoutine Initial routine management system for websocket +func WebsocketRoutine(verbose bool) { + log.Println("Connecting exchange websocket services...") + + for i := range bot.exchanges { + go func(i int) { + if verbose { + log.Printf("Establishing websocket connection for %s", + bot.exchanges[i].GetName()) + } + + ws, err := bot.exchanges[i].GetWebsocket() + if err != nil { + return + } + + // Data handler routine + go WebsocketDataHandler(ws, verbose) + + err = ws.Connect() + if err != nil { + switch err.Error() { + case exchange.WebsocketNotEnabled: + // Store in memory if enabled in future + default: + log.Println(err) + } + } + }(i) + } +} + +var shutdowner = make(chan struct{}, 1) +var wg sync.WaitGroup + +// Websocketshutdown shuts down the exchange routines and then shuts down +// governing routines +func Websocketshutdown(ws *exchange.Websocket) error { + err := ws.Shutdown() // shutdown routines on the exchange + if err != nil { + log.Fatalf("routines.go error - failed to shutodwn %s", err) + } + + timer := time.NewTimer(5 * time.Second) + c := make(chan struct{}, 1) + + go func(c chan struct{}) { + close(shutdowner) + wg.Wait() + c <- struct{}{} + }(c) + + select { + case <-timer.C: + return errors.New("routines.go error - failed to shutdown routines") + + case <-c: + return nil + } +} + +// streamDiversion is a diversion switch from websocket to REST or other +// alternative feed +func streamDiversion(ws *exchange.Websocket, verbose bool) { + wg.Add(1) + defer wg.Done() + + for { + select { + case <-shutdowner: + return + + case <-ws.Connected: + if verbose { + log.Printf("exchange %s websocket feed connected", ws.GetName()) + } + + case <-ws.Disconnected: + if verbose { + log.Printf("exchange %s websocket feed disconnected, switching to REST functionality", + ws.GetName()) + } + } + } +} + +// WebsocketDataHandler handles websocket data coming from a websocket feed +// associated with an exchange +func WebsocketDataHandler(ws *exchange.Websocket, verbose bool) { + wg.Add(1) + defer wg.Done() + + go streamDiversion(ws, verbose) + + for { + select { + case <-shutdowner: + return + + case data := <-ws.DataHandler: + switch data.(type) { + case string: + switch data.(string) { + case exchange.WebsocketNotEnabled: + if verbose { + log.Printf("routines.go warning - exchange %s weboscket not enabled", + ws.GetName()) + } + + default: + log.Println(data.(string)) + } + + case error: + switch { + case common.StringContains(data.(error).Error(), "close 1006"): + go WebsocketReconnect(ws, verbose) + continue + default: + log.Fatalf("routines.go exchange %s websocket error - %s", ws.GetName(), data) + } + + case exchange.TradeData: + // Trade Data + if verbose { + log.Println("Websocket trades Updated: ", data.(exchange.TradeData)) + } + + case exchange.TickerData: + // Ticker data + if verbose { + log.Println("Websocket Ticker Updated: ", data.(exchange.TickerData)) + } + case exchange.KlineData: + // Kline data + if verbose { + log.Println("Websocket Kline Updated: ", data.(exchange.KlineData)) + } + case exchange.WebsocketOrderbookUpdate: + // Orderbook data + if verbose { + log.Println("Websocket Orderbook Updated:", data.(exchange.WebsocketOrderbookUpdate)) + } + default: + if verbose { + log.Println("Websocket Unknown type: ", data) + } + } + } + } +} + +// WebsocketReconnect tries to reconnect to a websocket stream +func WebsocketReconnect(ws *exchange.Websocket, verbose bool) { + if verbose { + log.Printf("Websocket reconnection requested for %s", ws.GetName()) + } + + err := ws.Shutdown() + if err != nil { + log.Fatal(err) + } + + wg.Add(1) + defer wg.Done() + + ticker := time.NewTicker(3 * time.Second) + for { + select { + case <-shutdowner: + return + + case <-ticker.C: + err = ws.Connect() + if err == nil { + return + } + } + } +} diff --git a/testdata/configtest.json b/testdata/configtest.json index d4e3c6c3..2a4aa7f0 100644 --- a/testdata/configtest.json +++ b/testdata/configtest.json @@ -139,6 +139,8 @@ "apiSecret": "Secret", "apiUrl": "NON_DEFAULT_HTTP_LINK_TO_EXCHANGE_API", "apiUrlSecondary": "NON_DEFAULT_HTTP_LINK_TO_EXCHANGE_API", + "proxyAddress": "", + "websocketUrl": "NON_DEFAULT_HTTP_LINK_TO_WEBSOCKET_EXCHANGE_API", "availablePairs": "ETH_HKD,START_GBP,BTC_CAD,OAX_ETH,START_SGD,LTC_BTC,STR_BTC,ATENC_NZD,BTC_AUD,BTC_SGD,ETH_BTC,XRP_BTC,START_JPY,ATENC_CAD,BTC_GBP,ETH_USD,GNT_ETH,START_AUD,START_HKD,ATENC_GBP,BTC_USD,START_BTC,START_CAD,START_EUR,BTC_JPY,BTC_NZD,DOGE_BTC,ATENC_EUR,ATENC_JPY,ATENC_USD,BTC_EUR,BTC_HKD,START_NZD,START_USD,ATENC_AUD,ATENC_HKD,ATENC_SGD", "enabledPairs": "BTC_USD,BTC_HKD,BTC_EUR,BTC_CAD,BTC_AUD,BTC_SGD,BTC_JPY,BTC_GBP,BTC_NZD,LTC_BTC,STR_BTC,XRP_BTC", "baseCurrencies": "USD,HKD,EUR,CAD,AUD,SGD,JPY,GBP,NZD", @@ -177,6 +179,8 @@ "apiSecret": "Secret", "apiUrl": "NON_DEFAULT_HTTP_LINK_TO_EXCHANGE_API", "apiUrlSecondary": "NON_DEFAULT_HTTP_LINK_TO_EXCHANGE_API", + "proxyAddress": "", + "websocketUrl": "NON_DEFAULT_HTTP_LINK_TO_WEBSOCKET_EXCHANGE_API", "availablePairs": "ETH-BTC,LTC-BTC,BNB-BTC,NEO-BTC,QTUM-ETH,EOS-ETH,SNT-ETH,BNT-ETH,BCC-BTC,GAS-BTC,BNB-ETH,BTC-USDT,ETH-USDT,OAX-ETH,DNT-ETH,MCO-ETH,ICN-ETH,MCO-BTC,WTC-BTC,WTC-ETH,LRC-BTC,LRC-ETH,QTUM-BTC,YOYO-BTC,OMG-BTC,OMG-ETH,ZRX-BTC,ZRX-ETH,STRAT-BTC,STRAT-ETH,SNGLS-BTC,SNGLS-ETH,BQX-BTC,BQX-ETH,KNC-BTC,KNC-ETH,FUN-BTC,FUN-ETH,SNM-BTC,SNM-ETH,NEO-ETH,IOTA-BTC,IOTA-ETH,LINK-BTC,LINK-ETH,XVG-BTC,XVG-ETH,SALT-BTC,SALT-ETH,MDA-BTC,MDA-ETH,MTL-BTC,MTL-ETH,SUB-BTC,SUB-ETH,EOS-BTC,SNT-BTC,ETC-ETH,ETC-BTC,MTH-BTC,MTH-ETH,ENG-BTC,ENG-ETH,DNT-BTC,ZEC-BTC,ZEC-ETH,BNT-BTC,AST-BTC,AST-ETH,DASH-BTC,DASH-ETH,OAX-BTC,ICN-BTC,BTG-BTC,BTG-ETH,EVX-BTC,EVX-ETH,REQ-BTC,REQ-ETH,VIB-BTC,VIB-ETH,TRX-BTC,TRX-ETH,POWR-BTC,POWR-ETH,ARK-BTC,ARK-ETH,YOYO-ETH,XRP-BTC,XRP-ETH,MOD-BTC,MOD-ETH,ENJ-BTC,ENJ-ETH,STORJ-BTC,STORJ-ETH,BNB-USDT,YOYO-BNB,POWR-BNB,KMD-BTC,KMD-ETH,NULS-BNB,RCN-BTC,RCN-ETH,RCN-BNB,NULS-BTC,NULS-ETH,RDN-BTC,RDN-ETH,RDN-BNB,XMR-BTC,XMR-ETH,DLT-BNB,WTC-BNB,DLT-BTC,DLT-ETH,AMB-BTC,AMB-ETH,AMB-BNB,BCC-ETH,BCC-USDT,BCC-BNB,BAT-BTC,BAT-ETH,BAT-BNB,BCPT-BTC,BCPT-ETH,BCPT-BNB,ARN-BTC,ARN-ETH,GVT-BTC,GVT-ETH,CDT-BTC,CDT-ETH,GXS-BTC,GXS-ETH,NEO-USDT,NEO-BNB,POE-BTC,POE-ETH,QSP-BTC,QSP-ETH,QSP-BNB,BTS-BTC,BTS-ETH,BTS-BNB,XZC-BTC,XZC-ETH,XZC-BNB,LSK-BTC,LSK-ETH,LSK-BNB,TNT-BTC,TNT-ETH,FUEL-BTC,FUEL-ETH,MANA-BTC,MANA-ETH,BCD-BTC,BCD-ETH,DGD-BTC,DGD-ETH,IOTA-BNB,ADX-BTC,ADX-ETH,ADX-BNB,ADA-BTC,ADA-ETH,PPT-BTC,PPT-ETH,CMT-BTC,CMT-ETH,CMT-BNB,XLM-BTC,XLM-ETH,XLM-BNB,CND-BTC,CND-ETH,CND-BNB,LEND-BTC,LEND-ETH,WABI-BTC,WABI-ETH,WABI-BNB,LTC-ETH,LTC-USDT,LTC-BNB,TNB-BTC,TNB-ETH,WAVES-BTC,WAVES-ETH,WAVES-BNB,GTO-BTC,GTO-ETH,GTO-BNB,ICX-BTC,ICX-ETH,ICX-BNB,OST-BTC,OST-ETH,OST-BNB,ELF-BTC,ELF-ETH,AION-BTC,AION-ETH,AION-BNB,NEBL-BTC,NEBL-ETH,NEBL-BNB,BRD-BTC,BRD-ETH,BRD-BNB,MCO-BNB,EDO-BTC,EDO-ETH,WINGS-BTC,WINGS-ETH,NAV-BTC,NAV-ETH,NAV-BNB,LUN-BTC,LUN-ETH,TRIG-BTC,TRIG-ETH,TRIG-BNB,APPC-BTC,APPC-ETH,APPC-BNB,VIBE-BTC,VIBE-ETH,RLC-BTC,RLC-ETH,RLC-BNB,INS-BTC,INS-ETH,PIVX-BTC,PIVX-ETH,PIVX-BNB,IOST-BTC,IOST-ETH,CHAT-BTC,CHAT-ETH,STEEM-BTC,STEEM-ETH,STEEM-BNB,NANO-BTC,NANO-ETH,NANO-BNB,VIA-BTC,VIA-ETH,VIA-BNB,BLZ-BTC,BLZ-ETH,BLZ-BNB,AE-BTC,AE-ETH,AE-BNB,NCASH-BTC,NCASH-ETH,NCASH-BNB,POA-BTC,POA-ETH,POA-BNB,ZIL-BTC,ZIL-ETH,ZIL-BNB,ONT-BTC,ONT-ETH,ONT-BNB,STORM-BTC,STORM-ETH,STORM-BNB,QTUM-BNB,QTUM-USDT,XEM-BTC,XEM-ETH,XEM-BNB,WAN-BTC,WAN-ETH,WAN-BNB,WPR-BTC,WPR-ETH,QLC-BTC,QLC-ETH,SYS-BTC,SYS-ETH,SYS-BNB,QLC-BNB,GRS-BTC,GRS-ETH,ADA-USDT,ADA-BNB,CLOAK-BTC,CLOAK-ETH,GNT-BTC,GNT-ETH,GNT-BNB,LOOM-BTC,LOOM-ETH,LOOM-BNB,XRP-USDT,BCN-BTC,BCN-ETH,BCN-BNB,REP-BTC,REP-ETH,REP-BNB,TUSD-BTC,TUSD-ETH,TUSD-BNB,ZEN-BTC,ZEN-ETH,ZEN-BNB,SKY-BTC,SKY-ETH,SKY-BNB,EOS-USDT,EOS-BNB,CVC-BTC,CVC-ETH,CVC-BNB,THETA-BTC,THETA-ETH,THETA-BNB,XRP-BNB,TUSD-USDT,IOTA-USDT,XLM-USDT,IOTX-BTC,IOTX-ETH,QKC-BTC,QKC-ETH,AGI-BTC,AGI-ETH,AGI-BNB,NXS-BTC,NXS-ETH,NXS-BNB,ENJ-BNB,DATA-BTC,DATA-ETH,ONT-USDT,TRX-BNB,TRX-USDT,ETC-USDT,ETC-BNB,ICX-USDT,SC-BTC,SC-ETH,SC-BNB,NPXS-BTC,NPXS-ETH,KEY-BTC,KEY-ETH,NAS-BTC,NAS-ETH,NAS-BNB,MFT-BTC,MFT-ETH,MFT-BNB,DENT-BTC,DENT-ETH,ARDR-BTC,ARDR-ETH,ARDR-BNB,NULS-USDT,HOT-BTC,HOT-ETH,VET-BTC,VET-ETH,VET-USDT,VET-BNB,DOCK-BTC,DOCK-ETH,POLY-BTC,POLY-BNB,PHX-BTC,PHX-ETH,PHX-BNB,HC-BTC,HC-ETH,GO-BTC,GO-BNB,PAX-BTC,PAX-BNB,PAX-USDT,PAX-ETH", "enabledPairs": "BTC-USDT", "baseCurrencies": "USD", @@ -215,6 +219,8 @@ "apiSecret": "Secret", "apiUrl": "NON_DEFAULT_HTTP_LINK_TO_EXCHANGE_API", "apiUrlSecondary": "NON_DEFAULT_HTTP_LINK_TO_EXCHANGE_API", + "proxyAddress": "", + "websocketUrl": "NON_DEFAULT_HTTP_LINK_TO_WEBSOCKET_EXCHANGE_API", "availablePairs": "BTCUSD,LTCUSD,LTCBTC,ETHUSD,ETHBTC,ETCBTC,ETCUSD,RRTUSD,RRTBTC,ZECUSD,ZECBTC,XMRUSD,XMRBTC,DSHUSD,DSHBTC,BTCEUR,BTCJPY,XRPUSD,XRPBTC,IOTUSD,IOTBTC,IOTETH,EOSUSD,EOSBTC,EOSETH,SANUSD,SANBTC,SANETH,OMGUSD,OMGBTC,OMGETH,BCHUSD,BCHBTC,BCHETH,NEOUSD,NEOBTC,NEOETH,ETPUSD,ETPBTC,ETPETH,QTMUSD,QTMBTC,QTMETH,AVTUSD,AVTBTC,AVTETH,EDOUSD,EDOBTC,EDOETH,BTGUSD,BTGBTC,DATUSD,DATBTC,DATETH,QSHUSD,QSHBTC,QSHETH,YYWUSD,YYWBTC,YYWETH,GNTUSD,GNTBTC,GNTETH,SNTUSD,SNTBTC,SNTETH,IOTEUR,BATUSD,BATBTC,BATETH,MNAUSD,MNABTC,MNAETH,FUNUSD,FUNBTC,FUNETH,ZRXUSD,ZRXBTC,ZRXETH,TNBUSD,TNBBTC,TNBETH,SPKUSD,SPKBTC,SPKETH,TRXUSD,TRXBTC,TRXETH,RCNUSD,RCNBTC,RCNETH,RLCUSD,RLCBTC,RLCETH,AIDUSD,AIDBTC,AIDETH,SNGUSD,SNGBTC,SNGETH,REPUSD,REPBTC,REPETH,ELFUSD,ELFBTC,ELFETH,BTCGBP,ETHEUR,ETHJPY,ETHGBP,NEOEUR,NEOJPY,NEOGBP,EOSEUR,EOSJPY,EOSGBP,IOTJPY,IOTGBP,IOSUSD,IOSBTC,IOSETH,AIOUSD,AIOBTC,AIOETH,REQUSD,REQBTC,REQETH,RDNUSD,RDNBTC,RDNETH,LRCUSD,LRCBTC,LRCETH,WAXUSD,WAXBTC,WAXETH,DAIUSD,DAIBTC,DAIETH,CFIUSD,CFIBTC,CFIETH,AGIUSD,AGIBTC,AGIETH,BFTUSD,BFTBTC,BFTETH,MTNUSD,MTNBTC,MTNETH,ODEUSD,ODEBTC,ODEETH,ANTUSD,ANTBTC,ANTETH,DTHUSD,DTHBTC,DTHETH,MITUSD,MITBTC,MITETH,STJUSD,STJBTC,STJETH,XLMUSD,XLMEUR,XLMJPY,XLMGBP,XLMBTC,XLMETH,XVGUSD,XVGEUR,XVGJPY,XVGGBP,XVGBTC,XVGETH,BCIUSD,BCIBTC,MKRUSD,MKRBTC,MKRETH,KNCUSD,KNCBTC,KNCETH,POAUSD,POABTC,POAETH,LYMUSD,LYMBTC,LYMETH,UTKUSD,UTKBTC,UTKETH,VEEUSD,VEEBTC,VEEETH,DADUSD,DADBTC,DADETH,ORSUSD,ORSBTC,ORSETH,AUCUSD,AUCBTC,AUCETH,POYUSD,POYBTC,POYETH,FSNUSD,FSNBTC,FSNETH,CBTUSD,CBTBTC,CBTETH,ZCNUSD,ZCNBTC,ZCNETH,SENUSD,SENBTC,SENETH,NCAUSD,NCABTC,NCAETH,CNDUSD,CNDBTC,CNDETH,CTXUSD,CTXBTC,CTXETH,PAIUSD,PAIBTC,SEEUSD,SEEBTC,SEEETH,ESSUSD,ESSBTC,ESSETH,ATMUSD,ATMBTC,ATMETH,HOTUSD,HOTBTC,HOTETH,DTAUSD,DTABTC,DTAETH,IQXUSD,IQXBTC,IQXEOS,WPRUSD,WPRBTC,WPRETH,ZILUSD,ZILBTC,ZILETH,BNTUSD,BNTBTC,BNTETH,ABSUSD,ABSETH,XRAUSD,XRAETH,MANUSD,MANETH,BBNUSD,BBNETH,NIOUSD,NIOETH,DGXUSD,DGXETH,VETUSD,VETBTC,VETETH,UTNUSD,UTNETH,TKNUSD,TKNETH,GOTUSD,GOTEUR,GOTETH,XTZUSD,XTZBTC,CNNUSD,CNNETH,BOXUSD,BOXETH,TRXEUR,TRXGBP,TRXJPY,MGOUSD,MGOETH,RTEUSD,RTEETH", "enabledPairs": "BTCUSD,LTCUSD,LTCBTC,ETHUSD,ETHBTC", "baseCurrencies": "USD", @@ -261,6 +267,8 @@ "apiSecret": "Secret", "apiUrl": "NON_DEFAULT_HTTP_LINK_TO_EXCHANGE_API", "apiUrlSecondary": "NON_DEFAULT_HTTP_LINK_TO_EXCHANGE_API", + "proxyAddress": "", + "websocketUrl": "NON_DEFAULT_HTTP_LINK_TO_WEBSOCKET_EXCHANGE_API", "availablePairs": "BTC_JPY,FXBTC_JPY,ETH_BTC,BCH_BTC", "enabledPairs": "BTC_JPY,ETH_BTC,BCH_BTC", "baseCurrencies": "JPY", @@ -301,6 +309,8 @@ "apiSecret": "Secret", "apiUrl": "NON_DEFAULT_HTTP_LINK_TO_EXCHANGE_API", "apiUrlSecondary": "NON_DEFAULT_HTTP_LINK_TO_EXCHANGE_API", + "proxyAddress": "", + "websocketUrl": "NON_DEFAULT_HTTP_LINK_TO_WEBSOCKET_EXCHANGE_API", "clientId": "ClientID", "availablePairs": "BTCKRW,WAVESKRW,VETKRW,THETAKRW,BCHKRW,ADAKRW,PSTKRW,RDNKRW,STRATKRW,OMGKRW,LRCKRW,XEMKRW,ETHOSKRW,CTXCKRW,MITHKRW,RNTKRW,XMRKRW,ZECKRW,ZILKRW,ENJKRW,HSRKRW,LTCKRW,PAYKRW,DASHKRW,CMTKRW,XRPKRW,PLYKRW,BTGKRW,ABTKRW,POWRKRW,AEKRW,WTCKRW,ETHKRW,EOSKRW,ITCKRW,ICXKRW,KNCKRW,WAXKRW,TRUEKRW,QTUMKRW,REPKRW,TRXKRW,PPTKRW,STEEMKRW,GTOKRW,MCOKRW,ZRXKRW,ETCKRW,SALTKRW,GNTKRW,ELFKRW,LINKKRW,SNTKRW", "enabledPairs": "BTCKRW,ETHKRW,DASHKRW,LTCKRW,ETCKRW,XRPKRW,BCHKRW,XMRKRW,ZECKRW,QTUMKRW,BTGKRW,EOSKRW", @@ -340,6 +350,8 @@ "apiSecret": "Secret", "apiUrl": "NON_DEFAULT_HTTP_LINK_TO_EXCHANGE_API", "apiUrlSecondary": "NON_DEFAULT_HTTP_LINK_TO_EXCHANGE_API", + "proxyAddress": "", + "websocketUrl": "NON_DEFAULT_HTTP_LINK_TO_WEBSOCKET_EXCHANGE_API", "clientId": "ClientID", "availablePairs": "LTCUSD,ETHUSD,XRPEUR,BCHUSD,BCHEUR,BTCEUR,XRPBTC,EURUSD,BCHBTC,LTCEUR,BTCUSD,LTCBTC,XRPUSD,ETHBTC,ETHEUR", "enabledPairs": "BTCUSD,BTCEUR,EURUSD,XRPUSD,XRPEUR", @@ -378,6 +390,8 @@ "apiSecret": "Secret", "apiUrl": "NON_DEFAULT_HTTP_LINK_TO_EXCHANGE_API", "apiUrlSecondary": "NON_DEFAULT_HTTP_LINK_TO_EXCHANGE_API", + "proxyAddress": "", + "websocketUrl": "NON_DEFAULT_HTTP_LINK_TO_WEBSOCKET_EXCHANGE_API", "availablePairs": "BTC-LTC,BTC-DOGE,BTC-VTC,BTC-PPC,BTC-FTC,BTC-RDD,BTC-NXT,BTC-DASH,BTC-POT,BTC-BLK,BTC-EMC2,BTC-XMY,BTC-AUR,BTC-EFL,BTC-GLD,BTC-SLR,BTC-PTC,BTC-GRS,BTC-NLG,BTC-RBY,BTC-XWC,BTC-MONA,BTC-THC,BTC-ENRG,BTC-ERC,BTC-VRC,BTC-CURE,BTC-XMR,BTC-CLOAK,BTC-KORE,BTC-XDN,BTC-TRUST,BTC-NAV,BTC-XST,BTC-VIA,BTC-PINK,BTC-IOC,BTC-CANN,BTC-SYS,BTC-NEOS,BTC-DGB,BTC-BURST,BTC-EXCL,BTC-BITS,BTC-DOPE,BTC-BLOCK,BTC-ABY,BTC-BYC,BTC-XMG,BTC-BAY,BTC-SPR,BTC-XRP,BTC-GAME,BTC-COVAL,BTC-NXS,BTC-XCP,BTC-BITB,BTC-GEO,BTC-FLDC,BTC-GRC,BTC-FLO,BTC-NBT,BTC-MUE,BTC-XEM,BTC-DMD,BTC-GAM,BTC-SPHR,BTC-OK,BTC-AEON,BTC-ETH,BTC-TX,BTC-BCY,BTC-EXP,BTC-OMNI,BTC-AMP,BTC-XLM,USDT-BTC,BTC-RVR,BTC-EMC,BTC-FCT,BTC-EGC,BTC-SLS,BTC-RADS,BTC-DCR,BTC-BSD,BTC-XVG,BTC-PIVX,BTC-MEME,BTC-STEEM,BTC-2GIVE,BTC-LSK,BTC-BRK,BTC-WAVES,BTC-LBC,BTC-SBD,BTC-BRX,BTC-ETC,ETH-ETC,BTC-STRAT,BTC-UNB,BTC-SYNX,BTC-EBST,BTC-VRM,BTC-SEQ,BTC-REP,BTC-SHIFT,BTC-ARDR,BTC-XZC,BTC-NEO,BTC-ZEC,BTC-ZCL,BTC-IOP,BTC-GOLOS,BTC-UBQ,BTC-KMD,BTC-GBG,BTC-SIB,BTC-ION,BTC-LMC,BTC-QWARK,BTC-CRW,BTC-SWT,BTC-MLN,BTC-ARK,BTC-DYN,BTC-TKS,BTC-MUSIC,BTC-DTB,BTC-INCNT,BTC-GBYTE,BTC-GNT,BTC-NXC,BTC-EDG,BTC-MORE,ETH-GNT,ETH-REP,USDT-ETH,BTC-WINGS,BTC-RLC,BTC-GNO,BTC-GUP,BTC-LUN,ETH-RLC,ETH-GNO,BTC-HMQ,BTC-ANT,ETH-ANT,BTC-SC,ETH-BAT,BTC-BAT,BTC-ZEN,BTC-QRL,BTC-CRB,ETH-MORE,BTC-PTOY,BTC-BNT,ETH-BNT,BTC-NMR,ETH-LTC,ETH-XRP,BTC-SNT,ETH-SNT,BTC-DCT,BTC-XEL,BTC-MCO,ETH-MCO,BTC-ADT,BTC-PAY,ETH-PAY,BTC-STORJ,BTC-ADX,ETH-ADX,ETH-DASH,ETH-SC,ETH-ZEC,USDT-ZEC,USDT-LTC,USDT-ETC,USDT-XRP,BTC-OMG,ETH-OMG,BTC-CVC,ETH-CVC,BTC-PART,BTC-QTUM,ETH-QTUM,ETH-XMR,ETH-XEM,ETH-XLM,ETH-NEO,USDT-XMR,USDT-DASH,ETH-BCH,USDT-BCH,BTC-BCH,BTC-DNT,USDT-NEO,ETH-WAVES,ETH-STRAT,ETH-DGB,USDT-OMG,BTC-ADA,BTC-MANA,ETH-MANA,BTC-SALT,ETH-SALT,BTC-TIX,BTC-RCN,BTC-VIB,ETH-VIB,BTC-MER,BTC-POWR,ETH-POWR,ETH-ADA,BTC-ENG,ETH-ENG,USDT-ADA,USDT-XVG,USDT-NXT,BTC-UKG,ETH-UKG,BTC-IGNIS,BTC-SRN,ETH-SRN,BTC-WAX,ETH-WAX,BTC-ZRX,ETH-ZRX,BTC-VEE,BTC-BCPT,BTC-TRX,ETH-TRX,BTC-TUSD,BTC-LRC,ETH-TUSD,BTC-UP,BTC-DMT,ETH-DMT,USDT-TUSD,BTC-POLY,ETH-POLY,BTC-PRO,USDT-SC,USDT-TRX,BTC-BLT,BTC-STORM,ETH-STORM,BTC-AID,BTC-NGC,BTC-GTO,USDT-DCR,BTC-OCN,ETH-OCN,USD-BTC,USD-USDT,USD-TUSD,BTC-TUBE,BTC-CBC,USD-ETH,BTC-NLC2,BTC-BKX,BTC-MFT,BTC-LOOM,BTC-RFR,USDT-DGB,BTC-RVN,USD-XRP,USD-ETC,BTC-BFT,BTC-GO,BTC-HYDRO,BTC-UPP,USD-ADA,USD-ZEC,USDT-DOGE,BTC-ENJ,BTC-MET,USD-LTC,USD-TRX,BTC-DTA,BTC-EDR,BTC-BOXX,BTC-IHT,USD-BCH,BTC-XHV", "enabledPairs": "USDT-BTC", "baseCurrencies": "USD", @@ -417,6 +431,8 @@ "apiSecret": "Secret", "apiUrl": "NON_DEFAULT_HTTP_LINK_TO_EXCHANGE_API", "apiUrlSecondary": "NON_DEFAULT_HTTP_LINK_TO_EXCHANGE_API", + "proxyAddress": "", + "websocketUrl": "NON_DEFAULT_HTTP_LINK_TO_WEBSOCKET_EXCHANGE_API", "availablePairs": "BTCUSD", "enabledPairs": "BTCUSD", "baseCurrencies": "USD", @@ -454,7 +470,9 @@ "apiSecret": "Secret", "apiUrl": "NON_DEFAULT_HTTP_LINK_TO_EXCHANGE_API", "apiUrlSecondary": "NON_DEFAULT_HTTP_LINK_TO_EXCHANGE_API", - "availablePairs": "USDET_USD,BTCET_BTC,DSHET_DSH,XMR_BTC,BCH_USD,ETHET_ETH,BTC_RUR,BCH_ETH,LTCET_LTC,PPC_BTC,BCH_BTC,BCH_LTC,NMC_BTC,BCH_DSH,EUR_RUR,DSH_BTC,LTC_EUR,ETH_RUR,NVCET_NVC,ZEC_RUR,LTC_BTC,DSH_RUR,PPCET_PPC,XMR_USD,XMR_ETH,BTC_EUR,NMC_USD,PPC_USD,ETH_LTC,ETH_ZEC,BCH_EUR,NMCET_NMC,DSH_ETH,ETH_USD,ZEC_USD,USDT_USD,BTC_USD,NVC_USD,DSH_ZEC,ETH_BTC,BCH_RUR,USD_RUR,DSH_USD,ZEC_LTC,EURET_EUR,BTC_USDT,LTC_USD,LTC_RUR,NVC_BTC,EUR_USD,DSH_EUR,ZEC_BTC,BCHET_BCH,XMR_RUR,XMR_EUR,DSH_LTC,ETH_EUR,BCH_ZEC,RURET_RUR", + "proxyAddress": "", + "websocketUrl": "NON_DEFAULT_HTTP_LINK_TO_WEBSOCKET_EXCHANGE_API", + "availablePairs": "LTC_BTC,BCH_USD,BCHET_BCH,BTC_EUR,NMCET_NMC,NMC_USD,LTC_EUR,LTC_USD,BCH_RUR,NVCET_NVC,EUR_RUR,PPCET_PPC,EUR_USD,PPC_BTC,LTCET_LTC,ZEC_USD,DSH_EUR,LTC_RUR,NVC_USD,ETH_BTC,BCH_EUR,RURET_RUR,ETH_RUR,BCH_ETH,USDT_USD,BTC_USDT,DSH_ZEC,DSH_ETH,ETH_ZEC,BCH_LTC,USDET_USD,ZEC_BTC,NVC_BTC,PPC_USD,ETH_LTC,BCH_DSH,BTCET_BTC,BTC_RUR,USD_RUR,DSH_BTC,DSH_RUR,ETH_EUR,BCH_ZEC,EURET_EUR,BTC_USD,DSHET_DSH,ETHET_ETH,DSH_USD,ETH_USD,BCH_BTC,ZEC_LTC,NMC_BTC,DSH_LTC", "enabledPairs": "BTC_USD,LTC_USD,LTC_BTC,ETH_USD", "baseCurrencies": "USD,RUR,EUR", "assetTypes": "SPOT", @@ -494,6 +512,8 @@ "apiSecret": "Secret", "apiUrl": "NON_DEFAULT_HTTP_LINK_TO_EXCHANGE_API", "apiUrlSecondary": "NON_DEFAULT_HTTP_LINK_TO_EXCHANGE_API", + "proxyAddress": "", + "websocketUrl": "NON_DEFAULT_HTTP_LINK_TO_WEBSOCKET_EXCHANGE_API", "availablePairs": "BTC-AUD,LTC-AUD,LTC-BTC,ETH-BTC,ETH-AUD,ETC-AUD,ETC-BTC,XRP-AUD,XRP-BTC,BCH-AUD,BCH-BTC,POWR-AUD,POWR-BTC,OMG-AUD,OMG-BTC", "enabledPairs": "BTC-AUD", "baseCurrencies": "AUD", @@ -532,6 +552,8 @@ "apiSecret": "Secret", "apiUrl": "NON_DEFAULT_HTTP_LINK_TO_EXCHANGE_API", "apiUrlSecondary": "NON_DEFAULT_HTTP_LINK_TO_EXCHANGE_API", + "proxyAddress": "", + "websocketUrl": "NON_DEFAULT_HTTP_LINK_TO_WEBSOCKET_EXCHANGE_API", "clientId": "ClientID", "availablePairs": "ETCUSDT,ETHCAD,LTCUSD,XMRBTC,ETHLTC,LTCCAD,BTCCAD,ZECBTC,ZECUSD,ZECUSDT,USDTSGD,XMRUSDT,USDTUSD,ZECCAD,BTCUSD,LTCSGD,ETHBTC,ETHSGD,ETHUSD,ETHUSDT,XMRLTC,ZECSGD,BTCUSDT,ETCBTC,ZECLTC,ETCLTC,LTCUSDT,BTCSGD,LTCBTC", "enabledPairs": "LTCBTC,ETCBTC,ETHBTC", @@ -570,7 +592,9 @@ "apiSecret": "Secret", "apiUrl": "NON_DEFAULT_HTTP_LINK_TO_EXCHANGE_API", "apiUrlSecondary": "NON_DEFAULT_HTTP_LINK_TO_EXCHANGE_API", - "availablePairs": "EOS_BTC,EOS_USD,STQ_RUB,BTC_RUB,BTC_UAH,GNT_BTC,INK_ETH,XLM_BTC,XRP_BTC,DOGE_BTC,BTC_USDT,INK_BTC,XLM_USD,HBZ_USD,BTCZ_BTC,ZEC_EUR,USDT_RUB,ZRX_ETH,TRX_USD,BCH_ETH,ETH_LTC,ETH_RUB,STQ_BTC,ETH_BTC,XMR_EUR,KICK_BTC,KICK_ETH,NEO_USD,INK_USD,OMG_BTC,ETC_BTC,DXT_BTC,BCH_USD,DASH_BTC,ADA_USD,TRX_BTC,MNX_BTC,OMG_ETH,XLM_RUB,USD_RUB,BTC_TRY,OMG_USD,STQ_USD,LTC_RUB,ETH_USDT,ETC_RUB,LTC_BTC,XRP_USD,BTC_EUR,NEO_RUB,GAS_BTC,GAS_USD,DASH_USD,XMR_BTC,ETH_TRY,XMR_USD,USDT_USD,TRX_RUB,STQ_EUR,DASH_RUB,ETH_UAH,ZEC_RUB,ADA_BTC,ADA_ETH,BCH_RUB,LTC_EUR,ZEC_BTC,BTC_USD,NEO_BTC,BTG_BTC,HBZ_ETH,DXT_USD,BCH_BTC,XRP_RUB,WAVES_BTC,HBZ_BTC,ETC_USD,LTC_USD,ZRX_BTC,MNX_ETH,ETH_USD,ETH_EUR,ETH_PLN,WAVES_RUB,BTC_PLN,GNT_ETH,MNX_USD,BTG_USD,ZEC_USD", + "proxyAddress": "", + "websocketUrl": "NON_DEFAULT_HTTP_LINK_TO_WEBSOCKET_EXCHANGE_API", + "availablePairs": "DASH_RUB,ETC_BTC,ETC_USD,LTC_RUB,XMR_EUR,XLM_USD,STQ_RUB,BCH_ETH,ETH_USD,ETH_EUR,ZEC_EUR,ZEC_RUB,KICK_ETH,HBZ_USD,ETH_UAH,LTC_BTC,BTC_USDT,XLM_BTC,BCH_RUB,LTC_USD,USDT_USD,STQ_BTC,ETH_RUB,ETH_USDT,DXT_BTC,DXT_USD,EOS_USD,LTC_EUR,ZEC_BTC,XMR_USD,BTC_PLN,DASH_USD,XMR_BTC,USD_RUB,DOGE_BTC,BCH_USD,ETH_LTC,KICK_BTC,BTC_RUB,USDT_RUB,BTC_UAH,HBZ_BTC,EOS_BTC,BTCZ_BTC,BCH_BTC,XRP_USD,XRP_RUB,BTC_USD,BTG_BTC,BTG_USD,DASH_BTC,ETH_BTC,XRP_BTC,ETH_PLN,ETC_RUB,WAVES_BTC,STQ_EUR,HBZ_ETH,BTC_EUR,XLM_RUB,STQ_USD,ZEC_USD,WAVES_RUB", "enabledPairs": "BTC_USD,LTC_USD", "baseCurrencies": "USD,EUR,RUB,PLN,UAH", "assetTypes": "SPOT", @@ -610,6 +634,8 @@ "apiSecret": "Secret", "apiUrl": "NON_DEFAULT_HTTP_LINK_TO_EXCHANGE_API", "apiUrlSecondary": "NON_DEFAULT_HTTP_LINK_TO_EXCHANGE_API", + "proxyAddress": "", + "websocketUrl": "NON_DEFAULT_HTTP_LINK_TO_WEBSOCKET_EXCHANGE_API", "clientId": "ClientID", "availablePairs": "ETCEUR,BTCUSD,BCHBTC,BCHUSD,BTCEUR,BTCGBP,ETHBTC,ETHEUR,ETHUSD,LTCBTC,LTCEUR,LTCUSD,BCHEUR,ETCUSD,ETCBTC,ETCGBP,ETHGBP,LTCGBP,BCHGBP", "enabledPairs": "BTCUSD,BTCGBP,BTCEUR", @@ -649,6 +675,8 @@ "apiSecret": "Secret", "apiUrl": "NON_DEFAULT_HTTP_LINK_TO_EXCHANGE_API", "apiUrlSecondary": "NON_DEFAULT_HTTP_LINK_TO_EXCHANGE_API", + "proxyAddress": "", + "websocketUrl": "NON_DEFAULT_HTTP_LINK_TO_WEBSOCKET_EXCHANGE_API", "availablePairs": "BTC_USDT,BCH_USDT,ETH_USDT,ETC_USDT,QTUM_USDT,LTC_USDT,DASH_USDT,ZEC_USDT,BTM_USDT,EOS_USDT,REQ_USDT,SNT_USDT,OMG_USDT,PAY_USDT,CVC_USDT,ZRX_USDT,TNT_USDT,XMR_USDT,XRP_USDT,DOGE_USDT,BAT_USDT,PST_USDT,BTG_USDT,DPY_USDT,LRC_USDT,STORJ_USDT,RDN_USDT,STX_USDT,KNC_USDT,LINK_USDT,CDT_USDT,AE_USDT,AE_ETH,AE_BTC,CDT_ETH,RDN_ETH,STX_ETH,KNC_ETH,LINK_ETH,REQ_ETH,RCN_ETH,TRX_ETH,ARN_ETH,KICK_ETH,BNT_ETH,VET_ETH,MCO_ETH,FUN_ETH,DATA_ETH,RLC_ETH,RLC_USDT,ZSC_ETH,WINGS_ETH,MDA_ETH,RCN_USDT,TRX_USDT,KICK_USDT,VET_USDT,MCO_USDT,FUN_USDT,DATA_USDT,ZSC_USDT,MDA_USDT,XTZ_USDT,XTZ_BTC,XTZ_ETH,GNT_USDT,GNT_ETH,GEM_USDT,GEM_ETH,RFR_USDT,RFR_ETH,DADI_USDT,DADI_ETH,ABT_USDT,ABT_ETH,LEDU_BTC,LEDU_ETH,OST_USDT,OST_ETH,XLM_USDT,XLM_ETH,XLM_BTC,MOBI_USDT,MOBI_ETH,MOBI_BTC,OCN_USDT,OCN_ETH,OCN_BTC,ZPT_USDT,ZPT_ETH,ZPT_BTC,COFI_USDT,COFI_ETH,JNT_USDT,JNT_ETH,JNT_BTC,BLZ_USDT,BLZ_ETH,GXS_USDT,GXS_BTC,MTN_USDT,MTN_ETH,RUFF_USDT,RUFF_ETH,RUFF_BTC,TNC_USDT,TNC_ETH,TNC_BTC,ZIL_USDT,ZIL_ETH,TIO_USDT,TIO_ETH,BTO_USDT,BTO_ETH,THETA_USDT,THETA_ETH,DDD_USDT,DDD_ETH,DDD_BTC,MKR_USDT,MKR_ETH,DAI_USDT,SMT_USDT,SMT_ETH,MDT_USDT,MDT_ETH,MDT_BTC,MANA_USDT,MANA_ETH,LUN_USDT,LUN_ETH,SALT_USDT,SALT_ETH,FUEL_USDT,FUEL_ETH,ELF_USDT,ELF_ETH,DRGN_USDT,DRGN_ETH,GTC_USDT,GTC_ETH,GTC_BTC,QLC_USDT,QLC_BTC,QLC_ETH,DBC_USDT,DBC_BTC,DBC_ETH,BNTY_USDT,BNTY_ETH,LEND_USDT,LEND_ETH,ICX_USDT,ICX_ETH,BTF_USDT,BTF_BTC,ADA_USDT,ADA_BTC,LSK_USDT,LSK_BTC,WAVES_USDT,WAVES_BTC,BIFI_USDT,BIFI_BTC,MDS_ETH,MDS_USDT,DGD_USDT,DGD_ETH,QASH_USDT,QASH_ETH,QASH_BTC,POWR_USDT,POWR_ETH,POWR_BTC,FIL_USDT,BCD_USDT,BCD_BTC,SBTC_USDT,SBTC_BTC,GOD_USDT,GOD_BTC,BCX_USDT,BCX_BTC,QSP_USDT,QSP_ETH,INK_BTC,INK_USDT,INK_ETH,INK_QTUM,MED_QTUM,MED_ETH,MED_USDT,BOT_QTUM,BOT_USDT,BOT_ETH,QBT_QTUM,QBT_ETH,QBT_USDT,TSL_QTUM,TSL_USDT,GNX_USDT,GNX_ETH,NEO_USDT,GAS_USDT,NEO_BTC,GAS_BTC,IOTA_USDT,IOTA_BTC,NAS_USDT,NAS_ETH,NAS_BTC,ETH_BTC,ETC_BTC,ETC_ETH,ZEC_BTC,DASH_BTC,LTC_BTC,BCH_BTC,BTG_BTC,QTUM_BTC,QTUM_ETH,XRP_BTC,DOGE_BTC,XMR_BTC,ZRX_BTC,ZRX_ETH,DNT_ETH,DPY_ETH,OAX_ETH,REP_ETH,LRC_ETH,LRC_BTC,PST_ETH,BCDN_ETH,BCDN_USDT,TNT_ETH,SNT_ETH,SNT_BTC,BTM_ETH,BTM_BTC,LLT_ETH,SNET_ETH,SNET_USDT,LLT_SNET,OMG_ETH,OMG_BTC,PAY_ETH,PAY_BTC,BAT_ETH,BAT_BTC,CVC_ETH,STORJ_ETH,STORJ_BTC,EOS_ETH,EOS_BTC,BTS_USDT,BTS_BTC,TIPS_ETH,BU_USDT,BU_ETH,BU_BTC,DCR_USDT,DCR_BTC,BCN_USDT,BCN_BTC,XMC_USDT,XMC_BTC,PPS_USDT,BOE_ETH,BOE_USDT,PLY_ETH,MEDX_USDT,MEDX_ETH,CS_ETH,CS_USDT,MAN_ETH,MAN_USDT,REM_ETH,REM_USDT,LYM_ETH,LYM_BTC,LYM_USDT,ONT_ETH,ONT_USDT,BFT_ETH,BFT_USDT,IHT_ETH,IHT_USDT,SENC_ETH,SENC_USDT,TOMO_ETH,TOMO_USDT,ELEC_ETH,ELEC_USDT,HAV_ETH,HAV_USDT,SWTH_ETH,SWTH_USDT,NKN_ETH,NKN_USDT,SOUL_ETH,SOUL_USDT,LRN_ETH,LRN_USDT,EOSDAC_ETH,EOSDAC_USDT,ADD_ETH,IQ_ETH,MEETONE_ETH,DOCK_USDT,DOCK_ETH,GSE_USDT,GSE_ETH,RATING_USDT,RATING_ETH,HSC_USDT,HSC_ETH,HIT_USDT,HIT_ETH,DX_USDT,DX_ETH,BXC_USDT,BXC_ETH,PAX_USDT,HC_USDT,HC_BTC,HC_ETH,GARD_USDT,GARD_ETH,FTI_USDT,FTI_ETH,SOP_ETH,SOP_USDT,LEMO_USDT,LEMO_ETH,NPXS_ETH,QKC_USDT,QKC_ETH,IOTX_USDT,IOTX_ETH,RED_USDT,RED_ETH,LBA_USDT,LBA_ETH,OPEN_USDT,OPEN_ETH,MITH_USDT,MITH_ETH,SKM_USDT,SKM_ETH,XVG_USDT,XVG_BTC,NANO_USDT,NANO_BTC,HT_USDT,BNB_USDT,MET_ETH,MET_USDT,TCT_ETH,TCT_USDT", "enabledPairs": "BTC_USDT", "baseCurrencies": "USD", @@ -688,6 +716,8 @@ "apiSecret": "Secret", "apiUrl": "NON_DEFAULT_HTTP_LINK_TO_EXCHANGE_API", "apiUrlSecondary": "NON_DEFAULT_HTTP_LINK_TO_EXCHANGE_API", + "proxyAddress": "", + "websocketUrl": "NON_DEFAULT_HTTP_LINK_TO_WEBSOCKET_EXCHANGE_API", "availablePairs": "BTCUSD,ETHBTC,ETHUSD,ZECUSD,ZECBTC,ZECETH", "enabledPairs": "BTCUSD", "baseCurrencies": "USD", @@ -725,6 +755,8 @@ "apiSecret": "Secret", "apiUrl": "NON_DEFAULT_HTTP_LINK_TO_EXCHANGE_API", "apiUrlSecondary": "NON_DEFAULT_HTTP_LINK_TO_EXCHANGE_API", + "proxyAddress": "", + "websocketUrl": "NON_DEFAULT_HTTP_LINK_TO_WEBSOCKET_EXCHANGE_API", "availablePairs": "BCN-BTC,BTC-USD,DASH-BTC,DOGE-BTC,DOGE-USD,DSH-BTC,EMC-BTC,ETH-BTC,FCN-BTC,LSK-BTC,LTC-BTC,LTC-USD,NXT-BTC,SBD-BTC,SC-BTC,STEEM-BTC,XDN-BTC,XEM-BTC,XMR-BTC,ARDR-BTC,ZEC-BTC,WAVES-BTC,MAID-BTC,AMP-BTC,BUS-BTC,DGD-BTC,ICN-BTC,SNGLS-BTC,1ST-BTC,TRST-BTC,TIME-BTC,GNO-BTC,REP-BTC,XMR-USD,DASH-USD,ETH-USD,NXT-USD,ZRC-BTC,BOS-BTC,DCT-BTC,ANT-BTC,AEON-BTC,GUP-BTC,PLU-BTC,LUN-BTC,TAAS-BTC,NXC-BTC,EDG-BTC,RLC-BTC,SWT-BTC,TKN-BTC,WINGS-BTC,XAUR-BTC,AE-BTC,PTOY-BTC,ZEC-USD,XEM-USD,BCN-USD,XDN-USD,MAID-USD,ETC-BTC,ETC-USD,PLBT-BTC,BNT-BTC,FYN-ETH,SNM-BTC,SNM-ETH,SNT-ETH,CVC-USD,PAY-ETH,OAX-ETH,OMG-ETH,BQX-ETH,XTZ-BTC,DICE-BTC,PTOY-ETH,1ST-ETH,XAUR-ETH,TAAS-ETH,TIME-ETH,DICE-ETH,SWT-ETH,XMR-ETH,ETC-ETH,DASH-ETH,ZEC-ETH,PLU-ETH,GNO-ETH,XRP-BTC,NET-ETH,STRAT-USD,STRAT-BTC,SNC-ETH,ADX-ETH,BET-ETH,EOS-ETH,DENT-ETH,SAN-ETH,EOS-BTC,EOS-USD,MNE-BTC,MSP-ETH,DDF-ETH,XTZ-ETH,XTZ-USD,UET-ETH,MYB-ETH,SUR-ETH,IXT-ETH,PLR-ETH,TIX-ETH,NDC-ETH,PRO-ETH,AVT-ETH,COSS-ETH,EVX-USD,DLT-BTC,BNT-ETH,BNT-USD,QAU-BTC,QAU-ETH,MANA-USD,DNT-BTC,FYP-BTC,OPT-BTC,TNT-ETH,IFT-BTC,STX-BTC,STX-ETH,STX-USD,TNT-USD,TNT-BTC,CAT-BTC,CAT-ETH,CAT-USD,BCH-BTC,BCH-ETH,BCH-USD,ENG-ETH,XUC-USD,SNC-BTC,SNC-USD,OAX-USD,OAX-BTC,BAS-ETH,ZRX-BTC,ZRX-ETH,ZRX-USD,RVT-BTC,ICOS-BTC,ICOS-ETH,ICOS-USD,PPC-BTC,PPC-USD,QTUM-ETH,VERI-BTC,VERI-ETH,VERI-USD,IGNIS-ETH,PRG-BTC,PRG-ETH,PRG-USD,BMC-BTC,BMC-ETH,BMC-USD,CND-BTC,CND-ETH,CND-USD,SKIN-BTC,EMGO-BTC,EMGO-USD,CDT-ETH,CDT-USD,FUN-BTC,FUN-ETH,FUN-USD,HVN-BTC,HVN-ETH,FUEL-BTC,FUEL-ETH,FUEL-USD,POE-BTC,POE-ETH,AIR-BTC,AIR-ETH,AIR-USD,AMB-USD,AMB-ETH,AMB-BTC,NTO-BTC,ICO-BTC,PING-BTC,GAME-BTC,TKR-ETH,HPC-BTC,PPT-ETH,MTH-BTC,MTH-ETH,WMGO-BTC,WMGO-USD,LRC-BTC,LRC-ETH,ICX-BTC,ICX-ETH,NEO-BTC,NEO-ETH,NEO-USD,CSNO-BTC,ORME-BTC,ICX-USD,PIX-BTC,PIX-ETH,IND-ETH,KICK-BTC,YOYOW-BTC,MIPS-BTC,CDT-BTC,XVG-BTC,XVG-ETH,XVG-USD,DGB-BTC,DGB-ETH,DGB-USD,DCN-BTC,DCN-ETH,DCN-USD,CCT-ETH,EBET-ETH,VIBE-BTC,VOISE-BTC,ENJ-BTC,ENJ-ETH,ENJ-USD,ZSC-BTC,ZSC-ETH,ZSC-USD,ETBS-BTC,TRX-BTC,TRX-ETH,TRX-USD,ART-BTC,EVX-BTC,EVX-ETH,QVT-ETH,EXN-BTC,ATS-ETH,BMT-BTC,BMT-ETH,SUB-BTC,SUB-ETH,SUB-USD,WTC-BTC,CNX-BTC,ODN-BTC,BTM-BTC,BTM-ETH,BTM-USD,B2X-BTC,B2X-ETH,B2X-USD,ATM-BTC,ATM-ETH,ATM-USD,LIFE-BTC,VIB-BTC,VIB-ETH,VIB-USD,DRT-ETH,STU-USD,OMG-BTC,PAY-BTC,COSS-BTC,PPT-BTC,SNT-BTC,BTG-BTC,BTG-ETH,BTG-USD,SMART-BTC,SMART-ETH,SMART-USD,XUC-ETH,XUC-BTC,LA-ETH,CLD-BTC,CLD-ETH,CLD-USD,EDO-BTC,EDO-ETH,EDO-USD,HGT-ETH,IXT-BTC,ATS-BTC,SCL-BTC,ATL-BTC,ETP-BTC,ETP-ETH,ETP-USD,OTX-BTC,CDX-ETH,DRPU-BTC,NEBL-BTC,NEBL-ETH,HAC-BTC,CTX-BTC,CTX-ETH,ELE-BTC,ARN-BTC,ARN-ETH,STU-BTC,STU-ETH,GVT-ETH,INDI-BTC,BTX-BTC,LTC-ETH,BCN-ETH,MAID-ETH,NXT-ETH,STRAT-ETH,XDN-ETH,XEM-ETH,PLR-BTC,SUR-BTC,BQX-BTC,DOGE-ETH,ITS-BTC,AMM-BTC,AMM-ETH,AMM-USD,DBIX-BTC,PRE-BTC,KBR-BTC,TBT-BTC,ERO-BTC,SMS-BTC,SMS-ETH,SMS-USD,ZAP-BTC,DOV-BTC,DOV-ETH,DRPU-ETH,OTN-BTC,XRP-ETH,XRP-USD,HSR-BTC,LEND-BTC,LEND-ETH,SPF-BTC,SPF-ETH,SBTC-BTC,SBTC-ETH,WRC-BTC,WRC-ETH,WRC-USD,LOC-BTC,LOC-ETH,LOC-USD,SWFTC-BTC,SWFTC-ETH,SWFTC-USD,STAR-ETH,SBTC-USD,STORM-BTC,DIM-ETH,DIM-USD,DIM-BTC,NGC-BTC,NGC-ETH,NGC-USD,EMC-ETH,EMC-USD,MCO-BTC,MCO-ETH,MCO-USD,MANA-ETH,MANA-BTC,ECH-BTC,CPAY-ETH,DATA-BTC,DATA-ETH,DATA-USD,UTT-BTC,UTT-ETH,UTT-USD,KMD-BTC,KMD-ETH,KMD-USD,QTUM-USD,QTUM-BTC,SNT-USD,OMG-USD,EKO-BTC,EKO-ETH,ADX-BTC,ADX-USD,LSK-ETH,LSK-USD,PLR-USD,SUR-USD,BQX-USD,DRT-USD,REP-ETH,REP-USD,TIO-BTC,TIO-ETH,TIO-USD,WAX-BTC,WAX-ETH,WAX-USD,EET-BTC,EET-ETH,EET-USD,C20-BTC,C20-ETH,IDH-BTC,IDH-ETH,IPL-BTC,COV-BTC,COV-ETH,SENT-BTC,SENT-ETH,SENT-USD,SMT-BTC,SMT-ETH,SMT-USD,CAS-BTC,CAS-ETH,CAS-USD,CHAT-BTC,CHAT-ETH,CHAT-USD,GRMD-BTC,AVH-BTC,TRAC-ETH,JNT-ETH,PCL-BTC,PCL-ETH,CLOUT-BTC,UTK-BTC,UTK-ETH,UTK-USD,GNX-ETH,CHSB-BTC,CHSB-ETH,AVH-ETH,DAY-BTC,DAY-ETH,DAY-USD,NEU-BTC,NEU-ETH,NEU-USD,AVH-USD,CLOUT-ETH,CLOUT-USD,TAU-BTC,MEK-BTC,FLP-BTC,FLP-ETH,FLP-USD,R-BTC,R-ETH,EKO-USD,BCPT-ETH,BCPT-USD,PKT-BTC,PKT-ETH,WLK-BTC,WLK-ETH,WLK-USD,CPG-BTC,CPG-ETH,BPTN-BTC,BPTN-ETH,BPTN-USD,BETR-BTC,BETR-ETH,ARCT-BTC,ARCT-USD,DBET-BTC,DBET-ETH,DBET-USD,RNTB-ETH,HAND-ETH,HAND-USD,ACO-ETH,CTE-BTC,CTE-ETH,CTE-USD,CPY-BTC,CPY-ETH,CHP-ETH,BCPT-BTC,ACT-BTC,ACT-ETH,ACT-USD,HIRE-ETH,ADA-BTC,ADA-ETH,ADA-USD,SIG-BTC,RPM-BTC,RPM-ETH,MTX-BTC,MTX-ETH,MTX-USD,BGG-BTC,BGG-ETH,BGG-USD,SETH-ETH,WIZ-BTC,WIZ-ETH,WIZ-USD,DADI-BTC,DADI-ETH,BDG-ETH,DATX-BTC,DATX-ETH,TRUE-BTC,DRG-BTC,DRG-ETH,BANCA-BTC,BANCA-ETH,ZAP-ETH,ZAP-USD,AUTO-BTC,NOAH-BTC,SOC-BTC,WILD-BTC,INSUR-BTC,INSUR-ETH,OCN-BTC,OCN-ETH,STQ-BTC,STQ-ETH,XLM-BTC,XLM-ETH,XLM-USD,IOTA-BTC,IOTA-ETH,IOTA-USD,DRT-BTC,MLD-BTC,MLD-ETH,MLD-USD,BETR-USD,CGC-ETH,ERT-BTC,CRPT-BTC,CRPT-USD,MESH-BTC,MESH-ETH,MESH-USD,HLW-ETH,IHT-BTC,IHT-ETH,IHT-USD,SCC-BTC,YCC-BTC,DAN-BTC,TEL-BTC,TEL-ETH,BUBO-BTC,BUBO-ETH,BUBO-USD,VIT-BTC,VIT-ETH,VIT-USD,NCT-BTC,NCT-ETH,NCT-USD,BMH-BTC,BANCA-USD,NOAH-ETH,NOAH-USD,LDC-BTC,XMO-BTC,XMO-USD,XMO-ETH,BERRY-BTC,BERRY-ETH,BERRY-USD,GBX-BTC,GBX-ETH,GBX-USD,SHIP-BTC,SHIP-ETH,NANO-BTC,NANO-ETH,NANO-USD,LNC-BTC,UNC-BTC,UNC-ETH,KIN-ETH,ARDR-USD,DAXT-BTC,DAXT-ETH,FOTA-ETH,FOTA-BTC,SETH-BTC,CVT-BTC,CVT-ETH,CVT-USD,STQ-USD,GNT-BTC,GNT-ETH,GNT-USD,ADH-BTC,ADH-ETH,BBC-BTC,BBC-ETH,GET-BTC,MITH-BTC,MITH-ETH,MITH-USD,SUNC-ETH,DADI-USD,TKY-BTC,ACAT-BTC,ACAT-ETH,ACAT-USD,BTX-USD,TCN-BTC,VIO-ETH,WIKI-BTC,WIKI-ETH,WIKI-USD,ONT-BTC,ONT-ETH,ONT-USD,CVCOIN-BTC,CVCOIN-ETH,CVCOIN-USD,FTX-BTC,FTX-ETH,FREC-BTC,NAVI-BTC,FREC-ETH,FREC-USD,VME-ETH,NAVI-ETH,BTCP-BTC,LND-ETH,CSM-BTC,NANJ-BTC,NTK-BTC,NTK-ETH,NTK-USD,AUC-BTC,AUC-ETH,CMCT-BTC,CMCT-ETH,CMCT-USD,MAN-BTC,MAN-ETH,MAN-USD,HIRE-BTC,TKA-BTC,TKA-ETH,TKA-USD,PNT-BTC,PNT-ETH,FXT-BTC,NEXO-BTC,CHX-BTC,CHX-ETH,CHX-USD,PAT-BTC,PAT-ETH,XMC-BTC,EJOY-BTC,EJOY-ETH,EJOY-USD,FXT-ETH,HERO-BTC,HERO-ETH,XMC-ETH,XMC-USD,STAK-BTC,STAK-ETH,FDZ-BTC,FDZ-ETH,FDZ-USD,SPD-BTC,SPD-ETH,LUC-BTC,MITX-BTC,TIV-BTC,B2G-BTC,B2G-USD,ZPT-BTC,ZPT-ETH,HBZ-BTC,FACE-BTC,FACE-ETH,HBZ-ETH,HBZ-USD,ZPT-USD,MORPH-BTC,MORPH-ETH,MORPH-USD,EBKC-BTC,CPT-BTC,PAT-USD,HTML-BTC,HTML-ETH,MITX-ETH,JOT-BTC,JBC-BTC,JBC-ETH,BTS-BTC,BNK-BTC,KBC-BTC,KBC-ETH,BNK-ETH,BNK-USD,TIV-ETH,TIV-USD,LUC-ETH,LUC-USD,CSM-ETH,CSM-USD,INK-BTC,SPC-BTC,IOST-BTC,INK-ETH,INK-USD,SPC-ETH,SPC-USD,CBC-BTC,IOST-USD,COIN-BTC,ZIL-BTC,COIN-USD,COIN-ETH,PMNT-BTC,ABYSS-BTC,ABYSS-ETH,ZIL-USD,BCI-BTC,CBC-ETH,CBC-USD,PITCH-BTC,PITCH-ETH,HTML-USD,TDS-BTC,TDS-ETH,TDS-USD,SBD-ETH,SBD-USD,DPN-BTC,UUU-BTC,UUU-ETH,XBP-BTC,KRM-USD,CLN-BTC,IVY-BTC,IVY-ETH,TTU-BTC,TTU-ETH,TTU-USD,CLN-ETH,DOR-BTC,DOR-ETH,DOR-USD,ELEC-BTC,ELEC-ETH,ELEC-USD,QNTU-BTC,QNTU-ETH,QNTU-USD,NLC2-BTC,IPL-ETH,IPL-USD,CENNZ-BTC,BTCP-ETH,BTCP-USD,CENNZ-ETH,SWM-BTC,MXM-BTC,MXM-ETH,SPF-USD,LCC-BTC,HGT-BTC,BTC-DAI,ETH-DAI,MKR-DAI,EOS-DAI,USD-DAI,ETH-TUSD,BTC-TUSD,LTC-TUSD,XMR-TUSD,ZRX-TUSD,NEO-TUSD,BCH-TUSD,USD-TUSD,MKR-BTC,MKR-ETH,MKR-USD,TUSD-DAI,NEO-DAI,LTC-DAI,XMR-DAI,BCH-DAI,XRP-DAI,NEXO-ETH,NEXO-USD,PROC-BTC,DWS-BTC,DWS-ETH,DWS-USD,APPC-BTC,APPC-ETH,APPC-USD,BIT-ETH,DASH-EURS,ZEC-EURS,BTC-EURS,EOS-EURS,ETH-EURS,LTC-EURS,BCH-EURS,NEO-EURS,XMR-EURS,XRP-EURS,REX-BTC,REX-ETH,REX-USD,BCD-BTC,ELF-BTC,ELF-USD,BCD-USD,EBKC-ETH,EBKC-USD,EDG-ETH,EDG-USD,COSM-BTC,COSM-ETH,DCNT-BTC,DCNT-ETH,DCNT-USD,EURS-USD,EURS-TUSD,EURS-DAI,MNX-USD,ROX-ETH,ZPR-ETH,MNX-BTC,MNX-ETH,KIND-BTC,KIND-ETH,ENGT-BTC,ENGT-ETH,PMA-BTC,PMA-ETH,TV-BTC,TV-ETH,TV-USD,XCLR-BTC,BAT-BTC,BAT-ETH,BAT-USD,SRN-BTC,SRN-ETH,SRN-USD,SVD-BTC,SVD-ETH,SVD-USD,GST-BTC,GST-ETH,GST-USD,BNB-BTC,BNB-ETH,BNB-USD,DIT-BTC,DIT-ETH,UMT-BTC,UMT-ETH,BCST-BTC,BCST-ETH,BCST-USD,POA20-BTC,RIK-BTC,RIK-ETH,RIK-USD,CCL-USD,POA20-ETH,POA20-USD,POA20-DAI,NIM-BTC,USE-BTC,USE-ETH,ABTC-BTC,DAV-BTC,DAV-ETH,ABA-BTC,ABA-ETH,ABA-USD,NIM-ETH,LWF-BTC,LWF-USD,BCN-EOS,LTC-EOS,XMR-EOS,DASH-EOS,TRX-EOS,NEO-EOS,ZEC-EOS,LSK-EOS,XEM-EOS,XRP-EOS,MESSE-BTC,MESSE-ETH,MESSE-USD,CCL-ETH,RCN-BTC,RCN-ETH,RCN-USD,HMQ-BTC,HMQ-ETH,MYST-BTC,MYST-ETH,TOLL-BTC,TOLL-ETH,TOLL-USD,BTC-GUSD,ETH-GUSD,USD-GUSD,EOS-GUSD,AXPR-BTC,AXPR-ETH,DAG-BTC,DAG-ETH,BITS-BTC,BITS-ETH,BITS-USD,USC-BTC,USC-ETH,CDCC-BTC,CDCC-ETH,CDCC-USD,VET-BTC,VET-ETH,VET-USD,SILK-ETH,BOX-BTC,BOX-ETH,BOX-EURS,BOX-EOS", "enabledPairs": "BTC-USD", "baseCurrencies": "USD", @@ -764,7 +796,9 @@ "apiAuthPemKey": "-----BEGIN EC PRIVATE KEY-----\nMHcCAQEEIPVSj8YkpXibCAL9HwpGkDNSEXR9jcpiCthdikJqipNooAoGCCqGSM49\nAwEHoUQDQgAEHiB7q/HCqUrCNqPeTtRmKjyi2T+2O2JgoU8Mjx2R4z1h81uOZHCk\nxbsDg1fb7ACRMpKWPs59QWpQxhqMQrNw8w==\n-----END EC PRIVATE KEY-----\n", "apiUrl": "NON_DEFAULT_HTTP_LINK_TO_EXCHANGE_API", "apiUrlSecondary": "NON_DEFAULT_HTTP_LINK_TO_EXCHANGE_API", - "availablePairs": "BTC-USDT,BCH-USDT,ETH-USDT,ETC-USDT,LTC-USDT,EOS-USDT,XRP-USDT,OMG-USDT,DASH-USDT,ZEC-USDT,ADA-USDT,STEEM-USDT,IOTA-USDT,OCN-USDT,SOC-USDT,CTXC-USDT,ACT-USDT,BTM-USDT,BTS-USDT,ONT-USDT,IOST-USDT,HT-USDT,TRX-USDT,DTA-USDT,NEO-USDT,QTUM-USDT,SMT-USDT,ELA-USDT,VEN-USDT,THETA-USDT,SNT-USDT,ZIL-USDT,XEM-USDT,NAS-USDT,RUFF-USDT,HC-USDT,LET-USDT,MDS-USDT,STORJ-USDT,ELF-USDT,ITC-USDT,CVC-USDT,GNT-USDT,XMR-BTC,BCH-BTC,ETH-BTC,LTC-BTC,ETC-BTC,EOS-BTC,OMG-BTC,XRP-BTC,DASH-BTC,ZEC-BTC,ADA-BTC,STEEM-BTC,IOTA-BTC,POLY-BTC,KAN-BTC,LBA-BTC,WAN-BTC,BFT-BTC,BTM-BTC,ONT-BTC,IOST-BTC,HT-BTC,TRX-BTC,SMT-BTC,ELA-BTC,WICC-BTC,OCN-BTC,ZLA-BTC,ABT-BTC,MTX-BTC,NAS-BTC,VEN-BTC,DTA-BTC,NEO-BTC,WAX-BTC,BTS-BTC,ZIL-BTC,THETA-BTC,CTXC-BTC,SRN-BTC,XEM-BTC,ICX-BTC,DGD-BTC,CHAT-BTC,WPR-BTC,LUN-BTC,SWFTC-BTC,SNT-BTC,MEET-BTC,YEE-BTC,ELF-BTC,LET-BTC,QTUM-BTC,LSK-BTC,ITC-BTC,SOC-BTC,QASH-BTC,MDS-BTC,EKO-BTC,TOPC-BTC,MTN-BTC,ACT-BTC,HC-BTC,STK-BTC,STORJ-BTC,GNX-BTC,DBC-BTC,SNC-BTC,CMT-BTC,TNB-BTC,RUFF-BTC,QUN-BTC,ZRX-BTC,KNC-BTC,BLZ-BTC,PROPY-BTC,PHX-BTC,APPC-BTC,AIDOC-BTC,POWR-BTC,CVC-BTC,PAY-BTC,QSP-BTC,DAT-BTC,RDN-BTC,MCO-BTC,RCN-BTC,MANA-BTC,UTK-BTC,TNT-BTC,GAS-BTC,BAT-BTC,OST-BTC,LINK-BTC,GNT-BTC,MTL-BTC,EVX-BTC,REQ-BTC,ADX-BTC,AST-BTC,ENG-BTC,SALT-BTC,EDU-BTC,XVG-BTC,WTC-BTC,BIFI-BTC,BCX-BTC,BCD-BTC,SBTC-BTC,BTG-BTC,XMR-ETH,EOS-ETH,OMG-ETH,IOTA-ETH,ADA-ETH,STEEM-ETH,POLY-ETH,KAN-ETH,LBA-ETH,WAN-ETH,BFT-ETH,ZRX-ETH,AST-ETH,KNC-ETH,ONT-ETH,HT-ETH,BTM-ETH,IOST-ETH,SMT-ETH,ELA-ETH,TRX-ETH,ABT-ETH,NAS-ETH,OCN-ETH,WICC-ETH,ZIL-ETH,CTXC-ETH,ZLA-ETH,WPR-ETH,DTA-ETH,MTX-ETH,THETA-ETH,SRN-ETH,VEN-ETH,BTS-ETH,WAX-ETH,HC-ETH,ICX-ETH,MTN-ETH,ACT-ETH,BLZ-ETH,QASH-ETH,RUFF-ETH,CMT-ETH,ELF-ETH,MEET-ETH,SOC-ETH,QTUM-ETH,ITC-ETH,SWFTC-ETH,YEE-ETH,LSK-ETH,LUN-ETH,LET-ETH,GNX-ETH,CHAT-ETH,EKO-ETH,TOPC-ETH,DGD-ETH,STK-ETH,MDS-ETH,DBC-ETH,SNC-ETH,PAY-ETH,QUN-ETH,AIDOC-ETH,TNB-ETH,APPC-ETH,RDN-ETH,UTK-ETH,POWR-ETH,BAT-ETH,PROPY-ETH,MANA-ETH,REQ-ETH,CVC-ETH,QSP-ETH,EVX-ETH,DAT-ETH,MCO-ETH,GNT-ETH,GAS-ETH,OST-ETH,LINK-ETH,RCN-ETH,TNT-ETH,ENG-ETH,SALT-ETH,ADX-ETH,EDU-ETH,XVG-ETH,WTC-ETH,XRP-HT,IOST-HT,DASH-HT,WICC-USDT,EOS-HT,BCH-HT,LTC-HT,ETC-HT,WAVES-BTC,WAVES-ETH,HB10-USDT,CMT-USDT,DCR-BTC,DCR-ETH,PAI-BTC,PAI-ETH,BOX-BTC,BOX-ETH,DGB-BTC,DGB-ETH,GXS-BTC,GXS-ETH,XLM-BTC,XLM-ETH,BIX-BTC,BIX-ETH,BIX-USDT,HIT-BTC,HIT-ETH,PAI-USDT,BT1-BTC,BT2-BTC,XZC-BTC,XZC-ETH,VET-USDT,VET-ETH,VET-BTC,NCASH-ETH,NCASH-BTC,GRS-BTC,GRS-ETH,RCCC-ETH,EGCC-ETH,IIC-ETH,SHE-ETH,RCCC-BTC,MEX-ETH,EKT-ETH,BKBT-ETH,GTC-ETH,HOT-ETH,FTI-ETH,GSC-ETH,PC-ETH,XMX-ETH,LYM-ETH,CNN-ETH,MAN-ETH,UC-ETH,AAC-ETH,FAIR-ETH,SEELE-ETH,UIP-ETH,LXT-ETH,DATX-ETH,GET-ETH,AE-ETH,UUU-ETH,YCC-ETH,CDC-ETH,BUT-ETH,PORTAL-ETH,SSP-ETH,REN-ETH,MT-ETH,RTE-BTC,FTI-BTC,EKT-BTC,REN-BTC,ZJLT-ETH,TOS-BTC,GET-BTC,SSP-BTC,MUSK-BTC,CNN-BTC,TOS-ETH,GVE-ETH,AE-BTC,NCC-BTC,KCASH-ETH,YCC-BTC,18C-ETH,PNT-ETH,CVCOIN-ETH,NCC-ETH,BCV-BTC,UIP-BTC,PNT-BTC,DAC-ETH,TRIO-ETH,SEELE-BTC,HOT-BTC,BCV-ETH,MUSK-ETH,GTC-BTC,BKBT-BTC,MAN-BTC,AAC-BTC,UC-BTC,SHE-BTC,BUT-BTC,IDT-ETH,MEX-BTC,IDT-BTC,DATX-BTC,ZJLT-BTC,FAIR-BTC,IIC-BTC,RTE-ETH,CDC-BTC,PC-BTC,DAC-BTC,EGCC-BTC,XMX-BTC,GSC-BTC,LXT-BTC,PORTAL-BTC,LYM-BTC,UUU-BTC,TRIO-BTC,KCASH-BTC,MT-HT,MT-BTC,KCASH-HT,18C-BTC,GVE-BTC,CVCOIN-BTC,ARDR-BTC,ARDR-ETH,HPT-USDT,HPT-BTC,HPT-HT,XLM-USDT", + "proxyAddress": "", + "websocketUrl": "NON_DEFAULT_HTTP_LINK_TO_WEBSOCKET_EXCHANGE_API", + "availablePairs": "BTC-USDT,BCH-USDT,ETH-USDT,ETC-USDT,LTC-USDT,EOS-USDT,XRP-USDT,OMG-USDT,DASH-USDT,ZEC-USDT,ADA-USDT,STEEM-USDT,IOTA-USDT,OCN-USDT,SOC-USDT,CTXC-USDT,ACT-USDT,BTM-USDT,BTS-USDT,ONT-USDT,IOST-USDT,HT-USDT,TRX-USDT,DTA-USDT,NEO-USDT,QTUM-USDT,SMT-USDT,ELA-USDT,VEN-USDT,THETA-USDT,SNT-USDT,ZIL-USDT,XEM-USDT,NAS-USDT,RUFF-USDT,HSR-USDT,LET-USDT,MDS-USDT,STORJ-USDT,ELF-USDT,ITC-USDT,CVC-USDT,GNT-USDT,XMR-BTC,BCH-BTC,ETH-BTC,LTC-BTC,ETC-BTC,EOS-BTC,OMG-BTC,XRP-BTC,DASH-BTC,ZEC-BTC,ADA-BTC,STEEM-BTC,IOTA-BTC,POLY-BTC,KAN-BTC,LBA-BTC,WAN-BTC,BFT-BTC,BTM-BTC,ONT-BTC,IOST-BTC,HT-BTC,TRX-BTC,SMT-BTC,ELA-BTC,WICC-BTC,OCN-BTC,ZLA-BTC,ABT-BTC,MTX-BTC,NAS-BTC,VEN-BTC,DTA-BTC,NEO-BTC,WAX-BTC,BTS-BTC,ZIL-BTC,THETA-BTC,CTXC-BTC,SRN-BTC,XEM-BTC,ICX-BTC,DGD-BTC,CHAT-BTC,WPR-BTC,LUN-BTC,SWFTC-BTC,SNT-BTC,MEET-BTC,YEE-BTC,ELF-BTC,LET-BTC,QTUM-BTC,LSK-BTC,ITC-BTC,SOC-BTC,QASH-BTC,MDS-BTC,EKO-BTC,TOPC-BTC,MTN-BTC,ACT-BTC,HSR-BTC,STK-BTC,STORJ-BTC,GNX-BTC,DBC-BTC,SNC-BTC,CMT-BTC,TNB-BTC,RUFF-BTC,QUN-BTC,ZRX-BTC,KNC-BTC,BLZ-BTC,PROPY-BTC,RPX-BTC,APPC-BTC,AIDOC-BTC,POWR-BTC,CVC-BTC,PAY-BTC,QSP-BTC,DAT-BTC,RDN-BTC,MCO-BTC,RCN-BTC,MANA-BTC,UTK-BTC,TNT-BTC,GAS-BTC,BAT-BTC,OST-BTC,LINK-BTC,GNT-BTC,MTL-BTC,EVX-BTC,REQ-BTC,ADX-BTC,AST-BTC,ENG-BTC,SALT-BTC,EDU-BTC,WTC-BTC,BIFI-BTC,BCX-BTC,BCD-BTC,SBTC-BTC,BTG-BTC,XMR-ETH,EOS-ETH,OMG-ETH,IOTA-ETH,ADA-ETH,STEEM-ETH,POLY-ETH,KAN-ETH,LBA-ETH,WAN-ETH,BFT-ETH,ZRX-ETH,AST-ETH,KNC-ETH,ONT-ETH,HT-ETH,BTM-ETH,IOST-ETH,SMT-ETH,ELA-ETH,TRX-ETH,ABT-ETH,NAS-ETH,OCN-ETH,WICC-ETH,ZIL-ETH,CTXC-ETH,ZLA-ETH,WPR-ETH,DTA-ETH,MTX-ETH,THETA-ETH,SRN-ETH,VEN-ETH,BTS-ETH,WAX-ETH,HSR-ETH,ICX-ETH,MTN-ETH,ACT-ETH,BLZ-ETH,QASH-ETH,RUFF-ETH,CMT-ETH,ELF-ETH,MEET-ETH,SOC-ETH,QTUM-ETH,ITC-ETH,SWFTC-ETH,YEE-ETH,LSK-ETH,LUN-ETH,LET-ETH,GNX-ETH,CHAT-ETH,EKO-ETH,TOPC-ETH,DGD-ETH,STK-ETH,MDS-ETH,DBC-ETH,SNC-ETH,PAY-ETH,QUN-ETH,AIDOC-ETH,TNB-ETH,APPC-ETH,RDN-ETH,UTK-ETH,POWR-ETH,BAT-ETH,PROPY-ETH,MANA-ETH,REQ-ETH,CVC-ETH,QSP-ETH,EVX-ETH,DAT-ETH,MCO-ETH,GNT-ETH,GAS-ETH,OST-ETH,LINK-ETH,RCN-ETH,TNT-ETH,ENG-ETH,SALT-ETH,ADX-ETH,EDU-ETH,WTC-ETH,XRP-HT,IOST-HT,DASH-HT,WICC-USDT,EOS-HT,BCH-HT,LTC-HT,ETC-HT,WAVES-BTC,WAVES-ETH,HB10-USDT,CMT-USDT,DCR-BTC,DCR-ETH,PAI-BTC,PAI-ETH", "enabledPairs": "BTC-USDT", "baseCurrencies": "USD", "assetTypes": "SPOT", @@ -803,8 +837,10 @@ "apiAuthPemKey": "-----BEGIN EC PRIVATE KEY-----\nJUSTADUMMY\n-----END EC PRIVATE KEY-----\n", "apiUrl": "NON_DEFAULT_HTTP_LINK_TO_EXCHANGE_API", "apiUrlSecondary": "NON_DEFAULT_HTTP_LINK_TO_EXCHANGE_API", - "availablePairs": "NCC-BTC,MUSK-BTC,TOS-BTC,BCV-BTC,DAC-BTC,IDT-BTC,PNT-BTC,ZJLT-BTC,LYM-BTC,SSP-BTC,FAIR-BTC,YCC-BTC,XMX-BTC,EKT-BTC,FTI-BTC,SEELE-BTC,GVE-BTC,BKBT-BTC,AE-BTC,REN-BTC,PC-BTC,GET-BTC,MAN-BTC,HOT-BTC,GTC-BTC,PORTAL-BTC,DATX-BTC,18C-BTC,BUT-BTC,LXT-BTC,CDC-BTC,UUU-BTC,AAC-BTC,CNN-BTC,UIP-BTC,UC-BTC,GSC-BTC,IIC-BTC,MEX-BTC,EGCC-BTC,SHE-BTC,NCC-ETH,MUSK-ETH,TOS-ETH,BCV-ETH,DAC-ETH,IDT-ETH,PNT-ETH,ZJLT-ETH,LYM-ETH,SSP-ETH,FAIR-ETH,YCC-ETH,XMX-ETH,EKT-ETH,FTI-ETH,SEELE-ETH,GVE-ETH,BKBT-ETH,AE-ETH,REN-ETH,PC-ETH,GET-ETH,MAN-ETH,HOT-ETH,GTC-ETH,PORTAL-ETH,DATX-ETH,18C-ETH,BUT-ETH,LXT-ETH,CDC-ETH,UUU-ETH,AAC-ETH,CNN-ETH,UIP-ETH,UC-ETH,GSC-ETH,IIC-ETH,MEX-ETH,EGCC-ETH,SHE-ETH,MT-HT,KCASH-HT,RCCC-ETH,RCCC-BTC,CVCOIN-ETH,CVCOIN-BTC,RTE-ETH,RTE-BTC,KCASH-BTC,KCASH-ETH,MT-ETH,MT-BTC,TRIO-BTC,TRIO-ETH,HPT-USDT,HPT-BTC,HPT-HT", - "enabledPairs": "NCC-BTC", + "proxyAddress": "", + "websocketUrl": "NON_DEFAULT_HTTP_LINK_TO_WEBSOCKET_EXCHANGE_API", + "availablePairs": "BTC-USDT,BCH-USDT,ETH-USDT,ETC-USDT,LTC-USDT,EOS-USDT,XRP-USDT,OMG-USDT,DASH-USDT,ZEC-USDT,ADA-USDT,STEEM-USDT,IOTA-USDT,OCN-USDT,SOC-USDT,CTXC-USDT,ACT-USDT,BTM-USDT,BTS-USDT,ONT-USDT,IOST-USDT,HT-USDT,TRX-USDT,DTA-USDT,NEO-USDT,QTUM-USDT,SMT-USDT,ELA-USDT,VEN-USDT,THETA-USDT,SNT-USDT,ZIL-USDT,XEM-USDT,NAS-USDT,RUFF-USDT,HSR-USDT,LET-USDT,MDS-USDT,STORJ-USDT,ELF-USDT,ITC-USDT,CVC-USDT,GNT-USDT,XMR-BTC,BCH-BTC,ETH-BTC,LTC-BTC,ETC-BTC,EOS-BTC,OMG-BTC,XRP-BTC,DASH-BTC,ZEC-BTC,ADA-BTC,STEEM-BTC,IOTA-BTC,POLY-BTC,KAN-BTC,LBA-BTC,WAN-BTC,BFT-BTC,BTM-BTC,ONT-BTC,IOST-BTC,HT-BTC,TRX-BTC,SMT-BTC,ELA-BTC,WICC-BTC,OCN-BTC,ZLA-BTC,ABT-BTC,MTX-BTC,NAS-BTC,VEN-BTC,DTA-BTC,NEO-BTC,WAX-BTC,BTS-BTC,ZIL-BTC,THETA-BTC,CTXC-BTC,SRN-BTC,XEM-BTC,ICX-BTC,DGD-BTC,CHAT-BTC,WPR-BTC,LUN-BTC,SWFTC-BTC,SNT-BTC,MEET-BTC,YEE-BTC,ELF-BTC,LET-BTC,QTUM-BTC,LSK-BTC,ITC-BTC,SOC-BTC,QASH-BTC,MDS-BTC,EKO-BTC,TOPC-BTC,MTN-BTC,ACT-BTC,HSR-BTC,STK-BTC,STORJ-BTC,GNX-BTC,DBC-BTC,SNC-BTC,CMT-BTC,TNB-BTC,RUFF-BTC,QUN-BTC,ZRX-BTC,KNC-BTC,BLZ-BTC,PROPY-BTC,RPX-BTC,APPC-BTC,AIDOC-BTC,POWR-BTC,CVC-BTC,PAY-BTC,QSP-BTC,DAT-BTC,RDN-BTC,MCO-BTC,RCN-BTC,MANA-BTC,UTK-BTC,TNT-BTC,GAS-BTC,BAT-BTC,OST-BTC,LINK-BTC,GNT-BTC,MTL-BTC,EVX-BTC,REQ-BTC,ADX-BTC,AST-BTC,ENG-BTC,SALT-BTC,EDU-BTC,XVG-BTC,WTC-BTC,BIFI-BTC,BCX-BTC,BCD-BTC,SBTC-BTC,BTG-BTC,XMR-ETH,EOS-ETH,OMG-ETH,IOTA-ETH,ADA-ETH,STEEM-ETH,POLY-ETH,KAN-ETH,LBA-ETH,WAN-ETH,BFT-ETH,ZRX-ETH,AST-ETH,KNC-ETH,ONT-ETH,HT-ETH,BTM-ETH,IOST-ETH,SMT-ETH,ELA-ETH,TRX-ETH,ABT-ETH,NAS-ETH,OCN-ETH,WICC-ETH,ZIL-ETH,CTXC-ETH,ZLA-ETH,WPR-ETH,DTA-ETH,MTX-ETH,THETA-ETH,SRN-ETH,VEN-ETH,BTS-ETH,WAX-ETH,HSR-ETH,ICX-ETH,MTN-ETH,ACT-ETH,BLZ-ETH,QASH-ETH,RUFF-ETH,CMT-ETH,ELF-ETH,MEET-ETH,SOC-ETH,QTUM-ETH,ITC-ETH,SWFTC-ETH,YEE-ETH,LSK-ETH,LUN-ETH,LET-ETH,GNX-ETH,CHAT-ETH,EKO-ETH,TOPC-ETH,DGD-ETH,STK-ETH,MDS-ETH,DBC-ETH,SNC-ETH,PAY-ETH,QUN-ETH,AIDOC-ETH,TNB-ETH,APPC-ETH,RDN-ETH,UTK-ETH,POWR-ETH,BAT-ETH,PROPY-ETH,MANA-ETH,REQ-ETH,CVC-ETH,QSP-ETH,EVX-ETH,DAT-ETH,MCO-ETH,GNT-ETH,GAS-ETH,OST-ETH,LINK-ETH,RCN-ETH,TNT-ETH,ENG-ETH,SALT-ETH,ADX-ETH,EDU-ETH,XVG-ETH,WTC-ETH,XRP-HT,IOST-HT,DASH-HT,WICC-USDT,EOS-HT,BCH-HT,LTC-HT,ETC-HT,WAVES-BTC,WAVES-ETH,HB10-USDT,CMT-USDT,DCR-BTC,DCR-ETH,PAI-BTC,PAI-ETH,BOX-BTC,BOX-ETH,DGB-BTC,DGB-ETH,GXS-BTC,GXS-ETH,XLM-BTC,XLM-ETH,BIX-BTC,BIX-ETH,BIX-USDT,HIT-BTC,HIT-ETH,BT1-BTC,BT2-BTC", + "enabledPairs": "BTC-USDT", "baseCurrencies": "USD", "assetTypes": "SPOT", "supportsAutoPairUpdates": true, @@ -841,6 +877,8 @@ "apiSecret": "Secret", "apiUrl": "NON_DEFAULT_HTTP_LINK_TO_EXCHANGE_API", "apiUrlSecondary": "NON_DEFAULT_HTTP_LINK_TO_EXCHANGE_API", + "proxyAddress": "", + "websocketUrl": "NON_DEFAULT_HTTP_LINK_TO_WEBSOCKET_EXCHANGE_API", "clientId": "ClientID", "availablePairs": "XBTUSD,XBTSGD,XBTEUR", "enabledPairs": "XBTUSD,XBTSGD,XBTEUR", @@ -880,7 +918,9 @@ "apiSecret": "Secret", "apiUrl": "NON_DEFAULT_HTTP_LINK_TO_EXCHANGE_API", "apiUrlSecondary": "NON_DEFAULT_HTTP_LINK_TO_EXCHANGE_API", - "availablePairs": "EOS-XBT,QTUM-USD,REP-XBT,ADA-ETH,ADA-XBT,DASH-USD,GNO-EUR,XLM-USD,ADA-USD,DASH-EUR,EOS-ETH,ETH-GBP,ADA-EUR,ZEC-XBT,DASH-XBT,LTC-EUR,REP-ETH,REP-EUR,XLM-XBT,XRP-USD,ETH-USD,ETH-CAD,XBT-EUR,ZEC-EUR,ZEC-JPY,ZEC-USD,EOS-EUR,GNO-USD,ETC-USD,XBT-GBP,BCH-USD,QTUM-ETH,XBT-CAD,EOS-USD,GNO-XBT,XRP-CAD,BCH-EUR,ETH-EUR,ICN-ETH,MLN-ETH,XRP-XBT,BCH-XBT,XMR-XBT,XMR-EUR,LTC-XBT,ETC-ETH,ETH-XBT,MLN-XBT,QTUM-CAD,USDT-USD,ETC-EUR,ETH-JPY,ICN-XBT,LTC-USD,XRP-EUR,QTUM-EUR,QTUM-XBT,ETC-XBT,XBT-JPY,XMR-USD,GNO-ETH,REP-USD,XBT-USD,XDG-XBT,XLM-EUR,XRP-JPY,ADA-CAD", + "proxyAddress": "", + "websocketUrl": "NON_DEFAULT_HTTP_LINK_TO_WEBSOCKET_EXCHANGE_API", + "availablePairs": "XMR-EUR,DASH-USD,ETC-ETH,XLM-XBT,XLM-USD,DASH-EUR,REP-XBT,XBT-CAD,ZEC-XBT,GNO-USD,ETC-EUR,ICN-ETH,LTC-USD,XLM-EUR,BCH-XBT,DASH-XBT,GNO-ETH,ETC-XBT,XRP-CAD,ETH-EUR,XBT-USD,XDG-XBT,REP-EUR,ETH-XBT,LTC-XBT,MLN-ETH,REP-ETH,XRP-XBT,ZEC-EUR,ZEC-USD,EOS-ETH,ETH-GBP,XBT-EUR,XMR-USD,XRP-JPY,ZEC-JPY,ETH-JPY,ICN-XBT,MLN-XBT,GNO-EUR,EOS-EUR,ETH-USD,USDT-USD,ETH-CAD,XBT-GBP,BCH-USD,EOS-USD,EOS-XBT,XRP-EUR,BCH-EUR,ETC-USD,LTC-EUR,GNO-XBT,REP-USD,XBT-JPY,XMR-XBT,XRP-USD", "enabledPairs": "XBT-USD", "baseCurrencies": "EUR,USD,CAD,GBP,JPY", "assetTypes": "SPOT", @@ -919,7 +959,9 @@ "apiSecret": "Secret", "apiUrl": "NON_DEFAULT_HTTP_LINK_TO_EXCHANGE_API", "apiUrlSecondary": "NON_DEFAULT_HTTP_LINK_TO_EXCHANGE_API", - "availablePairs": "BTCCHF,NZDUSD,USDCHF,BTCGBP,BTCNGN,USDHKD,BTCEUR,BTCJPY,GBPUSD,BTCNZD,BTCUSD,USDCAD,USDSGD,LTCBTC,USDJPY,BTCAUD,EURUSD,USDNGN,BACETH,BTCCAD,BTCSGD,ETHBTC,BCHBTC,AUDUSD,XRPBTC", + "proxyAddress": "", + "websocketUrl": "NON_DEFAULT_HTTP_LINK_TO_WEBSOCKET_EXCHANGE_API", + "availablePairs": "AUDUSD,BTCCAD,EURUSD,BTCAUD,USDJPY,XRPBTC,LTCBTC,USDCAD,USDNGN,ETHBTC,GBPUSD,NZDUSD,USDCHF,BTCUSD,USDSGD,BTCJPY,USDHKD,BCHBTC,BACETH,BTCNZD,BTCGBP,BTCSGD,BTCCHF,BTCNGN", "enabledPairs": "BTCUSD,BTCAUD", "baseCurrencies": "USD,EUR,HKD,AUD,GBP,NZD,JPY,SGD,NGN,CHF,CAD", "assetTypes": "SPOT", @@ -956,7 +998,9 @@ "apiSecret": "Secret", "apiUrl": "NON_DEFAULT_HTTP_LINK_TO_EXCHANGE_API", "apiUrlSecondary": "NON_DEFAULT_HTTP_LINK_TO_EXCHANGE_API", - "availablePairs": "NET_ETH,STORJ_BTC,BMC_USDT,SRN_ETH,ANT_USDT,ANT_BTC,SRN_USDT,BMC_BTC,INS_BTC,CVC_ETH,GNT_USDT,INS_USDT,OAX_ETH,KNC_BTC,AST_ETH,ENJ_BTC,TTU_BTC,REP_ETH,SNGLS_BTC,AE_BTC,AION_USDT,HOT_BTC,SNM_ETH,SALT_USDT,TNT_USDT,TNT_ETH,TRX_ETH,ADX_USDT,GNO_ETH,XID_BTC,NET_BTC,BCC_BTC,TRST_ETH,WINGS_ETH,STX_BTC,ENJ_USDT,GNT_ETH,AE_USDT,OMG_BTC,INS_ETH,AGI_USDT,RLC_BTC,TRST_BTC,PTOY_USDT,DGD_USDT,ZRX_BTC,NEU_ETH,AE_ETH,LDC_BTC,TTU_ETH,QRL_ETH,BNT_ETH,ZRX_ETH,SAN_ETH,SRN_BTC,AION_BTC,SNGLS_USDT,WPR_USDT,DNT_USDT,SNT_BTC,KNC_USDT,ETH_BTC,REP_USDT,BNT_USDT,SNT_ETH,DGD_ETH,BTC_USDT,NEU_BTC,NEU_USDT,AGI_BTC,LTC_ETH,ADX_BTC,NET_USDT,CVC_USDT,TRX_BTC,STORJ_ETH,IND_ETH,AST_BTC,WPR_BTC,REN_USDT,LTC_USDT,GNO_USDT,BMC_ETH,WINGS_USDT,CVC_BTC,KNC_ETH,REN_ETH,HOT_USDT,ANT_ETH,CLN_USDT,DNT_ETH,GUP_BTC,BCC_ETH,DASH_ETH,CLN_BTC,XID_USDT,OAX_USDT,ENG_USDT,IND_BTC,IND_USDT,PTOY_BTC,SNM_BTC,MANA_USDT,ENG_BTC,PRO_BTC,LTC_BTC,PAY_BTC,AGI_ETH,SAN_BTC,MANA_BTC,HOT_ETH,ETH_USDT,BNT_BTC,CLN_ETH,SNGLS_ETH,PRO_ETH,OMG_USDT,LDC_ETH,DASH_BTC,DASH_USDT,PTOY_ETH,AION_ETH,WPR_ETH,STX_ETH,MANA_ETH,ENG_ETH,STORJ_USDT,AST_USDT,RLC_ETH,SAN_USDT,LDC_USDT,SNM_USDT,SNT_USDT,XID_ETH,TRX_USDT,SALT_ETH,REN_BTC,TRST_USDT,SALT_BTC,REP_BTC,ENJ_ETH,GNO_BTC,ZRX_USDT,TNT_BTC,RLC_USDT,QRL_BTC,QRL_USDT,ADX_ETH,PRO_USDT,GUP_ETH,DGD_BTC,OAX_BTC,STX_USDT,PAY_USDT,OMG_ETH,DNT_BTC,TTU_USDT,PAY_ETH,WINGS_BTC,GUP_USDT,GNT_BTC,BCC_USDT", + "proxyAddress": "", + "websocketUrl": "NON_DEFAULT_HTTP_LINK_TO_WEBSOCKET_EXCHANGE_API", + "availablePairs": "ADX_USDT,OMG_ETH,TRX_ETH,AION_USDT,REP_ETH,GUP_ETH,SNT_USDT,TNT_BTC,AGI_ETH,AST_ETH,SRN_ETH,WAVES_ETH,WINGS_ETH,PTOY_ETH,BCC_ETH,ZRX_BTC,KNC_BTC,MLN_ETH,LTC_USDT,ADX_BTC,REN_ETH,DASH_ETH,ANT_USDT,PTOY_USDT,MCO_ETH,DASH_USDT,GNT_USDT,MGO_USDT,OMG_USDT,AION_ETH,EDG_USDT,GNO_BTC,MANA_USDT,AGI_BTC,WINGS_BTC,SNGLS_ETH,BMC_ETH,WPR_ETH,MLN_USDT,TNT_ETH,MCO_USDT,PAY_ETH,NET_USDT,AST_BTC,QRL_USDT,SNM_USDT,STX_BTC,ZRX_USDT,INS_ETH,WAVES_BTC,GUP_USDT,MYST_BTC,PAY_USDT,ICN_USDT,CFI_USDT,TRX_USDT,QRL_BTC,AE_USDT,SNGLS_BTC,SNM_BTC,REQ_USDT,RLC_USDT,TKN_BTC,DGD_BTC,NEU_ETH,LDC_USDT,REP_BTC,EDG_ETH,CVC_USDT,ANT_ETH,ZRX_ETH,INS_USDT,EOS_BTC,OAX_BTC,TNT_USDT,DASH_BTC,GNO_ETH,CLN_USDT,GNO_USDT,BNT_USDT,IND_USDT,ENG_ETH,ENJ_BTC,CVC_ETH,NEU_BTC,AION_BTC,SNT_BTC,PRO_BTC,ENJ_ETH,BTC_USDT,SNGLS_USDT,SNM_ETH,WINGS_USDT,XID_USDT,IND_ETH,EDG_BTC,RLC_ETH,TAAS_ETH,TRST_BTC,GUP_BTC,VEN_BTC,SRN_BTC,VEN_USDT,REP_USDT,BAT_USDT,AE_ETH,NET_ETH,WPR_BTC,BNT_ETH,BNT_BTC,DGD_ETH,BCC_BTC,EOS_ETH,OAX_ETH,GNT_BTC,BMC_BTC,KNC_ETH,SRN_USDT,REN_USDT,DNT_USDT,BAT_ETH,ICN_BTC,ETH_USDT,TAAS_BTC,ANT_BTC,LDC_BTC,SAN_ETH,OAX_USDT,PRO_USDT,AST_USDT,INS_BTC,SNT_ETH,STORJ_ETH,DNT_BTC,ENG_USDT,LDC_ETH,PTOY_BTC,STX_ETH,STORJ_USDT,ADX_ETH,TIME_BTC,GNT_ETH,TRX_BTC,REQ_ETH,ICN_ETH,TKN_ETH,DGD_USDT,WPR_USDT,VEN_ETH,XID_ETH,NEU_USDT,OMG_BTC,SAN_USDT,CLN_ETH,REN_BTC,ENJ_USDT,MGO_ETH,MYST_ETH,CFI_BTC,STORJ_BTC,AE_BTC,SALT_BTC,SALT_ETH,ENG_BTC,AGI_USDT,TRST_USDT,TAAS_USDT,QRL_ETH,MCO_BTC,KNC_USDT,SALT_USDT,BMC_USDT,MANA_ETH,TIME_ETH,TKN_USDT,PAY_BTC,SAN_BTC,NET_BTC,IND_BTC,LTC_ETH,EOS_USDT,ETH_BTC,RLC_BTC,TRST_ETH,CLN_BTC,CFI_ETH,MANA_BTC,REQ_BTC,LTC_BTC,XID_BTC,CVC_BTC,STX_USDT,PRO_ETH,MLN_BTC,MYST_USDT,BCC_USDT,WAVES_USDT,TIME_USDT,BAT_BTC,MGO_BTC,DNT_ETH", "enabledPairs": "ETH_BTC,LTC_BTC,DASH_BTC", "baseCurrencies": "USD", "assetTypes": "SPOT", @@ -996,6 +1040,8 @@ "apiSecret": "Secret", "apiUrl": "NON_DEFAULT_HTTP_LINK_TO_EXCHANGE_API", "apiUrlSecondary": "NON_DEFAULT_HTTP_LINK_TO_EXCHANGE_API", + "proxyAddress": "", + "websocketUrl": "NON_DEFAULT_HTTP_LINK_TO_WEBSOCKET_EXCHANGE_API", "availablePairs": "BTCARS,BTCAUD,BTCBRL,BTCCAD,BTCCHF,BTCCZK,BTCDKK,BTCEUR,BTCGBP,BTCHKD,BTCILS,BTCINR,BTCMXN,BTCNOK,BTCNZD,BTCPLN,BTCRUB,BTCSEK,BTCSGD,BTCTHB,BTCUSD,BTCZAR", "enabledPairs": "BTCARS,BTCAUD,BTCBRL,BTCCAD,BTCCHF,BTCCZK,BTCDKK,BTCEUR,BTCGBP,BTCHKD,BTCILS,BTCINR,BTCMXN,BTCNOK,BTCNZD,BTCPLN,BTCRUB,BTCSEK,BTCSGD,BTCTHB,BTCUSD,BTCZAR", "baseCurrencies": "ARS,AUD,BRL,CAD,CHF,CZK,DKK,EUR,GBP,HKD,ILS,INR,MXN,NOK,NZD,PLN,RUB,SEK,SGD,THB,USD,ZAR", @@ -1034,6 +1080,8 @@ "apiSecret": "Secret", "apiUrl": "NON_DEFAULT_HTTP_LINK_TO_EXCHANGE_API", "apiUrlSecondary": "NON_DEFAULT_HTTP_LINK_TO_EXCHANGE_API", + "proxyAddress": "", + "websocketUrl": "NON_DEFAULT_HTTP_LINK_TO_WEBSOCKET_EXCHANGE_API", "availablePairs": "BTCCNY,LTCCNY", "enabledPairs": "BTCCNY,LTCCNY", "baseCurrencies": "CNY", @@ -1073,6 +1121,8 @@ "apiSecret": "Secret", "apiUrl": "NON_DEFAULT_HTTP_LINK_TO_EXCHANGE_API", "apiUrlSecondary": "NON_DEFAULT_HTTP_LINK_TO_EXCHANGE_API", + "proxyAddress": "", + "websocketUrl": "NON_DEFAULT_HTTP_LINK_TO_WEBSOCKET_EXCHANGE_API", "availablePairs": "BTC_USD,LTC_USD,ETH_USD,ETC_USD,BCH_USD,USDT_USD,ADA_USD,TUSD_USD,XLM_USD,XRP_USD,ZEC_USD,ZRX_USD,LTC_BTC,ETH_BTC,ETC_BTC,BCH_BTC,USDT_BTC,ADA_BTC,TUSD_BTC,XLM_BTC,XRP_BTC,ZEC_BTC,ZRX_BTC,LTC_ETH,ETC_ETH,BCH_ETH,USDT_ETH,ADA_ETH,TUSD_ETH,XLM_ETH,XRP_ETH,ZEC_ETH,ZRX_ETH", "enabledPairs": "BTC_USD", "baseCurrencies": "USD", @@ -1113,6 +1163,8 @@ "apiSecret": "Secret", "apiUrl": "NON_DEFAULT_HTTP_LINK_TO_EXCHANGE_API", "apiUrlSecondary": "NON_DEFAULT_HTTP_LINK_TO_EXCHANGE_API", + "proxyAddress": "", + "websocketUrl": "NON_DEFAULT_HTTP_LINK_TO_WEBSOCKET_EXCHANGE_API", "availablePairs": "btc_usdt,eos_usdt,eth_usdt,bch_usdt,etc_usdt,eth_btc,etc_btc,xrp_usdt,bch_btc,trx_usdt,eos_btc,trx_btc,ltc_usdt,etc_eth,gto_usdt,gto_btc,ae_usdt,gto_eth,ont_usdt,dash_usdt,xrp_btc,btm_usdt,ltc_btc,eos_eth,hyc_usdt,zip_btc,hyc_eth,mith_usdt,dpy_usdt,qtum_usdt,ors_btc,mith_btc,xmr_usdt,zip_eth,mkr_btc,iost_usdt,zip_usdt,pst_usdt,mith_eth,hpb_btc,ae_btc,nas_usdt,cmt_usdt,true_usdt,hpb_usdt,ada_usdt,dash_btc,xlm_usdt,rct_eth,lba_usdt,neo_btc,egt_eth,hyc_btc,elf_usdt,int_usdt,qtum_btc,dadi_btc,xrp_eth,btm_btc,elf_btc,zil_usdt,hpb_eth,ors_eth,xmr_btc,lrc_usdt,lba_btc,iost_btc,bkx_btc,rfr_btc,of_usdt,bch_eth,neo_usdt,light_eth,vite_eth,mof_eth,ont_btc,light_usdt,let_usdt,ctxc_usdt,abl_eth,rnt_usdt,nas_btc,int_btc,trx_eth,bcn_btc,iota_usdt,zec_usdt,tnb_eth,show_usdt,bkx_usdt,xlm_btc,cmt_btc,zco_btc,zco_eth,ins_btc,bcd_usdt,itc_btc,vite_btc,cit_eth,kan_usdt,mda_btc,rfr_usdt,btm_eth,kcash_usdt,mdt_usdt,iota_btc,aac_usdt,wtc_usdt,storj_btc,mof_btc,ugc_usdt,aac_btc,chat_usdt,zil_btc,icx_usdt,zrx_usdt,mda_usdt,theta_usdt,act_usdt,mvp_eth,omg_usdt,ada_btc,vib_btc,lrc_btc,mco_btc,snt_usdt,xlm_eth,of_btc,soc_usdt,hc_usdt,tct_usdt,cvc_usdt,abl_btc,ugc_btc,ltc_eth,cvc_btc,iost_eth,kan_eth,true_btc,trio_eth,rfr_eth,swftc_eth,itc_usdt,lba_eth,xuc_usdt,ctxc_btc,dadi_eth,can_btc,zec_btc,hmc_btc,abt_usdt,elf_eth,mana_usdt,zrx_btc,stc_usdt,xem_usdt,knc_usdt,snt_btc,ae_eth,evx_btc,gnt_usdt,spf_btc,icx_btc,ace_btc,ont_eth,can_usdt,chat_btc,ssc_btc,gnx_btc,vib_usdt,storj_usdt,pra_usdt,ace_usdt,int_eth,insur_usdt,enj_btc,mdt_btc,snc_btc,swftc_usdt,dpy_btc,gnt_btc,xem_btc,mof_usdt,itc_eth,wtc_btc,ctxc_eth,let_btc,mana_btc,ins_usdt,of_eth,abt_eth,tnb_usdt,bcn_usdt,omg_btc,nas_eth,xas_usdt,knc_btc,soc_btc,topc_usdt,dat_usdt,cit_btc,mkr_eth,bcd_btc,hc_btc,hmc_usdt,bkx_eth,gas_usdt,act_btc,ugc_eth,aac_eth,ost_btc,cvt_usdt,tnb_btc,mdt_eth,dat_btc,ssc_usdt,true_eth,insur_btc,btg_btc,cvc_eth,qtum_eth,mkr_usdt,fair_btc,qun_btc,ark_btc,kan_btc,mco_usdt,swftc_btc,gnx_eth,pay_usdt,yee_usdt,qun_eth,ppt_btc,win_usdt,zen_usdt,gas_btc,light_btc,btg_usdt,pay_btc,nuls_btc,read_btc,key_usdt,abt_btc,ipc_usdt,theta_btc,key_btc,stc_btc,rnt_btc,nuls_usdt,wrc_usdt,kcash_btc,lrc_eth,knc_eth,sc_usdt,ppt_usdt,fair_usdt,qun_usdt,enj_eth,stc_eth,auto_usdt,hc_eth,dash_eth,insur_eth,iota_eth,rct_usdt,1st_usdt,zil_eth,sc_btc,dat_eth,ark_usdt,fair_eth,yoyo_btc,auto_btc,vib_eth,ardr_btc,evx_usdt,fun_btc,mana_eth,zrx_eth,xmr_eth,soc_eth,let_eth,mtl_btc,trio_usdt,can_eth,snt_eth,poe_btc,nano_btc,omg_eth,win_eth,1st_btc,wtc_eth,icx_eth,enj_usdt,neo_eth,zec_eth,pra_btc,lsk_btc,gnt_eth,nano_usdt,hot_usdt,gnx_usdt,hot_btc,ors_usdt,fun_usdt,storj_eth,show_btc,dpy_eth,bcx_btc,mvp_usdt,egt_usdt,salt_usdt,ssc_eth,xas_btc,aidoc_usdt,dgb_usdt,ost_usdt,hot_eth,hmc_eth,poe_usdt,uct_eth,utk_usdt,sub_btc,sc_eth,topc_btc,sngls_btc,lsk_usdt,rct_btc,key_eth,r_btc,1st_eth,mvp_btc,gas_eth,mth_btc,icn_btc,ren_btc,dent_usdt,cmt_eth,ins_eth,lev_usdt,eng_usdt,pst_btc,lend_usdt,pra_eth,ada_eth,ark_eth,kcash_eth,pay_eth,uct_usdt,sbtc_btc,cvt_eth,link_usdt,trio_btc,xem_eth,oax_btc,mtl_usdt,dcr_usdt,aidoc_btc,cvt_btc,zen_btc,mda_eth,r_usdt,ast_usdt,bnt_btc,tct_btc,auto_eth,mag_usdt,rcn_btc,gsc_usdt,act_eth,rcn_usdt,edo_btc,chat_eth,ipc_eth,dna_btc,ast_btc,xuc_btc,nuls_eth,dgb_btc,dnt_usdt,rnt_eth,bnt_usdt,lend_btc,link_btc,yee_btc,dcr_btc,mco_eth,mth_usdt,theta_eth,salt_btc,ppt_eth,mot_usdt,poe_eth,ref_usdt,lev_eth,ren_usdt,win_btc,lev_btc,bnt_eth,dent_btc,waves_btc,show_eth,yee_eth,sda_btc,fun_eth,tct_eth,rdn_usdt,cbt_btc,ren_eth,edo_usdt,eng_btc,sub_usdt,ref_eth,you_btc,lend_eth,egt_btc,ngc_btc,waves_usdt,spf_usdt,ipc_btc,oax_usdt,san_usdt,dna_usdt,dent_eth,uct_btc,mot_eth,ost_eth,mot_btc,icn_eth,link_eth,dgb_eth,cbt_usdt,hit_eth,rdn_btc,nxt_btc,r_eth,req_eth,xuc_eth,la_eth,snm_eth,ace_eth,yoyo_usdt,pst_eth,gsc_eth,ngc_usdt,sda_eth,yoyo_eth,amm_usdt,rdn_eth,vee_usdt,dadi_usdt,evx_eth,snc_usdt,dgd_btc,dgd_usdt,salt_eth,ubtc_usdt,sngls_eth,mtl_eth,topc_eth,cbt_eth,gsc_btc,dna_eth,xas_eth,you_eth,sub_eth,mag_btc,icn_usdt,zen_eth,lsk_eth,vee_eth,nano_eth,aidoc_eth,req_usdt,amm_btc,avt_btc,brd_eth,ngc_eth,eng_eth,spf_eth,mth_eth,avt_eth,waves_eth,viu_btc,dcr_eth,snc_eth,edo_eth,amm_eth,viu_eth,rcn_eth,ast_eth,hit_btc,atl_usdt,cag_btc,ukg_usdt,ssp_usdt,qvt_usdt,viu_usdt,ssp_btc,dgd_eth,cag_eth,mag_eth,avt_usdt,ssp_eth,cag_usdt,ukg_eth,brd_usdt,sngls_usdt,atl_eth", "enabledPairs": "tct_eth", "baseCurrencies": "USD", @@ -1153,6 +1205,8 @@ "apiSecret": "Secret", "apiUrl": "NON_DEFAULT_HTTP_LINK_TO_EXCHANGE_API", "apiUrlSecondary": "NON_DEFAULT_HTTP_LINK_TO_EXCHANGE_API", + "proxyAddress": "", + "websocketUrl": "NON_DEFAULT_HTTP_LINK_TO_WEBSOCKET_EXCHANGE_API", "availablePairs": "BTC_XPM,USDT_BCH,ETH_LOOM,USDT_GNT,XMR_NXT,ETH_ETC,BTC_REP,XMR_ZEC,BTC_CLAM,BTC_DOGE,USDT_XMR,USDT_XRP,ETH_OMG,ETH_SNT,BTC_MAID,BTC_XCP,ETH_BCH,ETH_ZRX,BTC_DGB,BTC_XMR,USDT_ETC,BTC_ARDR,BTC_LTC,BTC_SNT,ETH_KNC,ETH_ZEC,BTC_PASC,USDC_BTC,BTC_XEM,XMR_DASH,XMR_LTC,USDT_ETH,BTC_KNC,BTC_LOOM,XMR_MAID,ETH_STEEM,ETH_GNT,BTC_EOS,ETH_BAT,USDT_LOOM,BTC_STR,BTC_XRP,BTC_STEEM,BTC_ZEC,BTC_STRAT,BTC_GAS,BTC_BCN,BTC_SYS,USDT_DASH,BTC_LBC,ETH_REP,ETH_EOS,USDT_SNT,USDT_LSK,USDT_LTC,USDT_STR,ETH_LSK,BTC_ETC,ETH_QTUM,BTC_HUC,BTC_OMNI,USDT_BTC,BTC_LSK,USDC_USDT,BTC_BCH,ETH_CVC,USDT_EOS,USDT_BAT,USDT_ZRX,BTC_NMC,BTC_GNT,BTC_STORJ,BTC_BAT,ETH_GAS,USDT_KNC,USDT_DOGE,USDC_ETH,BTC_BTS,BTC_VIA,BTC_ETH,BTC_OMG,USDT_NXT,XMR_BCN,BTC_SC,BTC_FCT,BTC_DASH,BTC_NXT,BTC_PPC,BTC_VTC,USDT_QTUM,BTC_SBD,USDT_REP,USDT_ZEC,BTC_ZRX,BTC_BURST,BTC_GAME,BTC_NAV,BTC_DCR,BTC_CVC,USDT_SC,BTC_QTUM", "enabledPairs": "BTC_LTC,BTC_ETH,BTC_DOGE,BTC_DASH,BTC_XRP", "baseCurrencies": "USD", @@ -1192,6 +1246,8 @@ "apiSecret": "Secret", "apiUrl": "NON_DEFAULT_HTTP_LINK_TO_EXCHANGE_API", "apiUrlSecondary": "NON_DEFAULT_HTTP_LINK_TO_EXCHANGE_API", + "proxyAddress": "", + "websocketUrl": "NON_DEFAULT_HTTP_LINK_TO_WEBSOCKET_EXCHANGE_API", "availablePairs": "DASH_BTC,WAVES_BTC,LSK_BTC,LIZA_BTC,BCC_BTC,ETH_BTC,LTC_BTC,TRX_BTC,DOGE_BTC,VNTX_BTC,SW_BTC,ZEC_BTC,DASH_ETH,WAVES_ETH,LSK_ETH,LIZA_ETH,BCC_ETH,LTC_ETH,TRX_ETH,DOGE_ETH,VNTX_ETH,SW_ETH,ZEC_ETH,DASH_DOGE,WAVES_DOGE,LSK_DOGE,LIZA_DOGE,BCC_DOGE,LTC_DOGE,TRX_DOGE,VNTX_DOGE,SW_DOGE,ZEC_DOGE,DASH_USD,WAVES_USD,LSK_USD,LIZA_USD,BCC_USD,LTC_USD,TRX_USD,VNTX_USD,SW_USD,ZEC_USD,ETH_USD,BTC_USD,DASH_RUR,WAVES_BTC,WAVES_RUR,LSK_RUR,LIZA_RUR,BCC_RUR,LTC_RUR,TRX_RUR,VNTX_RUR,SW_RUR,ETH_RUR,ZEC_RUR", "enabledPairs": "LTC_BTC,ETH_BTC,BTC_USD,DASH_BTC", "baseCurrencies": "USD", @@ -1233,6 +1289,8 @@ "apiSecret": "Secret", "apiUrl": "NON_DEFAULT_HTTP_LINK_TO_EXCHANGE_API", "apiUrlSecondary": "NON_DEFAULT_HTTP_LINK_TO_EXCHANGE_API", + "proxyAddress": "", + "websocketUrl": "NON_DEFAULT_HTTP_LINK_TO_WEBSOCKET_EXCHANGE_API", "availablePairs": "HOTC_USDT,MANA_BTC,ETH_ZB,TOPC_QC,CDC_QC,EDO_QC,ZB_BTC,PDX_BTC,XUC_BTC,TV_QC,BITCNY_QC,SNT_USDT,BTP_BTC,DOGE_QC,DOGE_BTC,SBTC_USDT,XLM_QC,ICX_USDT,ETC_QC,GNT_USDT,ENT_QC,HOTC_BTC,HC_BTC,LTC_USDT,RCN_BTC,SLT_BTC,EOSDAC_QC,KNC_BTC,XLM_USDT,BCX_BTC,QUN_QC,MITH_QC,BCW_QC,ICX_QC,UBTC_QC,UBTC_BTC,BCH_QC,LBTC_BTC,HSR_BTC,HLC_USDT,BCH_BTC,SUB_BTC,SNT_QC,GRAM_USDT,BTH_QC,FUN_BTC,AE_USDT,TOPC_USDT,ADA_BTC,GRAM_QC,ETC_USDT,LTC_PAX,BCD_QC,SUB_USDT,QUN_USDT,PAX_USDT,QTUM_USDT,XWC_USDT,BTS_QC,PDX_QC,TOPC_BTC,HLC_QC,HLC_BTC,AE_QC,SLT_QC,ETH_BTC,KAN_BTC,BCC_ZB,EOSDAC_USDT,MITH_BTC,EDO_USDT,MITH_USDT,1ST_BTC,TRUE_BTC,NEO_QC,ADA_QC,DOGE_USDT,KNC_USDT,GRAM_BTC,BCC_BTC,BTN_USDT,SAFE_QC,MTL_BTC,BCW_USDT,CHAT_BTC,HPY_USDT,USDT_QC,QTUM_ZB,CHAT_USDT,BTS_ZB,FUN_USDT,HC_USDT,XWC_BTC,EOS_BTC,ZRX_USDT,MTL_USDT,ICX_BTC,BTP_USDT,BAT_QC,HSR_QC,TV_USDT,BITE_BTC,BCC_USDT,HC_QC,UBTC_USDT,TRUE_USDT,XWC_QC,EOS_ZB,ENT_BTC,BTC_PAX,1ST_USDT,BCH_PAX,OMG_QC,DASH_ZB,ETC_PAX,XRP_ZB,ETC_BTC,ADA_USDT,GNT_QC,RCN_QC,ETH_PAX,MANA_USDT,XRP_BTC,BCH_ZB,QUN_BTC,BTH_BTC,INK_BTC,OMG_BTC,EOS_USDT,XUC_QC,XRP_QC,BTS_USDT,KNC_QC,GNT_BTC,HC_ZB,TV_BTC,SLT_USDT,XEM_USDT,CDC_USDT,MCO_QC,BTH_USDT,ETH_USDT,1ST_QC,SUB_QC,BTP_QC,DDM_USDT,HSR_ZB,SAFE_USDT,FUN_QC,ZB_USDT,HOTC_QC,BTN_BTC,MTL_QC,BCH_USDT,EOS_QC,NEO_BTC,HPY_BTC,LBTC_QC,RCN_USDT,BTC_QC,BTM_BTC,XRP_USDT,LTC_ZB,DDM_QC,TRUE_QC,AAA_QC,BCD_USDT,DASH_QC,CHAT_QC,BCW_BTC,BCD_BTC,BDS_QC,LTC_BTC,ENT_USDT,ZRX_BTC,AE_BTC,CDC_BTC,ZB_QC,BTS_BTC,MANA_QC,EDO_BTC,EPC_BTC,DDM_BTC,MCO_USDT,BCC_QC,SNT_BTC,HPY_QC,KAN_USDT,NEO_USDT,QTUM_QC,XTZ_USDT,SAFE_BTC,BTM_QC,OMG_USDT,XEM_QC,EPC_QC,BAT_USDT,SBTC_BTC,KAN_QC,QTUM_BTC,PDX_USDT,MCO_BTC,ETH_QC,BDS_BTC,ETC_ZB,HSR_USDT,INK_QC,LBTC_USDT,BTM_USDT,BTC_USDT,DASH_USDT,EOSDAC_BTC,BCX_QC,DASH_BTC,BCX_USDT,SBTC_QC,XLM_BTC,BAT_BTC,BTN_QC,INK_USDT,XEM_BTC,ZRX_QC,LTC_QC,BCC_PAX", "enabledPairs": "BTC_USDT,ETH_USDT", "baseCurrencies": "USD", @@ -1272,8 +1330,10 @@ "apiSecret": "Secret", "apiUrl": "NON_DEFAULT_HTTP_LINK_TO_EXCHANGE_API", "apiUrlSecondary": "NON_DEFAULT_HTTP_LINK_TO_EXCHANGE_API", - "availablePairs": "XRPZ18,BCHZ18,ADAZ18,EOSZ18,TRXZ18,XBTUSD,XBT7D_U105,XBT7D_D95,XBTZ18,XBTH19,ETHUSD,ETHZ18,LTCZ18", - "enabledPairs": "XBTUSD", + "proxyAddress": "", + "websocketUrl": "NON_DEFAULT_HTTP_LINK_TO_WEBSOCKET_EXCHANGE_API", + "availablePairs": "XRPXBT", + "enabledPairs": "XRPXBT", "baseCurrencies": "USD", "assetTypes": "SPOT", "supportsAutoPairUpdates": true, diff --git a/websocket.go b/websocket.go index 6d2de95f..04307982 100644 --- a/websocket.go +++ b/websocket.go @@ -1,6 +1,7 @@ package main import ( + "errors" "log" "net/http" @@ -235,6 +236,10 @@ func StartWebsocketHandler() { // BroadcastWebsocketMessage meow func BroadcastWebsocketMessage(evt WebsocketEvent) error { + if !wsHubStarted { + return errors.New("websocket service not started") + } + data, err := common.JSONEncode(evt) if err != nil { return err