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

@@ -6,16 +6,15 @@ import (
"errors"
"fmt"
"net/http"
"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"
)
@@ -47,9 +46,8 @@ const (
// COINUT is the overarching type across the coinut package
type COINUT struct {
exchange.Base
WebsocketConn *websocket.Conn
WebsocketConn *wshandler.WebsocketConnection
InstrumentMap map[string]int
wsRequestMtx sync.Mutex
}
// SetDefaults sets current default values
@@ -76,15 +74,18 @@ func (c *COINUT) SetDefaults() {
common.NewHTTPClientWithTimeout(exchange.DefaultHTTPTimeout))
c.APIUrlDefault = coinutAPIURL
c.APIUrl = c.APIUrlDefault
c.WebsocketInit()
c.Websocket.Functionality = exchange.WebsocketTickerSupported |
exchange.WebsocketOrderbookSupported |
exchange.WebsocketTradeDataSupported |
exchange.WebsocketSubscribeSupported |
exchange.WebsocketUnsubscribeSupported |
exchange.WebsocketAuthenticatedEndpointsSupported |
exchange.WebsocketSubmitOrderSupported |
exchange.WebsocketCancelOrderSupported
c.Websocket = wshandler.New()
c.Websocket.Functionality = wshandler.WebsocketTickerSupported |
wshandler.WebsocketOrderbookSupported |
wshandler.WebsocketTradeDataSupported |
wshandler.WebsocketSubscribeSupported |
wshandler.WebsocketUnsubscribeSupported |
wshandler.WebsocketAuthenticatedEndpointsSupported |
wshandler.WebsocketSubmitOrderSupported |
wshandler.WebsocketCancelOrderSupported |
wshandler.WebsocketMessageCorrelationSupported
c.WebsocketResponseMaxLimit = exchange.DefaultWebsocketResponseMaxLimit
c.WebsocketResponseCheckTimeout = exchange.DefaultWebsocketResponseCheckTimeout
}
// Setup sets the current exchange configuration
@@ -125,17 +126,27 @@ func (c *COINUT) Setup(exch *config.ExchangeConfig) {
if err != nil {
log.Fatal(err)
}
err = c.WebsocketSetup(c.WsConnect,
err = c.Websocket.Setup(c.WsConnect,
c.Subscribe,
c.Unsubscribe,
exch.Name,
exch.Websocket,
exch.Verbose,
coinutWebsocketURL,
exch.WebsocketURL)
exch.WebsocketURL,
exch.AuthenticatedWebsocketAPISupport)
if err != nil {
log.Fatal(err)
}
c.WebsocketConn = &wshandler.WebsocketConnection{
ExchangeName: c.Name,
URL: c.Websocket.GetWebsocketURL(),
ProxyURL: c.Websocket.GetProxyAddress(),
Verbose: c.Verbose,
ResponseCheckTimeout: exch.WebsocketResponseCheckTimeout,
ResponseMaxLimit: exch.WebsocketResponseMaxLimit,
RateLimit: coinutWebsocketRateLimit,
}
}
}

View File

