Files
gocryptotrader/exchanges/zb/zb_websocket.go
Ryan O'Hara-Reid 22ff33cd54 Engine QA (#367)
* Improved error message when no config is set on startup

* Change inccorect error wording

* bump Bitfinex websocket orderbook return length to max

* temporary fix of incorrect orderbook updates, limit to bid and ask len of 100, will be extended later if needed

* Fixed issue in binance websocket that appended 0 volume bid/ask items

* Fix panic when unmarshalling an empty pair from config

* Add get pair asset method for exchange base
Fix Bitmex orderbook stream
Unbuffer Bitmex orderbook stream

* force syncer to update ticker instead of fetch, which allows a stream

* Fix websocket last price for coinbasepro

* fix websocket ticker for coinut

* Fix websocket orderbook stream Huobi

* increase orderbook depth REST for Huobi

* Fix websocket support and ensure data integrity

* Fix time parsing issue after error checks

* check error, only process enabled currency pairs, signal websocket data processing

* expanded websocket functionality for okgroup

* Add logic to not process zero length slice for orderbooks

* fix websocket ticker only updating enabled and individual book updates

* ZB fixes to order submission/retrieval/cancellation w/ general fixes

* Quiet unnecessary warning

* updated config entry values for REST and websocket (initial hack until I come up with a better solution for asset types)

* Ch GetName function to field access modifyer & rm useless code

* Add in error I missed

* Nits addressed

* some more fixes

* Turned kraken default websocket to true and some small changes

* fixes linter issues

* Ensured okgroup books and sent update through to datahandler. Zb update as well.

* Add test case to get asset type from pair

* Add test for pairs unmarshal

* Add testing and addressed nits

* FIX linter issue

* Addressed Gees nits

* Thanks glorious spotter

* more nitorinos

* Addres even more nits

* Add stringerino 4000

* Fix for panic cause by sort slice out of range, also nits addressed

* fix linter issues

* Changed from function to field access

* Changed from function to field access

* fix for orderbook update panic, removes quick fix - caused by sync item fetching through same protocol

* Add new test and update random generator

* pass in invalid string to future ob fetching, due to futures contract expire and a http 400 error is returned
2019-11-04 15:34:30 +11:00

573 lines
18 KiB
Go

