Websocket request-response correlation (#328)

* Establishes new websocket functionality. Begins the creation of the websocket request

* Adding a wrapper over gorilla websocket connect,send,receive to handle ID messages. Doesn't work

* Successfully moved exchange_websocket into its own wshandler namespace. oof

* Sets up ZB to use a round trip WS request system

* Adds Kraken ID support to subscriptions. Renames duplicate func name UnsubscribeToChannels to RemoveSubscribedChannels. Adds some helper methods in the WebsocketConn to reduce duplicate code. Cleans up ZB implementation

* Fixes double locking which caused no websocket data to be read. Fixes requestid for kraken subscriptions

* Completes Huobi and Hadax implementation. Extends ZB error handling. Adds GZip support for reading messages

* Adds HitBTC support. Adds GetCurrencies, GetSymbols, GetTrades WS funcs. Adds super fun new parameter to GenerateMessageID for Unix and UnixNano

* Adds GateIO id support

* Adds Coinut support. Prevents nil reference error in constatus when there isnt one

* Standardises all Exchange websockets to use the wshandler websocket. Removes the wsRequestMtx as wshandler handles that now. Makes the Dialer a dialer, its not externally referenced that I can see.

* Fixes issue with coinut implementation. Updates bitmex currencies. Removes redundant log messages which are used to log messages

* Starts testing. Renames files

* Adds tests for websocket connection

* Reverts request.go change

* Linting everything

* Fixes rebase issue

* Final changes. Fixes variable names, removes log.Debug, removes lines, rearranges test types, removes order correlation websocket type

* Final final commit, fixing ZB issues.

* Adds traffic alerts where missed. Changes empty struct pointer addresses to nil instead. Removes empty lines

* Fixed string conversion

* Fixes issue with ZB not sending success codes

* Fixes issue with coinut processing due to nonce handling with subscriptions

* Fixes issue where ZB test failure was not caught. Removes unnecessary error handling from other ZB tests

* Removes unused interface

* Renames wshandler.Init() to wshandler.Run()

* Updates template file

* Capitalises cryptocurrencies in struct. Moves websocketResponseCheckTimeout and websocketResponseMaxLimit into config options. Moves connection configuration to main exchange Setup (where appropriate). Reverts currencylastupdated ticks. Improves reader close error checking

* Fixes two inconsistent websocket delay times

* Creates a default variable for websocket ResponseMaxLimit and ResponseCheckTimeout, then applies it to setdefaults and all tests

* Updates exchange template to set and use default websocket response limits
This commit is contained in:
Scott
2019-08-07 15:15:01 +10:00
committed by Adrian Gallagher
parent 6e70f0642a
commit e209d85d0d
113 changed files with 3269 additions and 2594 deletions

View File

@@ -7,16 +7,15 @@ import (
"net/http"
"net/url"
"strconv"
"sync"
"time"
"github.com/gorilla/websocket"
"github.com/thrasher-/gocryptotrader/common"
"github.com/thrasher-/gocryptotrader/config"
"github.com/thrasher-/gocryptotrader/currency"
exchange "github.com/thrasher-/gocryptotrader/exchanges"
"github.com/thrasher-/gocryptotrader/exchanges/request"
"github.com/thrasher-/gocryptotrader/exchanges/ticker"
"github.com/thrasher-/gocryptotrader/exchanges/wshandler"
log "github.com/thrasher-/gocryptotrader/logger"
)
@@ -54,8 +53,7 @@ const (
// HitBTC is the overarching type across the hitbtc package
type HitBTC struct {
exchange.Base
WebsocketConn *websocket.Conn
wsRequestMtx sync.Mutex
WebsocketConn *wshandler.WebsocketConnection
}
// SetDefaults sets default settings for hitbtc
@@ -80,14 +78,17 @@ func (h *HitBTC) SetDefaults() {
common.NewHTTPClientWithTimeout(exchange.DefaultHTTPTimeout))
h.APIUrlDefault = apiURL
h.APIUrl = h.APIUrlDefault
h.WebsocketInit()
h.Websocket.Functionality = exchange.WebsocketTickerSupported |
exchange.WebsocketOrderbookSupported |
exchange.WebsocketSubscribeSupported |
exchange.WebsocketUnsubscribeSupported |
exchange.WebsocketAuthenticatedEndpointsSupported |
exchange.WebsocketSubmitOrderSupported |
exchange.WebsocketCancelOrderSupported
h.Websocket = wshandler.New()
h.Websocket.Functionality = wshandler.WebsocketTickerSupported |
wshandler.WebsocketOrderbookSupported |
wshandler.WebsocketSubscribeSupported |
wshandler.WebsocketUnsubscribeSupported |
wshandler.WebsocketAuthenticatedEndpointsSupported |
wshandler.WebsocketSubmitOrderSupported |
wshandler.WebsocketCancelOrderSupported |
wshandler.WebsocketMessageCorrelationSupported
h.WebsocketResponseMaxLimit = exchange.DefaultWebsocketResponseMaxLimit
h.WebsocketResponseCheckTimeout = exchange.DefaultWebsocketResponseCheckTimeout
}
// Setup sets user exchange configuration settings
@@ -128,17 +129,27 @@ func (h *HitBTC) Setup(exch *config.ExchangeConfig) {
if err != nil {
log.Fatal(err)
}
err = h.WebsocketSetup(h.WsConnect,
err = h.Websocket.Setup(h.WsConnect,
h.Subscribe,
h.Unsubscribe,
exch.Name,
exch.Websocket,
exch.Verbose,
hitbtcWebsocketAddress,
exch.WebsocketURL)
exch.WebsocketURL,
exch.AuthenticatedWebsocketAPISupport)
if err != nil {
log.Fatal(err)
}
h.WebsocketConn = &wshandler.WebsocketConnection{
ExchangeName: h.Name,
URL: h.Websocket.GetWebsocketURL(),
ProxyURL: h.Websocket.GetProxyAddress(),
Verbose: h.Verbose,
RateLimit: rateLimit,
ResponseCheckTimeout: exch.WebsocketResponseCheckTimeout,
ResponseMaxLimit: exch.WebsocketResponseMaxLimit,
}
}
}