@@ -11,6 +11,7 @@ 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 c COINUT
@@ -55,12 +56,18 @@ func setupWSTestAuth(t *testing.T) {
c.SetDefaults()
TestSetup(t)
if !c.Websocket.IsEnabled() && !c.AuthenticatedWebsocketAPISupport || !areTestAPIKeysSet() {
t.Skip(exchange.WebsocketNotEnabled)
t.Skip(wshandler.WebsocketNotEnabled)
}
c.WebsocketConn = &wshandler.WebsocketConnection{
ExchangeName: c.Name,
URL: coinutWebsocketURL,
Verbose: c.Verbose,
RateLimit: coinutWebsocketRateLimit,
ResponseMaxLimit: exchange.DefaultWebsocketResponseMaxLimit,
ResponseCheckTimeout: exchange.DefaultWebsocketResponseCheckTimeout,
}
var err error
var dialer websocket.Dialer
c.WebsocketConn, _, err = dialer.Dial(c.Websocket.GetWebsocketURL(),
http.Header{})
err := c.WebsocketConn.Dial(&dialer, http.Header{})
if err != nil {
t.Fatal(err)
}
@@ -72,17 +79,6 @@ func setupWSTestAuth(t *testing.T) {
t.Error(err)
}
timer := time.NewTimer(5 * time.Second)
select {
case resp := <-c.Websocket.DataHandler:
if resp.(WsLoginResponse).Username != clientID {
t.Fatal("Unsuccessful login")
}
case <-timer.C:
t.Fatal("Expected response")
}
timer.Stop()
time.Sleep(2 * time.Second)
instrumentListByString = make(map[string]int64)
instrumentListByString[currency.NewPair(currency.LTC, currency.BTC).String()] = 1
wsSetupRan = true
@@ -448,28 +444,21 @@ func TestGetDepositAddress(t *testing.T) {
}
}
// TestWsAuthGetAccountBalance dials websocket, sends login request.
// TestWsAuthGetAccountBalance dials websocket, retrieves account balance
func TestWsAuthGetAccountBalance(t *testing.T) {
setupWSTestAuth(t)
err := c.wsGetAccountBalance()
_, err := c.wsGetAccountBalance()
if err != nil {
t.Error(err)
}
timer := time.NewTimer(sharedtestvalues.WebsocketResponseExtendedTimeout)
select {
case resp := <-c.Websocket.DataHandler:
if resp.(WsUserBalanceResponse).Status[0] != "OK" {
t.Error("Expected successful response")
}
case <-timer.C:
t.Error("Expected response")
}
timer.Stop()
}
// TestWsAuthSubmitOrders dials websocket, sends login request.
func TestWsAuthSubmitOrders(t *testing.T) {
// TestWsAuthSubmitOrder dials websocket, submit order
func TestWsAuthSubmitOrder(t *testing.T) {
setupWSTestAuth(t)
if !canManipulateRealOrders {
t.Skip("API keys set, canManipulateRealOrders false, skipping test")
}
order := WsSubmitOrderParameters{
Amount: 1,
Currency: currency.NewPair(currency.LTC, currency.BTC),
@@ -477,42 +466,64 @@ func TestWsAuthSubmitOrders(t *testing.T) {
Price: 1,
Side: exchange.BuyOrderSide,
}
err := c.wsSubmitOrders([]WsSubmitOrderParameters{order, order})
_, err := c.wsSubmitOrder(&order)
if err != nil {
t.Error(err)
}
timer := time.NewTimer(sharedtestvalues.WebsocketResponseExtendedTimeout)
select {
case <-c.Websocket.DataHandler:
case <-timer.C:
t.Error("Expected response")
}
timer.Stop()
}
// TestWsAuthCancelOrders dials websocket, sends login request.
// TestWsAuthCancelOrders dials websocket, submit orders
func TestWsAuthSubmitOrders(t *testing.T) {
setupWSTestAuth(t)
if !canManipulateRealOrders {
t.Skip("API keys set, canManipulateRealOrders false, skipping test")
}
order1 := WsSubmitOrderParameters{
Amount: 1,
Currency: currency.NewPair(currency.LTC, currency.BTC),
OrderID: 1,
Price: 1,
Side: exchange.BuyOrderSide,
}
order2 := WsSubmitOrderParameters{
Amount: 3,
Currency: currency.NewPair(currency.LTC, currency.BTC),
OrderID: 2,
Price: 2,
Side: exchange.BuyOrderSide,
}
_, err := c.wsSubmitOrders([]WsSubmitOrderParameters{order1, order2})
if err != nil {
t.Error(err)
}
}
// TestWsAuthCancelOrders dials websocket, cancels orders
func TestWsAuthCancelOrders(t *testing.T) {
setupWSTestAuth(t)
if !canManipulateRealOrders {
t.Skip("API keys set, canManipulateRealOrders false, skipping test")
}
order := WsCancelOrderParameters{
Currency: currency.NewPair(currency.LTC, currency.BTC),
OrderID: 1,
}
err := c.wsCancelOrders([]WsCancelOrderParameters{order, order})
if err != nil {
t.Error(err)
order2 := WsCancelOrderParameters{
Currency: currency.NewPair(currency.LTC, currency.BTC),
OrderID: 2,
}
timer := time.NewTimer(sharedtestvalues.WebsocketResponseExtendedTimeout)
select {
case <-c.Websocket.DataHandler:
case <-timer.C:
t.Error("Expected response")
_, errs := c.wsCancelOrders([]WsCancelOrderParameters{order, order2})
if len(errs) > 0 {
t.Error(errs)
}
timer.Stop()
}
// TestWsAuthCancelOrder dials websocket, sends login request.
// TestWsAuthCancelOrder dials websocket, cancels order
func TestWsAuthCancelOrder(t *testing.T) {
setupWSTestAuth(t)
if !canManipulateRealOrders {
t.Skip("API keys set, canManipulateRealOrders false, skipping test")
}
order := WsCancelOrderParameters{
Currency: currency.NewPair(currency.LTC, currency.BTC),
OrderID: 1,
@@ -521,27 +532,13 @@ func TestWsAuthCancelOrder(t *testing.T) {
if err != nil {
t.Error(err)
}
timer := time.NewTimer(sharedtestvalues.WebsocketResponseExtendedTimeout)
select {
case <-c.Websocket.DataHandler:
case <-timer.C:
t.Error("Expected response")
}
timer.Stop()
}
// TestWsAuthGetOpenOrders dials websocket, sends login request.
// TestWsAuthGetOpenOrders dials websocket, retrieves open orders
func TestWsAuthGetOpenOrders(t *testing.T) {
setupWSTestAuth(t)
err := c.wsGetOpenOrders(currency.NewPair(currency.LTC, currency.BTC))
if err != nil {
t.Error(err)
}
timer := time.NewTimer(sharedtestvalues.WebsocketResponseExtendedTimeout)
select {
case <-c.Websocket.DataHandler:
case <-timer.C:
t.Error("Expected response")
}
timer.Stop()
}

View File

@@ -263,11 +263,12 @@ type wsRequest struct {
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"`
Subscribe bool `json:"subscribe,omitempty"`
Nonce int64 `json:"nonce,omitempty"`
}
type wsResponse struct {
Nonce int64 `json:"nonce,omitempty"`
Reply string `json:"reply"`
}
@@ -400,14 +401,14 @@ type WsCancelOrderParameters struct {
OrderID int64
}
// WsCancelOrderRequest ws request
// WsCancelOrderRequest data required for cancelling an order
type WsCancelOrderRequest struct {
InstID int64 `json:"inst_id"`
OrderID int64 `json:"order_id"`
WsRequest
}
// WsCancelOrderResponse ws response
// WsCancelOrderResponse contains cancelled order data
type WsCancelOrderResponse struct {
Nonce int64 `json:"nonce"`
Reply string `json:"reply"`
@@ -416,16 +417,20 @@ type WsCancelOrderResponse struct {
Status []string `json:"status"`
}
// WsCancelOrdersResponse ws response
// WsCancelOrdersResponse contains all cancelled order data
type WsCancelOrdersResponse struct {
WsRequest
Entries []WsCancelOrdersResponseEntry `json:"entries"`
Nonce int64 `json:"nonce"`
Reply string `json:"reply"`
Results []WsCancelOrdersResponseData `json:"results"`
Status []string `json:"status"`
TransID int64 `json:"trans_id"`
}
// WsCancelOrdersResponseEntry ws response entry
type WsCancelOrdersResponseEntry struct {
InstID int64 `json:"inst_id"`
OrderID int64 `json:"order_id"`
// WsCancelOrdersResponseData individual cancellation response data
type WsCancelOrdersResponseData struct {
InstID int64 `json:"inst_id"`
OrderID int64 `json:"order_id"`
Status string `json:"status"`
}
// WsGetOpenOrdersRequest ws request
@@ -547,6 +552,25 @@ type WsOrderRejectedResponse struct {
TransID int64 `json:"trans_id"`
}
// WsStandardOrderResponse a standardised order
type WsStandardOrderResponse struct {
InstID int64
OrderID int64
ClientOrdID int64
TransID int64
Nonce int64
Status []string
Qty float64
OpenQty float64
Price float64
Side string
Reasons []string
Timestamp int64
OrderType string
CommissionAmount float64
CommissionCurrency currency.Pair
}
// WsUserOpenOrdersResponse ws response
type WsUserOpenOrdersResponse struct {
Nonce int64 `json:"nonce"`
@@ -612,3 +636,25 @@ type WsNewOrderResponse struct {
Reply string `json:"reply"`
Status []string `json:"status"`
}
// WsGetAccountBalanceResponse contains values of each currency
type WsGetAccountBalanceResponse struct {
BCH string `json:"BCH"`
BTC string `json:"BTC"`
BTG string `json:"BTG"`
CAD string `json:"CAD"`
ETC string `json:"ETC"`
ETH string `json:"ETH"`
LCH string `json:"LCH"`
LTC string `json:"LTC"`
MYR string `json:"MYR"`
SGD string `json:"SGD"`
USD string `json:"USD"`
USDT string `json:"USDT"`
XMR string `json:"XMR"`
ZEC string `json:"ZEC"`
Nonce int64 `json:"nonce"`
Reply string `json:"reply"`
Status []string `json:"status"`
TransID int64 `json:"trans_id"`
}

View File

@@ -4,7 +4,6 @@ import (
"errors"
"fmt"
"net/http"
"net/url"
"strings"
"time"
@@ -13,11 +12,11 @@ import (
"github.com/thrasher-/gocryptotrader/currency"
exchange "github.com/thrasher-/gocryptotrader/exchanges"
"github.com/thrasher-/gocryptotrader/exchanges/orderbook"
log "github.com/thrasher-/gocryptotrader/logger"
"github.com/thrasher-/gocryptotrader/exchanges/wshandler"
)
const coinutWebsocketURL = "wss://wsapi.coinut.com"
const coinutWebsocketRateLimit = 30 * time.Millisecond
const coinutWebsocketRateLimit = 30
var nNonce map[int64]string
var channels map[string]chan []byte
@@ -33,32 +32,18 @@ var populatedList bool
// WsConnect intiates a websocket connection
func (c *COINUT) WsConnect() error {
if !c.Websocket.IsEnabled() || !c.IsEnabled() {
return errors.New(exchange.WebsocketNotEnabled)
return errors.New(wshandler.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{})
var dialer websocket.Dialer
err := c.WebsocketConn.Dial(&dialer, http.Header{})
if err != nil {
return err
}
go c.WsHandleData()
if !populatedList {
instrumentListByString = make(map[string]int64)
instrumentListByCode = make(map[int64]string)
err = c.WsSetInstrumentList()
if err != nil {
return err
@@ -72,21 +57,9 @@ func (c *COINUT) WsConnect() error {
channels = make(map[string]chan []byte)
channels["hb"] = make(chan []byte, 1)
go c.WsHandleData()
return nil
}
// WsReadData reads data from the websocket connection
func (c *COINUT) WsReadData() (exchange.WebsocketResponse, error) {
_, resp, err := c.WebsocketConn.ReadMessage()
if err != nil {
return exchange.WebsocketResponse{}, err
}
c.Websocket.TrafficAlert <- struct{}{}
return exchange.WebsocketResponse{Raw: resp}, nil
}
// WsHandleData handles read data
func (c *COINUT) WsHandleData() {
c.Websocket.Wg.Add(1)
@@ -101,11 +74,12 @@ func (c *COINUT) WsHandleData() {
return
default:
resp, err := c.WsReadData()
resp, err := c.WebsocketConn.ReadMessage()
if err != nil {
c.Websocket.DataHandler <- err
return
}
c.Websocket.TrafficAlert <- struct{}{}
if strings.HasPrefix(string(resp.Raw), "[") {
var incoming []wsResponse
@@ -115,6 +89,10 @@ func (c *COINUT) WsHandleData() {
continue
}
for i := range incoming {
if incoming[i].Nonce > 0 {
c.WebsocketConn.AddResponseWithID(incoming[i].Nonce, resp.Raw)
break
}
var individualJSON []byte
individualJSON, err = common.JSONEncode(incoming[i])
if err != nil {
@@ -131,6 +109,7 @@ func (c *COINUT) WsHandleData() {
c.Websocket.DataHandler <- err
continue
}
c.wsProcessResponse(resp.Raw)
}
@@ -146,15 +125,6 @@ func (c *COINUT) wsProcessResponse(resp []byte) {
return
}
switch incoming.Reply {
case "login":
var login WsLoginResponse
err := common.JSONDecode(resp, &login)
if err != nil {
c.Websocket.DataHandler <- err
return
}
c.Websocket.SetCanUseAuthenticatedEndpoints(login.Username == c.ClientID)
c.Websocket.DataHandler <- login
case "hb":
channels["hb"] <- resp
case "inst_tick":
@@ -164,7 +134,7 @@ func (c *COINUT) wsProcessResponse(resp []byte) {
c.Websocket.DataHandler <- err
return
}
c.Websocket.DataHandler <- exchange.TickerData{
c.Websocket.DataHandler <- wshandler.TickerData{
Timestamp: time.Unix(0, ticker.Timestamp),
Exchange: c.GetName(),
AssetType: "SPOT",
@@ -187,7 +157,7 @@ func (c *COINUT) wsProcessResponse(resp []byte) {
return
}
currencyPair := instrumentListByCode[orderbooksnapshot.InstID]
c.Websocket.DataHandler <- exchange.WebsocketOrderbookUpdate{
c.Websocket.DataHandler <- wshandler.WebsocketOrderbookUpdate{
Exchange: c.GetName(),
Asset: "SPOT",
Pair: currency.NewPairFromString(currencyPair),
@@ -205,7 +175,7 @@ func (c *COINUT) wsProcessResponse(resp []byte) {
return
}
currencyPair := instrumentListByCode[orderbookUpdate.InstID]
c.Websocket.DataHandler <- exchange.WebsocketOrderbookUpdate{
c.Websocket.DataHandler <- wshandler.WebsocketOrderbookUpdate{
Exchange: c.GetName(),
Asset: "SPOT",
Pair: currency.NewPairFromString(currencyPair),
@@ -226,7 +196,7 @@ func (c *COINUT) wsProcessResponse(resp []byte) {
return
}
currencyPair := instrumentListByCode[tradeUpdate.InstID]
c.Websocket.DataHandler <- exchange.TradeData{
c.Websocket.DataHandler <- wshandler.TradeData{
Timestamp: time.Unix(tradeUpdate.Timestamp, 0),
CurrencyPair: currency.NewPairFromString(currencyPair),
AssetType: "SPOT",
@@ -234,78 +204,12 @@ func (c *COINUT) wsProcessResponse(resp []byte) {
Price: tradeUpdate.Price,
Side: tradeUpdate.Side,
}
case "user_balance":
var userBalance WsUserBalanceResponse
err := common.JSONDecode(resp, &userBalance)
if err != nil {
c.Websocket.DataHandler <- err
default:
if incoming.Nonce > 0 {
c.WebsocketConn.AddResponseWithID(incoming.Nonce, resp)
return
}
c.Websocket.DataHandler <- userBalance
case "new_order":
var newOrder WsNewOrderResponse
err := common.JSONDecode(resp, &newOrder)
if err != nil {
c.Websocket.DataHandler <- err
return
}
c.Websocket.DataHandler <- newOrder
case "order_accepted":
var orderAccepted WsOrderAcceptedResponse
err := common.JSONDecode(resp, &orderAccepted)
if err != nil {
c.Websocket.DataHandler <- err
return
}
c.Websocket.DataHandler <- orderAccepted
case "order_filled":
var orderFilled WsOrderFilledResponse
err := common.JSONDecode(resp, &orderFilled)
if err != nil {
c.Websocket.DataHandler <- err
return
}
c.Websocket.DataHandler <- orderFilled
case "order_rejected":
var orderRejected WsOrderRejectedResponse
err := common.JSONDecode(resp, &orderRejected)
if err != nil {
c.Websocket.DataHandler <- err
return
}
c.Websocket.DataHandler <- orderRejected
case "user_open_orders":
var openOrders WsUserOpenOrdersResponse
err := common.JSONDecode(resp, &openOrders)
if err != nil {
c.Websocket.DataHandler <- err
return
}
c.Websocket.DataHandler <- openOrders
case "trade_history":
var tradeHistory WsTradeHistoryResponse
err := common.JSONDecode(resp, &tradeHistory)
if err != nil {
c.Websocket.DataHandler <- err
return
}
c.Websocket.DataHandler <- tradeHistory
case "cancel_orders":
var cancelOrders WsCancelOrdersResponse
err := common.JSONDecode(resp, &cancelOrders)
if err != nil {
c.Websocket.DataHandler <- err
return
}
c.Websocket.DataHandler <- cancelOrders
case "cancel_order":
var cancelOrder WsCancelOrderResponse
err := common.JSONDecode(resp, &cancelOrder)
if err != nil {
c.Websocket.DataHandler <- err
return
}
c.Websocket.DataHandler <- cancelOrder
c.Websocket.DataHandler <- fmt.Errorf("%v unhandled websocket response: %s", c.Name, resp)
}
}
@@ -322,37 +226,27 @@ func (c *COINUT) GetNonce() int64 {
// WsSetInstrumentList fetches instrument list and propagates a local cache
func (c *COINUT) WsSetInstrumentList() error {
err := c.wsSend(wsRequest{
request := wsRequest{
Request: "inst_list",
SecType: "SPOT",
Nonce: c.GetNonce(),
})
Nonce: c.WebsocketConn.GenerateMessageID(false),
}
resp, err := c.WebsocketConn.SendMessageReturnResponse(request.Nonce, 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
}
@@ -409,11 +303,11 @@ func (c *COINUT) WsProcessOrderbookUpdate(ob *WsOrderbookUpdate) error {
// GenerateDefaultSubscriptions Adds default subscriptions to websocket to be handled by ManageSubscriptions()
func (c *COINUT) GenerateDefaultSubscriptions() {
var channels = []string{"inst_tick", "inst_order_book"}
var subscriptions []exchange.WebsocketChannelSubscription
var subscriptions []wshandler.WebsocketChannelSubscription
enabledCurrencies := c.GetEnabledCurrencies()
for i := range channels {
for j := range enabledCurrencies {
subscriptions = append(subscriptions, exchange.WebsocketChannelSubscription{
subscriptions = append(subscriptions, wshandler.WebsocketChannelSubscription{
Channel: channels[i],
Currency: enabledCurrencies[j],
})
@@ -423,42 +317,37 @@ func (c *COINUT) GenerateDefaultSubscriptions() {
}
// Subscribe sends a websocket message to receive data from the channel
func (c *COINUT) Subscribe(channelToSubscribe exchange.WebsocketChannelSubscription) error {
func (c *COINUT) Subscribe(channelToSubscribe wshandler.WebsocketChannelSubscription) error {
subscribe := wsRequest{
Request: channelToSubscribe.Channel,
InstID: instrumentListByString[channelToSubscribe.Currency.String()],
Subscribe: true,
Nonce: c.GetNonce(),
Nonce: c.WebsocketConn.GenerateMessageID(false),
}
return c.wsSend(subscribe)
return c.WebsocketConn.SendMessage(subscribe)
}
// Unsubscribe sends a websocket message to stop receiving data from the channel
func (c *COINUT) Unsubscribe(channelToSubscribe exchange.WebsocketChannelSubscription) error {
func (c *COINUT) Unsubscribe(channelToSubscribe wshandler.WebsocketChannelSubscription) error {
subscribe := wsRequest{
Request: channelToSubscribe.Channel,
InstID: instrumentListByString[channelToSubscribe.Currency.String()],
Subscribe: false,
Nonce: c.GetNonce(),
Nonce: c.WebsocketConn.GenerateMessageID(false),
}
return c.wsSend(subscribe)
}
// WsSend sends data to the websocket server
func (c *COINUT) wsSend(data interface{}) error {
c.wsRequestMtx.Lock()
defer c.wsRequestMtx.Unlock()
json, err := common.JSONEncode(data)
resp, err := c.WebsocketConn.SendMessageReturnResponse(subscribe.Nonce, subscribe)
if err != nil {
return err
}
if c.Verbose {
log.Debugf("%v sending message to websocket %v", c.Name, string(json))
var response map[string]interface{}
err = common.JSONDecode(resp, &response)
if err != nil {
return err
}
// Basic rate limiter
time.Sleep(coinutWebsocketRateLimit)
return c.WebsocketConn.WriteMessage(websocket.TextMessage, json)
if response["status"].([]interface{})[0] != "OK" {
return fmt.Errorf("%v unsubscribe failed for channel %v", c.Name, channelToSubscribe.Channel)
}
return nil
}
func (c *COINUT) wsAuthenticate() error {
@@ -466,7 +355,7 @@ func (c *COINUT) wsAuthenticate() error {
return fmt.Errorf("%v AuthenticatedWebsocketAPISupport not enabled", c.Name)
}
timestamp := time.Now().Unix()
nonce := c.GetNonce()
nonce := c.WebsocketConn.GenerateMessageID(false)
payload := fmt.Sprintf("%v|%v|%v", c.ClientID, timestamp, nonce)
hmac := common.GetHMAC(common.HashSHA256, []byte(payload), []byte(c.APIKey))
loginRequest := struct {
@@ -483,34 +372,54 @@ func (c *COINUT) wsAuthenticate() error {
Timestamp: timestamp,
}
err := c.wsSend(loginRequest)
resp, err := c.WebsocketConn.SendMessageReturnResponse(loginRequest.Nonce, loginRequest)
if err != nil {
c.Websocket.SetCanUseAuthenticatedEndpoints(false)
return err
}
var response map[string]interface{}
err = common.JSONDecode(resp, &response)
if err != nil {
return err
}
if response["status"].([]interface{})[0] != "OK" {
c.Websocket.SetCanUseAuthenticatedEndpoints(false)
return fmt.Errorf("%v failed to authenticate", c.Name)
}
c.Websocket.SetCanUseAuthenticatedEndpoints(true)
return nil
}
func (c *COINUT) wsGetAccountBalance() error {
func (c *COINUT) wsGetAccountBalance() (*WsGetAccountBalanceResponse, error) {
if !c.Websocket.CanUseAuthenticatedEndpoints() {
return fmt.Errorf("%v not authorised to submit order", c.Name)
return nil, fmt.Errorf("%v not authorised to submit order", c.Name)
}
accBalance := wsRequest{
Request: "user_balance",
Nonce: c.GetNonce(),
Nonce: c.WebsocketConn.GenerateMessageID(false),
}
return c.wsSend(accBalance)
resp, err := c.WebsocketConn.SendMessageReturnResponse(accBalance.Nonce, accBalance)
if err != nil {
return nil, err
}
var response WsGetAccountBalanceResponse
err = common.JSONDecode(resp, &response)
if err != nil {
return nil, err
}
if response.Status[0] != "OK" {
return &response, fmt.Errorf("%v get account balance failed", c.Name)
}
return &response, nil
}
func (c *COINUT) wsSubmitOrder(order *WsSubmitOrderParameters) error {
func (c *COINUT) wsSubmitOrder(order *WsSubmitOrderParameters) (*WsStandardOrderResponse, error) {
if !c.Websocket.CanUseAuthenticatedEndpoints() {
return fmt.Errorf("%v not authorised to submit order", c.Name)
return nil, fmt.Errorf("%v not authorised to submit order", c.Name)
}
currency := exchange.FormatExchangeCurrency(c.Name, order.Currency).String()
var orderSubmissionRequest WsSubmitOrderRequest
orderSubmissionRequest.Request = "new_order"
orderSubmissionRequest.Nonce = c.GetNonce()
orderSubmissionRequest.Nonce = c.WebsocketConn.GenerateMessageID(false)
orderSubmissionRequest.InstID = instrumentListByString[currency]
orderSubmissionRequest.Qty = order.Amount
orderSubmissionRequest.Price = order.Price
@@ -519,12 +428,100 @@ func (c *COINUT) wsSubmitOrder(order *WsSubmitOrderParameters) error {
if order.OrderID > 0 {
orderSubmissionRequest.OrderID = order.OrderID
}
return c.wsSend(orderSubmissionRequest)
resp, err := c.WebsocketConn.SendMessageReturnResponse(orderSubmissionRequest.Nonce, orderSubmissionRequest)
if err != nil {
return nil, err
}
var standardOrder WsStandardOrderResponse
standardOrder, err = c.wsStandardiseOrderResponse(resp)
if err != nil {
return nil, err
}
if standardOrder.Status[0] != "OK" {
return &standardOrder, fmt.Errorf("%v order submission failed. %v", c.Name, standardOrder)
}
if len(standardOrder.Reasons) > 0 && standardOrder.Reasons[0] != "" {
return &standardOrder, fmt.Errorf("%v order submission failed. %v", c.Name, standardOrder.Reasons[0])
}
return &standardOrder, nil
}
func (c *COINUT) wsSubmitOrders(orders []WsSubmitOrderParameters) error {
func (c *COINUT) wsStandardiseOrderResponse(resp []byte) (WsStandardOrderResponse, error) {
var response WsStandardOrderResponse
var incoming wsResponse
err := common.JSONDecode(resp, &incoming)
if err != nil {
return response, err
}
switch incoming.Reply {
case "order_accepted":
var orderAccepted WsOrderAcceptedResponse
err := common.JSONDecode(resp, &orderAccepted)
if err != nil {
return response, err
}
response = WsStandardOrderResponse{
InstID: orderAccepted.InstID,
Nonce: orderAccepted.Nonce,
OpenQty: orderAccepted.OpenQty,
OrderID: orderAccepted.OrderID,
OrderType: orderAccepted.Reply,
Price: orderAccepted.OrderPrice,
Qty: orderAccepted.Qty,
Side: orderAccepted.Side,
Status: orderAccepted.Status,
TransID: orderAccepted.TransID,
ClientOrdID: orderAccepted.ClientOrdID,
}
case "order_filled":
var orderFilled WsOrderFilledResponse
err := common.JSONDecode(resp, &orderFilled)
if err != nil {
return response, err
}
response = WsStandardOrderResponse{
InstID: orderFilled.Order.InstID,
Nonce: orderFilled.Nonce,
OpenQty: orderFilled.Order.OpenQty,
OrderID: orderFilled.Order.OrderID,
OrderType: orderFilled.Reply,
Price: orderFilled.Order.Price,
Qty: orderFilled.Order.Qty,
Side: orderFilled.Order.Side,
Status: orderFilled.Status,
TransID: orderFilled.TransID,
ClientOrdID: orderFilled.Order.ClientOrdID,
}
case "order_rejected":
var orderRejected WsOrderRejectedResponse
err := common.JSONDecode(resp, &orderRejected)
if err != nil {
return response, err
}
response = WsStandardOrderResponse{
InstID: orderRejected.InstID,
Nonce: orderRejected.Nonce,
OpenQty: orderRejected.OpenQty,
OrderID: orderRejected.OrderID,
OrderType: orderRejected.Reply,
Price: orderRejected.Price,
Qty: orderRejected.Qty,
Side: orderRejected.Side,
Status: orderRejected.Status,
TransID: orderRejected.TransID,
ClientOrdID: orderRejected.ClientOrdID,
Reasons: orderRejected.Reasons,
}
}
return response, nil
}
func (c *COINUT) wsSubmitOrders(orders []WsSubmitOrderParameters) ([]WsStandardOrderResponse, []error) {
var errors []error
var ordersResponse []WsStandardOrderResponse
if !c.Websocket.CanUseAuthenticatedEndpoints() {
return fmt.Errorf("%v not authorised to submit orders", c.Name)
errors = append(errors, fmt.Errorf("%v not authorised to submit orders", c.Name))
return nil, errors
}
orderRequest := WsSubmitOrdersRequest{}
for i := range orders {
@@ -539,9 +536,48 @@ func (c *COINUT) wsSubmitOrders(orders []WsSubmitOrderParameters) error {
})
}
orderRequest.Nonce = c.GetNonce()
orderRequest.Nonce = c.WebsocketConn.GenerateMessageID(false)
orderRequest.Request = "new_orders"
return c.wsSend(orderRequest)
resp, err := c.WebsocketConn.SendMessageReturnResponse(orderRequest.Nonce, orderRequest)
if err != nil {
errors = append(errors, err)
return nil, errors
}
var incoming []interface{}
err = common.JSONDecode(resp, &incoming)
if err != nil {
errors = append(errors, err)
return nil, errors
}
for i := range incoming {
var individualJSON []byte
individualJSON, err = common.JSONEncode(incoming[i])
if err != nil {
errors = append(errors, err)
continue
}
standardOrder, err := c.wsStandardiseOrderResponse(individualJSON)
if err != nil {
errors = append(errors, err)
continue
}
if standardOrder.Status[0] != "OK" {
errors = append(errors, fmt.Errorf("%v order submission failed. %v", c.Name, standardOrder))
continue
}
if len(standardOrder.Reasons) > 0 && standardOrder.Reasons[0] != "" {
errors = append(errors, fmt.Errorf("%v order submission failed for currency %v and orderID %v, message %v ",
c.Name,
instrumentListByCode[standardOrder.InstID],
standardOrder.OrderID,
standardOrder.Reasons[0]))
continue
}
ordersResponse = append(ordersResponse, standardOrder)
}
return ordersResponse, errors
}
func (c *COINUT) wsGetOpenOrders(p currency.Pair) error {
@@ -551,10 +587,24 @@ func (c *COINUT) wsGetOpenOrders(p currency.Pair) error {
currency := exchange.FormatExchangeCurrency(c.Name, p).String()
var openOrdersRequest WsGetOpenOrdersRequest
openOrdersRequest.Request = "user_open_orders"
openOrdersRequest.Nonce = c.GetNonce()
openOrdersRequest.Nonce = c.WebsocketConn.GenerateMessageID(false)
openOrdersRequest.InstID = instrumentListByString[currency]
return c.wsSend(openOrdersRequest)
resp, err := c.WebsocketConn.SendMessageReturnResponse(openOrdersRequest.Nonce, openOrdersRequest)
if err != nil {
return err
}
var response map[string]interface{}
err = common.JSONDecode(resp, &response)
if err != nil {
return err
}
if response["status"].([]interface{})[0] != "OK" {
return fmt.Errorf("%v get open orders failed for currency %v",
c.Name,
p)
}
return nil
}
func (c *COINUT) wsCancelOrder(cancellation WsCancelOrderParameters) error {
@@ -566,14 +616,31 @@ func (c *COINUT) wsCancelOrder(cancellation WsCancelOrderParameters) error {
cancellationRequest.Request = "cancel_order"
cancellationRequest.InstID = instrumentListByString[currency]
cancellationRequest.OrderID = cancellation.OrderID
cancellationRequest.Nonce = c.GetNonce()
cancellationRequest.Nonce = c.WebsocketConn.GenerateMessageID(false)
return c.wsSend(cancellationRequest)
resp, err := c.WebsocketConn.SendMessageReturnResponse(cancellationRequest.Nonce, cancellationRequest)
if err != nil {
return err
}
var response map[string]interface{}
err = common.JSONDecode(resp, &response)
if err != nil {
return err
}
if response["status"].([]interface{})[0] != "OK" {
return fmt.Errorf("%v order cancellation failed for currency %v and orderID %v, message %v",
c.Name,
cancellation.Currency,
cancellation.OrderID,
response["status"])
}
return nil
}
func (c *COINUT) wsCancelOrders(cancellations []WsCancelOrderParameters) error {
func (c *COINUT) wsCancelOrders(cancellations []WsCancelOrderParameters) (*WsCancelOrdersResponse, []error) {
var errors []error
if !c.Websocket.CanUseAuthenticatedEndpoints() {
return fmt.Errorf("%v not authorised to cancel orders", c.Name)
return nil, errors
}
cancelOrderRequest := WsCancelOrdersRequest{}
for i := range cancellations {
@@ -585,8 +652,29 @@ func (c *COINUT) wsCancelOrders(cancellations []WsCancelOrderParameters) error {
}
cancelOrderRequest.Request = "cancel_orders"
cancelOrderRequest.Nonce = c.GetNonce()
return c.wsSend(cancelOrderRequest)
cancelOrderRequest.Nonce = c.WebsocketConn.GenerateMessageID(false)
resp, err := c.WebsocketConn.SendMessageReturnResponse(cancelOrderRequest.Nonce, cancelOrderRequest)
if err != nil {
return nil, []error{err}
}
var response WsCancelOrdersResponse
err = common.JSONDecode(resp, &response)
if err != nil {
return nil, []error{err}
}
if response.Status[0] != "OK" {
return &response, []error{err}
}
for i := range response.Results {
if response.Results[i].Status != "OK" {
errors = append(errors, fmt.Errorf("%v order cancellation failed for currency %v and orderID %v, message %v",
c.Name,
instrumentListByCode[response.Results[i].InstID],
response.Results[i].OrderID,
response.Results[i].Status))
}
}
return &response, errors
}
func (c *COINUT) wsGetTradeHistory(p currency.Pair, start, limit int64) error {
@@ -597,9 +685,23 @@ func (c *COINUT) wsGetTradeHistory(p currency.Pair, start, limit int64) error {
var request WsTradeHistoryRequest
request.Request = "trade_history"
request.InstID = instrumentListByString[currency]
request.Nonce = c.GetNonce()
request.Nonce = c.WebsocketConn.GenerateMessageID(false)
request.Start = start
request.Limit = limit
return c.wsSend(request)
resp, err := c.WebsocketConn.SendMessageReturnResponse(request.Nonce, request)
if err != nil {
return err
}
var response map[string]interface{}
err = common.JSONDecode(resp, &response)
if err != nil {
return err
}
if response["status"].([]interface{})[0] != "OK" {
return fmt.Errorf("%v get trade history failed for %v",
c.Name,
request)
}
return nil
}

View File

@@ -13,6 +13,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"
)
@@ -377,7 +378,7 @@ func (c *COINUT) WithdrawFiatFundsToInternationalBank(withdrawRequest *exchange.
}
// GetWebsocket returns a pointer to the exchange websocket
func (c *COINUT) GetWebsocket() (*exchange.Websocket, error) {
func (c *COINUT) GetWebsocket() (*wshandler.Websocket, error) {
return c.Websocket, nil
}
@@ -506,20 +507,20 @@ func (c *COINUT) GetOrderHistory(getOrdersRequest *exchange.GetOrdersRequest) ([
// SubscribeToWebsocketChannels appends to ChannelsToSubscribe
// which lets websocket.manageSubscriptions handle subscribing
func (c *COINUT) SubscribeToWebsocketChannels(channels []exchange.WebsocketChannelSubscription) error {
func (c *COINUT) SubscribeToWebsocketChannels(channels []wshandler.WebsocketChannelSubscription) error {
c.Websocket.SubscribeToChannels(channels)
return nil
}
// UnsubscribeToWebsocketChannels removes from ChannelsToSubscribe
// which lets websocket.manageSubscriptions handle unsubscribing
func (c *COINUT) UnsubscribeToWebsocketChannels(channels []exchange.WebsocketChannelSubscription) error {
c.Websocket.UnsubscribeToChannels(channels)
func (c *COINUT) UnsubscribeToWebsocketChannels(channels []wshandler.WebsocketChannelSubscription) error {
c.Websocket.RemoveSubscribedChannels(channels)
return nil
}
// GetSubscriptions returns a copied list of subscriptions
func (c *COINUT) GetSubscriptions() ([]exchange.WebsocketChannelSubscription, error) {
func (c *COINUT) GetSubscriptions() ([]wshandler.WebsocketChannelSubscription, error) {
return c.Websocket.GetSubscriptions(), nil
}