Add EXMO exchange support

This commit is contained in:
Adrian Gallagher
2018-02-07 13:03:51 +11:00
parent 2dc4af00c3
commit 8fd514b2ad
8 changed files with 804 additions and 7 deletions

359
exchanges/exmo/exmo.go Normal file
View File

@@ -0,0 +1,359 @@
package exmo
import (
"errors"
"fmt"
"log"
"net/url"
"reflect"
"strconv"
"strings"
"time"
"github.com/thrasher-/gocryptotrader/common"
"github.com/thrasher-/gocryptotrader/config"
exchange "github.com/thrasher-/gocryptotrader/exchanges"
"github.com/thrasher-/gocryptotrader/exchanges/ticker"
)
const (
exmoAPIURL = "https://api.exmo.com"
exmoAPIVersion = "1"
exmoTrades = "trades"
exmoOrderbook = "order_book"
exmoTicker = "ticker"
exmoPairSettings = "pair_settings"
exmoCurrency = "currency"
exmoUserInfo = "user_info"
exmoOrderCreate = "order_create"
exmoOrderCancel = "order_cancel"
exmoOpenOrders = "user_open_orders"
exmoUserTrades = "user_trades"
exmoCancelledOrders = "user_cancelled_orders"
exmoOrderTrades = "order_trades"
exmoRequiredAmount = "required_amount"
exmoDepositAddress = "deposit_address"
exmoWithdrawCrypt = "withdraw_crypt"
exmoGetWithdrawTXID = "withdraw_get_txid"
exmoExcodeCreate = "excode_create"
exmoExcodeLoad = "excode_load"
exmoWalletHistory = "wallet_history"
)
// EXMO exchange struct
type EXMO struct {
exchange.Base
}
// Rate limit: 180 per/minute
// SetDefaults sets the basic defaults for exmo
func (e *EXMO) SetDefaults() {
e.Name = "EXMO"
e.Enabled = false
e.Verbose = false
e.Websocket = false
e.RESTPollingDelay = 10
e.RequestCurrencyPairFormat.Delimiter = "_"
e.RequestCurrencyPairFormat.Uppercase = true
e.RequestCurrencyPairFormat.Separator = ","
e.ConfigCurrencyPairFormat.Delimiter = "_"
e.ConfigCurrencyPairFormat.Uppercase = true
e.AssetTypes = []string{ticker.Spot}
}
// Setup takes in the supplied exchange configuration details and sets params
func (e *EXMO) Setup(exch config.ExchangeConfig) {
if !exch.Enabled {
e.SetEnabled(false)
} else {
e.Enabled = true
e.AuthenticatedAPISupport = exch.AuthenticatedAPISupport
e.SetAPIKeys(exch.APIKey, exch.APISecret, "", false)
e.RESTPollingDelay = exch.RESTPollingDelay
e.Verbose = exch.Verbose
e.Websocket = exch.Websocket
e.BaseCurrencies = common.SplitStrings(exch.BaseCurrencies, ",")
e.AvailablePairs = common.SplitStrings(exch.AvailablePairs, ",")
e.EnabledPairs = common.SplitStrings(exch.EnabledPairs, ",")
err := e.SetCurrencyPairFormat()
if err != nil {
log.Fatal(err)
}
err = e.SetAssetTypes()
if err != nil {
log.Fatal(err)
}
}
}
// GetTrades returns the trades for a symbol or symbols
func (e *EXMO) GetTrades(symbol string) (map[string][]Trades, error) {
v := url.Values{}
v.Set("pair", symbol)
result := make(map[string][]Trades)
url := fmt.Sprintf("%s/v%s/%s", exmoAPIURL, exmoAPIVersion, exmoTrades)
err := common.SendHTTPGetRequest(common.EncodeURLValues(url, v), true, e.Verbose, &result)
return result, err
}
// GetOrderbook returns the orderbook for a symbol or symbols
func (e *EXMO) GetOrderbook(symbol string) (map[string]Orderbook, error) {
v := url.Values{}
v.Set("pair", symbol)
result := make(map[string]Orderbook)
url := fmt.Sprintf("%s/v%s/%s", exmoAPIURL, exmoAPIVersion, exmoOrderbook)
err := common.SendHTTPGetRequest(common.EncodeURLValues(url, v), true, e.Verbose, &result)
return result, err
}
// GetTicker returns the ticker for a symbol or symbols
func (e *EXMO) GetTicker(symbol string) (map[string]Ticker, error) {
v := url.Values{}
v.Set("pair", symbol)
result := make(map[string]Ticker)
url := fmt.Sprintf("%s/v%s/%s", exmoAPIURL, exmoAPIVersion, exmoTicker)
err := common.SendHTTPGetRequest(common.EncodeURLValues(url, v), true, e.Verbose, &result)
return result, err
}
// GetPairSettings returns the pair settings for a symbol or symbols
func (e *EXMO) GetPairSettings() (map[string]PairSettings, error) {
result := make(map[string]PairSettings)
url := fmt.Sprintf("%s/v%s/%s", exmoAPIURL, exmoAPIVersion, exmoPairSettings)
err := common.SendHTTPGetRequest(url, true, e.Verbose, &result)
return result, err
}
// GetCurrency returns a list of currencies
func (e *EXMO) GetCurrency() ([]string, error) {
result := []string{}
url := fmt.Sprintf("%s/v%s/%s", exmoAPIURL, exmoAPIVersion, exmoCurrency)
err := common.SendHTTPGetRequest(url, true, e.Verbose, &result)
return result, err
}
// GetUserInfo returns the user info
func (e *EXMO) GetUserInfo() (UserInfo, error) {
var result UserInfo
err := e.SendAuthenticatedHTTPRequest("POST", exmoUserInfo, url.Values{}, &result)
return result, err
}
// CreateOrder creates an order
// Params: pair, quantity, price and type
// Type can be buy, sell, market_buy, market_sell, market_buy_total and market_sell_total
func (e *EXMO) CreateOrder(pair, orderType string, price, amount float64) (int64, error) {
type response struct {
OrderID int64 `json:"order_id"`
}
v := url.Values{}
v.Set("pair", pair)
v.Set("type", orderType)
v.Set("price", strconv.FormatFloat(price, 'f', -1, 64))
v.Set("quantity", strconv.FormatFloat(amount, 'f', -1, 64))
var result response
err := e.SendAuthenticatedHTTPRequest("POST", exmoOrderCreate, v, &result)
return result.OrderID, err
}
// CancelOrder cancels an order by the orderID
func (e *EXMO) CancelOrder(orderID int64) error {
v := url.Values{}
v.Set("order_id", strconv.FormatInt(orderID, 10))
var result interface{}
return e.SendAuthenticatedHTTPRequest("POST", exmoOrderCancel, v, &result)
}
// GetOpenOrders returns the users open orders
func (e *EXMO) GetOpenOrders() (map[string]OpenOrders, error) {
result := make(map[string]OpenOrders)
err := e.SendAuthenticatedHTTPRequest("POST", exmoOpenOrders, url.Values{}, &result)
return result, err
}
// GetUserTrades returns the user trades
func (e *EXMO) GetUserTrades(pair, offset, limit string) (map[string][]UserTrades, error) {
result := make(map[string][]UserTrades)
v := url.Values{}
v.Set("pair", pair)
if offset != "" {
v.Set("offset", offset)
}
if limit != "" {
v.Set("limit", limit)
}
err := e.SendAuthenticatedHTTPRequest("POST", exmoUserTrades, v, &result)
return result, err
}
// GetCancelledOrders returns a list of cancelled orders
func (e *EXMO) GetCancelledOrders(offset, limit string) ([]CancelledOrder, error) {
var result []CancelledOrder
v := url.Values{}
if offset != "" {
v.Set("offset", offset)
}
if limit != "" {
v.Set("limit", limit)
}
err := e.SendAuthenticatedHTTPRequest("POST", exmoCancelledOrders, v, &result)
return result, err
}
// GetOrderTrades returns a history of order trade details for the specific orderID
func (e *EXMO) GetOrderTrades(orderID int64) (OrderTrades, error) {
var result OrderTrades
v := url.Values{}
v.Set("order_id", strconv.FormatInt(orderID, 10))
err := e.SendAuthenticatedHTTPRequest("POST", exmoOrderTrades, v, &result)
return result, err
}
// GetRequiredAmount calculates the sum of buying a certain amount of currency
// for the particular currency pair
func (e *EXMO) GetRequiredAmount(pair string, amount float64) (RequiredAmount, error) {
v := url.Values{}
v.Set("pair", pair)
v.Set("quantity", strconv.FormatFloat(amount, 'f', -1, 64))
var result RequiredAmount
err := e.SendAuthenticatedHTTPRequest("POST", exmoRequiredAmount, v, &result)
return result, err
}
// GetDepositAddress returns a list of addresses for cryptocurrency deposits
func (e *EXMO) GetDepositAddress() (map[string]string, error) {
result := make(map[string]string)
err := e.SendAuthenticatedHTTPRequest("POST", exmoDepositAddress, url.Values{}, &result)
log.Println(reflect.TypeOf(result).String())
return result, err
}
// WithdrawCryptocurrency withdraws a cryptocurrency from the exchange to the desired address
// NOTE: This API function is available only after request to their tech support team
func (e *EXMO) WithdrawCryptocurrency(currency, address, invoice string, amount float64) (int64, error) {
type response struct {
TaskID int64 `json:"task_id,string"`
}
v := url.Values{}
v.Set("currency", currency)
v.Set("address", address)
if common.StringToUpper(currency) == "XRP" {
v.Set(invoice, invoice)
}
v.Set("amount", strconv.FormatFloat(amount, 'f', -1, 64))
var result response
err := e.SendAuthenticatedHTTPRequest("POST", exmoWithdrawCrypt, v, &result)
return result.TaskID, err
}
// GetWithdrawTXID gets the result of a withdrawal request
func (e *EXMO) GetWithdrawTXID(taskID int64) (string, error) {
type response struct {
Status bool `json:"status"`
TXID string `json:"txid"`
}
v := url.Values{}
v.Set("task_id", strconv.FormatInt(taskID, 10))
var result response
err := e.SendAuthenticatedHTTPRequest("POST", exmoGetWithdrawTXID, v, &result)
return result.TXID, err
}
// ExcodeCreate creates an EXMO coupon
func (e *EXMO) ExcodeCreate(currency string, amount float64) (ExcodeCreate, error) {
v := url.Values{}
v.Set("currency", currency)
v.Set("amount", strconv.FormatFloat(amount, 'f', -1, 64))
var result ExcodeCreate
err := e.SendAuthenticatedHTTPRequest("POST", exmoExcodeCreate, v, &result)
return result, err
}
// ExcodeLoad loads an EXMO coupon
func (e *EXMO) ExcodeLoad(excode string) (ExcodeLoad, error) {
v := url.Values{}
v.Set("code", excode)
var result ExcodeLoad
err := e.SendAuthenticatedHTTPRequest("POST", exmoExcodeLoad, v, &result)
return result, err
}
// GetWalletHistory returns the users deposit/withdrawal history
func (e *EXMO) GetWalletHistory(date int64) (WalletHistory, error) {
v := url.Values{}
v.Set("date", strconv.FormatInt(date, 10))
var result WalletHistory
err := e.SendAuthenticatedHTTPRequest("POST", exmoWalletHistory, v, &result)
return result, err
}
// SendAuthenticatedHTTPRequest sends an authenticated HTTP request
func (e *EXMO) SendAuthenticatedHTTPRequest(method, endpoint string, vals url.Values, result interface{}) error {
if !e.AuthenticatedAPISupport {
return fmt.Errorf(exchange.WarningAuthenticatedRequestWithoutCredentialsSet, e.Name)
}
if e.Nonce.Get() == 0 {
e.Nonce.Set(time.Now().UnixNano())
} else {
e.Nonce.Inc()
}
vals.Set("nonce", e.Nonce.String())
payload := vals.Encode()
hash := common.GetHMAC(common.HashSHA512, []byte(payload), []byte(e.APISecret))
if e.Verbose {
log.Printf("Sending %s request to %s with params %s\n", method, endpoint, payload)
}
headers := make(map[string]string)
headers["Key"] = e.APIKey
headers["Sign"] = common.HexEncodeToString(hash)
headers["Content-Type"] = "application/x-www-form-urlencoded"
path := fmt.Sprintf("%s/v%s/%s", exmoAPIURL, exmoAPIVersion, endpoint)
resp, err := common.SendHTTPRequest(method, path, headers, strings.NewReader(payload))
if err != nil {
return err
}
if e.Verbose {
log.Printf("Received raw: \n%s\n", resp)
}
var authResp AuthResponse
err = common.JSONDecode([]byte(resp), &authResp)
if err != nil {
return errors.New("unable to JSON Unmarshal auth response")
}
if !authResp.Result && authResp.Error != "" {
return fmt.Errorf("auth error: %s", authResp.Error)
}
err = common.JSONDecode([]byte(resp), &result)
if err != nil {
return errors.New("unable to JSON Unmarshal response")
}
return nil
}

