Files
gocryptotrader/exchanges/okgroup/okgroup_websocket.go
Andrew 7fccc03164 (Logger) Rename package to log (#444)
* 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
2020-02-12 18:09:56 +11:00

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)
}