From 1f6f160c23c4e8dcf702188d1beba5faa2e2c907 Mon Sep 17 00:00:00 2001 From: Adrian Gallagher Date: Mon, 15 Jun 2015 21:29:48 +1000 Subject: [PATCH] Added Gemini Beta Exchange HTTP support. --- README.md | 1 + config_example.json | 13 ++ geminihttp.go | 337 ++++++++++++++++++++++++++++++++++++++++++++ main.go | 16 +++ 4 files changed, 367 insertions(+) create mode 100644 geminihttp.go diff --git a/README.md b/README.md index f4e5e379..cdb4eca6 100644 --- a/README.md +++ b/README.md @@ -16,6 +16,7 @@ A cryptocurrency trading bot supporting multiple exchanges written in Golang. | Coinbase | Yes | Yes | No| | Cryptsy | Yes | Yes | NA| | DWVX | Yes | Yes | NA | +| Gemini | Yes | NA | NA | | Huobi | Yes | Yes |No | ItBit | Yes | NA | NA | | Kraken | Yes | NA | NA diff --git a/config_example.json b/config_example.json index 4eb806ed..fb009d27 100644 --- a/config_example.json +++ b/config_example.json @@ -130,6 +130,19 @@ "EnabledPairs": "BTCAUD", "BaseCurrencies": "AUD" }, + { + "Name": "Gemini", + "Enabled": true, + "Verbose": false, + "Websocket": false, + "RESTPollingDelay": 10, + "AuthenticatedAPISupport": false, + "APIKey": "Key", + "APISecret": "Secret", + "AvailablePairs": "BTCUSD", + "EnabledPairs": "BTCUSD", + "BaseCurrencies": "USD" + }, { "Name": "Huobi", "Enabled": true, diff --git a/geminihttp.go b/geminihttp.go new file mode 100644 index 00000000..a9b2f25e --- /dev/null +++ b/geminihttp.go @@ -0,0 +1,337 @@ +package main + +import ( + "errors" + "fmt" + "log" + "net/url" + "strconv" + "strings" + "time" +) + +const ( + GEMINI_API_URL = "https://api.gemini.com" + GEMINI_API_VERSION = "1" + + GEMINI_SYMBOLS = "symbols" + GEMINI_ORDERBOOK = "book" + GEMINI_TRADES = "trades" + GEMINI_ORDERS = "orders" + GEMINI_ORDER_NEW = "order/new" + GEMINI_ORDER_CANCEL = "order/cancel" + GEMINI_ORDER_CANCEL_MULTI = "order/cancel/multi" + GEMINI_ORDER_CANCEL_ALL = "order/cancel/all" + GEMINI_ORDER_STATUS = "order/status" + GEMINI_MYTRADES = "mytrades" + GEMINI_BALANCES = "balances" +) + +type Gemini struct { + Name string + Enabled bool + Verbose bool + Websocket bool + RESTPollingDelay time.Duration + AuthenticatedAPISupport bool + APIKey, APISecret string + BaseCurrencies []string + AvailablePairs []string + EnabledPairs []string +} + +type GeminiOrderbookEntry struct { + Price float64 `json:"price,string"` + Quantity float64 `json:"quantity,string"` +} + +type GeminiOrderbook struct { + Bids []GeminiOrderbookEntry `json:"bids"` + Asks []GeminiOrderbookEntry `json:"asks"` +} + +type GeminiTrade struct { + Timestamp int64 `json:"timestamp"` + TID int64 `json:"tid"` + Price float64 `json:"price"` + Amount float64 `json:"amount"` + Side string `json:"taker"` +} + +type GeminiOrder struct { + OrderID int64 `json:"order_id"` + Symbol string `json:"symbol"` + Price float64 `json:"price,string"` + AvgExecutionPrice float64 `json:"avg_execution_price,string"` + Side string `json:"side"` + Type string `json:"type"` + Timestamp int64 `json:"timestamp"` + IsLive bool `json:"is_live"` + IsCancelled bool `json:"is_cancelled"` + ExecutedAmount float64 `json:"executed_amount,string"` + RemainingAmount float64 `json:"remaining_amount,string"` + OriginalAmount float64 `json:"original_amount,string"` +} + +type GeminiOrderResult struct { + Result bool `json:"result"` +} + +type GeminiTradeHistory struct { + Price float64 `json:"price"` + Amount float64 `json:"amount"` + Timestamp int64 `json:"timestamp"` + Type string `json:"type"` + FeeCurrency string `json:"fee_currency"` + FeeAmount float64 `json:"fee_amount"` + TID int64 `json:"tid"` + OrderID int64 `json:"order_id"` +} + +type GeminiBalance struct { + Currency string `json:"currency"` + Amount float64 `json:"amount"` + Available float64 `json:"available"` +} + +func (g *Gemini) SetDefaults() { + g.Name = "Gemini" + g.Enabled = true + g.Verbose = false + g.Websocket = false + g.RESTPollingDelay = 10 +} + +func (g *Gemini) GetName() string { + return g.Name +} + +func (g *Gemini) SetEnabled(enabled bool) { + g.Enabled = enabled +} + +func (g *Gemini) IsEnabled() bool { + return g.Enabled +} + +func (g *Gemini) SetAPIKeys(apiKey, apiSecret string) { + g.APIKey = apiKey + g.APISecret = apiSecret +} + +func (g *Gemini) Run() { + if g.Verbose { + log.Printf("%s polling delay: %ds.\n", g.GetName(), g.RESTPollingDelay) + log.Printf("%s %d currencies enabled: %s.\n", g.GetName(), len(g.EnabledPairs), g.EnabledPairs) + } + + exchangeProducts, err := g.GetSymbols() + if err != nil { + log.Printf("%s Failed to get available symbols.\n", g.GetName()) + } else { + exchangeProducts = SplitStrings(StringToUpper(JoinStrings(exchangeProducts, ",")), ",") + diff := StringSliceDifference(g.AvailablePairs, exchangeProducts) + if len(diff) > 0 { + exch, err := GetExchangeConfig(g.Name) + if err != nil { + log.Println(err) + } else { + log.Printf("%s Updating available pairs. Difference: %s.\n", g.Name, diff) + exch.AvailablePairs = JoinStrings(exchangeProducts, ",") + UpdateExchangeConfig(exch) + } + } + } + + for g.Enabled { + /* Ticker has not been implemented yet + for _, x := range g.EnabledPairs { + currency := x + log.Println(currency) + go func() { + //ticker := g.GetTicker(currency) + //log.Printf("Gemini %s Last %f High %f Low %f Volume %f\n", currency, ticker.Last, ticker.High, ticker.Low, ticker.Volume) + //AddExchangeInfo(g.GetName(), currency[0:3], currency[3:], ticker.Last, ticker.Volume) + }() + } + */ + time.Sleep(time.Second * g.RESTPollingDelay) + } +} + +func (g *Gemini) GetSymbols() ([]string, error) { + symbols := []string{} + path := fmt.Sprintf("%s/v%s/%s", GEMINI_API_URL, GEMINI_API_VERSION, GEMINI_SYMBOLS) + err := SendHTTPGetRequest(path, true, &symbols) + if err != nil { + return nil, err + } + return symbols, nil +} + +func (g *Gemini) GetOrderbook(currency string, params url.Values) (GeminiOrderbook, error) { + orderbook := GeminiOrderbook{} + path := fmt.Sprintf("%s/v%s/%s/%s", GEMINI_API_URL, GEMINI_API_VERSION, GEMINI_ORDERBOOK, currency) + + if params.Encode() != "" { + path += "?" + params.Encode() + } + + err := SendHTTPGetRequest(path, true, &orderbook) + if err != nil { + return GeminiOrderbook{}, err + } + + return orderbook, nil +} + +func (g *Gemini) GetTrades(currency string, params url.Values) ([]GeminiTrade, error) { + trades := []GeminiTrade{} + path := fmt.Sprintf("%s/v%s/%s/%s", GEMINI_API_URL, GEMINI_API_VERSION, GEMINI_TRADES, currency) + + if params.Encode() != "" { + path += "?" + params.Encode() + } + + err := SendHTTPGetRequest(path, true, &trades) + if err != nil { + return []GeminiTrade{}, err + } + + return trades, nil +} + +func (g *Gemini) NewOrder(symbol string, amount, price float64, side, orderType string) (int64, error) { + request := make(map[string]interface{}) + request["symbol"] = symbol + request["amount"] = strconv.FormatFloat(amount, 'f', -1, 64) + request["price"] = strconv.FormatFloat(price, 'f', -1, 64) + request["side"] = side + request["type"] = orderType + + response := GeminiOrder{} + err := g.SendAuthenticatedHTTPRequest("POST", GEMINI_ORDER_NEW, request, &response) + if err != nil { + return 0, err + } + return response.OrderID, nil +} + +func (g *Gemini) CancelOrder(OrderID int64) (GeminiOrder, error) { + request := make(map[string]interface{}) + request["order_id"] = OrderID + + response := GeminiOrder{} + err := g.SendAuthenticatedHTTPRequest("POST", GEMINI_ORDER_CANCEL, request, &response) + if err != nil { + return GeminiOrder{}, err + } + return response, nil +} + +func (g *Gemini) CancelMultipleOrders(orders []int64) ([]GeminiOrderResult, error) { + request := make(map[string]interface{}) + request["order_ids"] = orders + + response := []GeminiOrderResult{} + err := g.SendAuthenticatedHTTPRequest("POST", GEMINI_ORDER_CANCEL_MULTI, request, &response) + if err != nil { + return nil, err + } + return response, nil +} + +func (g *Gemini) CancelAllOrders() ([]GeminiOrderResult, error) { + response := []GeminiOrderResult{} + err := g.SendAuthenticatedHTTPRequest("POST", GEMINI_ORDER_CANCEL_ALL, nil, &response) + if err != nil { + return nil, err + } + return response, nil +} + +func (g *Gemini) GetOrderStatus(orderID int64) (GeminiOrder, error) { + request := make(map[string]interface{}) + request["order_id"] = orderID + + response := GeminiOrder{} + err := g.SendAuthenticatedHTTPRequest("POST", GEMINI_ORDER_STATUS, request, &response) + if err != nil { + return GeminiOrder{}, err + } + return response, nil +} + +func (g *Gemini) GetOrders() ([]GeminiOrder, error) { + response := []GeminiOrder{} + err := g.SendAuthenticatedHTTPRequest("POST", GEMINI_ORDERS, nil, &response) + if err != nil { + return nil, err + } + return response, nil +} + +func (g *Gemini) GetTradeHistory(symbol string, timestamp int64) ([]GeminiTradeHistory, error) { + request := make(map[string]interface{}) + request["symbol"] = symbol + request["timestamp"] = timestamp + + response := []GeminiTradeHistory{} + err := g.SendAuthenticatedHTTPRequest("POST", GEMINI_MYTRADES, request, &response) + if err != nil { + return nil, err + } + return response, nil +} + +func (g *Gemini) GetBalances() ([]GeminiBalance, error) { + response := []GeminiBalance{} + err := g.SendAuthenticatedHTTPRequest("POST", GEMINI_BALANCES, nil, &response) + if err != nil { + return nil, err + } + return response, nil +} + +func (g *Gemini) SendAuthenticatedHTTPRequest(method, path string, params map[string]interface{}, result interface{}) (err error) { + request := make(map[string]interface{}) + request["request"] = fmt.Sprintf("/v%s/%s", GEMINI_API_VERSION, path) + request["nonce"] = time.Now().UnixNano() + + if params != nil { + for key, value := range params { + request[key] = value + } + } + + PayloadJson, err := JSONEncode(request) + + if err != nil { + return errors.New("SendAuthenticatedHTTPRequest: Unable to JSON request") + } + + if g.Verbose { + log.Printf("Request JSON: %s\n", PayloadJson) + } + + PayloadBase64 := Base64Encode(PayloadJson) + hmac := GetHMAC(HASH_SHA512_384, []byte(PayloadBase64), []byte(g.APISecret)) + headers := make(map[string]string) + headers["X-GEMINI-APIKEY"] = g.APIKey + headers["X-GEMINI-PAYLOAD"] = PayloadBase64 + headers["X-GEMINI-SIGNATURE"] = HexEncodeToString(hmac) + + resp, err := SendHTTPRequest(method, BITFINEX_API_URL+path, headers, strings.NewReader("")) + + if g.Verbose { + log.Printf("Recieved raw: \n%s\n", resp) + } + + err = JSONDecode([]byte(resp), &result) + + if err != nil { + return errors.New("Unable to JSON Unmarshal response.") + } + + return nil +} diff --git a/main.go b/main.go index 894fe06f..0892c421 100644 --- a/main.go +++ b/main.go @@ -20,6 +20,7 @@ type Exchange struct { coinbase Coinbase cryptsy Cryptsy dwvx DWVX + gemini Gemini okcoinChina OKCoin okcoinIntl OKCoin itbit ItBit @@ -101,6 +102,7 @@ func main() { bot.exchange.coinbase.SetDefaults() bot.exchange.cryptsy.SetDefaults() bot.exchange.dwvx.SetDefaults() + bot.exchange.gemini.SetDefaults() bot.exchange.okcoinChina.SetURL(OKCOIN_API_URL_CHINA) bot.exchange.okcoinChina.SetDefaults() bot.exchange.okcoinIntl.SetURL(OKCOIN_API_URL) @@ -248,6 +250,20 @@ func main() { bot.exchange.dwvx.EnabledPairs = SplitStrings(exch.EnabledPairs, ",") go bot.exchange.dwvx.Run() } + } else if bot.exchange.gemini.GetName() == exch.Name { + if !exch.Enabled { + bot.exchange.gemini.SetEnabled(false) + } else { + bot.exchange.gemini.AuthenticatedAPISupport = exch.AuthenticatedAPISupport + bot.exchange.gemini.SetAPIKeys(exch.APIKey, exch.APISecret) + bot.exchange.gemini.RESTPollingDelay = exch.RESTPollingDelay + bot.exchange.gemini.Verbose = exch.Verbose + bot.exchange.gemini.Websocket = exch.Websocket + bot.exchange.gemini.BaseCurrencies = SplitStrings(exch.BaseCurrencies, ",") + bot.exchange.gemini.AvailablePairs = SplitStrings(exch.AvailablePairs, ",") + bot.exchange.gemini.EnabledPairs = SplitStrings(exch.EnabledPairs, ",") + go bot.exchange.gemini.Run() + } } else if bot.exchange.okcoinChina.GetName() == exch.Name { if !exch.Enabled { bot.exchange.okcoinChina.SetEnabled(false)