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_TICKER = "pubticker" GEMINI_AUCTION = "auction" GEMINI_AUCTION_HISTORY = "history" GEMINI_ORDERBOOK = "book" GEMINI_TRADES = "trades" GEMINI_ORDERS = "orders" GEMINI_ORDER_NEW = "order/new" GEMINI_ORDER_CANCEL = "order/cancel" GEMINI_ORDER_CANCEL_SESSION = "order/cancel/session" GEMINI_ORDER_CANCEL_ALL = "order/cancel/all" GEMINI_ORDER_STATUS = "order/status" GEMINI_MYTRADES = "mytrades" GEMINI_BALANCES = "balances" GEMINI_HEARTBEAT = "heartbeat" ) 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"` ClientOrderID string `json:"client_order_id"` Symbol string `json:"symbol"` Exchange string `json:"exchange"` Price float64 `json:"price,string"` AvgExecutionPrice float64 `json:"avg_execution_price,string"` Side string `json:"side"` Type string `json:"type"` Timestamp int64 `json:"timestamp"` TimestampMS int64 `json:"timestampms"` IsLive bool `json:"is_live"` IsCancelled bool `json:"is_cancelled"` WasForced bool `json:"was_forced"` 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"` TimestampMS int64 `json:"timestampms"` Type string `json:"type"` FeeCurrency string `json:"fee_currency"` FeeAmount float64 `json:"fee_amount"` TID int64 `json:"tid"` OrderID int64 `json:"order_id"` ClientOrderID string `json:"client_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 = false 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) Setup(exch Exchanges) { if !exch.Enabled { g.SetEnabled(false) } else { g.Enabled = true g.AuthenticatedAPISupport = exch.AuthenticatedAPISupport g.SetAPIKeys(exch.APIKey, exch.APISecret) g.RESTPollingDelay = exch.RESTPollingDelay g.Verbose = exch.Verbose g.Websocket = exch.Websocket g.BaseCurrencies = SplitStrings(exch.BaseCurrencies, ",") g.AvailablePairs = SplitStrings(exch.AvailablePairs, ",") g.EnabledPairs = SplitStrings(exch.EnabledPairs, ",") } } func (k *Gemini) GetEnabledCurrencies() []string { return k.EnabledPairs } func (g *Gemini) Start() { go g.Run() } 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 { for _, x := range g.EnabledPairs { currency := x go func() { ticker, err := g.GetTickerPrice(currency) if err != nil { log.Println(err) return } log.Printf("Gemini %s Last %f Bid %f Ask %f Volume %f\n", currency, ticker.Last, ticker.Bid, ticker.Ask, ticker.Volume) AddExchangeInfo(g.GetName(), currency[0:3], currency[3:], ticker.Last, ticker.Volume) }() } time.Sleep(time.Second * g.RESTPollingDelay) } } type GeminiTicker struct { Ask float64 `json:"ask,string"` Bid float64 `json:"bid,string"` Last float64 `json:"last,string"` Volume struct { Currency float64 USD float64 Timestamp int64 } } func (g *Gemini) GetTicker(currency string) (GeminiTicker, error) { type TickerResponse struct { Ask float64 `json:"ask,string"` Bid float64 `json:"bid,string"` Last float64 `json:"last,string"` Volume map[string]interface{} } ticker := GeminiTicker{} resp := TickerResponse{} path := fmt.Sprintf("%s/v%s/%s/%s", GEMINI_API_URL, GEMINI_API_VERSION, GEMINI_TICKER, currency) err := SendHTTPGetRequest(path, true, &resp) if err != nil { return ticker, err } ticker.Ask = resp.Ask ticker.Bid = resp.Bid ticker.Last = resp.Last ticker.Volume.Currency, _ = strconv.ParseFloat(resp.Volume[currency[0:3]].(string), 64) ticker.Volume.USD, _ = strconv.ParseFloat(resp.Volume["USD"].(string), 64) time, _ := resp.Volume["timestamp"].(float64) ticker.Volume.Timestamp = int64(time) return ticker, nil } func (g *Gemini) GetTickerPrice(currency string) (TickerPrice, error) { tickerNew, err := GetTicker(g.GetName(), currency[0:3], currency[3:]) if err == nil { return tickerNew, nil } var tickerPrice TickerPrice ticker, err := g.GetTicker(currency) if err != nil { return tickerPrice, err } tickerPrice.Ask = ticker.Ask tickerPrice.Bid = ticker.Bid tickerPrice.FirstCurrency = currency[0:3] tickerPrice.SecondCurrency = currency[3:] tickerPrice.Last = ticker.Last tickerPrice.Volume = ticker.Volume.USD ProcessTicker(g.GetName(), tickerPrice.FirstCurrency, tickerPrice.SecondCurrency, tickerPrice) return tickerPrice, nil } 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 } type GeminiAuction struct { LastAuctionPrice float64 `json:"last_auction_price,string"` LastAuctionQuantity float64 `json:"last_auction_quantity,string"` LastHighestBidPrice float64 `json:"last_highest_bid_price,string"` LastLowestAskPrice float64 `json:"last_lowest_ask_price,string"` NextUpdateMS int64 `json:"next_update_ms"` NextAuctionMS int64 `json:"next_auction_ms"` LastAuctionEID int64 `json:"last_auction_eid"` } func (g *Gemini) GetAuction(currency string) (GeminiAuction, error) { path := fmt.Sprintf("%s/v%s/%s/%s", GEMINI_API_URL, GEMINI_API_VERSION, GEMINI_AUCTION, currency) auction := GeminiAuction{} err := SendHTTPGetRequest(path, true, &auction) if err != nil { return auction, err } return auction, nil } type GeminiAuctionHistory struct { AuctionID int64 `json:"auction_id"` AuctionPrice float64 `json:"auction_price,string"` AuctionQuantity float64 `json:"auction_quantity,string"` EID int64 `json:"eid"` HighestBidPrice float64 `json:"highest_bid_price,string"` LowestAskPrice float64 `json:"lowest_ask_price,string"` AuctionResult string `json:"auction_result"` Timestamp int64 `json:"timestamp"` TimestampMS int64 `json:"timestampms"` EventType string `json:"event_type"` } func (g *Gemini) GetAuctionHistory(currency string, params url.Values) ([]GeminiAuctionHistory, error) { path := EncodeURLValues(fmt.Sprintf("%s/v%s/%s/%s/%s", GEMINI_API_URL, GEMINI_API_VERSION, GEMINI_AUCTION, currency, GEMINI_AUCTION_HISTORY), params) auctionHist := []GeminiAuctionHistory{} err := SendHTTPGetRequest(path, true, &auctionHist) if err != nil { return nil, err } return auctionHist, nil } func (g *Gemini) GetOrderbook(currency string, params url.Values) (GeminiOrderbook, error) { path := EncodeURLValues(fmt.Sprintf("%s/v%s/%s/%s", GEMINI_API_URL, GEMINI_API_VERSION, GEMINI_ORDERBOOK, currency), params) orderbook := GeminiOrderbook{} 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) { path := EncodeURLValues(fmt.Sprintf("%s/v%s/%s/%s", GEMINI_API_URL, GEMINI_API_VERSION, GEMINI_TRADES, currency), params) trades := []GeminiTrade{} 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) CancelOrders(sessions bool) ([]GeminiOrderResult, error) { response := []GeminiOrderResult{} path := GEMINI_ORDER_CANCEL_ALL if sessions { path = GEMINI_ORDER_CANCEL_SESSION } err := g.SendAuthenticatedHTTPRequest("POST", path, 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 } //GetExchangeAccountInfo : Retrieves balances for all enabled currencies for the Gemini exchange func (e *Gemini) GetExchangeAccountInfo() (ExchangeAccountInfo, error) { var response ExchangeAccountInfo response.ExchangeName = e.GetName() accountBalance, err := e.GetBalances() if err != nil { return response, err } for i := 0; i < len(accountBalance); i++ { var exchangeCurrency ExchangeAccountCurrencyInfo exchangeCurrency.CurrencyName = accountBalance[i].Currency exchangeCurrency.TotalValue = accountBalance[i].Amount exchangeCurrency.Hold = accountBalance[i].Available response.Currencies = append(response.Currencies, exchangeCurrency) } return response, nil } func (g *Gemini) PostHeartbeat() (bool, error) { type Response struct { Result bool `json:"result"` } response := Response{} err := g.SendAuthenticatedHTTPRequest("POST", GEMINI_HEARTBEAT, nil, &response) if err != nil { return false, err } return response.Result, 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 }