View File

@@ -0,0 +1,93 @@
package exmo
import "testing"
const (
APIKey = ""
APISecret = ""
)
var (
e EXMO
)
func TestSetup(t *testing.T) {
e.AuthenticatedAPISupport = true
e.APIKey = APIKey
e.APISecret = APISecret
}
func TestGetTrades(t *testing.T) {
_, err := e.GetTrades("BTC_USD")
if err != nil {
t.Errorf("Test failed. Err: %s", err)
}
}
func TestGetOrderbook(t *testing.T) {
t.Parallel()
_, err := e.GetOrderbook("BTC_USD")
if err != nil {
t.Errorf("Test failed. Err: %s", err)
}
}
func TestGetTicker(t *testing.T) {
t.Parallel()
_, err := e.GetTicker("BTC_USD")
if err != nil {
t.Errorf("Test failed. Err: %s", err)
}
}
func TestGetPairSettings(t *testing.T) {
t.Parallel()
_, err := e.GetPairSettings()
if err != nil {
t.Errorf("Test failed. Err: %s", err)
}
}
func TestGetCurrency(t *testing.T) {
t.Parallel()
_, err := e.GetCurrency()
if err != nil {
t.Errorf("Test failed. Err: %s", err)
}
}
func TestGetUserInfo(t *testing.T) {
t.Parallel()
if APIKey == "" || APISecret == "" {
t.Skip()
}
TestSetup(t)
_, err := e.GetUserInfo()
if err != nil {
t.Errorf("Test failed. Err: %s", err)
}
}
func TestGetRequiredAmount(t *testing.T) {
t.Parallel()
if APIKey == "" || APISecret == "" {
t.Skip()
}
TestSetup(t)
_, err := e.GetRequiredAmount("BTC_USD", 100)
if err != nil {
t.Errorf("Test failed. Err: %s", err)
}
}
func TestGetDepositAddress(t *testing.T) {
t.Parallel()
if APIKey == "" || APISecret == "" {
t.Skip()
}
TestSetup(t)
_, err := e.GetDepositAddress()
if err == nil {
t.Errorf("Test failed. Err: %s", err)
}
}

