mirror of
https://github.com/d0zingcat/gocryptotrader.git
synced 2026-05-18 15:10:03 +00:00
* Establishes new websocket functionality. Begins the creation of the websocket request * Adding a wrapper over gorilla websocket connect,send,receive to handle ID messages. Doesn't work * Successfully moved exchange_websocket into its own wshandler namespace. oof * Sets up ZB to use a round trip WS request system * Adds Kraken ID support to subscriptions. Renames duplicate func name UnsubscribeToChannels to RemoveSubscribedChannels. Adds some helper methods in the WebsocketConn to reduce duplicate code. Cleans up ZB implementation * Fixes double locking which caused no websocket data to be read. Fixes requestid for kraken subscriptions * Completes Huobi and Hadax implementation. Extends ZB error handling. Adds GZip support for reading messages * Adds HitBTC support. Adds GetCurrencies, GetSymbols, GetTrades WS funcs. Adds super fun new parameter to GenerateMessageID for Unix and UnixNano * Adds GateIO id support * Adds Coinut support. Prevents nil reference error in constatus when there isnt one * Standardises all Exchange websockets to use the wshandler websocket. Removes the wsRequestMtx as wshandler handles that now. Makes the Dialer a dialer, its not externally referenced that I can see. * Fixes issue with coinut implementation. Updates bitmex currencies. Removes redundant log messages which are used to log messages * Starts testing. Renames files * Adds tests for websocket connection * Reverts request.go change * Linting everything * Fixes rebase issue * Final changes. Fixes variable names, removes log.Debug, removes lines, rearranges test types, removes order correlation websocket type * Final final commit, fixing ZB issues. * Adds traffic alerts where missed. Changes empty struct pointer addresses to nil instead. Removes empty lines * Fixed string conversion * Fixes issue with ZB not sending success codes * Fixes issue with coinut processing due to nonce handling with subscriptions * Fixes issue where ZB test failure was not caught. Removes unnecessary error handling from other ZB tests * Removes unused interface * Renames wshandler.Init() to wshandler.Run() * Updates template file * Capitalises cryptocurrencies in struct. Moves websocketResponseCheckTimeout and websocketResponseMaxLimit into config options. Moves connection configuration to main exchange Setup (where appropriate). Reverts currencylastupdated ticks. Improves reader close error checking * Fixes two inconsistent websocket delay times * Creates a default variable for websocket ResponseMaxLimit and ResponseCheckTimeout, then applies it to setdefaults and all tests * Updates exchange template to set and use default websocket response limits
339 lines
9.2 KiB
Go
339 lines
9.2 KiB
Go
// Package gemini exchange documentation can be found at
|
|
// https://docs.sandbox.gemini.com
|
|
package gemini
|
|
|
|
import (
|
|
"errors"
|
|
"fmt"
|
|
"net/http"
|
|
"net/url"
|
|
"time"
|
|
|
|
"github.com/gorilla/websocket"
|
|
"github.com/thrasher-/gocryptotrader/common"
|
|
"github.com/thrasher-/gocryptotrader/currency"
|
|
exchange "github.com/thrasher-/gocryptotrader/exchanges"
|
|
"github.com/thrasher-/gocryptotrader/exchanges/orderbook"
|
|
"github.com/thrasher-/gocryptotrader/exchanges/wshandler"
|
|
log "github.com/thrasher-/gocryptotrader/logger"
|
|
)
|
|
|
|
const (
|
|
geminiWebsocketEndpoint = "wss://api.gemini.com/v1/"
|
|
geminiWebsocketSandboxEndpoint = "wss://api.sandbox.gemini.com/v1/"
|
|
geminiWsEvent = "event"
|
|
geminiWsMarketData = "marketdata"
|
|
geminiWsOrderEvents = "order/events"
|
|
)
|
|
|
|
// Instantiates a communications channel between websocket connections
|
|
var comms = make(chan ReadData, 1)
|
|
var responseMaxLimit time.Duration
|
|
var responseCheckTimeout time.Duration
|
|
|
|
// WsConnect initiates a websocket connection
|
|
func (g *Gemini) WsConnect() error {
|
|
if !g.Websocket.IsEnabled() || !g.IsEnabled() {
|
|
return errors.New(wshandler.WebsocketNotEnabled)
|
|
}
|
|
|
|
var dialer websocket.Dialer
|
|
if g.Websocket.GetProxyAddress() != "" {
|
|
proxy, err := url.Parse(g.Websocket.GetProxyAddress())
|
|
if err != nil {
|
|
return err
|
|
}
|
|
dialer.Proxy = http.ProxyURL(proxy)
|
|
}
|
|
|
|
go g.WsHandleData()
|
|
err := g.WsSecureSubscribe(&dialer, geminiWsOrderEvents)
|
|
if err != nil {
|
|
log.Errorf("%v - authentication failed: %v", g.Name, err)
|
|
}
|
|
return g.WsSubscribe(&dialer)
|
|
}
|
|
|
|
// WsSubscribe subscribes to the full websocket suite on gemini exchange
|
|
func (g *Gemini) WsSubscribe(dialer *websocket.Dialer) error {
|
|
enabledCurrencies := g.GetEnabledCurrencies()
|
|
for i, c := range enabledCurrencies {
|
|
val := url.Values{}
|
|
val.Set("heartbeat", "true")
|
|
endpoint := fmt.Sprintf("%s%s/%s?%s",
|
|
g.WebsocketURL,
|
|
geminiWsMarketData,
|
|
c.String(),
|
|
val.Encode())
|
|
connection := &wshandler.WebsocketConnection{
|
|
ExchangeName: g.Name,
|
|
URL: endpoint,
|
|
Verbose: g.Verbose,
|
|
ResponseCheckTimeout: responseCheckTimeout,
|
|
ResponseMaxLimit: responseMaxLimit,
|
|
}
|
|
err := connection.Dial(dialer, http.Header{})
|
|
if err != nil {
|
|
return fmt.Errorf("%v Websocket connection %v error. Error %v", g.Name, endpoint, err)
|
|
}
|
|
go g.WsReadData(connection, c)
|
|
if len(enabledCurrencies)-1 == i {
|
|
return nil
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// WsSecureSubscribe will connect to Gemini's secure endpoint
|
|
func (g *Gemini) WsSecureSubscribe(dialer *websocket.Dialer, url string) error {
|
|
if !g.GetAuthenticatedAPISupport(exchange.WebsocketAuthentication) {
|
|
return fmt.Errorf("%v AuthenticatedWebsocketAPISupport not enabled", g.Name)
|
|
}
|
|
payload := WsRequestPayload{
|
|
Request: fmt.Sprintf("/v1/%v", url),
|
|
Nonce: time.Now().UnixNano(),
|
|
}
|
|
PayloadJSON, err := common.JSONEncode(payload)
|
|
if err != nil {
|
|
return fmt.Errorf("%v sendAuthenticatedHTTPRequest: Unable to JSON request", g.Name)
|
|
}
|
|
|
|
endpoint := fmt.Sprintf("%v%v", g.WebsocketURL, url)
|
|
PayloadBase64 := common.Base64Encode(PayloadJSON)
|
|
hmac := common.GetHMAC(common.HashSHA512_384, []byte(PayloadBase64), []byte(g.APISecret))
|
|
headers := http.Header{}
|
|
headers.Add("Content-Length", "0")
|
|
headers.Add("Content-Type", "text/plain")
|
|
headers.Add("X-GEMINI-PAYLOAD", PayloadBase64)
|
|
headers.Add("X-GEMINI-APIKEY", g.APIKey)
|
|
headers.Add("X-GEMINI-SIGNATURE", common.HexEncodeToString(hmac))
|
|
headers.Add("Cache-Control", "no-cache")
|
|
|
|
g.AuthenticatedWebsocketConn = &wshandler.WebsocketConnection{
|
|
ExchangeName: g.Name,
|
|
URL: endpoint,
|
|
Verbose: g.Verbose,
|
|
}
|
|
err = g.AuthenticatedWebsocketConn.Dial(dialer, headers)
|
|
if err != nil {
|
|
return fmt.Errorf("%v Websocket connection %v error. Error %v", g.Name, endpoint, err)
|
|
}
|
|
go g.WsReadData(g.AuthenticatedWebsocketConn, currency.Pair{})
|
|
return nil
|
|
}
|
|
|
|
// WsReadData reads from the websocket connection and returns the websocket
|
|
// response
|
|
func (g *Gemini) WsReadData(ws *wshandler.WebsocketConnection, c currency.Pair) {
|
|
g.Websocket.Wg.Add(1)
|
|
defer g.Websocket.Wg.Done()
|
|
for {
|
|
select {
|
|
case <-g.Websocket.ShutdownC:
|
|
return
|
|
default:
|
|
resp, err := ws.ReadMessage()
|
|
if err != nil {
|
|
g.Websocket.DataHandler <- err
|
|
return
|
|
}
|
|
g.Websocket.TrafficAlert <- struct{}{}
|
|
comms <- ReadData{Raw: resp.Raw, Currency: c}
|
|
}
|
|
}
|
|
}
|
|
|
|
// WsHandleData handles all the websocket data coming from the websocket
|
|
// connection
|
|
func (g *Gemini) WsHandleData() {
|
|
g.Websocket.Wg.Add(1)
|
|
defer g.Websocket.Wg.Done()
|
|
for {
|
|
select {
|
|
case <-g.Websocket.ShutdownC:
|
|
return
|
|
case resp := <-comms:
|
|
// Gemini likes to send empty arrays
|
|
if string(resp.Raw) == "[]" {
|
|
continue
|
|
}
|
|
var result map[string]interface{}
|
|
err := common.JSONDecode(resp.Raw, &result)
|
|
if err != nil {
|
|
g.Websocket.DataHandler <- fmt.Errorf("%v Error: %v, Raw: %v", g.Name, err, string(resp.Raw))
|
|
continue
|
|
}
|
|
switch result["type"] {
|
|
case "subscription_ack":
|
|
var result WsSubscriptionAcknowledgementResponse
|
|
err := common.JSONDecode(resp.Raw, &result)
|
|
if err != nil {
|
|
g.Websocket.DataHandler <- err
|
|
continue
|
|
}
|
|
g.Websocket.DataHandler <- result
|
|
case "initial":
|
|
var result WsSubscriptionAcknowledgementResponse
|
|
err := common.JSONDecode(resp.Raw, &result)
|
|
if err != nil {
|
|
g.Websocket.DataHandler <- err
|
|
continue
|
|
}
|
|
g.Websocket.DataHandler <- result
|
|
case "accepted":
|
|
var result WsActiveOrdersResponse
|
|
err := common.JSONDecode(resp.Raw, &result)
|
|
if err != nil {
|
|
g.Websocket.DataHandler <- err
|
|
continue
|
|
}
|
|
g.Websocket.DataHandler <- result
|
|
case "booked":
|
|
var result WsOrderBookedResponse
|
|
err := common.JSONDecode(resp.Raw, &result)
|
|
if err != nil {
|
|
g.Websocket.DataHandler <- err
|
|
continue
|
|
}
|
|
g.Websocket.DataHandler <- result
|
|
case "fill":
|
|
var result WsOrderFilledResponse
|
|
err := common.JSONDecode(resp.Raw, &result)
|
|
if err != nil {
|
|
g.Websocket.DataHandler <- err
|
|
continue
|
|
}
|
|
g.Websocket.DataHandler <- result
|
|
case "cancelled":
|
|
var result WsOrderCancelledResponse
|
|
err := common.JSONDecode(resp.Raw, &result)
|
|
if err != nil {
|
|
g.Websocket.DataHandler <- err
|
|
continue
|
|
}
|
|
g.Websocket.DataHandler <- result
|
|
case "closed":
|
|
var result WsOrderClosedResponse
|
|
err := common.JSONDecode(resp.Raw, &result)
|
|
if err != nil {
|
|
g.Websocket.DataHandler <- err
|
|
continue
|
|
}
|
|
g.Websocket.DataHandler <- result
|
|
case "heartbeat":
|
|
var result WsHeartbeatResponse
|
|
err := common.JSONDecode(resp.Raw, &result)
|
|
if err != nil {
|
|
g.Websocket.DataHandler <- err
|
|
continue
|
|
}
|
|
g.Websocket.DataHandler <- result
|
|
case "update":
|
|
if resp.Currency.IsEmpty() {
|
|
g.Websocket.DataHandler <- fmt.Errorf("%v - unhandled data %s",
|
|
g.Name, resp.Raw)
|
|
continue
|
|
}
|
|
var marketUpdate WsMarketUpdateResponse
|
|
err := common.JSONDecode(resp.Raw, &marketUpdate)
|
|
if err != nil {
|
|
g.Websocket.DataHandler <- err
|
|
continue
|
|
}
|
|
g.wsProcessUpdate(marketUpdate, resp.Currency)
|
|
default:
|
|
g.Websocket.DataHandler <- fmt.Errorf("%v - unhandled data %s",
|
|
g.Name, resp.Raw)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// wsProcessUpdate handles order book data
|
|
func (g *Gemini) wsProcessUpdate(result WsMarketUpdateResponse, pair currency.Pair) {
|
|
if result.Timestamp == 0 && result.TimestampMS == 0 {
|
|
var bids, asks []orderbook.Item
|
|
for _, event := range result.Events {
|
|
if event.Reason != "initial" {
|
|
g.Websocket.DataHandler <- errors.New("gemini_websocket.go orderbook should be snapshot only")
|
|
continue
|
|
}
|
|
|
|
if event.Side == "ask" {
|
|
asks = append(asks, orderbook.Item{
|
|
Amount: event.Remaining,
|
|
Price: event.Price,
|
|
})
|
|
} else {
|
|
bids = append(bids, orderbook.Item{
|
|
Amount: event.Remaining,
|
|
Price: event.Price,
|
|
})
|
|
}
|
|
}
|
|
|
|
var newOrderBook orderbook.Base
|
|
newOrderBook.Asks = asks
|
|
newOrderBook.Bids = bids
|
|
newOrderBook.AssetType = "SPOT"
|
|
newOrderBook.Pair = pair
|
|
|
|
err := g.Websocket.Orderbook.LoadSnapshot(&newOrderBook,
|
|
g.GetName(),
|
|
false)
|
|
if err != nil {
|
|
g.Websocket.DataHandler <- err
|
|
return
|
|
}
|
|
|
|
g.Websocket.DataHandler <- wshandler.WebsocketOrderbookUpdate{Pair: pair,
|
|
Asset: "SPOT",
|
|
Exchange: g.GetName()}
|
|
} else {
|
|
for _, event := range result.Events {
|
|
if event.Type == "trade" {
|
|
g.Websocket.DataHandler <- wshandler.TradeData{
|
|
Timestamp: time.Now(),
|
|
CurrencyPair: pair,
|
|
AssetType: "SPOT",
|
|
Exchange: g.Name,
|
|
EventTime: result.Timestamp,
|
|
Price: event.Price,
|
|
Amount: event.Amount,
|
|
Side: event.MakerSide,
|
|
}
|
|
|
|
} else {
|
|
var i orderbook.Item
|
|
i.Amount = event.Remaining
|
|
i.Price = event.Price
|
|
if event.Side == "ask" {
|
|
err := g.Websocket.Orderbook.Update(nil,
|
|
[]orderbook.Item{i},
|
|
pair,
|
|
time.Now(),
|
|
g.GetName(),
|
|
"SPOT")
|
|
if err != nil {
|
|
g.Websocket.DataHandler <- err
|
|
}
|
|
} else {
|
|
err := g.Websocket.Orderbook.Update([]orderbook.Item{i},
|
|
nil,
|
|
pair,
|
|
time.Now(),
|
|
g.GetName(),
|
|
"SPOT")
|
|
if err != nil {
|
|
g.Websocket.DataHandler <- err
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
g.Websocket.DataHandler <- wshandler.WebsocketOrderbookUpdate{Pair: pair,
|
|
Asset: "SPOT",
|
|
Exchange: g.GetName()}
|
|
}
|
|
}
|