mirror of
https://github.com/d0zingcat/gocryptotrader.git
synced 2026-05-23 23:16:49 +00:00
* Improves subscribing by not allowing duplicates. Adds bitmex auth support * Adds coinbase pro support. Partial BTCC support. Adds WebsocketAuthenticatedEndpointsSupported websocket feature. Adds GateIO support * Adds Coinut support * Moves Coinut WS types to file. Implements Gemini's secure WS endpoint * Adds HitBTC ws authenticated support. Fixes var names * Adds huobi and hadax authenticated websocket support * Adds auth to okgroup (okex, okcoin). Fixes some linting * Adds Poloniex support * Adds ZB support * Adds proper bitmex support * Improves bitfinex support, improves websocket functionality definitions * Fixes coinbasepro auth * Tests all endpoints * go formatting, importing, linting run * Adds wrapper supports * General clean up. Data race destruction * Improves testing on all exchanges except ZB * Fixes ZB hashing, parsing and tests * minor nits before someone else sees them <_< * Fixes some nits pertaining to variable usage, comments, typos and rate limiting * Addresses nits regarding types and test responses where applicable * fmt import * Fixes linting issues * No longer returns an error on failure to authenticate, just logs. Adds new AuthenticatedWebsocketAPISupport config value to allow a user to seperate auth from REST and WS. Prevents WS auth if AuthenticatedWebsocketAPISupport is false, adds additional login check 'CanUseAuthenticatedEndpoints' for when login only occurs once (not per request). Removes unnecessary time.Sleeps from code. Moves WS auth error logic to auth function so that wrappers can get involved in all the auth fun. New-fandangled shared test package, used exclusively in testing, will be the store of all the constant boilerplate things like timeout values. Moves WS test setup function to only run once when there are multiple WS endpoint tests. Cleans up some struct types * Increases test coverage with tests for config.areAuthenticatedCredentialsValid config.CheckExchangeConfigValues, exchange.SetAPIKeys, exchange.GetAuthenticatedAPISupport, exchange_websocket.CanUseAuthenticatedEndpoitns and exchange_websocket.SetCanUseAuthenticatedEndpoints. Adds b.Websocket.SetCanUseAuthenticatedEndpoints(false) when bitfinex fails to authenticate Fixes a typo. gofmt and goimport * Trim Test Typos * Reformats various websocket types. Adds more specific error messaging to config.areAuthenticatedCredentialsValid
748 lines
28 KiB
Go
748 lines
28 KiB
Go
package okgroup
|
|
|
|
import (
|
|
"bytes"
|
|
"compress/flate"
|
|
"errors"
|
|
"fmt"
|
|
"hash/crc32"
|
|
"io/ioutil"
|
|
"net/http"
|
|
"net/url"
|
|
"sort"
|
|
"strconv"
|
|
"strings"
|
|
"sync"
|
|
"time"
|
|
|
|
"github.com/thrasher-/gocryptotrader/currency"
|
|
|
|
"github.com/gorilla/websocket"
|
|
"github.com/thrasher-/gocryptotrader/common"
|
|
exchange "github.com/thrasher-/gocryptotrader/exchanges"
|
|
"github.com/thrasher-/gocryptotrader/exchanges/orderbook"
|
|
log "github.com/thrasher-/gocryptotrader/logger"
|
|
)
|
|
|
|
// List of all websocket channels to subscribe to
|
|
const (
|
|
// If a checksum fails, then resubscribing to the channel fails, fatal after these attempts
|
|
okGroupWsResubscribeFailureLimit = 3
|
|
okGroupWsResubscribeDelayInSeconds = 3
|
|
// Orderbook events
|
|
okGroupWsOrderbookUpdate = "update"
|
|
okGroupWsOrderbookPartial = "partial"
|
|
// API subsections
|
|
okGroupWsSwapSubsection = "swap/"
|
|
okGroupWsIndexSubsection = "index/"
|
|
okGroupWsFuturesSubsection = "futures/"
|
|
okGroupWsSpotSubsection = "spot/"
|
|
// Shared API endpoints
|
|
okGroupWsCandle = "candle"
|
|
okGroupWsCandle60s = okGroupWsCandle + "60s"
|
|
okGroupWsCandle180s = okGroupWsCandle + "180s"
|
|
okGroupWsCandle300s = okGroupWsCandle + "300s"
|
|
okGroupWsCandle900s = okGroupWsCandle + "900s"
|
|
okGroupWsCandle1800s = okGroupWsCandle + "1800s"
|
|
okGroupWsCandle3600s = okGroupWsCandle + "3600s"
|
|
okGroupWsCandle7200s = okGroupWsCandle + "7200s"
|
|
okGroupWsCandle14400s = okGroupWsCandle + "14400s"
|
|
okGroupWsCandle21600s = okGroupWsCandle + "21600"
|
|
okGroupWsCandle43200s = okGroupWsCandle + "43200s"
|
|
okGroupWsCandle86400s = okGroupWsCandle + "86400s"
|
|
okGroupWsCandle604900s = okGroupWsCandle + "604800s"
|
|
okGroupWsTicker = "ticker"
|
|
okGroupWsTrade = "trade"
|
|
okGroupWsDepth = "depth"
|
|
okGroupWsDepth5 = "depth5"
|
|
okGroupWsAccount = "account"
|
|
okGroupWsMarginAccount = "margin_account"
|
|
okGroupWsOrder = "order"
|
|
okGroupWsFundingRate = "funding_rate"
|
|
okGroupWsPriceRange = "price_range"
|
|
okGroupWsMarkPrice = "mark_price"
|
|
okGroupWsPosition = "position"
|
|
okGroupWsEstimatedPrice = "estimated_price"
|
|
// Spot endpoints
|
|
okGroupWsSpotTicker = okGroupWsSpotSubsection + okGroupWsTicker
|
|
okGroupWsSpotCandle60s = okGroupWsSpotSubsection + okGroupWsCandle60s
|
|
okGroupWsSpotCandle180s = okGroupWsSpotSubsection + okGroupWsCandle180s
|
|
okGroupWsSpotCandle300s = okGroupWsSpotSubsection + okGroupWsCandle300s
|
|
okGroupWsSpotCandle900s = okGroupWsSpotSubsection + okGroupWsCandle900s
|
|
okGroupWsSpotCandle1800s = okGroupWsSpotSubsection + okGroupWsCandle1800s
|
|
okGroupWsSpotCandle3600s = okGroupWsSpotSubsection + okGroupWsCandle3600s
|
|
okGroupWsSpotCandle7200s = okGroupWsSpotSubsection + okGroupWsCandle7200s
|
|
okGroupWsSpotCandle14400s = okGroupWsSpotSubsection + okGroupWsCandle14400s
|
|
okGroupWsSpotCandle21600s = okGroupWsSpotSubsection + okGroupWsCandle21600s
|
|
okGroupWsSpotCandle43200s = okGroupWsSpotSubsection + okGroupWsCandle43200s
|
|
okGroupWsSpotCandle86400s = okGroupWsSpotSubsection + okGroupWsCandle86400s
|
|
okGroupWsSpotCandle604900s = okGroupWsSpotSubsection + okGroupWsCandle604900s
|
|
okGroupWsSpotTrade = okGroupWsSpotSubsection + okGroupWsTrade
|
|
okGroupWsSpotDepth = okGroupWsSpotSubsection + okGroupWsDepth
|
|
okGroupWsSpotDepth5 = okGroupWsSpotSubsection + okGroupWsDepth5
|
|
okGroupWsSpotAccount = okGroupWsSpotSubsection + okGroupWsAccount
|
|
okGroupWsSpotMarginAccount = okGroupWsSpotSubsection + okGroupWsMarginAccount
|
|
okGroupWsSpotOrder = okGroupWsSpotSubsection + okGroupWsOrder
|
|
// Swap endpoints
|
|
okGroupWsSwapTicker = okGroupWsSwapSubsection + okGroupWsTicker
|
|
okGroupWsSwapCandle60s = okGroupWsSwapSubsection + okGroupWsCandle60s
|
|
okGroupWsSwapCandle180s = okGroupWsSwapSubsection + okGroupWsCandle180s
|
|
okGroupWsSwapCandle300s = okGroupWsSwapSubsection + okGroupWsCandle300s
|
|
okGroupWsSwapCandle900s = okGroupWsSwapSubsection + okGroupWsCandle900s
|
|
okGroupWsSwapCandle1800s = okGroupWsSwapSubsection + okGroupWsCandle1800s
|
|
okGroupWsSwapCandle3600s = okGroupWsSwapSubsection + okGroupWsCandle3600s
|
|
okGroupWsSwapCandle7200s = okGroupWsSwapSubsection + okGroupWsCandle7200s
|
|
okGroupWsSwapCandle14400s = okGroupWsSwapSubsection + okGroupWsCandle14400s
|
|
okGroupWsSwapCandle21600s = okGroupWsSwapSubsection + okGroupWsCandle21600s
|
|
okGroupWsSwapCandle43200s = okGroupWsSwapSubsection + okGroupWsCandle43200s
|
|
okGroupWsSwapCandle86400s = okGroupWsSwapSubsection + okGroupWsCandle86400s
|
|
okGroupWsSwapCandle604900s = okGroupWsSwapSubsection + okGroupWsCandle604900s
|
|
okGroupWsSwapTrade = okGroupWsSwapSubsection + okGroupWsTrade
|
|
okGroupWsSwapDepth = okGroupWsSwapSubsection + okGroupWsDepth
|
|
okGroupWsSwapDepth5 = okGroupWsSwapSubsection + okGroupWsDepth5
|
|
okGroupWsSwapFundingRate = okGroupWsSwapSubsection + okGroupWsFundingRate
|
|
okGroupWsSwapPriceRange = okGroupWsSwapSubsection + okGroupWsPriceRange
|
|
okGroupWsSwapMarkPrice = okGroupWsSwapSubsection + okGroupWsMarkPrice
|
|
okGroupWsSwapPosition = okGroupWsSwapSubsection + okGroupWsPosition
|
|
okGroupWsSwapAccount = okGroupWsSwapSubsection + okGroupWsAccount
|
|
okGroupWsSwapOrder = okGroupWsSwapSubsection + okGroupWsOrder
|
|
// Index endpoints
|
|
okGroupWsIndexTicker = okGroupWsIndexSubsection + okGroupWsTicker
|
|
okGroupWsIndexCandle60s = okGroupWsIndexSubsection + okGroupWsCandle60s
|
|
okGroupWsIndexCandle180s = okGroupWsIndexSubsection + okGroupWsCandle180s
|
|
okGroupWsIndexCandle300s = okGroupWsIndexSubsection + okGroupWsCandle300s
|
|
okGroupWsIndexCandle900s = okGroupWsIndexSubsection + okGroupWsCandle900s
|
|
okGroupWsIndexCandle1800s = okGroupWsIndexSubsection + okGroupWsCandle1800s
|
|
okGroupWsIndexCandle3600s = okGroupWsIndexSubsection + okGroupWsCandle3600s
|
|
okGroupWsIndexCandle7200s = okGroupWsIndexSubsection + okGroupWsCandle7200s
|
|
okGroupWsIndexCandle14400s = okGroupWsIndexSubsection + okGroupWsCandle14400s
|
|
okGroupWsIndexCandle21600s = okGroupWsIndexSubsection + okGroupWsCandle21600s
|
|
okGroupWsIndexCandle43200s = okGroupWsIndexSubsection + okGroupWsCandle43200s
|
|
okGroupWsIndexCandle86400s = okGroupWsIndexSubsection + okGroupWsCandle86400s
|
|
okGroupWsIndexCandle604900s = okGroupWsIndexSubsection + okGroupWsCandle604900s
|
|
// Futures endpoints
|
|
okGroupWsFuturesTicker = okGroupWsFuturesSubsection + okGroupWsTicker
|
|
okGroupWsFuturesCandle60s = okGroupWsFuturesSubsection + okGroupWsCandle60s
|
|
okGroupWsFuturesCandle180s = okGroupWsFuturesSubsection + okGroupWsCandle180s
|
|
okGroupWsFuturesCandle300s = okGroupWsFuturesSubsection + okGroupWsCandle300s
|
|
okGroupWsFuturesCandle900s = okGroupWsFuturesSubsection + okGroupWsCandle900s
|
|
okGroupWsFuturesCandle1800s = okGroupWsFuturesSubsection + okGroupWsCandle1800s
|
|
okGroupWsFuturesCandle3600s = okGroupWsFuturesSubsection + okGroupWsCandle3600s
|
|
okGroupWsFuturesCandle7200s = okGroupWsFuturesSubsection + okGroupWsCandle7200s
|
|
okGroupWsFuturesCandle14400s = okGroupWsFuturesSubsection + okGroupWsCandle14400s
|
|
okGroupWsFuturesCandle21600s = okGroupWsFuturesSubsection + okGroupWsCandle21600s
|
|
okGroupWsFuturesCandle43200s = okGroupWsFuturesSubsection + okGroupWsCandle43200s
|
|
okGroupWsFuturesCandle86400s = okGroupWsFuturesSubsection + okGroupWsCandle86400s
|
|
okGroupWsFuturesCandle604900s = okGroupWsFuturesSubsection + okGroupWsCandle604900s
|
|
okGroupWsFuturesTrade = okGroupWsFuturesSubsection + okGroupWsTrade
|
|
okGroupWsFuturesEstimatedPrice = okGroupWsFuturesSubsection + okGroupWsTrade
|
|
okGroupWsFuturesPriceRange = okGroupWsFuturesSubsection + okGroupWsPriceRange
|
|
okGroupWsFuturesDepth = okGroupWsFuturesSubsection + okGroupWsDepth
|
|
okGroupWsFuturesDepth5 = okGroupWsFuturesSubsection + okGroupWsDepth5
|
|
okGroupWsFuturesMarkPrice = okGroupWsFuturesSubsection + okGroupWsMarkPrice
|
|
okGroupWsFuturesAccount = okGroupWsFuturesSubsection + okGroupWsAccount
|
|
okGroupWsFuturesPosition = okGroupWsFuturesSubsection + okGroupWsPosition
|
|
okGroupWsFuturesOrder = okGroupWsFuturesSubsection + okGroupWsOrder
|
|
|
|
okGroupWsRateLimit = 30 * time.Millisecond
|
|
)
|
|
|
|
// orderbookMutex Ensures if two entries arrive at once, only one can be processed at a time
|
|
var orderbookMutex sync.Mutex
|
|
var defaultSubscribedChannels = []string{okGroupWsSpotDepth, okGroupWsSpotCandle300s, okGroupWsSpotTicker, okGroupWsSpotTrade}
|
|
|
|
// writeToWebsocket sends a message to the websocket endpoint
|
|
func (o *OKGroup) writeToWebsocket(message string) error {
|
|
o.wsRequestMtx.Lock()
|
|
defer o.wsRequestMtx.Unlock()
|
|
if o.Verbose {
|
|
log.Debugf("%v sending message to WS: %v", o.Name, message)
|
|
}
|
|
// Really basic WS rate limit
|
|
time.Sleep(okGroupWsRateLimit)
|
|
return o.WebsocketConn.WriteMessage(websocket.TextMessage, []byte(message))
|
|
}
|
|
|
|
// WsConnect initiates a websocket connection
|
|
func (o *OKGroup) WsConnect() error {
|
|
if !o.Websocket.IsEnabled() || !o.IsEnabled() {
|
|
return errors.New(exchange.WebsocketNotEnabled)
|
|
}
|
|
|
|
var dialer websocket.Dialer
|
|
if o.Websocket.GetProxyAddress() != "" {
|
|
proxy, err := url.Parse(o.Websocket.GetProxyAddress())
|
|
if err != nil {
|
|
return err
|
|
}
|
|
dialer.Proxy = http.ProxyURL(proxy)
|
|
}
|
|
|
|
var err error
|
|
if o.Verbose {
|
|
log.Debugf("Attempting to connect to %v", o.Websocket.GetWebsocketURL())
|
|
}
|
|
o.WebsocketConn, _, err = dialer.Dial(o.Websocket.GetWebsocketURL(),
|
|
http.Header{})
|
|
if err != nil {
|
|
return fmt.Errorf("%s Unable to connect to Websocket. Error: %s",
|
|
o.Name,
|
|
err)
|
|
}
|
|
if o.Verbose {
|
|
log.Debugf("Successful connection to %v",
|
|
o.Websocket.GetWebsocketURL())
|
|
}
|
|
wg := sync.WaitGroup{}
|
|
wg.Add(2)
|
|
go o.WsHandleData(&wg)
|
|
go o.wsPingHandler(&wg)
|
|
if o.GetAuthenticatedAPISupport(exchange.WebsocketAuthentication) {
|
|
err = o.WsLogin()
|
|
if err != nil {
|
|
log.Errorf("%v - authentication failed: %v", o.Name, err)
|
|
}
|
|
}
|
|
|
|
o.GenerateDefaultSubscriptions()
|
|
// Ensures that we start the routines and we dont race when shutdown occurs
|
|
wg.Wait()
|
|
return nil
|
|
}
|
|
|
|
// WsReadData reads data from the websocket connection
|
|
func (o *OKGroup) WsReadData() (exchange.WebsocketResponse, error) {
|
|
mType, resp, err := o.WebsocketConn.ReadMessage()
|
|
if err != nil {
|
|
return exchange.WebsocketResponse{}, err
|
|
}
|
|
|
|
o.Websocket.TrafficAlert <- struct{}{}
|
|
var standardMessage []byte
|
|
switch mType {
|
|
case websocket.TextMessage:
|
|
standardMessage = resp
|
|
|
|
case websocket.BinaryMessage:
|
|
reader := flate.NewReader(bytes.NewReader(resp))
|
|
standardMessage, err = ioutil.ReadAll(reader)
|
|
reader.Close()
|
|
if err != nil {
|
|
return exchange.WebsocketResponse{}, err
|
|
}
|
|
}
|
|
if o.Verbose {
|
|
log.Debugf("%v Websocket message received: %v", o.Name, string(standardMessage))
|
|
}
|
|
|
|
return exchange.WebsocketResponse{Raw: standardMessage}, nil
|
|
}
|
|
|
|
// wsPingHandler sends a message "ping" every 27 to maintain the connection to the websocket
|
|
func (o *OKGroup) wsPingHandler(wg *sync.WaitGroup) {
|
|
o.Websocket.Wg.Add(1)
|
|
defer o.Websocket.Wg.Done()
|
|
|
|
ticker := time.NewTicker(time.Second * 27)
|
|
defer ticker.Stop()
|
|
|
|
wg.Done()
|
|
|
|
for {
|
|
select {
|
|
case <-o.Websocket.ShutdownC:
|
|
return
|
|
|
|
case <-ticker.C:
|
|
err := o.writeToWebsocket("ping")
|
|
if o.Verbose {
|
|
log.Debugf("%v sending ping", o.GetName())
|
|
}
|
|
if err != nil {
|
|
o.Websocket.DataHandler <- err
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// WsHandleData handles the read data from the websocket connection
|
|
func (o *OKGroup) WsHandleData(wg *sync.WaitGroup) {
|
|
o.Websocket.Wg.Add(1)
|
|
defer func() {
|
|
o.Websocket.Wg.Done()
|
|
}()
|
|
|
|
wg.Done()
|
|
|
|
for {
|
|
select {
|
|
case <-o.Websocket.ShutdownC:
|
|
return
|
|
|
|
default:
|
|
resp, err := o.WsReadData()
|
|
if err != nil {
|
|
time.Sleep(time.Second)
|
|
o.Websocket.DataHandler <- err
|
|
}
|
|
var dataResponse WebsocketDataResponse
|
|
err = common.JSONDecode(resp.Raw, &dataResponse)
|
|
if err == nil && dataResponse.Table != "" {
|
|
if len(dataResponse.Data) > 0 {
|
|
o.WsHandleDataResponse(&dataResponse)
|
|
}
|
|
continue
|
|
}
|
|
var errorResponse WebsocketErrorResponse
|
|
err = common.JSONDecode(resp.Raw, &errorResponse)
|
|
if err == nil && errorResponse.ErrorCode > 0 {
|
|
if o.Verbose {
|
|
log.Debugf("WS Error Event: %v Message: %v", errorResponse.Event, errorResponse.Message)
|
|
}
|
|
o.WsHandleErrorResponse(errorResponse)
|
|
continue
|
|
}
|
|
var eventResponse WebsocketEventResponse
|
|
err = common.JSONDecode(resp.Raw, &eventResponse)
|
|
if err == nil && eventResponse.Event != "" {
|
|
if eventResponse.Event == "login" {
|
|
o.Websocket.SetCanUseAuthenticatedEndpoints(eventResponse.Success)
|
|
}
|
|
if o.Verbose {
|
|
log.Debugf("WS Event: %v on Channel: %v", eventResponse.Event, eventResponse.Channel)
|
|
}
|
|
o.Websocket.DataHandler <- eventResponse
|
|
continue
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// WsLogin sends a login request to websocket to enable access to authenticated endpoints
|
|
func (o *OKGroup) WsLogin() error {
|
|
o.Websocket.SetCanUseAuthenticatedEndpoints(true)
|
|
utcTime := time.Now().UTC()
|
|
unixTime := utcTime.Unix()
|
|
signPath := "/users/self/verify"
|
|
hmac := common.GetHMAC(common.HashSHA256, []byte(fmt.Sprintf("%v", unixTime)+http.MethodGet+signPath), []byte(o.APISecret))
|
|
base64 := common.Base64Encode(hmac)
|
|
resp := WebsocketEventRequest{
|
|
Operation: "login",
|
|
Arguments: []string{o.APIKey, o.ClientID, fmt.Sprintf("%v", unixTime), base64},
|
|
}
|
|
json, err := common.JSONEncode(resp)
|
|
if err != nil {
|
|
o.Websocket.SetCanUseAuthenticatedEndpoints(false)
|
|
return err
|
|
}
|
|
err = o.writeToWebsocket(string(json))
|
|
if err != nil {
|
|
o.Websocket.SetCanUseAuthenticatedEndpoints(false)
|
|
return err
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// WsHandleErrorResponse sends an error message to ws handler
|
|
func (o *OKGroup) WsHandleErrorResponse(event WebsocketErrorResponse) {
|
|
errorMessage := fmt.Sprintf("%v error - %v message: %s ",
|
|
o.GetName(), event.ErrorCode, event.Message)
|
|
if o.Verbose {
|
|
log.Error(errorMessage)
|
|
}
|
|
o.Websocket.DataHandler <- fmt.Errorf(errorMessage)
|
|
}
|
|
|
|
// GetWsChannelWithoutOrderType takes WebsocketDataResponse.Table and returns
|
|
// The base channel name eg receive "spot/depth5:BTC-USDT" return "depth5"
|
|
func (o *OKGroup) GetWsChannelWithoutOrderType(table string) string {
|
|
index := strings.Index(table, "/")
|
|
if index == -1 {
|
|
return table
|
|
}
|
|
channel := table[index+1:]
|
|
index = strings.Index(channel, ":")
|
|
// Some events do not contain a currency
|
|
if index == -1 {
|
|
return channel
|
|
}
|
|
|
|
return channel[:index]
|
|
}
|
|
|
|
// GetAssetTypeFromTableName gets the asset type from the table name
|
|
// eg "spot/ticker:BTCUSD" results in "SPOT"
|
|
func (o *OKGroup) GetAssetTypeFromTableName(table string) string {
|
|
assetIndex := strings.Index(table, "/")
|
|
return strings.ToUpper(table[:assetIndex])
|
|
}
|
|
|
|
// WsHandleDataResponse classifies the WS response and sends to appropriate handler
|
|
func (o *OKGroup) WsHandleDataResponse(response *WebsocketDataResponse) {
|
|
switch o.GetWsChannelWithoutOrderType(response.Table) {
|
|
|
|
case okGroupWsCandle60s, okGroupWsCandle180s, okGroupWsCandle300s, okGroupWsCandle900s,
|
|
okGroupWsCandle1800s, okGroupWsCandle3600s, okGroupWsCandle7200s, okGroupWsCandle14400s,
|
|
okGroupWsCandle21600s, okGroupWsCandle43200s, okGroupWsCandle86400s, okGroupWsCandle604900s:
|
|
if o.Verbose {
|
|
log.Debugf("%v Websocket candle data received", o.GetName())
|
|
}
|
|
o.wsProcessCandles(response)
|
|
case okGroupWsDepth, okGroupWsDepth5:
|
|
if o.Verbose {
|
|
log.Debugf("%v Websocket orderbook data received", o.GetName())
|
|
}
|
|
// Locking, orderbooks cannot be processed out of order
|
|
orderbookMutex.Lock()
|
|
err := o.WsProcessOrderBook(response)
|
|
if err != nil {
|
|
pair := currency.NewPairDelimiter(response.Data[0].InstrumentID, "-")
|
|
channelToResubscribe := exchange.WebsocketChannelSubscription{
|
|
Channel: response.Table,
|
|
Currency: pair,
|
|
}
|
|
o.Websocket.ResubscribeToChannel(channelToResubscribe)
|
|
}
|
|
orderbookMutex.Unlock()
|
|
case okGroupWsTicker:
|
|
if o.Verbose {
|
|
log.Debugf("%v Websocket ticker data received", o.GetName())
|
|
}
|
|
o.wsProcessTickers(response)
|
|
case okGroupWsTrade:
|
|
if o.Verbose {
|
|
log.Debugf("%v Websocket trade data received", o.GetName())
|
|
}
|
|
o.wsProcessTrades(response)
|
|
default:
|
|
logDataResponse(response)
|
|
}
|
|
}
|
|
|
|
// logDataResponse will log the details of any websocket data event
|
|
// where there is no websocket datahandler for it
|
|
func logDataResponse(response *WebsocketDataResponse) {
|
|
for i := range response.Data {
|
|
log.Errorf("Unhandled channel: '%v'. Instrument '%v' Timestamp '%v', Data '%v",
|
|
response.Table,
|
|
response.Data[i].InstrumentID,
|
|
response.Data[i].Timestamp,
|
|
response.Data[i])
|
|
}
|
|
}
|
|
|
|
// wsProcessTickers converts ticker data and sends it to the datahandler
|
|
func (o *OKGroup) wsProcessTickers(response *WebsocketDataResponse) {
|
|
for i := range response.Data {
|
|
instrument := currency.NewPairDelimiter(response.Data[i].InstrumentID, "-")
|
|
o.Websocket.DataHandler <- exchange.TickerData{
|
|
Timestamp: response.Data[i].Timestamp,
|
|
Exchange: o.GetName(),
|
|
AssetType: o.GetAssetTypeFromTableName(response.Table),
|
|
HighPrice: response.Data[i].High24H,
|
|
LowPrice: response.Data[i].Low24H,
|
|
ClosePrice: response.Data[i].Last,
|
|
Pair: instrument,
|
|
}
|
|
}
|
|
}
|
|
|
|
// wsProcessTrades converts trade data and sends it to the datahandler
|
|
func (o *OKGroup) wsProcessTrades(response *WebsocketDataResponse) {
|
|
for i := range response.Data {
|
|
instrument := currency.NewPairDelimiter(response.Data[i].InstrumentID, "-")
|
|
o.Websocket.DataHandler <- exchange.TradeData{
|
|
Amount: response.Data[i].Qty,
|
|
AssetType: o.GetAssetTypeFromTableName(response.Table),
|
|
CurrencyPair: instrument,
|
|
EventTime: time.Now().Unix(),
|
|
Exchange: o.GetName(),
|
|
Price: response.Data[i].WebsocketTradeResponse.Price,
|
|
Side: response.Data[i].Side,
|
|
Timestamp: response.Data[i].Timestamp,
|
|
}
|
|
}
|
|
}
|
|
|
|
// wsProcessCandles converts candle data and sends it to the data handler
|
|
func (o *OKGroup) wsProcessCandles(response *WebsocketDataResponse) {
|
|
for i := range response.Data {
|
|
instrument := currency.NewPairDelimiter(response.Data[i].InstrumentID, "-")
|
|
timeData, err := time.Parse(time.RFC3339Nano, response.Data[i].WebsocketCandleResponse.Candle[0])
|
|
if err != nil {
|
|
log.Warnf("%v Time data could not be parsed: %v", o.GetName(), response.Data[i].Candle[0])
|
|
}
|
|
|
|
candleIndex := strings.LastIndex(response.Table, okGroupWsCandle)
|
|
secondIndex := strings.LastIndex(response.Table, "0s")
|
|
candleInterval := ""
|
|
if candleIndex > 0 || secondIndex > 0 {
|
|
candleInterval = response.Table[candleIndex+len(okGroupWsCandle) : secondIndex]
|
|
}
|
|
|
|
klineData := exchange.KlineData{
|
|
AssetType: o.GetAssetTypeFromTableName(response.Table),
|
|
Pair: instrument,
|
|
Exchange: o.GetName(),
|
|
Timestamp: timeData,
|
|
Interval: candleInterval,
|
|
}
|
|
klineData.OpenPrice, _ = strconv.ParseFloat(response.Data[i].Candle[1], 64)
|
|
klineData.HighPrice, _ = strconv.ParseFloat(response.Data[i].Candle[2], 64)
|
|
klineData.LowPrice, _ = strconv.ParseFloat(response.Data[i].Candle[3], 64)
|
|
klineData.ClosePrice, _ = strconv.ParseFloat(response.Data[i].Candle[4], 64)
|
|
klineData.Volume, _ = strconv.ParseFloat(response.Data[i].Candle[5], 64)
|
|
|
|
o.Websocket.DataHandler <- klineData
|
|
}
|
|
}
|
|
|
|
// WsProcessOrderBook Validates the checksum and updates internal orderbook values
|
|
func (o *OKGroup) WsProcessOrderBook(response *WebsocketDataResponse) (err error) {
|
|
for i := range response.Data {
|
|
instrument := currency.NewPairDelimiter(response.Data[i].InstrumentID, "-")
|
|
if response.Action == okGroupWsOrderbookPartial {
|
|
err = o.WsProcessPartialOrderBook(&response.Data[i], instrument, response.Table)
|
|
} else if response.Action == okGroupWsOrderbookUpdate {
|
|
err = o.WsProcessUpdateOrderbook(&response.Data[i], instrument, response.Table)
|
|
}
|
|
}
|
|
return
|
|
}
|
|
|
|
// AppendWsOrderbookItems adds websocket orderbook data bid/asks into an orderbook item array
|
|
func (o *OKGroup) AppendWsOrderbookItems(entries [][]interface{}) (orderbookItems []orderbook.Item) {
|
|
for j := range entries {
|
|
amount, _ := strconv.ParseFloat(entries[j][1].(string), 64)
|
|
price, _ := strconv.ParseFloat(entries[j][0].(string), 64)
|
|
orderbookItems = append(orderbookItems, orderbook.Item{
|
|
Amount: amount,
|
|
Price: price,
|
|
})
|
|
}
|
|
return
|
|
}
|
|
|
|
// WsProcessPartialOrderBook takes websocket orderbook data and creates an orderbook
|
|
// Calculates checksum to ensure it is valid
|
|
func (o *OKGroup) WsProcessPartialOrderBook(wsEventData *WebsocketDataWrapper, instrument currency.Pair, tableName string) error {
|
|
signedChecksum := o.CalculatePartialOrderbookChecksum(wsEventData)
|
|
if signedChecksum != wsEventData.Checksum {
|
|
return fmt.Errorf("channel: %v. Orderbook partial for %v checksum invalid", tableName, instrument)
|
|
}
|
|
if o.Verbose {
|
|
log.Debug("Passed checksum!")
|
|
}
|
|
asks := o.AppendWsOrderbookItems(wsEventData.Asks)
|
|
bids := o.AppendWsOrderbookItems(wsEventData.Bids)
|
|
newOrderBook := orderbook.Base{
|
|
Asks: asks,
|
|
Bids: bids,
|
|
AssetType: o.GetAssetTypeFromTableName(tableName),
|
|
LastUpdated: wsEventData.Timestamp,
|
|
Pair: instrument,
|
|
ExchangeName: o.GetName(),
|
|
}
|
|
|
|
err := o.Websocket.Orderbook.LoadSnapshot(&newOrderBook, o.GetName(), true)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
o.Websocket.DataHandler <- exchange.WebsocketOrderbookUpdate{
|
|
Exchange: o.GetName(),
|
|
Asset: o.GetAssetTypeFromTableName(tableName),
|
|
Pair: instrument,
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// WsProcessUpdateOrderbook updates an existing orderbook using websocket data
|
|
// After merging WS data, it will sort, validate and finally update the existing orderbook
|
|
func (o *OKGroup) WsProcessUpdateOrderbook(wsEventData *WebsocketDataWrapper, instrument currency.Pair, tableName string) error {
|
|
internalOrderbook, err := o.GetOrderbookEx(instrument, o.GetAssetTypeFromTableName(tableName))
|
|
if err != nil {
|
|
return errors.New("orderbook nil, could not load existing orderbook")
|
|
}
|
|
if internalOrderbook.LastUpdated.After(wsEventData.Timestamp) {
|
|
if o.Verbose {
|
|
log.Errorf("Orderbook update out of order. Existing: %v, Attempted: %v", internalOrderbook.LastUpdated.Unix(), wsEventData.Timestamp.Unix())
|
|
}
|
|
return errors.New("updated orderbook is older than existing")
|
|
}
|
|
internalOrderbook.Asks = o.WsUpdateOrderbookEntry(wsEventData.Asks, internalOrderbook.Asks)
|
|
internalOrderbook.Bids = o.WsUpdateOrderbookEntry(wsEventData.Bids, internalOrderbook.Bids)
|
|
sort.Slice(internalOrderbook.Asks, func(i, j int) bool {
|
|
return internalOrderbook.Asks[i].Price < internalOrderbook.Asks[j].Price
|
|
})
|
|
sort.Slice(internalOrderbook.Bids, func(i, j int) bool {
|
|
return internalOrderbook.Bids[i].Price > internalOrderbook.Bids[j].Price
|
|
})
|
|
checksum := o.CalculateUpdateOrderbookChecksum(&internalOrderbook)
|
|
if checksum == wsEventData.Checksum {
|
|
if o.Verbose {
|
|
log.Debug("Orderbook valid")
|
|
}
|
|
internalOrderbook.LastUpdated = wsEventData.Timestamp
|
|
if o.Verbose {
|
|
log.Debug("Internalising orderbook")
|
|
}
|
|
|
|
err := o.Websocket.Orderbook.LoadSnapshot(&internalOrderbook, o.GetName(), true)
|
|
if err != nil {
|
|
log.Error(err)
|
|
}
|
|
o.Websocket.DataHandler <- exchange.WebsocketOrderbookUpdate{
|
|
Exchange: o.GetName(),
|
|
Asset: o.GetAssetTypeFromTableName(tableName),
|
|
Pair: instrument,
|
|
}
|
|
} else {
|
|
if o.Verbose {
|
|
log.Debug("Orderbook invalid")
|
|
}
|
|
return fmt.Errorf("channel: %v. Orderbook update for %v checksum invalid. Received %v Calculated %v", tableName, instrument, wsEventData.Checksum, checksum)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// WsUpdateOrderbookEntry takes WS bid or ask data and merges it with existing orderbook bid or ask data
|
|
func (o *OKGroup) WsUpdateOrderbookEntry(wsEntries [][]interface{}, existingOrderbookEntries []orderbook.Item) []orderbook.Item {
|
|
for j := range wsEntries {
|
|
wsEntryPrice, _ := strconv.ParseFloat(wsEntries[j][0].(string), 64)
|
|
wsEntryAmount, _ := strconv.ParseFloat(wsEntries[j][1].(string), 64)
|
|
matchFound := false
|
|
for k := 0; k < len(existingOrderbookEntries); k++ {
|
|
if existingOrderbookEntries[k].Price != wsEntryPrice {
|
|
continue
|
|
}
|
|
matchFound = true
|
|
if wsEntryAmount == 0 {
|
|
existingOrderbookEntries = append(existingOrderbookEntries[:k], existingOrderbookEntries[k+1:]...)
|
|
k--
|
|
continue
|
|
}
|
|
existingOrderbookEntries[k].Amount = wsEntryAmount
|
|
continue
|
|
}
|
|
if !matchFound {
|
|
existingOrderbookEntries = append(existingOrderbookEntries, orderbook.Item{
|
|
Amount: wsEntryAmount,
|
|
Price: wsEntryPrice,
|
|
})
|
|
}
|
|
}
|
|
return existingOrderbookEntries
|
|
}
|
|
|
|
// CalculatePartialOrderbookChecksum alternates over the first 25 bid and ask entries from websocket data
|
|
// The checksum is made up of the price and the quantity with a semicolon (:) deliminating them
|
|
// This will also work when there are less than 25 entries (for whatever reason)
|
|
// eg Bid:Ask:Bid:Ask:Ask:Ask
|
|
func (o *OKGroup) CalculatePartialOrderbookChecksum(orderbookData *WebsocketDataWrapper) int32 {
|
|
var checksum string
|
|
iterations := 25
|
|
for i := 0; i < iterations; i++ {
|
|
bidsMessage := ""
|
|
askMessage := ""
|
|
if len(orderbookData.Bids)-1 >= i {
|
|
bidsMessage = fmt.Sprintf("%v:%v:", orderbookData.Bids[i][0], orderbookData.Bids[i][1])
|
|
}
|
|
if len(orderbookData.Asks)-1 >= i {
|
|
askMessage = fmt.Sprintf("%v:%v:", orderbookData.Asks[i][0], orderbookData.Asks[i][1])
|
|
|
|
}
|
|
if checksum == "" {
|
|
checksum = fmt.Sprintf("%v%v", bidsMessage, askMessage)
|
|
} else {
|
|
checksum = fmt.Sprintf("%v%v%v", checksum, bidsMessage, askMessage)
|
|
}
|
|
}
|
|
checksum = strings.TrimSuffix(checksum, ":")
|
|
return int32(crc32.ChecksumIEEE([]byte(checksum)))
|
|
}
|
|
|
|
// CalculateUpdateOrderbookChecksum alternates over the first 25 bid and ask entries of a merged orderbook
|
|
// The checksum is made up of the price and the quantity with a semicolon (:) deliminating them
|
|
// This will also work when there are less than 25 entries (for whatever reason)
|
|
// eg Bid:Ask:Bid:Ask:Ask:Ask
|
|
func (o *OKGroup) CalculateUpdateOrderbookChecksum(orderbookData *orderbook.Base) int32 {
|
|
var checksum string
|
|
iterations := 25
|
|
for i := 0; i < iterations; i++ {
|
|
bidsMessage := ""
|
|
askMessage := ""
|
|
if len(orderbookData.Bids)-1 >= i {
|
|
price := strconv.FormatFloat(orderbookData.Bids[i].Price, 'f', -1, 64)
|
|
amount := strconv.FormatFloat(orderbookData.Bids[i].Amount, 'f', -1, 64)
|
|
bidsMessage = fmt.Sprintf("%v:%v:", price, amount)
|
|
}
|
|
if len(orderbookData.Asks)-1 >= i {
|
|
price := strconv.FormatFloat(orderbookData.Asks[i].Price, 'f', -1, 64)
|
|
amount := strconv.FormatFloat(orderbookData.Asks[i].Amount, 'f', -1, 64)
|
|
askMessage = fmt.Sprintf("%v:%v:", price, amount)
|
|
}
|
|
if checksum == "" {
|
|
checksum = fmt.Sprintf("%v%v", bidsMessage, askMessage)
|
|
} else {
|
|
checksum = fmt.Sprintf("%v%v%v", checksum, bidsMessage, askMessage)
|
|
}
|
|
}
|
|
checksum = strings.TrimSuffix(checksum, ":")
|
|
return int32(crc32.ChecksumIEEE([]byte(checksum)))
|
|
}
|
|
|
|
// GenerateDefaultSubscriptions Adds default subscriptions to websocket to be handled by ManageSubscriptions()
|
|
func (o *OKGroup) GenerateDefaultSubscriptions() {
|
|
enabledCurrencies := o.GetEnabledCurrencies()
|
|
var subscriptions []exchange.WebsocketChannelSubscription
|
|
if o.GetAuthenticatedAPISupport(exchange.WebsocketAuthentication) {
|
|
defaultSubscribedChannels = append(defaultSubscribedChannels, okGroupWsSpotMarginAccount, okGroupWsSpotAccount, okGroupWsSpotOrder)
|
|
}
|
|
for i := range defaultSubscribedChannels {
|
|
for j := range enabledCurrencies {
|
|
enabledCurrencies[j].Delimiter = "-"
|
|
subscriptions = append(subscriptions, exchange.WebsocketChannelSubscription{
|
|
Channel: defaultSubscribedChannels[i],
|
|
Currency: enabledCurrencies[j],
|
|
})
|
|
}
|
|
}
|
|
o.Websocket.SubscribeToChannels(subscriptions)
|
|
}
|
|
|
|
// Subscribe sends a websocket message to receive data from the channel
|
|
func (o *OKGroup) Subscribe(channelToSubscribe exchange.WebsocketChannelSubscription) error {
|
|
resp := WebsocketEventRequest{
|
|
Operation: "subscribe",
|
|
Arguments: []string{fmt.Sprintf("%v:%v", channelToSubscribe.Channel, channelToSubscribe.Currency.String())},
|
|
}
|
|
if strings.EqualFold(channelToSubscribe.Channel, okGroupWsSpotAccount) {
|
|
resp.Arguments = []string{fmt.Sprintf("%v:%v", channelToSubscribe.Channel, channelToSubscribe.Currency.Base.String())}
|
|
}
|
|
|
|
json, err := common.JSONEncode(resp)
|
|
if err != nil {
|
|
if o.Verbose {
|
|
log.Debugf("%v subscribe error: %v", o.Name, err)
|
|
}
|
|
return err
|
|
}
|
|
return o.writeToWebsocket(string(json))
|
|
}
|
|
|
|
// Unsubscribe sends a websocket message to stop receiving data from the channel
|
|
func (o *OKGroup) Unsubscribe(channelToSubscribe exchange.WebsocketChannelSubscription) error {
|
|
resp := WebsocketEventRequest{
|
|
Operation: "unsubscribe",
|
|
Arguments: []string{fmt.Sprintf("%v:%v", channelToSubscribe.Channel, channelToSubscribe.Currency.String())},
|
|
}
|
|
json, err := common.JSONEncode(resp)
|
|
if err != nil {
|
|
if o.Verbose {
|
|
log.Debugf("%v unsubscribe error: %v", o.Name, err)
|
|
}
|
|
return err
|
|
}
|
|
return o.writeToWebsocket(string(json))
|
|
}
|