View File

@@ -0,0 +1,144 @@
package exmo
// Trades holds trade data
type Trades struct {
TradeID int64 `json:"trade_id"`
Type string `json:"string"`
Quantity float64 `json:"quantity,string"`
Price float64 `json:"price,string"`
Amount float64 `json:"amount,string"`
Date int64 `json:"date"`
}
// Orderbook holds the orderbook data
type Orderbook struct {
AskQuantity float64 `json:"ask_quantity,string"`
AskAmount float64 `json:"ask_amount,string"`
AskTop float64 `json:"ask_top,string"`
BidQuantity float64 `json:"bid_quantity,string"`
BidTop float64 `json:"bid_top,string"`
Ask [][]string `json:"ask"`
Bid [][]string `json:"bid"`
}
// Ticker holds the ticker data
type Ticker struct {
Buy float64 `json:"buy_price,string"`
Sell float64 `json:"sell_price,string"`
Last float64 `json:"last_trade,string"`
High float64 `json:"high,string"`
Low float64 `json:"low,string"`
Average float64 `json:"average,string"`
Volume float64 `json:"vol,string"`
VolumeCurrent float64 `json:"vol_curr,string"`
Updated int64 `json:"updated"`
}
// PairSettings holds the pair settings
type PairSettings struct {
MinQuantity float64 `json:"min_quantity,string"`
MaxQuantity float64 `json:"max_quantity,string"`
MinPrice float64 `json:"min_price,string"`
MaxPrice float64 `json:"max_price,string"`
MaxAmount float64 `json:"max_amount,string"`
MinAmount float64 `json:"min_amount,string"`
}
// AuthResponse stores the auth reponse
type AuthResponse struct {
Result bool `json:"bool"`
Error string `json:"error"`
}
// UserInfo stores the user info
type UserInfo struct {
AuthResponse
UID int `json:"uid"`
ServerDate int `json:"server_date"`
Balances map[string]string `json:"balances"`
Reserved map[string]string `json:"reserved"`
}
// OpenOrders stores the order info
type OpenOrders struct {
OrderID int64 `json:"order_id,string"`
Created int64 `json:"created,string"`
Type string `json:"type"`
Pair string `json:"pair"`
Price float64 `json:"price,string"`
Quantity float64 `json:"quantity,string"`
Amount float64 `json:"amount,string"`
}
// UserTrades stores the users trade info
type UserTrades struct {
TradeID int64 `json:"trade_id"`
Date int64 `json:"date"`
Type string `json:"type"`
Pair string `json:"pair"`
OrderID int64 `json:"order_id"`
Quantity float64 `json:"quantity"`
Price float64 `json:"price"`
Amount float64 `json:"amount"`
}
// CancelledOrder stores cancelled order data
type CancelledOrder struct {
Date int64 `json:"date"`
OrderID int64 `json:"order_id,string"`
Type string `json:"type"`
Pair string `json:"pair"`
Price float64 `json:"price,string"`
Quantity float64 `json:"quantity,string"`
Amount float64 `json:"amount,string"`
}
// OrderTrades stores order trade information
type OrderTrades struct {
Type string `json:"type"`
InCurrency string `json:"in_currency"`
InAmount float64 `json:"in_amount,string"`
OutCurrency string `json:"out_currency"`
OutAmount float64 `json:"out_amount,string"`
Trades []UserTrades `json:"trades"`
}
// RequiredAmount stores the calculation for buying a certain amount of currency
// for a particular currency
type RequiredAmount struct {
Quantity float64 `json:"quantity,string"`
Amount float64 `json:"amount,string"`
AvgPrice float64 `json:"avg_price,string"`
}
// ExcodeCreate stores the excode create coupon info
type ExcodeCreate struct {
TaskID int64 `json:"task_id"`
Code string `json:"code"`
Amount float64 `json:"amount,string"`
Currency string `json:"currency"`
Balances map[string]string `json:"balances"`
}
// ExcodeLoad stores the excode load coupon info
type ExcodeLoad struct {
TaskID int64 `json:"task_id"`
Amount float64 `json:"amount,string"`
Currency string `json:"currency"`
Balances map[string]string `json:"balances"`
}
// WalletHistory stores the users wallet history
type WalletHistory struct {
Begin int64 `json:"begin,string"`
End int64 `json:"end,string"`
History []struct {
Timestamp int64 `json:"dt"`
Type string `json:"string"`
Currency string `json:"curr"`
Status string `json:"status"`
Provider string `json:"provider"`
Amount float64 `json:"amount,string"`
Account string `json:"account,string"`
}
}