View File

@@ -11,9 +11,11 @@ import (
"github.com/thrasher-/gocryptotrader/currency"
exchange "github.com/thrasher-/gocryptotrader/exchanges"
"github.com/thrasher-/gocryptotrader/exchanges/sharedtestvalues"
"github.com/thrasher-/gocryptotrader/exchanges/wshandler"
)
var h HitBTC
var wsSetupRan bool
// Please supply your own APIKEYS here for due diligence testing
const (
@@ -102,7 +104,7 @@ func TestGetFee(t *testing.T) {
var feeBuilder = setFeeBuilder()
if areTestAPIKeysSet() {
// CryptocurrencyTradeFee Basic
if resp, err := h.GetFee(feeBuilder); resp != float64(0.001) || err != nil {
if resp, err := h.GetFee(feeBuilder); resp != float64(0.002) || err != nil {
t.Error(err)
t.Errorf("Test Failed - GetFee() error. Expected: %f, Received: %f", float64(0.002), resp)
}
@@ -111,7 +113,7 @@ func TestGetFee(t *testing.T) {
feeBuilder = setFeeBuilder()
feeBuilder.Amount = 1000
feeBuilder.PurchasePrice = 1000
if resp, err := h.GetFee(feeBuilder); resp != float64(1000) || err != nil {
if resp, err := h.GetFee(feeBuilder); resp != float64(2000) || err != nil {
t.Errorf("Test Failed - GetFee() error. Expected: %f, Received: %f", float64(2000), resp)
t.Error(err)
}
@@ -119,7 +121,7 @@ func TestGetFee(t *testing.T) {
// CryptocurrencyTradeFee IsMaker
feeBuilder = setFeeBuilder()
feeBuilder.IsMaker = true
if resp, err := h.GetFee(feeBuilder); resp != float64(-0.0001) || err != nil {
if resp, err := h.GetFee(feeBuilder); resp != float64(0.001) || err != nil {
t.Errorf("Test Failed - GetFee() error. Expected: %f, Received: %f", float64(0.001), resp)
t.Error(err)
}
@@ -127,7 +129,7 @@ func TestGetFee(t *testing.T) {
// CryptocurrencyTradeFee Negative purchase price
feeBuilder = setFeeBuilder()
feeBuilder.PurchasePrice = -1000
if resp, err := h.GetFee(feeBuilder); resp != float64(-1) || err != nil {
if resp, err := h.GetFee(feeBuilder); resp != float64(0) || err != nil {
t.Errorf("Test Failed - GetFee() error. Expected: %f, Received: %f", float64(0), resp)
t.Error(err)
}
@@ -135,7 +137,7 @@ func TestGetFee(t *testing.T) {
// CryptocurrencyWithdrawalFee Basic
feeBuilder = setFeeBuilder()
feeBuilder.FeeType = exchange.CryptocurrencyWithdrawalFee
if resp, err := h.GetFee(feeBuilder); resp != float64(0.009580) || err != nil {
if resp, err := h.GetFee(feeBuilder); resp != float64(0.042800) || err != nil {
t.Errorf("Test Failed - GetFee() error. Expected: %f, Received: %f", float64(0.042800), resp)
t.Error(err)
}
@@ -387,16 +389,25 @@ func TestGetDepositAddress(t *testing.T) {
}
}
func setupWsAuth(t *testing.T) {
if wsSetupRan {
return
}
TestSetDefaults(t)
TestSetup(t)
if !h.Websocket.IsEnabled() && !h.AuthenticatedWebsocketAPISupport || !areTestAPIKeysSet() {
t.Skip(exchange.WebsocketNotEnabled)
t.Skip(wshandler.WebsocketNotEnabled)
}
var err error
var dialer websocket.Dialer
h.Websocket.DataHandler = sharedtestvalues.GetWebsocketInterfaceChannelOverride()
h.Websocket.TrafficAlert = sharedtestvalues.GetWebsocketStructChannelOverride()
h.WebsocketConn, _, err = dialer.Dial(hitbtcWebsocketAddress, http.Header{})
h.WebsocketConn = &wshandler.WebsocketConnection{
ExchangeName: h.Name,
URL: hitbtcWebsocketAddress,
Verbose: h.Verbose,
ResponseMaxLimit: exchange.DefaultWebsocketResponseMaxLimit,
ResponseCheckTimeout: exchange.DefaultWebsocketResponseCheckTimeout,
}
var dialer websocket.Dialer
err := h.WebsocketConn.Dial(&dialer, http.Header{})
if err != nil {
t.Fatal(err)
}
@@ -409,84 +420,86 @@ func setupWsAuth(t *testing.T) {
case <-timer.C:
}
timer.Stop()
wsSetupRan = true
}
// TestWsCancelOrder dials websocket, sends cancel request.
func TestWsCancelOrder(t *testing.T) {
setupWsAuth(t)
err := h.wsCancelOrder("ImNotARealOrderID")
if !canManipulateRealOrders {
t.Skip("canManipulateRealOrders false, skipping test")
}
_, err := h.wsCancelOrder("ImNotARealOrderID")
if err != nil {
t.Fatal(err)
}
timer := time.NewTimer(sharedtestvalues.WebsocketResponseDefaultTimeout)
select {
case <-h.Websocket.DataHandler:
case <-timer.C:
t.Error("Expecting response")
}
timer.Stop()
}
// TestWsPlaceOrder dials websocket, sends order submission.
func TestWsPlaceOrder(t *testing.T) {
setupWsAuth(t)
err := h.wsPlaceOrder(currency.NewPair(currency.LTC, currency.BTC), exchange.BuyOrderSide.ToString(), 1, 1)
if !canManipulateRealOrders {
t.Skip("canManipulateRealOrders false, skipping test")
}
_, err := h.wsPlaceOrder(currency.NewPair(currency.LTC, currency.BTC), exchange.BuyOrderSide.ToString(), 1, 1)
if err != nil {
t.Fatal(err)
}
timer := time.NewTimer(sharedtestvalues.WebsocketResponseDefaultTimeout)
select {
case <-h.Websocket.DataHandler:
case <-timer.C:
t.Error("Expecting response")
}
timer.Stop()
}
// TestWsReplaceOrder dials websocket, sends replace order request.
func TestWsReplaceOrder(t *testing.T) {
setupWsAuth(t)
err := h.wsReplaceOrder("ImNotARealOrderID", 1, 1)
if !canManipulateRealOrders {
t.Skip("canManipulateRealOrders false, skipping test")
}
_, err := h.wsReplaceOrder("ImNotARealOrderID", 1, 1)
if err != nil {
t.Fatal(err)
}
timer := time.NewTimer(sharedtestvalues.WebsocketResponseDefaultTimeout)
select {
case <-h.Websocket.DataHandler:
case <-timer.C:
t.Error("Expecting response")
}
timer.Stop()
}
// TestWsGetActiveOrders dials websocket, sends get active orders request.
func TestWsGetActiveOrders(t *testing.T) {
setupWsAuth(t)
err := h.wsGetActiveOrders()
_, err := h.wsGetActiveOrders()
if err != nil {
t.Fatal(err)
}
timer := time.NewTimer(sharedtestvalues.WebsocketResponseDefaultTimeout)
select {
case <-h.Websocket.DataHandler:
case <-timer.C:
t.Error("Expecting response")
}
timer.Stop()
}
// TestWsGetTradingBalance dials websocket, sends get trading balance request.
func TestWsGetTradingBalance(t *testing.T) {
setupWsAuth(t)
err := h.wsGetTradingBalance()
_, err := h.wsGetTradingBalance()
if err != nil {
t.Fatal(err)
}
}
// TestWsGetTradingBalance dials websocket, sends get trading balance request.
func TestWsGetTrades(t *testing.T) {
setupWsAuth(t)
_, err := h.wsGetTrades(currency.NewPair(currency.ETH, currency.BTC), 1000, "ASC", "id")
if err != nil {
t.Fatal(err)
}
}
// TestWsGetTradingBalance dials websocket, sends get trading balance request.
func TestWsGetSymbols(t *testing.T) {
setupWsAuth(t)
_, err := h.wsGetSymbols(currency.NewPair(currency.ETH, currency.BTC))
if err != nil {
t.Fatal(err)
}
}
// TestWsGetTradingBalance dials websocket, sends get trading balance request.
func TestSsGetCurrencies(t *testing.T) {
setupWsAuth(t)
_, err := h.wsGetCurrencies(currency.BTC)
if err != nil {
t.Fatal(err)
}
timer := time.NewTimer(sharedtestvalues.WebsocketResponseDefaultTimeout)
select {
case <-h.Websocket.DataHandler:
case <-timer.C:
t.Error("Expecting response")
}
timer.Stop()
}

View File

@@ -299,13 +299,16 @@ type LendingHistory struct {
}
type capture struct {
Method string `json:"method,omitempty"`
Result interface{} `json:"result"`
Error struct {
Code int `json:"code"`
Message string `json:"message"`
} `json:"error"`
ID int64 `json:"id,omitempty"`
Method string `json:"method,omitempty"`
Result interface{} `json:"result"`
Error ResponseError `json:"error,omitempty"`
ID int64 `json:"id,omitempty"`
}
// ResponseError contains error codes from JSON responses
type ResponseError struct {
Code int `json:"code"`
Message string `json:"message"`
}
// WsRequest defines a request obj for the JSON-RPC and gets a websocket
@@ -393,12 +396,13 @@ type WsLoginData struct {
// WsActiveOrdersResponse Active order response for auth subscription to reports
type WsActiveOrdersResponse struct {
Params []WsActiveOrdersResponseData `json:"params"`
Error ResponseError `json:"error,omitempty"`
}
// WsActiveOrdersResponseData Active order data for WsActiveOrdersResponse
type WsActiveOrdersResponseData struct {
ID string `json:"id"`
ClientOrderID string `json:"clientOrderId"`
ClientOrderID string `json:"clientOrderId,omitempty"`
Symbol currency.Pair `json:"symbol"`
Side string `json:"side"`
Status string `json:"status"`
@@ -416,12 +420,13 @@ type WsActiveOrdersResponseData struct {
// WsReportResponse report response for auth subscription to reports
type WsReportResponse struct {
Params WsReportResponseData `json:"params"`
Error ResponseError `json:"error,omitempty"`
}
// WsReportResponseData Report data for WsReportResponse
type WsReportResponseData struct {
ID string `json:"id"`
ClientOrderID string `json:"clientOrderId"`
ClientOrderID string `json:"clientOrderId,omitempty"`
Symbol currency.Pair `json:"symbol"`
Side string `json:"side"`
Status string `json:"status"`
@@ -449,7 +454,7 @@ type WsSubmitOrderRequest struct {
// WsSubmitOrderRequestData WS request data
type WsSubmitOrderRequestData struct {
ClientOrderID string `json:"clientOrderId"`
ClientOrderID int64 `json:"clientOrderId,string,omitempty"`
Symbol currency.Pair `json:"symbol"`
Side string `json:"side"`
Price float64 `json:"price,string"`
@@ -460,6 +465,7 @@ type WsSubmitOrderRequestData struct {
type WsSubmitOrderSuccessResponse struct {
Result WsSubmitOrderSuccessResponseData `json:"result"`
ID int64 `json:"id"`
Error ResponseError `json:"error,omitempty"`
}
// WsSubmitOrderSuccessResponseData WS response data
@@ -482,7 +488,7 @@ type WsSubmitOrderSuccessResponseData struct {
// WsSubmitOrderErrorResponse WS error response
type WsSubmitOrderErrorResponse struct {
Error WsSubmitOrderErrorResponseData `json:"error"`
Error WsSubmitOrderErrorResponseData `json:"error,omitempty"`
ID int64 `json:"id"`
}
@@ -497,12 +503,13 @@ type WsSubmitOrderErrorResponseData struct {
type WsCancelOrderResponse struct {
Result WsCancelOrderResponseData `json:"result"`
ID int64 `json:"id"`
Error ResponseError `json:"error,omitempty"`
}
// WsCancelOrderResponseData WS response data
type WsCancelOrderResponseData struct {
ID string `json:"id"`
ClientOrderID string `json:"clientOrderId"`
ClientOrderID string `json:"clientOrderId,omitempty"`
Symbol currency.Pair `json:"symbol"`
Side string `json:"side"`
Status string `json:"status"`
@@ -521,12 +528,13 @@ type WsCancelOrderResponseData struct {
type WsReplaceOrderResponse struct {
Result WsReplaceOrderResponseData `json:"result"`
ID int64 `json:"id"`
Error ResponseError `json:"error,omitempty"`
}
// WsReplaceOrderResponseData WS response data
type WsReplaceOrderResponseData struct {
ID string `json:"id"`
ClientOrderID string `json:"clientOrderId"`
ClientOrderID string `json:"clientOrderId,omitempty"`
Symbol currency.Pair `json:"symbol"`
Side string `json:"side"`
Status string `json:"status"`
@@ -546,12 +554,13 @@ type WsReplaceOrderResponseData struct {
type WsGetActiveOrdersResponse struct {
Result []WsGetActiveOrdersResponseData `json:"result"`
ID int64 `json:"id"`
Error ResponseError `json:"error,omitempty"`
}
// WsGetActiveOrdersResponseData WS response data
type WsGetActiveOrdersResponseData struct {
ID string `json:"id"`
ClientOrderID string `json:"clientOrderId"`
ClientOrderID string `json:"clientOrderId,omitempty"`
Symbol currency.Pair `json:"symbol"`
Side string `json:"side"`
Status string `json:"status"`
@@ -571,6 +580,7 @@ type WsGetActiveOrdersResponseData struct {
type WsGetTradingBalanceResponse struct {
Result []WsGetTradingBalanceResponseData `json:"result"`
ID int64 `json:"id"`
Error ResponseError `json:"error,omitempty"`
}
// WsGetTradingBalanceResponseData WS response data
@@ -606,3 +616,106 @@ type WsReplaceOrderRequestData struct {
Quantity float64 `json:"quantity,string,omitempty"`
Price float64 `json:"price,string,omitempty"`
}
// WsGetCurrenciesRequest gets currencies
type WsGetCurrenciesRequest struct {
Method string `json:"method"`
Params WsGetCurrenciesRequestParameters `json:"params"`
ID int64 `json:"id"`
}
// WsGetCurrenciesRequestParameters parameters
type WsGetCurrenciesRequestParameters struct {
Currency currency.Code `json:"currency"`
}
// WsGetCurrenciesResponse currency response
type WsGetCurrenciesResponse struct {
Result WsGetCurrenciesResponseData `json:"result"`
ID int64 `json:"id"`
Error ResponseError `json:"error,omitempty"`
}
// WsGetCurrenciesResponseData currency response data
type WsGetCurrenciesResponseData struct {
ID currency.Code `json:"id"`
FullName string `json:"fullName"`
Crypto bool `json:"crypto"`
PayinEnabled bool `json:"payinEnabled"`
PayinPaymentID bool `json:"payinPaymentId"`
PayinConfirmations int64 `json:"payinConfirmations"`
PayoutEnabled bool `json:"payoutEnabled"`
PayoutIsPaymentID bool `json:"payoutIsPaymentId"`
TransferEnabled bool `json:"transferEnabled"`
Delisted bool `json:"delisted"`
PayoutFee string `json:"payoutFee"`
}
// WsGetSymbolsRequest request data
type WsGetSymbolsRequest struct {
Method string `json:"method"`
Params WsGetSymbolsRequestParameters `json:"params"`
ID int64 `json:"id"`
}
// WsGetSymbolsRequestParameters request parameters
type WsGetSymbolsRequestParameters struct {
Symbol currency.Pair `json:"symbol"`
}
// WsGetSymbolsResponse symbol response
type WsGetSymbolsResponse struct {
Result WsGetSymbolsResponseData `json:"result"`
ID int64 `json:"id"`
Error ResponseError `json:"error,omitempty"`
}
// WsGetSymbolsResponseData symbol response data
type WsGetSymbolsResponseData struct {
ID currency.Pair `json:"id"`
BaseCurrency currency.Code `json:"baseCurrency"`
QuoteCurrency currency.Code `json:"quoteCurrency"`
QuantityIncrement float64 `json:"quantityIncrement,string"`
TickSize float64 `json:"tickSize,string"`
TakeLiquidityRate float64 `json:"takeLiquidityRate,string"`
ProvideLiquidityRate float64 `json:"provideLiquidityRate,string"`
FeeCurrency currency.Code `json:"feeCurrency"`
}
// WsGetTradesRequest trade request
type WsGetTradesRequest struct {
Method string `json:"method"`
Params WsGetTradesRequestParameters `json:"params"`
ID int64 `json:"id"`
}
// WsGetTradesRequestParameters trade request params
type WsGetTradesRequestParameters struct {
Symbol currency.Pair `json:"symbol"`
Limit int64 `json:"limit"`
Sort string `json:"sort"`
By string `json:"by"`
}
// WsGetTradesResponse response
type WsGetTradesResponse struct {
Jsonrpc string `json:"jsonrpc"`
Result WsGetTradesResponseData `json:"result"`
ID int64 `json:"id"`
Error ResponseError `json:"error,omitempty"`
}
// WsGetTradesResponseData trade response data
type WsGetTradesResponseData struct {
Data []WsGetTradesResponseTrades `json:"data"`
Symbol string `json:"symbol"`
}
// WsGetTradesResponseTrades trade response
type WsGetTradesResponseTrades struct {
ID int64 `json:"id"`
Price float64 `json:"price,string"`
Quantity float64 `json:"quantity,string"`
Side string `json:"side"`
Timestamp time.Time `json:"timestamp"`
}

View File

@@ -4,7 +4,6 @@ import (
"errors"
"fmt"
"net/http"
"net/url"
"strings"
"time"
@@ -14,12 +13,14 @@ import (
exchange "github.com/thrasher-/gocryptotrader/exchanges"
"github.com/thrasher-/gocryptotrader/exchanges/nonce"
"github.com/thrasher-/gocryptotrader/exchanges/orderbook"
"github.com/thrasher-/gocryptotrader/exchanges/wshandler"
log "github.com/thrasher-/gocryptotrader/logger"
)
const (
hitbtcWebsocketAddress = "wss://api.hitbtc.com/api/2/ws"
rpcVersion = "2.0"
rateLimit = 20
)
var requestID nonce.Nonce
@@ -27,26 +28,13 @@ var requestID nonce.Nonce
// 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)
return errors.New(wshandler.WebsocketNotEnabled)
}
var dialer websocket.Dialer
if h.Websocket.GetProxyAddress() != "" {
proxy, err := url.Parse(h.Websocket.GetProxyAddress())
if err != nil {
return err
}
dialer.Proxy = http.ProxyURL(proxy)
}
var err error
h.WebsocketConn, _, err = dialer.Dial(hitbtcWebsocketAddress, http.Header{})
err := h.WebsocketConn.Dial(&dialer, http.Header{})
if err != nil {
return err
}
go h.WsHandleData()
err = h.wsLogin()
if err != nil {
@@ -58,17 +46,6 @@ func (h *HitBTC) WsConnect() error {
return nil
}
// WsReadData reads from the websocket connection
func (h *HitBTC) WsReadData() (exchange.WebsocketResponse, error) {
_, resp, err := h.WebsocketConn.ReadMessage()
if err != nil {
return exchange.WebsocketResponse{}, err
}
h.Websocket.TrafficAlert <- struct{}{}
return exchange.WebsocketResponse{Raw: resp}, nil
}
// WsHandleData handles websocket data
func (h *HitBTC) WsHandleData() {
h.Websocket.Wg.Add(1)
@@ -83,11 +60,12 @@ func (h *HitBTC) WsHandleData() {
return
default:
resp, err := h.WsReadData()
resp, err := h.WebsocketConn.ReadMessage()
if err != nil {
h.Websocket.DataHandler <- err
return
}
h.Websocket.TrafficAlert <- struct{}{}
var init capture
err = common.JSONDecode(resp.Raw, &init)
@@ -95,11 +73,14 @@ func (h *HitBTC) WsHandleData() {
h.Websocket.DataHandler <- err
continue
}
if init.Error.Code == 1002 {
h.Websocket.SetCanUseAuthenticatedEndpoints(false)
}
if init.ID > 0 {
h.WebsocketConn.AddResponseWithID(init.ID, resp.Raw)
continue
}
if init.Error.Message != "" || init.Error.Code != 0 {
if init.Error.Code == 1002 {
h.Websocket.SetCanUseAuthenticatedEndpoints(false)
}
h.Websocket.DataHandler <- fmt.Errorf("hitbtc.go error - Code: %d, Message: %s",
init.Error.Code,
init.Error.Message)
@@ -117,7 +98,7 @@ func (h *HitBTC) WsHandleData() {
}
}
func (h *HitBTC) handleSubscriptionUpdates(resp exchange.WebsocketResponse, init capture) {
func (h *HitBTC) handleSubscriptionUpdates(resp wshandler.WebsocketResponse, init capture) {
switch init.Method {
case "ticker":
var ticker WsTicker
@@ -131,7 +112,7 @@ func (h *HitBTC) handleSubscriptionUpdates(resp exchange.WebsocketResponse, init
h.Websocket.DataHandler <- err
return
}
h.Websocket.DataHandler <- exchange.TickerData{
h.Websocket.DataHandler <- wshandler.TickerData{
Exchange: h.GetName(),
AssetType: "SPOT",
Pair: currency.NewPairFromString(ticker.Params.Symbol),
@@ -187,7 +168,7 @@ func (h *HitBTC) handleSubscriptionUpdates(resp exchange.WebsocketResponse, init
}
}
func (h *HitBTC) handleCommandResponses(resp exchange.WebsocketResponse, init capture) {
func (h *HitBTC) handleCommandResponses(resp wshandler.WebsocketResponse, init capture) {
switch resultType := init.Result.(type) {
case map[string]interface{}:
switch resultType["reportType"].(string) {
@@ -266,7 +247,7 @@ func (h *HitBTC) WsProcessOrderbookSnapshot(ob WsOrderbook) error {
return err
}
h.Websocket.DataHandler <- exchange.WebsocketOrderbookUpdate{
h.Websocket.DataHandler <- wshandler.WebsocketOrderbookUpdate{
Exchange: h.GetName(),
Asset: "SPOT",
Pair: p,
@@ -297,7 +278,7 @@ func (h *HitBTC) WsProcessOrderbookUpdate(ob WsOrderbook) error {
return err
}
h.Websocket.DataHandler <- exchange.WebsocketOrderbookUpdate{
h.Websocket.DataHandler <- wshandler.WebsocketOrderbookUpdate{
Exchange: h.GetName(),
Asset: "SPOT",
Pair: p,
@@ -308,9 +289,9 @@ func (h *HitBTC) WsProcessOrderbookUpdate(ob WsOrderbook) error {
// GenerateDefaultSubscriptions Adds default subscriptions to websocket to be handled by ManageSubscriptions()
func (h *HitBTC) GenerateDefaultSubscriptions() {
var channels = []string{"subscribeTicker", "subscribeOrderbook", "subscribeTrades", "subscribeCandles"}
var subscriptions []exchange.WebsocketChannelSubscription
var subscriptions []wshandler.WebsocketChannelSubscription
if h.Websocket.CanUseAuthenticatedEndpoints() {
subscriptions = append(subscriptions, exchange.WebsocketChannelSubscription{
subscriptions = append(subscriptions, wshandler.WebsocketChannelSubscription{
Channel: "subscribeReports",
})
}
@@ -318,7 +299,7 @@ func (h *HitBTC) GenerateDefaultSubscriptions() {
for i := range channels {
for j := range enabledCurrencies {
enabledCurrencies[j].Delimiter = ""
subscriptions = append(subscriptions, exchange.WebsocketChannelSubscription{
subscriptions = append(subscriptions, wshandler.WebsocketChannelSubscription{
Channel: channels[i],
Currency: enabledCurrencies[j],
})
@@ -328,7 +309,7 @@ func (h *HitBTC) GenerateDefaultSubscriptions() {
}
// Subscribe sends a websocket message to receive data from the channel
func (h *HitBTC) Subscribe(channelToSubscribe exchange.WebsocketChannelSubscription) error {
func (h *HitBTC) Subscribe(channelToSubscribe wshandler.WebsocketChannelSubscription) error {
subscribe := WsNotification{
Method: channelToSubscribe.Channel,
}
@@ -350,11 +331,11 @@ func (h *HitBTC) Subscribe(channelToSubscribe exchange.WebsocketChannelSubscript
}
}
return h.wsSend(subscribe)
return h.WebsocketConn.SendMessage(subscribe)
}
// Unsubscribe sends a websocket message to stop receiving data from the channel
func (h *HitBTC) Unsubscribe(channelToSubscribe exchange.WebsocketChannelSubscription) error {
func (h *HitBTC) Unsubscribe(channelToSubscribe wshandler.WebsocketChannelSubscription) error {
unsubscribeChannel := strings.Replace(channelToSubscribe.Channel, "subscribe", "unsubscribe", 1)
subscribe := WsNotification{
JSONRPCVersion: rpcVersion,
@@ -376,21 +357,7 @@ func (h *HitBTC) Unsubscribe(channelToSubscribe exchange.WebsocketChannelSubscri
}
}
return h.wsSend(subscribe)
}
// WsSend sends data to the websocket server
func (h *HitBTC) wsSend(data interface{}) error {
h.wsRequestMtx.Lock()
defer h.wsRequestMtx.Unlock()
json, err := common.JSONEncode(data)
if err != nil {
return err
}
if h.Verbose {
log.Debugf("%v sending message to websocket %v", h.Name, string(json))
}
return h.WebsocketConn.WriteMessage(websocket.TextMessage, json)
return h.WebsocketConn.SendMessage(subscribe)
}
// Unsubscribe sends a websocket message to stop receiving data from the channel
@@ -411,7 +378,7 @@ func (h *HitBTC) wsLogin() error {
},
}
err := h.wsSend(request)
err := h.WebsocketConn.SendMessage(request)
if err != nil {
h.Websocket.SetCanUseAuthenticatedEndpoints(false)
return err
@@ -420,43 +387,68 @@ func (h *HitBTC) wsLogin() error {
}
// wsPlaceOrder sends a websocket message to submit an order
func (h *HitBTC) wsPlaceOrder(pair currency.Pair, side string, price, quantity float64) error {
func (h *HitBTC) wsPlaceOrder(pair currency.Pair, side string, price, quantity float64) (*WsSubmitOrderSuccessResponse, error) {
if !h.Websocket.CanUseAuthenticatedEndpoints() {
return fmt.Errorf("%v not authenticated, cannot place order", h.Name)
return nil, fmt.Errorf("%v not authenticated, cannot place order", h.Name)
}
id := h.WebsocketConn.GenerateMessageID(false)
request := WsSubmitOrderRequest{
Method: "newOrder",
Params: WsSubmitOrderRequestData{
ClientOrderID: fmt.Sprintf("%v", time.Now().Unix()),
ClientOrderID: id,
Symbol: pair,
Side: common.StringToLower(side),
Price: price,
Quantity: quantity,
},
ID: int64(requestID.GetInc()),
ID: id,
}
return h.wsSend(request)
resp, err := h.WebsocketConn.SendMessageReturnResponse(id, request)
if err != nil {
return nil, fmt.Errorf("%v %v", h.Name, err)
}
var response WsSubmitOrderSuccessResponse
err = common.JSONDecode(resp, &response)
if err != nil {
return nil, fmt.Errorf("%v %v", h.Name, err)
}
if response.Error.Code > 0 || response.Error.Message != "" {
return &response, fmt.Errorf("%v Error:%v Message:%v", h.Name, response.Error.Code, response.Error.Message)
}
return &response, nil
}
// wsCancelOrder sends a websocket message to cancel an order
func (h *HitBTC) wsCancelOrder(clientOrderID string) error {
func (h *HitBTC) wsCancelOrder(clientOrderID string) (*WsCancelOrderResponse, error) {
if !h.Websocket.CanUseAuthenticatedEndpoints() {
return fmt.Errorf("%v not authenticated, cannot place order", h.Name)
return nil, fmt.Errorf("%v not authenticated, cannot place order", h.Name)
}
request := WsCancelOrderRequest{
Method: "cancelOrder",
Params: WsCancelOrderRequestData{
ClientOrderID: clientOrderID,
},
ID: int64(requestID.GetInc()),
ID: h.WebsocketConn.GenerateMessageID(false),
}
return h.wsSend(request)
resp, err := h.WebsocketConn.SendMessageReturnResponse(request.ID, request)
if err != nil {
return nil, fmt.Errorf("%v %v", h.Name, err)
}
var response WsCancelOrderResponse
err = common.JSONDecode(resp, &response)
if err != nil {
return nil, fmt.Errorf("%v %v", h.Name, err)
}
if response.Error.Code > 0 || response.Error.Message != "" {
return &response, fmt.Errorf("%v Error:%v Message:%v", h.Name, response.Error.Code, response.Error.Message)
}
return &response, nil
}
// wsReplaceOrder sends a websocket message to replace an order
func (h *HitBTC) wsReplaceOrder(clientOrderID string, quantity, price float64) error {
func (h *HitBTC) wsReplaceOrder(clientOrderID string, quantity, price float64) (*WsReplaceOrderResponse, error) {
if !h.Websocket.CanUseAuthenticatedEndpoints() {
return fmt.Errorf("%v not authenticated, cannot place order", h.Name)
return nil, fmt.Errorf("%v not authenticated, cannot place order", h.Name)
}
request := WsReplaceOrderRequest{
Method: "cancelReplaceOrder",
@@ -466,33 +458,144 @@ func (h *HitBTC) wsReplaceOrder(clientOrderID string, quantity, price float64) e
Quantity: quantity,
Price: price,
},
ID: int64(requestID.GetInc()),
ID: h.WebsocketConn.GenerateMessageID(false),
}
return h.wsSend(request)
resp, err := h.WebsocketConn.SendMessageReturnResponse(request.ID, request)
if err != nil {
return nil, fmt.Errorf("%v %v", h.Name, err)
}
var response WsReplaceOrderResponse
err = common.JSONDecode(resp, &response)
if err != nil {
return nil, fmt.Errorf("%v %v", h.Name, err)
}
if response.Error.Code > 0 || response.Error.Message != "" {
return &response, fmt.Errorf("%v Error:%v Message:%v", h.Name, response.Error.Code, response.Error.Message)
}
return &response, nil
}
// wsGetActiveOrders sends a websocket message to get all active orders
func (h *HitBTC) wsGetActiveOrders() error {
func (h *HitBTC) wsGetActiveOrders() (*WsActiveOrdersResponse, error) {
if !h.Websocket.CanUseAuthenticatedEndpoints() {
return fmt.Errorf("%v not authenticated, cannot place order", h.Name)
return nil, fmt.Errorf("%v not authenticated, cannot place order", h.Name)
}
request := WsReplaceOrderRequest{
Method: "getOrders",
Params: WsReplaceOrderRequestData{},
ID: int64(requestID.GetInc()),
ID: h.WebsocketConn.GenerateMessageID(false),
}
return h.wsSend(request)
resp, err := h.WebsocketConn.SendMessageReturnResponse(request.ID, request)
if err != nil {
return nil, fmt.Errorf("%v %v", h.Name, err)
}
var response WsActiveOrdersResponse
err = common.JSONDecode(resp, &response)
if err != nil {
return nil, fmt.Errorf("%v %v", h.Name, err)
}
if response.Error.Code > 0 || response.Error.Message != "" {
return &response, fmt.Errorf("%v Error:%v Message:%v", h.Name, response.Error.Code, response.Error.Message)
}
return &response, nil
}
// wsGetTradingBalance sends a websocket message to get trading balance
func (h *HitBTC) wsGetTradingBalance() error {
func (h *HitBTC) wsGetTradingBalance() (*WsGetTradingBalanceResponse, error) {
if !h.Websocket.CanUseAuthenticatedEndpoints() {
return fmt.Errorf("%v not authenticated, cannot place order", h.Name)
return nil, fmt.Errorf("%v not authenticated, cannot place order", h.Name)
}
request := WsReplaceOrderRequest{
Method: "getTradingBalance",
Params: WsReplaceOrderRequestData{},
ID: int64(requestID.GetInc()),
ID: h.WebsocketConn.GenerateMessageID(false),
}
return h.wsSend(request)
resp, err := h.WebsocketConn.SendMessageReturnResponse(request.ID, request)
if err != nil {
return nil, fmt.Errorf("%v %v", h.Name, err)
}
var response WsGetTradingBalanceResponse
err = common.JSONDecode(resp, &response)
if err != nil {
return nil, fmt.Errorf("%v %v", h.Name, err)
}
if response.Error.Code > 0 || response.Error.Message != "" {
return &response, fmt.Errorf("%v Error:%v Message:%v", h.Name, response.Error.Code, response.Error.Message)
}
return &response, nil
}
// wsGetCurrencies sends a websocket message to get trading balance
func (h *HitBTC) wsGetCurrencies(currencyItem currency.Code) (*WsGetCurrenciesResponse, error) {
request := WsGetCurrenciesRequest{
Method: "getCurrency",
Params: WsGetCurrenciesRequestParameters{
Currency: currencyItem,
},
ID: h.WebsocketConn.GenerateMessageID(false),
}
resp, err := h.WebsocketConn.SendMessageReturnResponse(request.ID, request)
if err != nil {
return nil, fmt.Errorf("%v %v", h.Name, err)
}
var response WsGetCurrenciesResponse
err = common.JSONDecode(resp, &response)
if err != nil {
return nil, fmt.Errorf("%v %v", h.Name, err)
}
if response.Error.Code > 0 || response.Error.Message != "" {
return &response, fmt.Errorf("%v Error:%v Message:%v", h.Name, response.Error.Code, response.Error.Message)
}
return &response, nil
}
// wsGetSymbols sends a websocket message to get trading balance
func (h *HitBTC) wsGetSymbols(currencyItem currency.Pair) (*WsGetSymbolsResponse, error) {
request := WsGetSymbolsRequest{
Method: "getSymbol",
Params: WsGetSymbolsRequestParameters{
Symbol: currencyItem,
},
ID: h.WebsocketConn.GenerateMessageID(false),
}
resp, err := h.WebsocketConn.SendMessageReturnResponse(request.ID, request)
if err != nil {
return nil, fmt.Errorf("%v %v", h.Name, err)
}
var response WsGetSymbolsResponse
err = common.JSONDecode(resp, &response)
if err != nil {
return nil, fmt.Errorf("%v %v", h.Name, err)
}
if response.Error.Code > 0 || response.Error.Message != "" {
return &response, fmt.Errorf("%v Error:%v Message:%v", h.Name, response.Error.Code, response.Error.Message)
}
return &response, nil
}
// wsGetSymbols sends a websocket message to get trading balance
func (h *HitBTC) wsGetTrades(currencyItem currency.Pair, limit int64, sort, by string) (*WsGetTradesResponse, error) {
request := WsGetTradesRequest{
Method: "getTrades",
Params: WsGetTradesRequestParameters{
Symbol: currencyItem,
Limit: limit,
Sort: sort,
By: by,
},
ID: h.WebsocketConn.GenerateMessageID(false),
}
resp, err := h.WebsocketConn.SendMessageReturnResponse(request.ID, request)
if err != nil {
return nil, fmt.Errorf("%v %v", h.Name, err)
}
var response WsGetTradesResponse
err = common.JSONDecode(resp, &response)
if err != nil {
return nil, fmt.Errorf("%v %v", h.Name, err)
}
if response.Error.Code > 0 || response.Error.Message != "" {
return &response, fmt.Errorf("%v Error:%v Message:%v", h.Name, response.Error.Code, response.Error.Message)
}
return &response, nil
}

View File

@@ -12,7 +12,7 @@ import (
exchange "github.com/thrasher-/gocryptotrader/exchanges"
"github.com/thrasher-/gocryptotrader/exchanges/orderbook"
"github.com/thrasher-/gocryptotrader/exchanges/ticker"
"github.com/thrasher-/gocryptotrader/exchanges/wshandler"
log "github.com/thrasher-/gocryptotrader/logger"
)
@@ -285,7 +285,7 @@ func (h *HitBTC) WithdrawFiatFundsToInternationalBank(withdrawRequest *exchange.
}
// GetWebsocket returns a pointer to the exchange websocket
func (h *HitBTC) GetWebsocket() (*exchange.Websocket, error) {
func (h *HitBTC) GetWebsocket() (*wshandler.Websocket, error) {
return h.Websocket, nil
}
@@ -376,20 +376,20 @@ func (h *HitBTC) GetOrderHistory(getOrdersRequest *exchange.GetOrdersRequest) ([
// SubscribeToWebsocketChannels appends to ChannelsToSubscribe
// which lets websocket.manageSubscriptions handle subscribing
func (h *HitBTC) SubscribeToWebsocketChannels(channels []exchange.WebsocketChannelSubscription) error {
func (h *HitBTC) SubscribeToWebsocketChannels(channels []wshandler.WebsocketChannelSubscription) error {
h.Websocket.SubscribeToChannels(channels)
return nil
}
// UnsubscribeToWebsocketChannels removes from ChannelsToSubscribe
// which lets websocket.manageSubscriptions handle unsubscribing
func (h *HitBTC) UnsubscribeToWebsocketChannels(channels []exchange.WebsocketChannelSubscription) error {
h.Websocket.UnsubscribeToChannels(channels)
func (h *HitBTC) UnsubscribeToWebsocketChannels(channels []wshandler.WebsocketChannelSubscription) error {
h.Websocket.RemoveSubscribedChannels(channels)
return nil
}
// GetSubscriptions returns a copied list of subscriptions
func (h *HitBTC) GetSubscriptions() ([]exchange.WebsocketChannelSubscription, error) {
func (h *HitBTC) GetSubscriptions() ([]wshandler.WebsocketChannelSubscription, error) {
return h.Websocket.GetSubscriptions(), nil
}