mirror of
https://github.com/d0zingcat/gocryptotrader.git
synced 2026-05-13 15:09:42 +00:00
* renamed package to log to stop side import requirement * reverted comment changes * reverted comment changes * one more reverting wording back to logger * wording changes on comments
843 lines
29 KiB
Go
843 lines
29 KiB
Go
package okgroup
|
|
|
|
import (
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"hash/crc32"
|
|
"net/http"
|
|
"strconv"
|
|
"strings"
|
|
"sync"
|
|
"time"
|
|
|
|
"github.com/gorilla/websocket"
|
|
"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/ticker"
|
|
"github.com/thrasher-corp/gocryptotrader/exchanges/websocket/wshandler"
|
|
"github.com/thrasher-corp/gocryptotrader/exchanges/websocket/wsorderbook"
|
|
"github.com/thrasher-corp/gocryptotrader/log"
|
|
)
|
|
|
|
// List of all websocket channels to subscribe to
|
|
const (
|
|
// 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
|
|
|
|
allowableIterations = 25
|
|
delimiterColon = ":"
|
|
delimiterDash = "-"
|
|
delimiterUnderscore = "_"
|
|
)
|
|
|
|
// orderbookMutex Ensures if two entries arrive at once, only one can be
|
|
// processed at a time
|
|
var orderbookMutex sync.Mutex
|
|
|
|
var defaultSpotSubscribedChannels = []string{okGroupWsSpotDepth,
|
|
okGroupWsSpotCandle300s,
|
|
okGroupWsSpotTicker,
|
|
okGroupWsSpotTrade}
|
|
|
|
var defaultFuturesSubscribedChannels = []string{okGroupWsFuturesDepth,
|
|
okGroupWsFuturesCandle300s,
|
|
okGroupWsFuturesTicker,
|
|
okGroupWsFuturesTrade}
|
|
|
|
var defaultIndexSubscribedChannels = []string{okGroupWsIndexCandle300s,
|
|
okGroupWsIndexTicker}
|
|
|
|
var defaultSwapSubscribedChannels = []string{okGroupWsSwapDepth,
|
|
okGroupWsSwapCandle300s,
|
|
okGroupWsSwapTicker,
|
|
okGroupWsSwapTrade,
|
|
okGroupWsSwapFundingRate,
|
|
okGroupWsSwapMarkPrice}
|
|
|
|
// WsConnect initiates a websocket connection
|
|
func (o *OKGroup) WsConnect() error {
|
|
if !o.Websocket.IsEnabled() || !o.IsEnabled() {
|
|
return errors.New(wshandler.WebsocketNotEnabled)
|
|
}
|
|
var dialer websocket.Dialer
|
|
err := o.WebsocketConn.Dial(&dialer, http.Header{})
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if o.Verbose {
|
|
log.Debugf(log.ExchangeSys, "Successful connection to %v\n",
|
|
o.Websocket.GetWebsocketURL())
|
|
}
|
|
wg := sync.WaitGroup{}
|
|
wg.Add(1)
|
|
go o.WsHandleData(&wg)
|
|
if o.GetAuthenticatedAPISupport(exchange.WebsocketAuthentication) {
|
|
err = o.WsLogin()
|
|
if err != nil {
|
|
log.Errorf(log.ExchangeSys,
|
|
"%v - authentication failed: %v\n",
|
|
o.Name,
|
|
err)
|
|
}
|
|
}
|
|
|
|
o.GenerateDefaultSubscriptions()
|
|
// Ensures that we start the routines and we dont race when shutdown occurs
|
|
wg.Wait()
|
|
return nil
|
|
}
|
|
|
|
// 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.WebsocketConn.ReadMessage()
|
|
if err != nil {
|
|
o.Websocket.ReadMessageErrors <- err
|
|
return
|
|
}
|
|
o.Websocket.TrafficAlert <- struct{}{}
|
|
var dataResponse WebsocketDataResponse
|
|
err = json.Unmarshal(resp.Raw, &dataResponse)
|
|
if err == nil && dataResponse.Table != "" {
|
|
if len(dataResponse.Data) > 0 {
|
|
o.WsHandleDataResponse(&dataResponse)
|
|
}
|
|
continue
|
|
}
|
|
var errorResponse WebsocketErrorResponse
|
|
err = json.Unmarshal(resp.Raw, &errorResponse)
|
|
if err == nil && errorResponse.ErrorCode > 0 {
|
|
if o.Verbose {
|
|
log.Debugf(log.ExchangeSys,
|
|
"WS Error Event: %v Message: %v for %s",
|
|
errorResponse.Event,
|
|
errorResponse.Message,
|
|
o.Name)
|
|
}
|
|
o.WsHandleErrorResponse(errorResponse)
|
|
continue
|
|
}
|
|
var eventResponse WebsocketEventResponse
|
|
err = json.Unmarshal(resp.Raw, &eventResponse)
|
|
if err == nil && eventResponse.Event != "" {
|
|
if eventResponse.Event == "login" {
|
|
o.Websocket.SetCanUseAuthenticatedEndpoints(eventResponse.Success)
|
|
}
|
|
if o.Verbose {
|
|
log.Debugf(log.ExchangeSys,
|
|
"WS Event: %v on Channel: %v for %s",
|
|
eventResponse.Event,
|
|
eventResponse.Channel,
|
|
o.Name)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// WsLogin sends a login request to websocket to enable access to authenticated endpoints
|
|
func (o *OKGroup) WsLogin() error {
|
|
o.Websocket.SetCanUseAuthenticatedEndpoints(true)
|
|
unixTime := time.Now().UTC().Unix()
|
|
signPath := "/users/self/verify"
|
|
hmac := crypto.GetHMAC(crypto.HashSHA256,
|
|
[]byte(strconv.FormatInt(unixTime, 10)+http.MethodGet+signPath),
|
|
[]byte(o.API.Credentials.Secret),
|
|
)
|
|
base64 := crypto.Base64Encode(hmac)
|
|
request := WebsocketEventRequest{
|
|
Operation: "login",
|
|
Arguments: []string{
|
|
o.API.Credentials.Key,
|
|
o.API.Credentials.ClientID,
|
|
strconv.FormatInt(unixTime, 10),
|
|
base64,
|
|
},
|
|
}
|
|
err := o.WebsocketConn.SendJSONMessage(request)
|
|
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.Name,
|
|
event.ErrorCode,
|
|
event.Message)
|
|
if o.Verbose {
|
|
log.Error(log.ExchangeSys, 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) asset.Item {
|
|
assetIndex := strings.Index(table, "/")
|
|
switch table[:assetIndex] {
|
|
case asset.Futures.String():
|
|
return asset.Futures
|
|
case asset.Spot.String():
|
|
return asset.Spot
|
|
case "swap":
|
|
return asset.PerpetualSwap
|
|
case asset.Index.String():
|
|
return asset.Index
|
|
default:
|
|
log.Warnf(log.ExchangeSys, "%s unhandled asset type %s",
|
|
o.Name,
|
|
table[:assetIndex])
|
|
return asset.Item(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:
|
|
o.wsProcessCandles(response)
|
|
case okGroupWsDepth, okGroupWsDepth5:
|
|
// Locking, orderbooks cannot be processed out of order
|
|
orderbookMutex.Lock()
|
|
err := o.WsProcessOrderBook(response)
|
|
if err != nil {
|
|
for i := range response.Data {
|
|
a := o.GetAssetTypeFromTableName(response.Table)
|
|
var c currency.Pair
|
|
switch a {
|
|
case asset.Futures, asset.PerpetualSwap:
|
|
f := strings.Split(response.Data[i].InstrumentID, delimiterDash)
|
|
c = currency.NewPairWithDelimiter(f[0]+delimiterDash+f[1], f[2], delimiterDash)
|
|
default:
|
|
f := strings.Split(response.Data[i].InstrumentID, delimiterDash)
|
|
c = currency.NewPairWithDelimiter(f[0], f[1], delimiterDash)
|
|
}
|
|
|
|
channelToResubscribe := wshandler.WebsocketChannelSubscription{
|
|
Channel: response.Table,
|
|
Currency: c,
|
|
}
|
|
o.Websocket.ResubscribeToChannel(channelToResubscribe)
|
|
}
|
|
}
|
|
orderbookMutex.Unlock()
|
|
case okGroupWsTicker:
|
|
o.wsProcessTickers(response)
|
|
case okGroupWsTrade:
|
|
o.wsProcessTrades(response)
|
|
default:
|
|
logDataResponse(response, o.Name)
|
|
}
|
|
}
|
|
|
|
// logDataResponse will log the details of any websocket data event
|
|
// where there is no websocket datahandler for it
|
|
func logDataResponse(response *WebsocketDataResponse, exchangeName string) {
|
|
for i := range response.Data {
|
|
log.Warnf(log.ExchangeSys,
|
|
"%s Unhandled channel: '%v'. Instrument '%v' Timestamp '%v'",
|
|
exchangeName,
|
|
response.Table,
|
|
response.Data[i].InstrumentID,
|
|
response.Data[i].Timestamp)
|
|
}
|
|
}
|
|
|
|
// wsProcessTickers converts ticker data and sends it to the datahandler
|
|
func (o *OKGroup) wsProcessTickers(response *WebsocketDataResponse) {
|
|
for i := range response.Data {
|
|
a := o.GetAssetTypeFromTableName(response.Table)
|
|
var c currency.Pair
|
|
switch a {
|
|
case asset.Futures, asset.PerpetualSwap:
|
|
f := strings.Split(response.Data[i].InstrumentID, delimiterDash)
|
|
c = currency.NewPairWithDelimiter(f[0]+delimiterDash+f[1], f[2], delimiterUnderscore)
|
|
default:
|
|
f := strings.Split(response.Data[i].InstrumentID, delimiterDash)
|
|
c = currency.NewPairWithDelimiter(f[0], f[1], delimiterDash)
|
|
}
|
|
|
|
o.Websocket.DataHandler <- &ticker.Price{
|
|
ExchangeName: o.Name,
|
|
Open: response.Data[i].Open24h,
|
|
Close: response.Data[i].Last,
|
|
Volume: response.Data[i].BaseVolume24h,
|
|
QuoteVolume: response.Data[i].QuoteVolume24h,
|
|
High: response.Data[i].High24h,
|
|
Low: response.Data[i].Low24h,
|
|
Bid: response.Data[i].BestBid,
|
|
Ask: response.Data[i].BestAsk,
|
|
Last: response.Data[i].Last,
|
|
LastUpdated: response.Data[i].Timestamp,
|
|
AssetType: o.GetAssetTypeFromTableName(response.Table),
|
|
Pair: c,
|
|
}
|
|
}
|
|
}
|
|
|
|
// wsProcessTrades converts trade data and sends it to the datahandler
|
|
func (o *OKGroup) wsProcessTrades(response *WebsocketDataResponse) {
|
|
for i := range response.Data {
|
|
a := o.GetAssetTypeFromTableName(response.Table)
|
|
var c currency.Pair
|
|
switch a {
|
|
case asset.Futures, asset.PerpetualSwap:
|
|
f := strings.Split(response.Data[i].InstrumentID, delimiterDash)
|
|
c = currency.NewPairWithDelimiter(f[0]+delimiterDash+f[1], f[2], delimiterUnderscore)
|
|
default:
|
|
f := strings.Split(response.Data[i].InstrumentID, delimiterDash)
|
|
c = currency.NewPairWithDelimiter(f[0], f[1], delimiterDash)
|
|
}
|
|
|
|
o.Websocket.DataHandler <- wshandler.TradeData{
|
|
Amount: response.Data[i].Size,
|
|
AssetType: o.GetAssetTypeFromTableName(response.Table),
|
|
CurrencyPair: c,
|
|
Exchange: o.Name,
|
|
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 {
|
|
a := o.GetAssetTypeFromTableName(response.Table)
|
|
var c currency.Pair
|
|
switch a {
|
|
case asset.Futures, asset.PerpetualSwap:
|
|
f := strings.Split(response.Data[i].InstrumentID, delimiterDash)
|
|
c = currency.NewPairWithDelimiter(f[0]+delimiterDash+f[1], f[2], delimiterUnderscore)
|
|
default:
|
|
f := strings.Split(response.Data[i].InstrumentID, delimiterDash)
|
|
c = currency.NewPairWithDelimiter(f[0], f[1], delimiterDash)
|
|
}
|
|
|
|
timeData, err := time.Parse(time.RFC3339Nano,
|
|
response.Data[i].WebsocketCandleResponse.Candle[0])
|
|
if err != nil {
|
|
log.Errorf(log.ExchangeSys,
|
|
"%v Time data could not be parsed: %v",
|
|
o.Name,
|
|
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 := wshandler.KlineData{
|
|
AssetType: o.GetAssetTypeFromTableName(response.Table),
|
|
Pair: c,
|
|
Exchange: o.Name,
|
|
Timestamp: timeData,
|
|
Interval: candleInterval,
|
|
}
|
|
klineData.OpenPrice, err = strconv.ParseFloat(response.Data[i].Candle[1], 64)
|
|
if err != nil {
|
|
o.Websocket.DataHandler <- err
|
|
continue
|
|
}
|
|
klineData.HighPrice, err = strconv.ParseFloat(response.Data[i].Candle[2], 64)
|
|
if err != nil {
|
|
o.Websocket.DataHandler <- err
|
|
continue
|
|
}
|
|
klineData.LowPrice, err = strconv.ParseFloat(response.Data[i].Candle[3], 64)
|
|
if err != nil {
|
|
o.Websocket.DataHandler <- err
|
|
continue
|
|
}
|
|
klineData.ClosePrice, err = strconv.ParseFloat(response.Data[i].Candle[4], 64)
|
|
if err != nil {
|
|
o.Websocket.DataHandler <- err
|
|
continue
|
|
}
|
|
klineData.Volume, err = strconv.ParseFloat(response.Data[i].Candle[5], 64)
|
|
if err != nil {
|
|
o.Websocket.DataHandler <- err
|
|
continue
|
|
}
|
|
|
|
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 {
|
|
a := o.GetAssetTypeFromTableName(response.Table)
|
|
var c currency.Pair
|
|
switch a {
|
|
case asset.Futures, asset.PerpetualSwap:
|
|
f := strings.Split(response.Data[i].InstrumentID, delimiterDash)
|
|
c = currency.NewPairWithDelimiter(f[0]+delimiterDash+f[1], f[2], delimiterUnderscore)
|
|
default:
|
|
f := strings.Split(response.Data[i].InstrumentID, delimiterDash)
|
|
c = currency.NewPairWithDelimiter(f[0], f[1], delimiterDash)
|
|
}
|
|
|
|
if response.Action == okGroupWsOrderbookPartial {
|
|
err = o.WsProcessPartialOrderBook(&response.Data[i], c, a)
|
|
if err != nil {
|
|
return
|
|
}
|
|
} else if response.Action == okGroupWsOrderbookUpdate {
|
|
if len(response.Data[i].Asks) == 0 && len(response.Data[i].Bids) == 0 {
|
|
continue
|
|
}
|
|
err = o.WsProcessUpdateOrderbook(&response.Data[i], c, a)
|
|
if err != nil {
|
|
return
|
|
}
|
|
}
|
|
}
|
|
return
|
|
}
|
|
|
|
// AppendWsOrderbookItems adds websocket orderbook data bid/asks into an orderbook item array
|
|
func (o *OKGroup) AppendWsOrderbookItems(entries [][]interface{}) ([]orderbook.Item, error) {
|
|
var items []orderbook.Item
|
|
for j := range entries {
|
|
amount, err := strconv.ParseFloat(entries[j][1].(string), 64)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
price, err := strconv.ParseFloat(entries[j][0].(string), 64)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
items = append(items, orderbook.Item{Amount: amount, Price: price})
|
|
}
|
|
return items, nil
|
|
}
|
|
|
|
// 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, a asset.Item) error {
|
|
signedChecksum := o.CalculatePartialOrderbookChecksum(wsEventData)
|
|
if signedChecksum != wsEventData.Checksum {
|
|
return fmt.Errorf("%s channel: %s. Orderbook partial for %v checksum invalid",
|
|
o.Name,
|
|
a,
|
|
instrument)
|
|
}
|
|
if o.Verbose {
|
|
log.Debugf(log.ExchangeSys,
|
|
"%s passed checksum for instrument %s",
|
|
o.Name,
|
|
instrument)
|
|
}
|
|
|
|
asks, err := o.AppendWsOrderbookItems(wsEventData.Asks)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
bids, err := o.AppendWsOrderbookItems(wsEventData.Bids)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
newOrderBook := orderbook.Base{
|
|
Asks: asks,
|
|
Bids: bids,
|
|
AssetType: a,
|
|
LastUpdated: wsEventData.Timestamp,
|
|
Pair: instrument,
|
|
ExchangeName: o.Name,
|
|
}
|
|
|
|
err = o.Websocket.Orderbook.LoadSnapshot(&newOrderBook)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
o.Websocket.DataHandler <- wshandler.WebsocketOrderbookUpdate{
|
|
Exchange: o.Name,
|
|
Asset: a,
|
|
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, a asset.Item) error {
|
|
update := wsorderbook.WebsocketOrderbookUpdate{
|
|
Asset: a,
|
|
Pair: instrument,
|
|
UpdateTime: wsEventData.Timestamp,
|
|
}
|
|
|
|
var err error
|
|
update.Asks, err = o.AppendWsOrderbookItems(wsEventData.Asks)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
update.Bids, err = o.AppendWsOrderbookItems(wsEventData.Bids)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
err = o.Websocket.Orderbook.Update(&update)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
updatedOb := o.Websocket.Orderbook.GetOrderbook(instrument, a)
|
|
checksum := o.CalculateUpdateOrderbookChecksum(updatedOb)
|
|
|
|
if checksum != wsEventData.Checksum {
|
|
// re-sub
|
|
log.Warnf(log.ExchangeSys, "%s checksum failure for item %s",
|
|
o.Name,
|
|
wsEventData.InstrumentID)
|
|
return errors.New("checksum failed")
|
|
}
|
|
|
|
o.Websocket.DataHandler <- wshandler.WebsocketOrderbookUpdate{
|
|
Exchange: o.Name,
|
|
Asset: a,
|
|
Pair: instrument,
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// 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 strings.Builder
|
|
for i := 0; i < allowableIterations; i++ {
|
|
if len(orderbookData.Bids)-1 >= i {
|
|
checksum.WriteString(orderbookData.Bids[i][0].(string) +
|
|
delimiterColon +
|
|
orderbookData.Bids[i][1].(string) +
|
|
delimiterColon)
|
|
}
|
|
if len(orderbookData.Asks)-1 >= i {
|
|
checksum.WriteString(orderbookData.Asks[i][0].(string) +
|
|
delimiterColon +
|
|
orderbookData.Asks[i][1].(string) +
|
|
delimiterColon)
|
|
}
|
|
}
|
|
checksumStr := strings.TrimSuffix(checksum.String(), delimiterColon)
|
|
return int32(crc32.ChecksumIEEE([]byte(checksumStr)))
|
|
}
|
|
|
|
// 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 strings.Builder
|
|
for i := 0; i < allowableIterations; i++ {
|
|
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)
|
|
checksum.WriteString(price + delimiterColon + amount + delimiterColon)
|
|
}
|
|
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)
|
|
checksum.WriteString(price + delimiterColon + amount + delimiterColon)
|
|
}
|
|
}
|
|
checksumStr := strings.TrimSuffix(checksum.String(), delimiterColon)
|
|
return int32(crc32.ChecksumIEEE([]byte(checksumStr)))
|
|
}
|
|
|
|
// GenerateDefaultSubscriptions Adds default subscriptions to websocket to be
|
|
// handled by ManageSubscriptions()
|
|
func (o *OKGroup) GenerateDefaultSubscriptions() {
|
|
var subscriptions []wshandler.WebsocketChannelSubscription
|
|
assets := o.GetAssetTypes()
|
|
for x := range assets {
|
|
enabledCurrencies := o.GetEnabledPairs(assets[x])
|
|
if len(enabledCurrencies) == 0 {
|
|
continue
|
|
}
|
|
|
|
switch assets[x] {
|
|
case asset.Spot:
|
|
for i := range enabledCurrencies {
|
|
for y := range defaultSpotSubscribedChannels {
|
|
subscriptions = append(subscriptions,
|
|
wshandler.WebsocketChannelSubscription{
|
|
Channel: defaultSpotSubscribedChannels[y],
|
|
Currency: o.FormatExchangeCurrency(enabledCurrencies[i],
|
|
asset.Spot),
|
|
})
|
|
}
|
|
}
|
|
|
|
if o.GetAuthenticatedAPISupport(exchange.WebsocketAuthentication) {
|
|
subscriptions = append(subscriptions,
|
|
wshandler.WebsocketChannelSubscription{
|
|
Channel: okGroupWsSpotMarginAccount,
|
|
},
|
|
wshandler.WebsocketChannelSubscription{
|
|
Channel: okGroupWsSpotAccount,
|
|
},
|
|
wshandler.WebsocketChannelSubscription{
|
|
Channel: okGroupWsSpotOrder,
|
|
})
|
|
}
|
|
case asset.Futures:
|
|
for i := range enabledCurrencies {
|
|
for y := range defaultFuturesSubscribedChannels {
|
|
subscriptions = append(subscriptions,
|
|
wshandler.WebsocketChannelSubscription{
|
|
Channel: defaultFuturesSubscribedChannels[y],
|
|
Currency: o.FormatExchangeCurrency(enabledCurrencies[i],
|
|
asset.Futures),
|
|
})
|
|
}
|
|
}
|
|
|
|
if o.GetAuthenticatedAPISupport(exchange.WebsocketAuthentication) {
|
|
subscriptions = append(subscriptions,
|
|
wshandler.WebsocketChannelSubscription{
|
|
Channel: okGroupWsFuturesAccount,
|
|
},
|
|
wshandler.WebsocketChannelSubscription{
|
|
Channel: okGroupWsFuturesPosition,
|
|
},
|
|
wshandler.WebsocketChannelSubscription{
|
|
Channel: okGroupWsFuturesOrder,
|
|
})
|
|
}
|
|
case asset.PerpetualSwap:
|
|
for i := range enabledCurrencies {
|
|
for y := range defaultSwapSubscribedChannels {
|
|
subscriptions = append(subscriptions,
|
|
wshandler.WebsocketChannelSubscription{
|
|
Channel: defaultSwapSubscribedChannels[y],
|
|
Currency: o.FormatExchangeCurrency(enabledCurrencies[i],
|
|
asset.PerpetualSwap),
|
|
})
|
|
}
|
|
}
|
|
|
|
if o.GetAuthenticatedAPISupport(exchange.WebsocketAuthentication) {
|
|
subscriptions = append(subscriptions,
|
|
wshandler.WebsocketChannelSubscription{
|
|
Channel: okGroupWsSwapAccount,
|
|
},
|
|
wshandler.WebsocketChannelSubscription{
|
|
Channel: okGroupWsSwapPosition,
|
|
},
|
|
wshandler.WebsocketChannelSubscription{
|
|
Channel: okGroupWsSwapOrder,
|
|
})
|
|
}
|
|
case asset.Index:
|
|
for i := range enabledCurrencies {
|
|
for y := range defaultIndexSubscribedChannels {
|
|
subscriptions = append(subscriptions,
|
|
wshandler.WebsocketChannelSubscription{
|
|
Channel: defaultIndexSubscribedChannels[y],
|
|
Currency: o.FormatExchangeCurrency(enabledCurrencies[i], asset.Index),
|
|
})
|
|
}
|
|
}
|
|
default:
|
|
o.Websocket.DataHandler <- errors.New("unhandled asset type")
|
|
}
|
|
}
|
|
|
|
o.Websocket.SubscribeToChannels(subscriptions)
|
|
}
|
|
|
|
// Subscribe sends a websocket message to receive data from the channel
|
|
func (o *OKGroup) Subscribe(channelToSubscribe wshandler.WebsocketChannelSubscription) error {
|
|
c := channelToSubscribe.Currency.String()
|
|
request := WebsocketEventRequest{
|
|
Operation: "subscribe",
|
|
Arguments: []string{channelToSubscribe.Channel + delimiterColon + c},
|
|
}
|
|
if strings.EqualFold(channelToSubscribe.Channel, okGroupWsSpotAccount) {
|
|
request.Arguments = []string{channelToSubscribe.Channel +
|
|
delimiterColon +
|
|
channelToSubscribe.Currency.Base.String()}
|
|
}
|
|
|
|
return o.WebsocketConn.SendJSONMessage(request)
|
|
}
|
|
|
|
// Unsubscribe sends a websocket message to stop receiving data from the channel
|
|
func (o *OKGroup) Unsubscribe(channelToSubscribe wshandler.WebsocketChannelSubscription) error {
|
|
request := WebsocketEventRequest{
|
|
Operation: "unsubscribe",
|
|
Arguments: []string{channelToSubscribe.Channel +
|
|
delimiterColon +
|
|
channelToSubscribe.Currency.String()},
|
|
}
|
|
return o.WebsocketConn.SendJSONMessage(request)
|
|
}
|