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