mirror of
https://github.com/d0zingcat/gocryptotrader.git
synced 2026-05-13 23:16:45 +00:00
* drop common uuid v4 func and imported package as needed * removed common functions regarding json marshal and unmarshal and used the json package directly. WRT unmarshal it was calling reflect and converted to string which is also checked in the JSON package so it was doing a double up, this will be a tiny gain as it was directly used in the requester package for all our outbound requests. * add in string * explicitly throw away return error value * atleast return the error that websocket initialise returns * return error when not connected * fix comment * Adds comments * move package declarations * drop append whenever we call supported * remove unused import * Change incorrect spelling * fix tests * fix go import issue
336 lines
9.8 KiB
Go
336 lines
9.8 KiB
Go
// Package gemini exchange documentation can be found at
|
|
// https://docs.sandbox.gemini.com
|
|
package gemini
|
|
|
|
import (
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"net/http"
|
|
"net/url"
|
|
"strings"
|
|
"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/order"
|
|
"github.com/thrasher-corp/gocryptotrader/exchanges/orderbook"
|
|
"github.com/thrasher-corp/gocryptotrader/exchanges/websocket/wshandler"
|
|
"github.com/thrasher-corp/gocryptotrader/exchanges/websocket/wsorderbook"
|
|
log "github.com/thrasher-corp/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(log.ExchangeSys, "%v - authentication failed: %v\n", 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.GetEnabledPairs(asset.Spot)
|
|
for i, c := range enabledCurrencies {
|
|
val := url.Values{}
|
|
val.Set("heartbeat", "true")
|
|
endpoint := fmt.Sprintf("%s%s/%s?%s",
|
|
g.API.Endpoints.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 := json.Marshal(payload)
|
|
if err != nil {
|
|
return fmt.Errorf("%v sendAuthenticatedHTTPRequest: Unable to JSON request", g.Name)
|
|
}
|
|
|
|
endpoint := fmt.Sprintf("%v%v", g.API.Endpoints.WebsocketURL, url)
|
|
PayloadBase64 := crypto.Base64Encode(PayloadJSON)
|
|
hmac := crypto.GetHMAC(crypto.HashSHA512_384, []byte(PayloadBase64), []byte(g.API.Credentials.Secret))
|
|
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.API.Credentials.Key)
|
|
headers.Add("X-GEMINI-SIGNATURE", crypto.HexEncodeToString(hmac))
|
|
headers.Add("Cache-Control", "no-cache")
|
|
|
|
g.AuthenticatedWebsocketConn = &wshandler.WebsocketConnection{
|
|
ExchangeName: g.Name,
|
|
URL: endpoint,
|
|
Verbose: g.Verbose,
|
|
ResponseCheckTimeout: responseCheckTimeout,
|
|
ResponseMaxLimit: responseMaxLimit,
|
|
}
|
|
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 := json.Unmarshal(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 := json.Unmarshal(resp.Raw, &result)
|
|
if err != nil {
|
|
g.Websocket.DataHandler <- err
|
|
continue
|
|
}
|
|
g.Websocket.DataHandler <- result
|
|
case "initial":
|
|
var result WsSubscriptionAcknowledgementResponse
|
|
err := json.Unmarshal(resp.Raw, &result)
|
|
if err != nil {
|
|
g.Websocket.DataHandler <- err
|
|
continue
|
|
}
|
|
g.Websocket.DataHandler <- result
|
|
case "accepted":
|
|
var result WsActiveOrdersResponse
|
|
err := json.Unmarshal(resp.Raw, &result)
|
|
if err != nil {
|
|
g.Websocket.DataHandler <- err
|
|
continue
|
|
}
|
|
g.Websocket.DataHandler <- result
|
|
case "booked":
|
|
var result WsOrderBookedResponse
|
|
err := json.Unmarshal(resp.Raw, &result)
|
|
if err != nil {
|
|
g.Websocket.DataHandler <- err
|
|
continue
|
|
}
|
|
g.Websocket.DataHandler <- result
|
|
case "fill":
|
|
var result WsOrderFilledResponse
|
|
err := json.Unmarshal(resp.Raw, &result)
|
|
if err != nil {
|
|
g.Websocket.DataHandler <- err
|
|
continue
|
|
}
|
|
g.Websocket.DataHandler <- result
|
|
case "cancelled":
|
|
var result WsOrderCancelledResponse
|
|
err := json.Unmarshal(resp.Raw, &result)
|
|
if err != nil {
|
|
g.Websocket.DataHandler <- err
|
|
continue
|
|
}
|
|
g.Websocket.DataHandler <- result
|
|
case "closed":
|
|
var result WsOrderClosedResponse
|
|
err := json.Unmarshal(resp.Raw, &result)
|
|
if err != nil {
|
|
g.Websocket.DataHandler <- err
|
|
continue
|
|
}
|
|
g.Websocket.DataHandler <- result
|
|
case "heartbeat":
|
|
var result WsHeartbeatResponse
|
|
err := json.Unmarshal(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 := json.Unmarshal(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 i := range result.Events {
|
|
if result.Events[i].Reason != "initial" {
|
|
g.Websocket.DataHandler <- errors.New("gemini_websocket.go orderbook should be snapshot only")
|
|
continue
|
|
}
|
|
if result.Events[i].Side == "ask" {
|
|
asks = append(asks, orderbook.Item{
|
|
Amount: result.Events[i].Remaining,
|
|
Price: result.Events[i].Price,
|
|
})
|
|
} else {
|
|
bids = append(bids, orderbook.Item{
|
|
Amount: result.Events[i].Remaining,
|
|
Price: result.Events[i].Price,
|
|
})
|
|
}
|
|
}
|
|
var newOrderBook orderbook.Base
|
|
newOrderBook.Asks = asks
|
|
newOrderBook.Bids = bids
|
|
newOrderBook.AssetType = asset.Spot
|
|
newOrderBook.Pair = pair
|
|
newOrderBook.ExchangeName = g.Name
|
|
err := g.Websocket.Orderbook.LoadSnapshot(&newOrderBook)
|
|
if err != nil {
|
|
g.Websocket.DataHandler <- err
|
|
return
|
|
}
|
|
g.Websocket.DataHandler <- wshandler.WebsocketOrderbookUpdate{Pair: pair,
|
|
Asset: asset.Spot,
|
|
Exchange: g.Name}
|
|
} else {
|
|
var asks, bids []orderbook.Item
|
|
for i := 0; i < len(result.Events); i++ {
|
|
if result.Events[i].Type == "trade" {
|
|
g.Websocket.DataHandler <- wshandler.TradeData{
|
|
Timestamp: time.Now(),
|
|
CurrencyPair: pair,
|
|
AssetType: asset.Spot,
|
|
Exchange: g.Name,
|
|
EventTime: result.Timestamp,
|
|
Price: result.Events[i].Price,
|
|
Amount: result.Events[i].Amount,
|
|
Side: result.Events[i].MakerSide,
|
|
}
|
|
} else {
|
|
item := orderbook.Item{
|
|
Amount: result.Events[i].Remaining,
|
|
Price: result.Events[i].Price,
|
|
}
|
|
if strings.EqualFold(result.Events[i].Side, order.Ask.String()) {
|
|
asks = append(asks, item)
|
|
} else {
|
|
bids = append(bids, item)
|
|
}
|
|
}
|
|
}
|
|
err := g.Websocket.Orderbook.Update(&wsorderbook.WebsocketOrderbookUpdate{
|
|
Asks: asks,
|
|
Bids: bids,
|
|
Pair: pair,
|
|
UpdateTime: time.Unix(0, result.TimestampMS),
|
|
Asset: asset.Spot,
|
|
})
|
|
if err != nil {
|
|
g.Websocket.DataHandler <- fmt.Errorf("%v %v", g.Name, err)
|
|
}
|
|
g.Websocket.DataHandler <- wshandler.WebsocketOrderbookUpdate{Pair: pair,
|
|
Asset: asset.Spot,
|
|
Exchange: g.Name}
|
|
}
|
|
}
|