View File

@@ -0,0 +1,149 @@
package exmo
import (
"log"
"strconv"
"github.com/thrasher-/gocryptotrader/common"
"github.com/thrasher-/gocryptotrader/currency/pair"
exchange "github.com/thrasher-/gocryptotrader/exchanges"
"github.com/thrasher-/gocryptotrader/exchanges/orderbook"
"github.com/thrasher-/gocryptotrader/exchanges/ticker"
)
// Start starts the EXMO go routine
func (e *EXMO) Start() {
go e.Run()
}
// Run implements the EXMO wrapper
func (e *EXMO) Run() {
if e.Verbose {
log.Printf("%s polling delay: %ds.\n", e.GetName(), e.RESTPollingDelay)
log.Printf("%s %d currencies enabled: %s.\n", e.GetName(), len(e.EnabledPairs), e.EnabledPairs)
}
exchangeProducts, err := e.GetPairSettings()
if err != nil {
log.Printf("%s Failed to get available products.\n", e.GetName())
} else {
var currencies []string
for x := range exchangeProducts {
currencies = append(currencies, x)
}
err = e.UpdateAvailableCurrencies(currencies, false)
if err != nil {
log.Printf("%s Failed to update available currencies.\n", e.GetName())
}
}
}
// UpdateTicker updates and returns the ticker for a currency pair
func (e *EXMO) UpdateTicker(p pair.CurrencyPair, assetType string) (ticker.Price, error) {
var tickerPrice ticker.Price
pairsCollated, err := exchange.GetAndFormatExchangeCurrencies(e.Name, e.GetEnabledCurrencies())
if err != nil {
return tickerPrice, err
}
result, err := e.GetTicker(pairsCollated.String())
if err != nil {
return tickerPrice, err
}
for _, x := range e.GetEnabledCurrencies() {
currency := exchange.FormatExchangeCurrency(e.Name, x).String()
var tickerPrice ticker.Price
tickerPrice.Pair = x
tickerPrice.Last = result[currency].Last
tickerPrice.Ask = result[currency].Sell
tickerPrice.High = result[currency].High
tickerPrice.Bid = result[currency].Buy
tickerPrice.Last = result[currency].Last
tickerPrice.Low = result[currency].Low
tickerPrice.Volume = result[currency].Volume
ticker.ProcessTicker(e.Name, x, tickerPrice, assetType)
}
return ticker.GetTicker(e.Name, p, assetType)
}
// GetTickerPrice returns the ticker for a currency pair
func (e *EXMO) GetTickerPrice(p pair.CurrencyPair, assetType string) (ticker.Price, error) {
tick, err := ticker.GetTicker(e.GetName(), p, assetType)
if err != nil {
return e.UpdateTicker(p, assetType)
}
return tick, nil
}
// GetOrderbookEx returns the orderbook for a currency pair
func (e *EXMO) GetOrderbookEx(p pair.CurrencyPair, assetType string) (orderbook.Base, error) {
ob, err := orderbook.GetOrderbook(e.GetName(), p, assetType)
if err != nil {
return e.UpdateOrderbook(p, assetType)
}
return ob, nil
}
// UpdateOrderbook updates and returns the orderbook for a currency pair
func (e *EXMO) UpdateOrderbook(p pair.CurrencyPair, assetType string) (orderbook.Base, error) {
var orderBook orderbook.Base
pairsCollated, err := exchange.GetAndFormatExchangeCurrencies(e.Name, e.GetEnabledCurrencies())
if err != nil {
return orderBook, err
}
orderbookNew, err := e.GetOrderbook(pairsCollated.String())
if err != nil {
return orderBook, err
}
for _, x := range e.GetEnabledCurrencies() {
currency := exchange.FormatExchangeCurrency(e.Name, x).String()
data := orderbookNew[currency]
for x := range data.Bid {
obData := data.Bid[x]
price, _ := strconv.ParseFloat(obData[0], 64)
amount, _ := strconv.ParseFloat(obData[1], 64)
orderBook.Bids = append(orderBook.Bids, orderbook.Item{Price: price, Amount: amount})
}
for x := range data.Ask {
obData := data.Ask[x]
price, _ := strconv.ParseFloat(obData[0], 64)
amount, _ := strconv.ParseFloat(obData[1], 64)
orderBook.Asks = append(orderBook.Asks, orderbook.Item{Price: price, Amount: amount})
}
orderbook.ProcessOrderbook(e.GetName(), p, orderBook, assetType)
}
orderbook.ProcessOrderbook(e.GetName(), p, orderBook, assetType)
return orderbook.GetOrderbook(e.Name, p, assetType)
}
// GetExchangeAccountInfo retrieves balances for all enabled currencies for the
// Exmo exchange
func (e *EXMO) GetExchangeAccountInfo() (exchange.AccountInfo, error) {
var response exchange.AccountInfo
response.ExchangeName = e.GetName()
result, err := e.GetUserInfo()
if err != nil {
return response, err
}
for x, y := range result.Balances {
var exchangeCurrency exchange.AccountCurrencyInfo
exchangeCurrency.CurrencyName = common.StringToUpper(x)
for z, w := range result.Reserved {
if z == x {
avail, _ := strconv.ParseFloat(y, 64)
reserved, _ := strconv.ParseFloat(w, 64)
exchangeCurrency.TotalValue = avail + reserved
exchangeCurrency.Hold = reserved
}
}
response.Currencies = append(response.Currencies, exchangeCurrency)
}
return response, nil
}