mirror of
https://github.com/d0zingcat/gocryptotrader.git
synced 2026-05-22 23:16:48 +00:00
* bug fix for websocket orderbook processing * Fix more panics * fix linter issue * kick panic can down the road * temp fix for issue with a 404 returned error as chainz.cryptoid dropped eth support * Address nits and fixed orderbook updating * Fix trade data, rm'd event time from struct * fix time conversion for huobi * Actually process kline data and fix time stamps * btse time conversion fix and RM log, as it seems that the gain is reflecting transaction side. Drop ticker fetching support because there does not seem to be support on docs. And added trade fetching support. * revert huobi println * Adressed suggestion * rm unnecessary assignment * rm unnecessary check and assign * fix conversion mishap * fix currency conversion bug * update websocket logging * RM websocket type which stops conversion and copy * fix linter issue, add in unknown side type
930 lines
28 KiB
Go
930 lines
28 KiB
Go
package kraken
|
|
|
|
import (
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"math"
|
|
"net/http"
|
|
"strconv"
|
|
"time"
|
|
|
|
"github.com/gorilla/websocket"
|
|
"github.com/thrasher-corp/gocryptotrader/common/convert"
|
|
"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"
|
|
log "github.com/thrasher-corp/gocryptotrader/logger"
|
|
)
|
|
|
|
// List of all websocket channels to subscribe to
|
|
const (
|
|
krakenWSURL = "wss://ws.kraken.com"
|
|
krakenAuthWSURL = "wss://ws-auth.kraken.com"
|
|
krakenWSSandboxURL = "wss://sandbox.kraken.com"
|
|
krakenWSSupportedVersion = "0.3.0"
|
|
// WS endpoints
|
|
krakenWsHeartbeat = "heartbeat"
|
|
krakenWsPing = "ping"
|
|
krakenWsPong = "pong"
|
|
krakenWsSystemStatus = "systemStatus"
|
|
krakenWsSubscribe = "subscribe"
|
|
krakenWsSubscriptionStatus = "subscriptionStatus"
|
|
krakenWsUnsubscribe = "unsubscribe"
|
|
krakenWsTicker = "ticker"
|
|
krakenWsOHLC = "ohlc"
|
|
krakenWsTrade = "trade"
|
|
krakenWsSpread = "spread"
|
|
krakenWsOrderbook = "book"
|
|
krakenWsOwnTrades = "ownTrades"
|
|
krakenWsOpenOrders = "openOrders"
|
|
krakenWsAddOrder = "addOrder"
|
|
krakenWsCancelOrder = "cancelOrder"
|
|
krakenWsRateLimit = 50
|
|
)
|
|
|
|
// orderbookMutex Ensures if two entries arrive at once, only one can be processed at a time
|
|
var subscriptionChannelPair []WebsocketChannelData
|
|
var comms = make(chan wshandler.WebsocketResponse)
|
|
var authToken string
|
|
|
|
// Channels require a topic and a currency
|
|
// Format [[ticker,but-t4u],[orderbook,nce-btt]]
|
|
var defaultSubscribedChannels = []string{krakenWsTicker, krakenWsTrade, krakenWsOrderbook, krakenWsOHLC, krakenWsSpread}
|
|
var authenticatedChannels = []string{krakenWsOwnTrades, krakenWsOpenOrders}
|
|
|
|
// WsConnect initiates a websocket connection
|
|
func (k *Kraken) WsConnect() error {
|
|
if !k.Websocket.IsEnabled() || !k.IsEnabled() {
|
|
return errors.New(wshandler.WebsocketNotEnabled)
|
|
}
|
|
var dialer websocket.Dialer
|
|
err := k.WebsocketConn.Dial(&dialer, http.Header{})
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if k.GetAuthenticatedAPISupport(exchange.WebsocketAuthentication) {
|
|
authToken, err = k.GetWebsocketToken()
|
|
if err != nil {
|
|
k.Websocket.SetCanUseAuthenticatedEndpoints(false)
|
|
log.Errorf(log.ExchangeSys, "%v - authentication failed: %v\n", k.Name, err)
|
|
}
|
|
err = k.AuthenticatedWebsocketConn.Dial(&dialer, http.Header{})
|
|
if err != nil {
|
|
k.Websocket.SetCanUseAuthenticatedEndpoints(false)
|
|
log.Errorf(log.ExchangeSys, "%v - failed to connect to authenticated endpoint: %v\n", k.Name, err)
|
|
}
|
|
go k.WsReadData(k.AuthenticatedWebsocketConn)
|
|
k.GenerateAuthenticatedSubscriptions()
|
|
}
|
|
|
|
go k.WsReadData(k.WebsocketConn)
|
|
go k.WsHandleData()
|
|
go k.wsPingHandler()
|
|
k.GenerateDefaultSubscriptions()
|
|
|
|
return nil
|
|
}
|
|
|
|
// WsReadData funnels both auth and public ws data into one manageable place
|
|
func (k *Kraken) WsReadData(ws *wshandler.WebsocketConnection) {
|
|
k.Websocket.Wg.Add(1)
|
|
defer k.Websocket.Wg.Done()
|
|
for {
|
|
select {
|
|
case <-k.Websocket.ShutdownC:
|
|
return
|
|
default:
|
|
resp, err := ws.ReadMessage()
|
|
if err != nil {
|
|
k.Websocket.DataHandler <- err
|
|
return
|
|
}
|
|
k.Websocket.TrafficAlert <- struct{}{}
|
|
comms <- resp
|
|
}
|
|
}
|
|
}
|
|
|
|
// WsHandleData handles the read data from the websocket connection
|
|
func (k *Kraken) WsHandleData() {
|
|
k.Websocket.Wg.Add(1)
|
|
defer func() {
|
|
k.Websocket.Wg.Done()
|
|
}()
|
|
|
|
for {
|
|
select {
|
|
case <-k.Websocket.ShutdownC:
|
|
return
|
|
default:
|
|
resp := <-comms
|
|
// event response handling
|
|
var eventResponse WebsocketEventResponse
|
|
err := json.Unmarshal(resp.Raw, &eventResponse)
|
|
if err == nil && eventResponse.Event != "" {
|
|
k.WsHandleEventResponse(&eventResponse, resp.Raw)
|
|
continue
|
|
}
|
|
// Data response handling
|
|
var dataResponse WebsocketDataResponse
|
|
err = json.Unmarshal(resp.Raw, &dataResponse)
|
|
if err != nil {
|
|
log.Error(log.WebsocketMgr, fmt.Errorf("%s - unhandled websocket data: %v", k.Name, err))
|
|
continue
|
|
}
|
|
if _, ok := dataResponse[0].(float64); ok {
|
|
k.WsHandleDataResponse(dataResponse)
|
|
}
|
|
if _, ok := dataResponse[1].(string); ok {
|
|
k.wsHandleAuthDataResponse(dataResponse)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// wsPingHandler sends a message "ping" every 27 to maintain the connection to the websocket
|
|
func (k *Kraken) wsPingHandler() {
|
|
k.Websocket.Wg.Add(1)
|
|
defer k.Websocket.Wg.Done()
|
|
ticker := time.NewTicker(time.Second * 27)
|
|
defer ticker.Stop()
|
|
|
|
for {
|
|
select {
|
|
case <-k.Websocket.ShutdownC:
|
|
return
|
|
case <-ticker.C:
|
|
pingEvent := WebsocketBaseEventRequest{Event: krakenWsPing}
|
|
if k.Verbose {
|
|
log.Debugf(log.ExchangeSys, "%v sending ping",
|
|
k.Name)
|
|
}
|
|
err := k.WebsocketConn.SendMessage(pingEvent)
|
|
if err != nil {
|
|
k.Websocket.DataHandler <- err
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// WsHandleDataResponse classifies the WS response and sends to appropriate handler
|
|
func (k *Kraken) WsHandleDataResponse(response WebsocketDataResponse) {
|
|
if cID, ok := response[0].(float64); ok {
|
|
channelID := int64(cID)
|
|
channelData := getSubscriptionChannelData(channelID)
|
|
switch channelData.Subscription {
|
|
case krakenWsTicker:
|
|
if k.Verbose {
|
|
log.Debugf(log.ExchangeSys, "%v Websocket ticker data received",
|
|
k.Name)
|
|
}
|
|
k.wsProcessTickers(&channelData, response[1].(map[string]interface{}))
|
|
case krakenWsOHLC:
|
|
if k.Verbose {
|
|
log.Debugf(log.ExchangeSys, "%v Websocket OHLC data received",
|
|
k.Name)
|
|
}
|
|
k.wsProcessCandles(&channelData, response[1].([]interface{}))
|
|
case krakenWsOrderbook:
|
|
if k.Verbose {
|
|
log.Debugf(log.ExchangeSys, "%v Websocket Orderbook data received",
|
|
k.Name)
|
|
}
|
|
k.wsProcessOrderBook(&channelData, response[1].(map[string]interface{}))
|
|
case krakenWsSpread:
|
|
if k.Verbose {
|
|
log.Debugf(log.ExchangeSys, "%v Websocket Spread data received",
|
|
k.Name)
|
|
}
|
|
k.wsProcessSpread(&channelData, response[1].([]interface{}))
|
|
case krakenWsTrade:
|
|
if k.Verbose {
|
|
log.Debugf(log.ExchangeSys, "%v Websocket Trade data received",
|
|
k.Name)
|
|
}
|
|
k.wsProcessTrades(&channelData, response[1].([]interface{}))
|
|
default:
|
|
log.Errorf(log.ExchangeSys, "%v Unidentified websocket data received: %v",
|
|
k.Name,
|
|
response)
|
|
}
|
|
}
|
|
}
|
|
|
|
// WsHandleEventResponse classifies the WS response and sends to appropriate handler
|
|
func (k *Kraken) WsHandleEventResponse(response *WebsocketEventResponse, rawResponse []byte) {
|
|
switch response.Event {
|
|
case krakenWsHeartbeat:
|
|
if k.Verbose {
|
|
log.Debugf(log.ExchangeSys, "%v Websocket heartbeat data received",
|
|
k.Name)
|
|
}
|
|
case krakenWsPong:
|
|
if k.Verbose {
|
|
log.Debugf(log.ExchangeSys, "%v Websocket pong data received",
|
|
k.Name)
|
|
}
|
|
case krakenWsSystemStatus:
|
|
if k.Verbose {
|
|
log.Debugf(log.ExchangeSys, "%v Websocket status data received",
|
|
k.Name)
|
|
}
|
|
if response.Status != "online" {
|
|
k.Websocket.DataHandler <- fmt.Errorf("%v Websocket status '%v'",
|
|
k.Name, response.Status)
|
|
}
|
|
if response.WebsocketStatusResponse.Version > krakenWSSupportedVersion {
|
|
log.Warnf(log.ExchangeSys, "%v New version of Websocket API released. Was %v Now %v",
|
|
k.Name, krakenWSSupportedVersion, response.WebsocketStatusResponse.Version)
|
|
}
|
|
case krakenWsSubscriptionStatus:
|
|
k.WebsocketConn.AddResponseWithID(response.RequestID, rawResponse)
|
|
if response.Status != "subscribed" {
|
|
k.Websocket.DataHandler <- fmt.Errorf("%v %v %v", k.Name, response.RequestID, response.WebsocketErrorResponse.ErrorMessage)
|
|
return
|
|
}
|
|
addNewSubscriptionChannelData(response)
|
|
default:
|
|
log.Errorf(log.ExchangeSys, "%v Unidentified websocket data received: %v",
|
|
k.Name, response)
|
|
}
|
|
}
|
|
|
|
func (k *Kraken) wsHandleAuthDataResponse(response WebsocketDataResponse) {
|
|
if chName, ok := response[1].(string); ok {
|
|
switch chName {
|
|
case krakenWsOwnTrades:
|
|
if k.Verbose {
|
|
log.Debugf(log.ExchangeSys, "%v Websocket auth own trade data received",
|
|
k.Name)
|
|
}
|
|
k.wsProcessOwnTrades(&response[0])
|
|
case krakenWsOpenOrders:
|
|
if k.Verbose {
|
|
log.Debugf(log.ExchangeSys, "%v Websocket auth open order data received",
|
|
k.Name)
|
|
}
|
|
k.wsProcessOpenOrders(&response[0])
|
|
}
|
|
}
|
|
}
|
|
|
|
func (k *Kraken) wsProcessOwnTrades(ownOrders interface{}) {
|
|
if data, ok := ownOrders.([]interface{}); ok {
|
|
for i := range data {
|
|
ownTrade := data[i].(map[string]interface{})
|
|
for _, val := range ownTrade {
|
|
tradeData := val.(map[string]interface{})
|
|
cost, err := strconv.ParseFloat(tradeData["cost"].(string), 64)
|
|
if err != nil {
|
|
k.Websocket.DataHandler <- err
|
|
}
|
|
fee, err := strconv.ParseFloat(tradeData["fee"].(string), 64)
|
|
if err != nil {
|
|
k.Websocket.DataHandler <- err
|
|
}
|
|
margin, err := strconv.ParseFloat(tradeData["margin"].(string), 64)
|
|
if err != nil {
|
|
k.Websocket.DataHandler <- err
|
|
}
|
|
vol, err := strconv.ParseFloat(tradeData["vol"].(string), 64)
|
|
if err != nil {
|
|
k.Websocket.DataHandler <- err
|
|
}
|
|
price, err := strconv.ParseFloat(tradeData["price"].(string), 64)
|
|
if err != nil {
|
|
k.Websocket.DataHandler <- err
|
|
}
|
|
timeTogether, err := strconv.ParseFloat(tradeData["time"].(string), 64)
|
|
if err != nil {
|
|
k.Websocket.DataHandler <- err
|
|
}
|
|
first, second, err := convert.SplitFloatDecimals(timeTogether)
|
|
if err != nil {
|
|
k.Websocket.DataHandler <- err
|
|
}
|
|
k.Websocket.DataHandler <- WsOwnTrade{
|
|
Cost: cost,
|
|
Fee: fee,
|
|
Margin: margin,
|
|
OrderTransactionID: tradeData["ordertxid"].(string),
|
|
OrderType: tradeData["ordertype"].(string),
|
|
Pair: tradeData["pair"].(string),
|
|
PostTransactionID: tradeData["postxid"].(string),
|
|
Price: price,
|
|
Time: time.Unix(first, second),
|
|
Type: tradeData["type"].(string),
|
|
Vol: vol,
|
|
}
|
|
}
|
|
}
|
|
} else {
|
|
k.Websocket.DataHandler <- errors.New(k.Name + " - Invalid own trades data")
|
|
}
|
|
}
|
|
|
|
func (k *Kraken) wsProcessOpenOrders(ownOrders interface{}) {
|
|
if data, ok := ownOrders.([]interface{}); ok {
|
|
for i := range data {
|
|
ownTrade := data[i].(map[string]interface{})
|
|
for key, val := range ownTrade {
|
|
tradeData := val.(map[string]interface{})
|
|
if len(tradeData) == 1 {
|
|
// just a status update
|
|
if status, ok := tradeData["status"].(string); ok {
|
|
k.Websocket.DataHandler <- k.Name + " - Order " + key + " " + status
|
|
}
|
|
}
|
|
startTimeConv, err := strconv.ParseFloat(tradeData["starttm"].(string), 64)
|
|
if err != nil {
|
|
k.Websocket.DataHandler <- err
|
|
}
|
|
startTime, startTimeNano, err := convert.SplitFloatDecimals(startTimeConv)
|
|
if err != nil {
|
|
k.Websocket.DataHandler <- err
|
|
}
|
|
openTimeConv, err := strconv.ParseFloat(tradeData["opentm"].(string), 64)
|
|
if err != nil {
|
|
k.Websocket.DataHandler <- err
|
|
}
|
|
openTime, openTimeNano, err := convert.SplitFloatDecimals(openTimeConv)
|
|
if err != nil {
|
|
k.Websocket.DataHandler <- err
|
|
}
|
|
expireTimeConv, err := strconv.ParseFloat(tradeData["expiretm"].(string), 64)
|
|
if err != nil {
|
|
k.Websocket.DataHandler <- err
|
|
}
|
|
expireTime, expireTimeNano, err := convert.SplitFloatDecimals(expireTimeConv)
|
|
if err != nil {
|
|
k.Websocket.DataHandler <- err
|
|
}
|
|
cost, err := strconv.ParseFloat(tradeData["cost"].(string), 64)
|
|
if err != nil {
|
|
k.Websocket.DataHandler <- err
|
|
}
|
|
executedVolume, err := strconv.ParseFloat(tradeData["vol_exec"].(string), 64)
|
|
if err != nil {
|
|
k.Websocket.DataHandler <- err
|
|
}
|
|
volume, err := strconv.ParseFloat(tradeData["vol"].(string), 64)
|
|
if err != nil {
|
|
k.Websocket.DataHandler <- err
|
|
}
|
|
userReference, err := strconv.ParseFloat(tradeData["userref"].(string), 64)
|
|
if err != nil {
|
|
k.Websocket.DataHandler <- err
|
|
}
|
|
stopPrice, err := strconv.ParseFloat(tradeData["stopprice"].(string), 64)
|
|
if err != nil {
|
|
k.Websocket.DataHandler <- err
|
|
}
|
|
price, err := strconv.ParseFloat(tradeData["price"].(string), 64)
|
|
if err != nil {
|
|
k.Websocket.DataHandler <- err
|
|
}
|
|
limitPrice, err := strconv.ParseFloat(tradeData["limitprice"].(string), 64)
|
|
if err != nil {
|
|
k.Websocket.DataHandler <- err
|
|
}
|
|
fee, err := strconv.ParseFloat(tradeData["fee"].(string), 64)
|
|
if err != nil {
|
|
k.Websocket.DataHandler <- err
|
|
}
|
|
descriptionSubData := tradeData["description"].(map[string]interface{})
|
|
descriptionPrice, err := strconv.ParseFloat(descriptionSubData["price"].(string), 64)
|
|
if err != nil {
|
|
k.Websocket.DataHandler <- err
|
|
}
|
|
descriptionPrice2, err := strconv.ParseFloat(descriptionSubData["price2"].(string), 64)
|
|
if err != nil {
|
|
k.Websocket.DataHandler <- err
|
|
}
|
|
description := WsOpenOrderDescription{
|
|
Close: descriptionSubData["close"].(string),
|
|
Leverage: descriptionSubData["leverage"].(string),
|
|
Order: descriptionSubData["order"].(string),
|
|
OrderType: descriptionSubData["ordertype"].(string),
|
|
Pair: descriptionSubData["pair"].(string),
|
|
Price: descriptionPrice,
|
|
Price2: descriptionPrice2,
|
|
Type: descriptionSubData["type"].(string),
|
|
}
|
|
|
|
k.Websocket.DataHandler <- WsOpenOrders{
|
|
Cost: cost,
|
|
ExpireTime: time.Unix(expireTime, expireTimeNano),
|
|
Description: description,
|
|
Fee: fee,
|
|
LimitPrice: limitPrice,
|
|
Misc: tradeData["misc"].(string),
|
|
OFlags: tradeData["oflags"].(string),
|
|
OpenTime: time.Unix(openTime, openTimeNano),
|
|
Price: price,
|
|
RefID: tradeData["refid"].(string),
|
|
StartTime: time.Unix(startTime, startTimeNano),
|
|
Status: tradeData["status"].(string),
|
|
StopPrice: stopPrice,
|
|
UserReference: userReference,
|
|
Volume: volume,
|
|
ExecutedVolume: executedVolume,
|
|
}
|
|
}
|
|
}
|
|
} else {
|
|
k.Websocket.DataHandler <- errors.New(k.Name + " - Invalid own trades data")
|
|
}
|
|
}
|
|
|
|
// addNewSubscriptionChannelData stores channel ids, pairs and subscription types to an array
|
|
// allowing correlation between subscriptions and returned data
|
|
func addNewSubscriptionChannelData(response *WebsocketEventResponse) {
|
|
// We change the / to - to maintain compatibility with REST/config
|
|
pair := currency.NewPairWithDelimiter(response.Pair.Base.String(),
|
|
response.Pair.Quote.String(), "-")
|
|
subscriptionChannelPair = append(subscriptionChannelPair, WebsocketChannelData{
|
|
Subscription: response.Subscription.Name,
|
|
Pair: pair,
|
|
ChannelID: response.ChannelID,
|
|
})
|
|
}
|
|
|
|
// getSubscriptionChannelData retrieves WebsocketChannelData based on response ID
|
|
func getSubscriptionChannelData(id int64) WebsocketChannelData {
|
|
for i := range subscriptionChannelPair {
|
|
if id == subscriptionChannelPair[i].ChannelID {
|
|
return subscriptionChannelPair[i]
|
|
}
|
|
}
|
|
return WebsocketChannelData{}
|
|
}
|
|
|
|
// wsProcessTickers converts ticker data and sends it to the datahandler
|
|
func (k *Kraken) wsProcessTickers(channelData *WebsocketChannelData, data map[string]interface{}) {
|
|
closePrice, err := strconv.ParseFloat(data["c"].([]interface{})[0].(string), 64)
|
|
if err != nil {
|
|
k.Websocket.DataHandler <- err
|
|
return
|
|
}
|
|
|
|
openPrice, err := strconv.ParseFloat(data["o"].([]interface{})[0].(string), 64)
|
|
if err != nil {
|
|
k.Websocket.DataHandler <- err
|
|
return
|
|
}
|
|
|
|
highPrice, err := strconv.ParseFloat(data["h"].([]interface{})[0].(string), 64)
|
|
if err != nil {
|
|
k.Websocket.DataHandler <- err
|
|
return
|
|
}
|
|
|
|
lowPrice, err := strconv.ParseFloat(data["l"].([]interface{})[0].(string), 64)
|
|
if err != nil {
|
|
k.Websocket.DataHandler <- err
|
|
return
|
|
}
|
|
|
|
quantity, err := strconv.ParseFloat(data["v"].([]interface{})[0].(string), 64)
|
|
if err != nil {
|
|
k.Websocket.DataHandler <- err
|
|
return
|
|
}
|
|
|
|
ask, err := strconv.ParseFloat(data["a"].([]interface{})[0].(string), 64)
|
|
if err != nil {
|
|
k.Websocket.DataHandler <- err
|
|
return
|
|
}
|
|
|
|
bid, err := strconv.ParseFloat(data["b"].([]interface{})[0].(string), 64)
|
|
if err != nil {
|
|
k.Websocket.DataHandler <- err
|
|
return
|
|
}
|
|
|
|
k.Websocket.DataHandler <- &ticker.Price{
|
|
ExchangeName: k.Name,
|
|
Open: openPrice,
|
|
Close: closePrice,
|
|
Volume: quantity,
|
|
High: highPrice,
|
|
Low: lowPrice,
|
|
Bid: bid,
|
|
Ask: ask,
|
|
AssetType: asset.Spot,
|
|
Pair: channelData.Pair,
|
|
}
|
|
}
|
|
|
|
// wsProcessTickers converts ticker data and sends it to the datahandler
|
|
func (k *Kraken) wsProcessSpread(channelData *WebsocketChannelData, data []interface{}) {
|
|
bestBid := data[0].(string)
|
|
bestAsk := data[1].(string)
|
|
timeData, err := strconv.ParseFloat(data[2].(string), 64)
|
|
if err != nil {
|
|
k.Websocket.DataHandler <- err
|
|
return
|
|
}
|
|
|
|
bidVolume := data[3].(string)
|
|
askVolume := data[4].(string)
|
|
sec, dec := math.Modf(timeData)
|
|
spreadTimestamp := time.Unix(int64(sec), int64(dec*(1e9)))
|
|
if k.Verbose {
|
|
log.Debugf(log.ExchangeSys,
|
|
"%v Spread data for '%v' received. Best bid: '%v' Best ask: '%v' Time: '%v', Bid volume '%v', Ask volume '%v'",
|
|
k.Name,
|
|
channelData.Pair,
|
|
bestBid,
|
|
bestAsk,
|
|
spreadTimestamp,
|
|
bidVolume,
|
|
askVolume)
|
|
}
|
|
}
|
|
|
|
// wsProcessTrades converts trade data and sends it to the datahandler
|
|
func (k *Kraken) wsProcessTrades(channelData *WebsocketChannelData, data []interface{}) {
|
|
for i := range data {
|
|
trade := data[i].([]interface{})
|
|
timeData, err := strconv.ParseFloat(trade[2].(string), 64)
|
|
if err != nil {
|
|
k.Websocket.DataHandler <- err
|
|
return
|
|
}
|
|
sec, dec := math.Modf(timeData)
|
|
timeUnix := time.Unix(int64(sec), int64(dec*(1e9)))
|
|
|
|
price, err := strconv.ParseFloat(trade[0].(string), 64)
|
|
if err != nil {
|
|
k.Websocket.DataHandler <- err
|
|
return
|
|
}
|
|
|
|
amount, err := strconv.ParseFloat(trade[1].(string), 64)
|
|
if err != nil {
|
|
k.Websocket.DataHandler <- err
|
|
return
|
|
}
|
|
|
|
k.Websocket.DataHandler <- wshandler.TradeData{
|
|
AssetType: asset.Spot,
|
|
CurrencyPair: channelData.Pair,
|
|
Exchange: k.Name,
|
|
Price: price,
|
|
Amount: amount,
|
|
Timestamp: timeUnix,
|
|
Side: trade[3].(string),
|
|
}
|
|
}
|
|
}
|
|
|
|
// wsProcessOrderBook determines if the orderbook data is partial or update
|
|
// Then sends to appropriate fun
|
|
func (k *Kraken) wsProcessOrderBook(channelData *WebsocketChannelData, data map[string]interface{}) {
|
|
if fullAsk, ok := data["as"].([]interface{}); ok {
|
|
fullBids := data["as"].([]interface{})
|
|
k.wsProcessOrderBookPartial(channelData, fullAsk, fullBids)
|
|
} else {
|
|
askData, asksExist := data["a"].([]interface{})
|
|
bidData, bidsExist := data["b"].([]interface{})
|
|
if asksExist || bidsExist {
|
|
k.wsRequestMtx.Lock()
|
|
defer k.wsRequestMtx.Unlock()
|
|
err := k.wsProcessOrderBookUpdate(channelData, askData, bidData)
|
|
if err != nil {
|
|
subscriptionToRemove := wshandler.WebsocketChannelSubscription{
|
|
Channel: krakenWsOrderbook,
|
|
Currency: channelData.Pair,
|
|
}
|
|
k.Websocket.ResubscribeToChannel(subscriptionToRemove)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// wsProcessOrderBookPartial creates a new orderbook entry for a given currency pair
|
|
func (k *Kraken) wsProcessOrderBookPartial(channelData *WebsocketChannelData, askData, bidData []interface{}) {
|
|
base := orderbook.Base{
|
|
Pair: channelData.Pair,
|
|
AssetType: asset.Spot,
|
|
}
|
|
// Kraken ob data is timestamped per price, GCT orderbook data is
|
|
// timestamped per entry using the highest last update time, we can attempt
|
|
// to respect both within a reasonable degree
|
|
var highestLastUpdate time.Time
|
|
for i := range askData {
|
|
asks := askData[i].([]interface{})
|
|
price, err := strconv.ParseFloat(asks[0].(string), 64)
|
|
if err != nil {
|
|
k.Websocket.DataHandler <- err
|
|
return
|
|
}
|
|
amount, err := strconv.ParseFloat(asks[1].(string), 64)
|
|
if err != nil {
|
|
k.Websocket.DataHandler <- err
|
|
return
|
|
}
|
|
base.Asks = append(base.Asks, orderbook.Item{
|
|
Amount: amount,
|
|
Price: price,
|
|
})
|
|
timeData, err := strconv.ParseFloat(asks[2].(string), 64)
|
|
if err != nil {
|
|
k.Websocket.DataHandler <- err
|
|
return
|
|
}
|
|
sec, dec := math.Modf(timeData)
|
|
askUpdatedTime := time.Unix(int64(sec), int64(dec*(1e9)))
|
|
if highestLastUpdate.Before(askUpdatedTime) {
|
|
highestLastUpdate = askUpdatedTime
|
|
}
|
|
}
|
|
|
|
for i := range bidData {
|
|
bids := bidData[i].([]interface{})
|
|
price, err := strconv.ParseFloat(bids[0].(string), 64)
|
|
if err != nil {
|
|
k.Websocket.DataHandler <- err
|
|
return
|
|
}
|
|
amount, err := strconv.ParseFloat(bids[1].(string), 64)
|
|
if err != nil {
|
|
k.Websocket.DataHandler <- err
|
|
return
|
|
}
|
|
base.Bids = append(base.Bids, orderbook.Item{
|
|
Amount: amount,
|
|
Price: price,
|
|
})
|
|
timeData, err := strconv.ParseFloat(bids[2].(string), 64)
|
|
if err != nil {
|
|
k.Websocket.DataHandler <- err
|
|
return
|
|
}
|
|
sec, dec := math.Modf(timeData)
|
|
bidUpdateTime := time.Unix(int64(sec), int64(dec*(1e9)))
|
|
if highestLastUpdate.Before(bidUpdateTime) {
|
|
highestLastUpdate = bidUpdateTime
|
|
}
|
|
}
|
|
base.LastUpdated = highestLastUpdate
|
|
base.ExchangeName = k.Name
|
|
err := k.Websocket.Orderbook.LoadSnapshot(&base)
|
|
if err != nil {
|
|
k.Websocket.DataHandler <- err
|
|
return
|
|
}
|
|
k.Websocket.DataHandler <- wshandler.WebsocketOrderbookUpdate{
|
|
Exchange: k.Name,
|
|
Asset: asset.Spot,
|
|
Pair: channelData.Pair,
|
|
}
|
|
}
|
|
|
|
// wsProcessOrderBookUpdate updates an orderbook entry for a given currency pair
|
|
func (k *Kraken) wsProcessOrderBookUpdate(channelData *WebsocketChannelData, askData, bidData []interface{}) error {
|
|
update := wsorderbook.WebsocketOrderbookUpdate{
|
|
Asset: asset.Spot,
|
|
Pair: channelData.Pair,
|
|
}
|
|
|
|
var highestLastUpdate time.Time
|
|
// Ask data is not always sent
|
|
for i := range askData {
|
|
asks := askData[i].([]interface{})
|
|
price, err := strconv.ParseFloat(asks[0].(string), 64)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
amount, err := strconv.ParseFloat(asks[1].(string), 64)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
update.Asks = append(update.Asks, orderbook.Item{
|
|
Amount: amount,
|
|
Price: price,
|
|
})
|
|
timeData, err := strconv.ParseFloat(asks[2].(string), 64)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
sec, dec := math.Modf(timeData)
|
|
askUpdatedTime := time.Unix(int64(sec), int64(dec*(1e9)))
|
|
if highestLastUpdate.Before(askUpdatedTime) {
|
|
highestLastUpdate = askUpdatedTime
|
|
}
|
|
}
|
|
|
|
// Bid data is not always sent
|
|
for i := range bidData {
|
|
bids := bidData[i].([]interface{})
|
|
price, err := strconv.ParseFloat(bids[0].(string), 64)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
amount, err := strconv.ParseFloat(bids[1].(string), 64)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
update.Bids = append(update.Bids, orderbook.Item{
|
|
Amount: amount,
|
|
Price: price,
|
|
})
|
|
timeData, err := strconv.ParseFloat(bids[2].(string), 64)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
sec, dec := math.Modf(timeData)
|
|
bidUpdatedTime := time.Unix(int64(sec), int64(dec*(1e9)))
|
|
if highestLastUpdate.Before(bidUpdatedTime) {
|
|
highestLastUpdate = bidUpdatedTime
|
|
}
|
|
}
|
|
update.UpdateTime = highestLastUpdate
|
|
err := k.Websocket.Orderbook.Update(&update)
|
|
if err != nil {
|
|
k.Websocket.DataHandler <- err
|
|
return err
|
|
}
|
|
k.Websocket.DataHandler <- wshandler.WebsocketOrderbookUpdate{
|
|
Exchange: k.Name,
|
|
Asset: asset.Spot,
|
|
Pair: channelData.Pair,
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// wsProcessCandles converts candle data and sends it to the data handler
|
|
func (k *Kraken) wsProcessCandles(channelData *WebsocketChannelData, data []interface{}) {
|
|
startTime, err := strconv.ParseFloat(data[0].(string), 64)
|
|
if err != nil {
|
|
k.Websocket.DataHandler <- err
|
|
return
|
|
}
|
|
sec, dec := math.Modf(startTime)
|
|
startTimeUnix := time.Unix(int64(sec), int64(dec*(1e9)))
|
|
|
|
endTime, err := strconv.ParseFloat(data[1].(string), 64)
|
|
if err != nil {
|
|
k.Websocket.DataHandler <- err
|
|
return
|
|
}
|
|
sec, dec = math.Modf(endTime)
|
|
endTimeUnix := time.Unix(int64(sec), int64(dec*(1e9)))
|
|
|
|
openPrice, err := strconv.ParseFloat(data[2].(string), 64)
|
|
if err != nil {
|
|
k.Websocket.DataHandler <- err
|
|
return
|
|
}
|
|
|
|
highPrice, err := strconv.ParseFloat(data[3].(string), 64)
|
|
if err != nil {
|
|
k.Websocket.DataHandler <- err
|
|
return
|
|
}
|
|
|
|
lowPrice, err := strconv.ParseFloat(data[4].(string), 64)
|
|
if err != nil {
|
|
k.Websocket.DataHandler <- err
|
|
return
|
|
}
|
|
|
|
closePrice, err := strconv.ParseFloat(data[5].(string), 64)
|
|
if err != nil {
|
|
k.Websocket.DataHandler <- err
|
|
return
|
|
}
|
|
|
|
volume, err := strconv.ParseFloat(data[7].(string), 64)
|
|
if err != nil {
|
|
k.Websocket.DataHandler <- err
|
|
return
|
|
}
|
|
|
|
k.Websocket.DataHandler <- wshandler.KlineData{
|
|
AssetType: asset.Spot,
|
|
Pair: channelData.Pair,
|
|
Timestamp: time.Now(),
|
|
Exchange: k.Name,
|
|
StartTime: startTimeUnix,
|
|
CloseTime: endTimeUnix,
|
|
// Candles are sent every 60 seconds
|
|
Interval: "60",
|
|
HighPrice: highPrice,
|
|
LowPrice: lowPrice,
|
|
OpenPrice: openPrice,
|
|
ClosePrice: closePrice,
|
|
Volume: volume,
|
|
}
|
|
}
|
|
|
|
// GenerateDefaultSubscriptions Adds default subscriptions to websocket to be handled by ManageSubscriptions()
|
|
func (k *Kraken) GenerateDefaultSubscriptions() {
|
|
enabledCurrencies := k.GetEnabledPairs(asset.Spot)
|
|
var subscriptions []wshandler.WebsocketChannelSubscription
|
|
for i := range defaultSubscribedChannels {
|
|
for j := range enabledCurrencies {
|
|
enabledCurrencies[j].Delimiter = "/"
|
|
subscriptions = append(subscriptions, wshandler.WebsocketChannelSubscription{
|
|
Channel: defaultSubscribedChannels[i],
|
|
Currency: enabledCurrencies[j],
|
|
})
|
|
}
|
|
}
|
|
k.Websocket.SubscribeToChannels(subscriptions)
|
|
}
|
|
|
|
// GenerateAuthenticatedSubscriptions Adds default subscriptions to websocket to be handled by ManageSubscriptions()
|
|
func (k *Kraken) GenerateAuthenticatedSubscriptions() {
|
|
var subscriptions []wshandler.WebsocketChannelSubscription
|
|
for i := range authenticatedChannels {
|
|
params := make(map[string]interface{})
|
|
subscriptions = append(subscriptions, wshandler.WebsocketChannelSubscription{
|
|
Channel: authenticatedChannels[i],
|
|
Params: params,
|
|
})
|
|
}
|
|
k.Websocket.SubscribeToChannels(subscriptions)
|
|
}
|
|
|
|
// Subscribe sends a websocket message to receive data from the channel
|
|
func (k *Kraken) Subscribe(channelToSubscribe wshandler.WebsocketChannelSubscription) error {
|
|
resp := WebsocketSubscriptionEventRequest{
|
|
Event: krakenWsSubscribe,
|
|
Subscription: WebsocketSubscriptionData{
|
|
Name: channelToSubscribe.Channel,
|
|
},
|
|
RequestID: k.WebsocketConn.GenerateMessageID(false),
|
|
}
|
|
if channelToSubscribe.Channel == "book" {
|
|
// TODO: Add ability to make depth customisable
|
|
resp.Subscription.Depth = 1000
|
|
}
|
|
if !channelToSubscribe.Currency.IsEmpty() {
|
|
resp.Pairs = []string{channelToSubscribe.Currency.String()}
|
|
}
|
|
if channelToSubscribe.Params != nil {
|
|
resp.Subscription.Token = authToken
|
|
}
|
|
|
|
_, err := k.WebsocketConn.SendMessageReturnResponse(resp.RequestID, resp)
|
|
return err
|
|
}
|
|
|
|
// Unsubscribe sends a websocket message to stop receiving data from the channel
|
|
func (k *Kraken) Unsubscribe(channelToSubscribe wshandler.WebsocketChannelSubscription) error {
|
|
resp := WebsocketSubscriptionEventRequest{
|
|
Event: krakenWsUnsubscribe,
|
|
Pairs: []string{channelToSubscribe.Currency.String()},
|
|
Subscription: WebsocketSubscriptionData{
|
|
Name: channelToSubscribe.Channel,
|
|
},
|
|
RequestID: k.WebsocketConn.GenerateMessageID(false),
|
|
}
|
|
_, err := k.WebsocketConn.SendMessageReturnResponse(resp.RequestID, resp)
|
|
return err
|
|
}
|
|
|
|
func (k *Kraken) wsAddOrder(request *WsAddOrderRequest) (string, error) {
|
|
id := k.AuthenticatedWebsocketConn.GenerateMessageID(false)
|
|
request.UserReferenceID = strconv.FormatInt(id, 10)
|
|
request.Event = krakenWsAddOrder
|
|
request.Token = authToken
|
|
jsonResp, err := k.AuthenticatedWebsocketConn.SendMessageReturnResponse(id, request)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
var resp WsAddOrderResponse
|
|
err = json.Unmarshal(jsonResp, &resp)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
if resp.ErrorMessage != "" {
|
|
return "", fmt.Errorf(k.Name + " - " + resp.ErrorMessage)
|
|
}
|
|
return resp.TransactionID, nil
|
|
}
|
|
|
|
func (k *Kraken) wsCancelOrders(orderIDs []string) error {
|
|
request := WsCancelOrderRequest{
|
|
Event: krakenWsCancelOrder,
|
|
Token: authToken,
|
|
TransactionIDs: orderIDs,
|
|
}
|
|
return k.AuthenticatedWebsocketConn.SendMessage(request)
|
|
}
|