package zb
import (
"errors"
"fmt"
"net/http"
"regexp"
"strings"
"time"
"github.com/gorilla/websocket"
"github.com/thrasher-corp/gocryptotrader/common"
"github.com/thrasher-corp/gocryptotrader/common/crypto"
"github.com/thrasher-corp/gocryptotrader/currency"
exchange "github.com/thrasher-corp/gocryptotrader/exchanges"
"github.com/thrasher-corp/gocryptotrader/exchanges/asset"
"github.com/thrasher-corp/gocryptotrader/exchanges/orderbook"
"github.com/thrasher-corp/gocryptotrader/exchanges/websocket/wshandler"
log "github.com/thrasher-corp/gocryptotrader/logger"
)
const (
zbWebsocketAPI = "wss://api.zb.cn:9999/websocket"
zWebsocketAddChannel = "addChannel"
zbWebsocketRateLimit = 20
)
// WsConnect initiates a websocket connection
func (z *ZB) WsConnect() error {
if !z.Websocket.IsEnabled() || !z.IsEnabled() {
return errors.New(wshandler.WebsocketNotEnabled)
}
var dialer websocket.Dialer
err := z.WebsocketConn.Dial(&dialer, http.Header{})
if err != nil {
return err
}
go z.WsHandleData()
z.GenerateDefaultSubscriptions()
return nil
}
// WsHandleData handles all the websocket data coming from the websocket
// connection
func (z *ZB) WsHandleData() {
z.Websocket.Wg.Add(1)
defer func() {
z.Websocket.Wg.Done()
}()
for {
select {
case <-z.Websocket.ShutdownC:
return
default:
resp, err := z.WebsocketConn.ReadMessage()
if err != nil {
z.Websocket.ReadMessageErrors <- err
return
}
z.Websocket.TrafficAlert <- struct{}{}
fixedJSON := z.wsFixInvalidJSON(resp.Raw)
var result Generic
err = common.JSONDecode(fixedJSON, &result)
if err != nil {
z.Websocket.DataHandler <- err
continue
}
if result.No > 0 {
z.WebsocketConn.AddResponseWithID(result.No, fixedJSON)
continue
}
if result.Code > 0 && result.Code != 1000 {
z.Websocket.DataHandler <- fmt.Errorf("%v request failed, message: %v, error code: %v", z.Name, result.Message, wsErrCodes[result.Code])
continue
}
switch {
case strings.Contains(result.Channel, "markets"):
var markets Markets
err := common.JSONDecode(result.Data, &markets)
if err != nil {
z.Websocket.DataHandler <- err
continue
}
case strings.Contains(result.Channel, "ticker"):
cPair := strings.Split(result.Channel, "_")
var ticker WsTicker
err := common.JSONDecode(fixedJSON, &ticker)
if err != nil {
z.Websocket.DataHandler <- err
continue
}
z.Websocket.DataHandler <- wshandler.TickerData{
Exchange: z.Name,
Close: ticker.Data.Last,
Volume: ticker.Data.Volume24Hr,
High: ticker.Data.High,
Low: ticker.Data.Low,
Last: ticker.Data.Last,
Bid: ticker.Data.Buy,
Ask: ticker.Data.Sell,
Timestamp: time.Unix(0, ticker.Date*int64(time.Millisecond)),
AssetType: asset.Spot,
Pair: currency.NewPairFromString(cPair[0]),
}
case strings.Contains(result.Channel, "depth"):
var depth WsDepth
err := common.JSONDecode(fixedJSON, &depth)
if err != nil {
z.Websocket.DataHandler <- err
continue
}
var asks []orderbook.Item
for i := range depth.Asks {
asks = append(asks, orderbook.Item{
Amount: depth.Asks[i][1].(float64),
Price: depth.Asks[i][0].(float64),
})
}
var bids []orderbook.Item
for i := range depth.Bids {
bids = append(bids, orderbook.Item{
Amount: depth.Bids[i][1].(float64),
Price: depth.Bids[i][0].(float64),
})
}
channelInfo := strings.Split(result.Channel, "_")
cPair := currency.NewPairFromString(channelInfo[0])
var newOrderBook orderbook.Base
newOrderBook.Asks = asks
newOrderBook.Bids = bids
newOrderBook.AssetType = asset.Spot
newOrderBook.Pair = cPair
newOrderBook.ExchangeName = z.GetName()
err = z.Websocket.Orderbook.LoadSnapshot(&newOrderBook)
if err != nil {
z.Websocket.DataHandler <- err
continue
}
z.Websocket.DataHandler <- wshandler.WebsocketOrderbookUpdate{
Pair: cPair,
Asset: asset.Spot,
Exchange: z.GetName(),
}
case strings.Contains(result.Channel, "trades"):
var trades WsTrades
err := common.JSONDecode(fixedJSON, &trades)
if err != nil {
z.Websocket.DataHandler <- err
continue
}
// Most up to date trade
if len(trades.Data) == 0 {
continue
}
t := trades.Data[len(trades.Data)-1]
channelInfo := strings.Split(result.Channel, "_")
cPair := currency.NewPairFromString(channelInfo[0])
z.Websocket.DataHandler <- wshandler.TradeData{
Timestamp: time.Unix(0, t.Date*int64(time.Millisecond)),
CurrencyPair: cPair,
AssetType: asset.Spot,
Exchange: z.GetName(),
EventTime: t.Date,
Price: t.Price,
Amount: t.Amount,
Side: t.TradeType,
}
default:
z.Websocket.DataHandler <- errors.New("zb_websocket.go error - unhandled websocket response")
continue
}
}
}
}
var wsErrCodes = map[int64]string{
1000: "Successful call",
1001: "General error message",
1002: "internal error",
1003: "Verification failed",
1004: "Financial security password lock",
1005: "The fund security password is incorrect. Please confirm and re-enter.",
1006: "Real-name certification is awaiting review or review",
1007: "Channel is empty",
1008: "Event is empty",
1009: "This interface is being maintained",
1011: "Not open yet",
1012: "Insufficient permissions",
1013: "Can not trade, if you have any questions, please contact online customer service",
1014: "Cannot be sold during the pre-sale period",
2002: "Insufficient balance in Bitcoin account",
2003: "Insufficient balance of Litecoin account",
2005: "Insufficient balance in Ethereum account",
2006: "Insufficient balance in ETC currency account",
2007: "Insufficient balance of BTS currency account",
2008: "Insufficient balance in EOS currency account",
2009: "Insufficient account balance",
3001: "Pending order not found",
3002: "Invalid amount",
3003: "Invalid quantity",
3004: "User does not exist",
3005: "Invalid parameter",
3006: "Invalid IP or inconsistent with the bound IP",
3007: "Request time has expired",
3008: "Transaction history not found",
4001: "API interface is locked",
4002: "Request too frequently",
}
// GenerateDefaultSubscriptions Adds default subscriptions to websocket to be handled by ManageSubscriptions()
func (z *ZB) GenerateDefaultSubscriptions() {
var subscriptions []wshandler.WebsocketChannelSubscription
// Tickerdata is its own channel
subscriptions = append(subscriptions, wshandler.WebsocketChannelSubscription{
Channel: "markets",
})
channels := []string{"%s_ticker", "%s_depth", "%s_trades"}
enabledCurrencies := z.GetEnabledPairs(asset.Spot)
for i := range channels {
for j := range enabledCurrencies {
enabledCurrencies[j].Delimiter = ""
subscriptions = append(subscriptions, wshandler.WebsocketChannelSubscription{
Channel: fmt.Sprintf(channels[i], enabledCurrencies[j].Lower().String()),
Currency: enabledCurrencies[j].Lower(),
})
}
}
z.Websocket.SubscribeToChannels(subscriptions)
}
// Subscribe sends a websocket message to receive data from the channel
func (z *ZB) Subscribe(channelToSubscribe wshandler.WebsocketChannelSubscription) error {
subscriptionRequest := Subscription{
Event: zWebsocketAddChannel,
Channel: channelToSubscribe.Channel,
}
return z.WebsocketConn.SendMessage(subscriptionRequest)
}
func (z *ZB) wsGenerateSignature(request interface{}) string {
jsonResponse, err := common.JSONEncode(request)
if err != nil {
log.Error(log.ExchangeSys, err)
return ""
}
hmac := crypto.GetHMAC(crypto.HashMD5,
jsonResponse,
[]byte(crypto.Sha1ToHex(z.API.Credentials.Secret)))
return fmt.Sprintf("%x", hmac)
}
func (z *ZB) wsFixInvalidJSON(json []byte) []byte {
invalidZbJSONRegex := `(\"\[|\"\{)(.*)(\]\"|\}\")`
regexChecker := regexp.MustCompile(invalidZbJSONRegex)
matchingResults := regexChecker.Find(json)
if matchingResults == nil {
return json
}
// Remove first quote character
capturedInvalidZBJSON := strings.Replace(string(matchingResults), "\"", "", 1)
// Remove last quote character
fixedJSON := capturedInvalidZBJSON[:len(capturedInvalidZBJSON)-1]
return []byte(strings.Replace(string(json), string(matchingResults), fixedJSON, 1))
}
func (z *ZB) wsAddSubUser(username, password string) (*WsGetSubUserListResponse, error) {
if !z.GetAuthenticatedAPISupport(exchange.WebsocketAuthentication) {
return nil, fmt.Errorf("%v AuthenticatedWebsocketAPISupport not enabled", z.Name)
}
request := WsAddSubUserRequest{
Memo: "memo",
Password: password,
SubUserName: username,
}
request.Channel = "addSubUser"
request.Event = zWebsocketAddChannel
request.Accesskey = z.API.Credentials.Key
request.No = z.WebsocketConn.GenerateMessageID(true)
request.Sign = z.wsGenerateSignature(request)
resp, err := z.WebsocketConn.SendMessageReturnResponse(request.No, request)
if err != nil {
return nil, err
}
var genericResponse Generic
err = common.JSONDecode(resp, &genericResponse)
if err != nil {
return nil, err
}
if genericResponse.Code > 0 && genericResponse.Code != 1000 {
return nil, fmt.Errorf("%v request failed, message: %v, error code: %v", z.Name, genericResponse.Message, wsErrCodes[genericResponse.Code])
}
var response WsGetSubUserListResponse
err = common.JSONDecode(resp, &response)
if err != nil {
return nil, err
}
return &response, nil
}
func (z *ZB) wsGetSubUserList() (*WsGetSubUserListResponse, error) {
if !z.GetAuthenticatedAPISupport(exchange.WebsocketAuthentication) {
return nil, fmt.Errorf("%v AuthenticatedWebsocketAPISupport not enabled", z.Name)
}
request := WsAuthenticatedRequest{}
request.Channel = "getSubUserList"
request.Event = zWebsocketAddChannel
request.Accesskey = z.API.Credentials.Key
request.No = z.WebsocketConn.GenerateMessageID(true)
request.Sign = z.wsGenerateSignature(request)
resp, err := z.WebsocketConn.SendMessageReturnResponse(request.No, request)
if err != nil {
return nil, err
}
var response WsGetSubUserListResponse
err = common.JSONDecode(resp, &response)
if err != nil {
return nil, err
}
if response.Code > 0 && response.Code != 1000 {
return &response, fmt.Errorf("%v request failed, message: %v, error code: %v", z.Name, response.Message, wsErrCodes[response.Code])
}
return &response, nil
}
func (z *ZB) wsDoTransferFunds(pair currency.Code, amount float64, fromUserName, toUserName string) (*WsRequestResponse, error) {
if !z.GetAuthenticatedAPISupport(exchange.WebsocketAuthentication) {
return nil, fmt.Errorf("%v AuthenticatedWebsocketAPISupport not enabled", z.Name)
}
request := WsDoTransferFundsRequest{
Amount: amount,
Currency: pair,
FromUserName: fromUserName,
ToUserName: toUserName,
No: z.WebsocketConn.GenerateMessageID(true),
}
request.Channel = "doTransferFunds"
request.Event = zWebsocketAddChannel
request.Accesskey = z.API.Credentials.Key
request.Sign = z.wsGenerateSignature(request)
resp, err := z.WebsocketConn.SendMessageReturnResponse(request.No, request)
if err != nil {
return nil, err
}
var response WsRequestResponse
err = common.JSONDecode(resp, &response)
if err != nil {
return nil, err
}
if response.Code > 0 && response.Code != 1000 {
return &response, fmt.Errorf("%v request failed, message: %v, error code: %v", z.Name, response.Message, wsErrCodes[response.Code])
}
return &response, nil
}
func (z *ZB) wsCreateSubUserKey(assetPerm, entrustPerm, leverPerm, moneyPerm bool, keyName, toUserID string) (*WsRequestResponse, error) {
if !z.GetAuthenticatedAPISupport(exchange.WebsocketAuthentication) {
return nil, fmt.Errorf("%v AuthenticatedWebsocketAPISupport not enabled", z.Name)
}
request := WsCreateSubUserKeyRequest{
AssetPerm: assetPerm,
EntrustPerm: entrustPerm,
KeyName: keyName,
LeverPerm: leverPerm,
MoneyPerm: moneyPerm,
No: z.WebsocketConn.GenerateMessageID(true),
ToUserID: toUserID,
}
request.Channel = "createSubUserKey"
request.Event = zWebsocketAddChannel
request.Accesskey = z.API.Credentials.Key
request.Sign = z.wsGenerateSignature(request)
resp, err := z.WebsocketConn.SendMessageReturnResponse(request.No, request)
if err != nil {
return nil, err
}
var response WsRequestResponse
err = common.JSONDecode(resp, &response)
if err != nil {
return nil, err
}
if response.Code > 0 && response.Code != 1000 {
return &response, fmt.Errorf("%v request failed, message: %v, error code: %v", z.Name, response.Message, wsErrCodes[response.Code])
}
return &response, nil
}
func (z *ZB) wsSubmitOrder(pair currency.Pair, amount, price float64, tradeType int64) (*WsSubmitOrderResponse, error) {
if !z.GetAuthenticatedAPISupport(exchange.WebsocketAuthentication) {
return nil, fmt.Errorf("%v AuthenticatedWebsocketAPISupport not enabled", z.Name)
}
request := WsSubmitOrderRequest{
Amount: amount,
Price: price,
TradeType: tradeType,
No: z.WebsocketConn.GenerateMessageID(true),
}
request.Channel = fmt.Sprintf("%v_order", pair.String())
request.Event = zWebsocketAddChannel
request.Accesskey = z.API.Credentials.Key
request.Sign = z.wsGenerateSignature(request)
resp, err := z.WebsocketConn.SendMessageReturnResponse(request.No, request)
if err != nil {
return nil, err
}
var response WsSubmitOrderResponse
err = common.JSONDecode(resp, &response)
if err != nil {
return nil, err
}
if response.Code > 0 && response.Code != 1000 {
return &response, fmt.Errorf("%v request failed, message: %v, error code: %v", z.Name, response.Message, wsErrCodes[response.Code])
}
return &response, nil
}
func (z *ZB) wsCancelOrder(pair currency.Pair, orderID int64) (*WsCancelOrderResponse, error) {
if !z.GetAuthenticatedAPISupport(exchange.WebsocketAuthentication) {
return nil, fmt.Errorf("%v AuthenticatedWebsocketAPISupport not enabled", z.Name)
}
request := WsCancelOrderRequest{
ID: orderID,
No: z.WebsocketConn.GenerateMessageID(true),
}
request.Channel = fmt.Sprintf("%v_cancelorder", pair.String())
request.Event = zWebsocketAddChannel
request.Accesskey = z.API.Credentials.Key
request.Sign = z.wsGenerateSignature(request)
resp, err := z.WebsocketConn.SendMessageReturnResponse(request.No, request)
if err != nil {
return nil, err
}
var response WsCancelOrderResponse
err = common.JSONDecode(resp, &response)
if err != nil {
return nil, err
}
if response.Code > 0 && response.Code != 1000 {
return &response, fmt.Errorf("%v request failed, message: %v, error code: %v", z.Name, response.Message, wsErrCodes[response.Code])
}
return &response, nil
}
func (z *ZB) wsGetOrder(pair currency.Pair, orderID int64) (*WsGetOrderResponse, error) {
if !z.GetAuthenticatedAPISupport(exchange.WebsocketAuthentication) {
return nil, fmt.Errorf("%v AuthenticatedWebsocketAPISupport not enabled", z.Name)
}
request := WsGetOrderRequest{
ID: orderID,
No: z.WebsocketConn.GenerateMessageID(true),
}
request.Channel = fmt.Sprintf("%v_getorder", pair.String())
request.Event = zWebsocketAddChannel
request.Accesskey = z.API.Credentials.Key
request.Sign = z.wsGenerateSignature(request)
resp, err := z.WebsocketConn.SendMessageReturnResponse(request.No, request)
if err != nil {
return nil, err
}
var response WsGetOrderResponse
err = common.JSONDecode(resp, &response)
if err != nil {
return nil, err
}
if response.Code > 0 && response.Code != 1000 {
return &response, fmt.Errorf("%v request failed, message: %v, error code: %v", z.Name, response.Message, wsErrCodes[response.Code])
}
return &response, nil
}
func (z *ZB) wsGetOrders(pair currency.Pair, pageIndex, tradeType int64) (*WsGetOrdersResponse, error) {
if !z.GetAuthenticatedAPISupport(exchange.WebsocketAuthentication) {
return nil, fmt.Errorf("%v AuthenticatedWebsocketAPISupport not enabled", z.Name)
}
request := WsGetOrdersRequest{
PageIndex: pageIndex,
TradeType: tradeType,
No: z.WebsocketConn.GenerateMessageID(true),
}
request.Channel = fmt.Sprintf("%v_getorders", pair.String())
request.Event = zWebsocketAddChannel
request.Accesskey = z.API.Credentials.Key
request.Sign = z.wsGenerateSignature(request)
resp, err := z.WebsocketConn.SendMessageReturnResponse(request.No, request)
if err != nil {
return nil, err
}
var response WsGetOrdersResponse
err = common.JSONDecode(resp, &response)
if err != nil {
return nil, err
}
if response.Code > 0 && response.Code != 1000 {
return &response, fmt.Errorf("%v request failed, message: %v, error code: %v", z.Name, response.Message, wsErrCodes[response.Code])
}
return &response, nil
}
func (z *ZB) wsGetOrdersIgnoreTradeType(pair currency.Pair, pageIndex, pageSize int64) (*WsGetOrdersIgnoreTradeTypeResponse, error) {
if !z.GetAuthenticatedAPISupport(exchange.WebsocketAuthentication) {
return nil, fmt.Errorf("%v AuthenticatedWebsocketAPISupport not enabled", z.Name)
}
request := WsGetOrdersIgnoreTradeTypeRequest{
PageIndex: pageIndex,
PageSize: pageSize,
No: z.WebsocketConn.GenerateMessageID(true),
}
request.Channel = fmt.Sprintf("%v_getordersignoretradetype", pair.String())
request.Event = zWebsocketAddChannel
request.Accesskey = z.API.Credentials.Key
request.Sign = z.wsGenerateSignature(request)
resp, err := z.WebsocketConn.SendMessageReturnResponse(request.No, request)
if err != nil {
return nil, err
}
var response WsGetOrdersIgnoreTradeTypeResponse
err = common.JSONDecode(resp, &response)
if err != nil {
return nil, err
}
if response.Code > 0 && response.Code != 1000 {
return &response, fmt.Errorf("%v request failed, message: %v, error code: %v", z.Name, response.Message, wsErrCodes[response.Code])
}
return &response, nil
}
func (z *ZB) wsGetAccountInfoRequest() (*WsGetAccountInfoResponse, error) {
if !z.GetAuthenticatedAPISupport(exchange.WebsocketAuthentication) {
return nil, fmt.Errorf("%v AuthenticatedWebsocketAPISupport not enabled", z.Name)
}
request := WsAuthenticatedRequest{
Channel: "getaccountinfo",
Event: zWebsocketAddChannel,
Accesskey: z.API.Credentials.Key,
No: z.WebsocketConn.GenerateMessageID(true),
}
request.Sign = z.wsGenerateSignature(request)
resp, err := z.WebsocketConn.SendMessageReturnResponse(request.No, request)
if err != nil {
return nil, err
}
var response WsGetAccountInfoResponse
err = common.JSONDecode(resp, &response)
if err != nil {
return nil, err
}
if response.Code > 0 && response.Code != 1000 {
return &response, fmt.Errorf("%v request failed, message: %v, error code: %v", z.Name, response.Message, wsErrCodes[response.Code])
}
return &response, nil
}