From 8fd514b2ad2cb49b03fadb3c244f47401375c66a Mon Sep 17 00:00:00 2001 From: Adrian Gallagher Date: Wed, 7 Feb 2018 13:03:51 +1100 Subject: [PATCH] Add EXMO exchange support --- README.md | 1 + config_example.json | 30 ++- exchange.go | 5 +- exchanges/exmo/exmo.go | 359 +++++++++++++++++++++++++++++++++ exchanges/exmo/exmo_test.go | 93 +++++++++ exchanges/exmo/exmo_types.go | 144 +++++++++++++ exchanges/exmo/exmo_wrapper.go | 149 ++++++++++++++ testdata/configtest.json | 30 ++- 8 files changed, 804 insertions(+), 7 deletions(-) create mode 100644 exchanges/exmo/exmo.go create mode 100644 exchanges/exmo/exmo_test.go create mode 100644 exchanges/exmo/exmo_types.go create mode 100644 exchanges/exmo/exmo_wrapper.go diff --git a/README.md b/README.md index c54baa4b..4d127ec2 100644 --- a/README.md +++ b/README.md @@ -28,6 +28,7 @@ Join our slack to discuss all things related to GoCryptoTrader! [GoCryptoTrader | BTCC | Yes | Yes | No | | BTCMarkets | Yes | NA | NA | | COINUT | Yes | No | NA | +| Exmo | Yes | NA | NA | | GDAX(Coinbase) | Yes | Yes | No| | Gemini | Yes | NA | NA | | HitBTC | Yes | Yes | NA | diff --git a/config_example.json b/config_example.json index c6781056..b1123c2f 100644 --- a/config_example.json +++ b/config_example.json @@ -13,7 +13,7 @@ { "Address": "1JCe8z4jJVNXSjohjM4i9Hh813dLCNx2Sy", "CoinType": "BTC", - "Balance": 53000.0074432, + "Balance": 53000.0124432, "Description": "" }, { @@ -31,7 +31,7 @@ { "Address": "0xb794f5ea0ba39494ce839613fffba74279579268", "CoinType": "ETH", - "Balance": 1925000.288118, + "Balance": 1925000.2881249, "Description": "" } ] @@ -254,6 +254,30 @@ "Uppercase": true } }, + { + "Name": "EXMO", + "Enabled": true, + "Verbose": false, + "Websocket": false, + "UseSandbox": false, + "RESTPollingDelay": 10, + "AuthenticatedAPISupport": false, + "APIKey": "Key", + "APISecret": "Secret", + "AvailablePairs": "XRP_USD,XMR_USD,BTC_USD,ETH_USD,ETH_UAH,ETC_BTC,ETC_USD,XRP_BTC,ETH_USDT,USD_RUB,XMR_EUR,BTC_USDT,BTC_PLN,BCH_ETH,DASH_BTC,ETH_PLN,ZEC_BTC,ZEC_EUR,BCH_BTC,LTC_BTC,WAVES_RUB,KICK_ETH,ETH_EUR,ETH_RUB,USDT_USD,ZEC_RUB,USDT_RUB,DOGE_BTC,ETH_BTC,LTC_EUR,BTC_EUR,BTC_UAH,BCH_USD,BCH_RUB,DASH_USD,DASH_RUB,ZEC_USD,XMR_BTC,BTC_RUB,KICK_BTC,ETH_LTC,ETC_RUB,LTC_USD,LTC_RUB,XRP_RUB,WAVES_BTC", + "EnabledPairs": "BTC_USD,LTC_USD", + "BaseCurrencies": "USD,EUR,RUB,PLN,UAH", + "AssetTypes": "SPOT", + "ConfigCurrencyPairFormat": { + "Uppercase": true, + "Delimiter": "_" + }, + "RequestCurrencyPairFormat": { + "Uppercase": true, + "Delimiter": "_", + "Separator": "," + } + }, { "Name": "GDAX", "Enabled": true, @@ -330,7 +354,7 @@ "AuthenticatedAPISupport": false, "APIKey": "Key", "APISecret": "Secret", - "AvailablePairs": "OMG-USDT,LINK-BTC,NAS-ETH,EOS-ETH,SWFTC-BTC,XEM-USDT,ZEC-USDT,DASH-BTC,PAY-BTC,EVX-BTC,MDS-ETH,TNT-BTC,QASH-ETH,SMT-ETH,TRX-ETH,THETA-USDT,LUN-ETH,RUFF-ETH,BCH-BTC,ELA-ETH,IOST-ETH,TNB-BTC,GNX-ETH,THETA-BTC,SNT-USDT,DAT-BTC,SOC-ETH,EOS-USDT,CHAT-ETH,MANA-BTC,SMT-USDT,XRP-BTC,LTC-USDT,QTUM-USDT,LET-BTC,BCD-BTC,SNT-BTC,CVC-USDT,ELF-ETH,GNT-ETH,UTK-BTC,SBTC-BTC,NEO-USDT,MCO-BTC,OST-ETH,HT-BTC,RCN-BTC,BT2-BTC,QUN-BTC,HSR-ETH,TOPC-ETH,SALT-ETH,AIDOC-ETH,WAX-BTC,CVC-ETH,DTA-ETH,BTC-USDT,MEE-ETH,POWR-ETH,GAS-ETH,ADX-ETH,NEO-BTC,SALT-BTC,LET-USDT,BTM-BTC,EKO-ETH,BAT-ETH,EKO-BTC,SRN-BTC,APPC-BTC,OCN-ETH,CMT-BTC,VEN-ETH,QTUM-ETH,REQ-BTC,BIFI-BTC,BTM-ETH,ICX-BTC,OCN-BTC,ZEC-BTC,ACT-BTC,DGD-ETH,DAT-ETH,ETC-USDT,OST-BTC,IOST-USDT,STK-ETH,MCO-ETH,HT-ETH,STORJ-BTC,HSR-BTC,QUN-ETH,SOC-BTC,ELF-BTC,CMT-ETH,VEN-BTC,GNT-BTC,DBC-BTC,STORJ-USDT,WAX-ETH,TRX-BTC,POWR-BTC,DTA-USDT,DTA-BTC,SNC-BTC,ZIL-BTC,MEE-BTC,LSK-BTC,NAS-BTC,TNB-ETH,SWFTC-ETH,LTC-BTC,EOS-BTC,LINK-ETH,IOST-BTC,YEE-BTC,HT-USDT,RUFF-BTC,RDN-BTC,LUN-BTC,GNX-BTC,ELA-BTC,LET-ETH,EVX-ETH,AST-BTC,ACT-ETH,BCH-USDT,DASH-USDT,ICX-ETH,BCX-BTC,PROPY-ETH,DGD-BTC,XRP-USDT,ZIL-ETH,ZRX-BTC,THETA-ETH,ETH-BTC,SNC-ETH,DBC-ETH,REQ-ETH,WICC-ETH,SMT-BTC,LSK-ETH,RPX-BTC,TNT-ETH,SRN-ETH,ETH-USDT,ITC-BTC,OMG-BTC,PAY-ETH,STK-BTC,VEN-USDT,MDS-BTC,ADX-BTC,ETC-BTC,AIDOC-BTC,KNC-BTC,HSR-USDT,QTUM-BTC,CVC-BTC,QSP-BTC,QSP-ETH,BTG-BTC,BAT-BTC,ZLA-ETH,QASH-BTC,ITC-ETH,XEM-BTC,MANA-ETH,GAS-BTC,CHAT-BTC,BT1-BTC,ZLA-BTC,OMG-ETH,RCN-ETH,UTK-ETH,TOPC-BTC,MTL-BTC,GNT-USDT,APPC-ETH,PROPY-BTC,WICC-BTC,RDN-ETH,ELF-USDT,YEE-ETH", + "AvailablePairs": "OMG-USDT,LINK-BTC,NAS-ETH,EOS-ETH,SWFTC-BTC,XEM-USDT,ZEC-USDT,DASH-BTC,PAY-BTC,EVX-BTC,MDS-ETH,TNT-BTC,QASH-ETH,SMT-ETH,TRX-ETH,THETA-USDT,LUN-ETH,RUFF-ETH,BCH-BTC,ELA-ETH,IOST-ETH,TNB-BTC,GNX-ETH,THETA-BTC,SNT-USDT,DAT-BTC,SOC-ETH,EOS-USDT,CHAT-ETH,MANA-BTC,SMT-USDT,XRP-BTC,LTC-USDT,QTUM-USDT,LET-BTC,BCD-BTC,SNT-BTC,CVC-USDT,ELF-ETH,GNT-ETH,UTK-BTC,SBTC-BTC,NEO-USDT,MCO-BTC,OST-ETH,HT-BTC,RCN-BTC,BT2-BTC,QUN-BTC,HSR-ETH,TOPC-ETH,SALT-ETH,AIDOC-ETH,WAX-BTC,CVC-ETH,DTA-ETH,BTC-USDT,MEE-ETH,POWR-ETH,GAS-ETH,ADX-ETH,NEO-BTC,SALT-BTC,LET-USDT,BTM-BTC,EKO-ETH,BAT-ETH,EKO-BTC,SRN-BTC,APPC-BTC,OCN-ETH,CMT-BTC,VEN-ETH,QTUM-ETH,REQ-BTC,BIFI-BTC,BTM-ETH,ICX-BTC,OCN-BTC,ZEC-BTC,ACT-BTC,DGD-ETH,DAT-ETH,ETC-USDT,OST-BTC,IOST-USDT,STK-ETH,MCO-ETH,HT-ETH,STORJ-BTC,HSR-BTC,QUN-ETH,SOC-BTC,ELF-BTC,CMT-ETH,VEN-BTC,GNT-BTC,DBC-BTC,STORJ-USDT,WAX-ETH,TRX-BTC,POWR-BTC,DTA-USDT,DTA-BTC,SNC-BTC,ZIL-BTC,MEE-BTC,LSK-BTC,NAS-BTC,TNB-ETH,SWFTC-ETH,LTC-BTC,EOS-BTC,LINK-ETH,IOST-BTC,YEE-BTC,HT-USDT,RUFF-BTC,RDN-BTC,LUN-BTC,GNX-BTC,ELA-BTC,LET-ETH,EVX-ETH,AST-BTC,ACT-ETH,BCH-USDT,DASH-USDT,ICX-ETH,BCX-BTC,MTN-ETH,PROPY-ETH,DGD-BTC,XRP-USDT,ZIL-ETH,ZRX-BTC,THETA-ETH,ETH-BTC,SNC-ETH,DBC-ETH,REQ-ETH,WICC-ETH,SMT-BTC,LSK-ETH,RPX-BTC,TNT-ETH,SRN-ETH,ETH-USDT,ITC-BTC,OMG-BTC,PAY-ETH,STK-BTC,VEN-USDT,MDS-BTC,ADX-BTC,ETC-BTC,AIDOC-BTC,KNC-BTC,HSR-USDT,QTUM-BTC,CVC-BTC,QSP-BTC,QSP-ETH,BTG-BTC,BAT-BTC,ZLA-ETH,QASH-BTC,ITC-ETH,XEM-BTC,MANA-ETH,GAS-BTC,MTN-BTC,CHAT-BTC,BT1-BTC,ZLA-BTC,OMG-ETH,RCN-ETH,UTK-ETH,TOPC-BTC,MTL-BTC,GNT-USDT,APPC-ETH,PROPY-BTC,WICC-BTC,RDN-ETH,ELF-USDT,YEE-ETH", "EnabledPairs": "BTC-USDT", "BaseCurrencies": "USD", "AssetTypes": "SPOT", diff --git a/exchange.go b/exchange.go index 08a6124c..2bc94604 100644 --- a/exchange.go +++ b/exchange.go @@ -15,9 +15,10 @@ import ( "github.com/thrasher-/gocryptotrader/exchanges/btcc" "github.com/thrasher-/gocryptotrader/exchanges/btcmarkets" "github.com/thrasher-/gocryptotrader/exchanges/coinut" + "github.com/thrasher-/gocryptotrader/exchanges/exmo" "github.com/thrasher-/gocryptotrader/exchanges/gdax" "github.com/thrasher-/gocryptotrader/exchanges/gemini" - "github.com/thrasher-/gocryptotrader/exchanges/hitbtc" + "github.com/thrasher-/gocryptotrader/exchanges/hitbtc" "github.com/thrasher-/gocryptotrader/exchanges/huobi" "github.com/thrasher-/gocryptotrader/exchanges/itbit" "github.com/thrasher-/gocryptotrader/exchanges/kraken" @@ -147,6 +148,8 @@ func LoadExchange(name string) error { exch = new(btcmarkets.BTCMarkets) case "coinut": exch = new(coinut.COINUT) + case "exmo": + exch = new(exmo.EXMO) case "gdax": exch = new(gdax.GDAX) case "gemini": diff --git a/exchanges/exmo/exmo.go b/exchanges/exmo/exmo.go new file mode 100644 index 00000000..08543b50 --- /dev/null +++ b/exchanges/exmo/exmo.go @@ -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 +} diff --git a/exchanges/exmo/exmo_test.go b/exchanges/exmo/exmo_test.go new file mode 100644 index 00000000..189c2df1 --- /dev/null +++ b/exchanges/exmo/exmo_test.go @@ -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) + } +} diff --git a/exchanges/exmo/exmo_types.go b/exchanges/exmo/exmo_types.go new file mode 100644 index 00000000..33c5b044 --- /dev/null +++ b/exchanges/exmo/exmo_types.go @@ -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"` + } +} diff --git a/exchanges/exmo/exmo_wrapper.go b/exchanges/exmo/exmo_wrapper.go new file mode 100644 index 00000000..7cc88594 --- /dev/null +++ b/exchanges/exmo/exmo_wrapper.go @@ -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 +} diff --git a/testdata/configtest.json b/testdata/configtest.json index 0c3ff8b7..c292e7fb 100644 --- a/testdata/configtest.json +++ b/testdata/configtest.json @@ -13,7 +13,7 @@ { "Address": "1JCe8z4jJVNXSjohjM4i9Hh813dLCNx2Sy", "CoinType": "BTC", - "Balance": 53000.0074432, + "Balance": 53000.0124432, "Description": "" }, { @@ -31,7 +31,7 @@ { "Address": "0xb794f5ea0ba39494ce839613fffba74279579268", "CoinType": "ETH", - "Balance": 1925000.288118, + "Balance": 1925000.2881249, "Description": "" } ] @@ -277,6 +277,30 @@ "Uppercase": true } }, + { + "Name": "EXMO", + "Enabled": true, + "Verbose": false, + "Websocket": false, + "UseSandbox": false, + "RESTPollingDelay": 10, + "AuthenticatedAPISupport": false, + "APIKey": "Key", + "APISecret": "Secret", + "AvailablePairs": "XRP_USD,XMR_USD,BTC_USD,ETH_USD,ETH_UAH,ETC_BTC,ETC_USD,XRP_BTC,ETH_USDT,USD_RUB,XMR_EUR,BTC_USDT,BTC_PLN,BCH_ETH,DASH_BTC,ETH_PLN,ZEC_BTC,ZEC_EUR,BCH_BTC,LTC_BTC,WAVES_RUB,KICK_ETH,ETH_EUR,ETH_RUB,USDT_USD,ZEC_RUB,USDT_RUB,DOGE_BTC,ETH_BTC,LTC_EUR,BTC_EUR,BTC_UAH,BCH_USD,BCH_RUB,DASH_USD,DASH_RUB,ZEC_USD,XMR_BTC,BTC_RUB,KICK_BTC,ETH_LTC,ETC_RUB,LTC_USD,LTC_RUB,XRP_RUB,WAVES_BTC", + "EnabledPairs": "BTC_USD,LTC_USD", + "BaseCurrencies": "USD,EUR,RUB,PLN,UAH", + "AssetTypes": "SPOT", + "ConfigCurrencyPairFormat": { + "Uppercase": true, + "Delimiter": "_" + }, + "RequestCurrencyPairFormat": { + "Uppercase": true, + "Delimiter": "_", + "Separator": "," + } + }, { "Name": "GDAX", "Enabled": true, @@ -353,7 +377,7 @@ "AuthenticatedAPISupport": false, "APIKey": "Key", "APISecret": "Secret", - "AvailablePairs": "OMG-USDT,LINK-BTC,NAS-ETH,EOS-ETH,SWFTC-BTC,XEM-USDT,ZEC-USDT,DASH-BTC,PAY-BTC,EVX-BTC,MDS-ETH,TNT-BTC,QASH-ETH,SMT-ETH,TRX-ETH,THETA-USDT,LUN-ETH,RUFF-ETH,BCH-BTC,ELA-ETH,IOST-ETH,TNB-BTC,GNX-ETH,THETA-BTC,SNT-USDT,DAT-BTC,SOC-ETH,EOS-USDT,CHAT-ETH,MANA-BTC,SMT-USDT,XRP-BTC,LTC-USDT,QTUM-USDT,LET-BTC,BCD-BTC,SNT-BTC,CVC-USDT,ELF-ETH,GNT-ETH,UTK-BTC,SBTC-BTC,NEO-USDT,MCO-BTC,OST-ETH,HT-BTC,RCN-BTC,BT2-BTC,QUN-BTC,HSR-ETH,TOPC-ETH,SALT-ETH,AIDOC-ETH,WAX-BTC,CVC-ETH,DTA-ETH,BTC-USDT,MEE-ETH,POWR-ETH,GAS-ETH,ADX-ETH,NEO-BTC,SALT-BTC,LET-USDT,BTM-BTC,EKO-ETH,BAT-ETH,EKO-BTC,SRN-BTC,APPC-BTC,OCN-ETH,CMT-BTC,VEN-ETH,QTUM-ETH,REQ-BTC,BIFI-BTC,BTM-ETH,ICX-BTC,OCN-BTC,ZEC-BTC,ACT-BTC,DGD-ETH,DAT-ETH,ETC-USDT,OST-BTC,IOST-USDT,STK-ETH,MCO-ETH,HT-ETH,STORJ-BTC,HSR-BTC,QUN-ETH,SOC-BTC,ELF-BTC,CMT-ETH,VEN-BTC,GNT-BTC,DBC-BTC,STORJ-USDT,WAX-ETH,TRX-BTC,POWR-BTC,DTA-USDT,DTA-BTC,SNC-BTC,ZIL-BTC,MEE-BTC,LSK-BTC,NAS-BTC,TNB-ETH,SWFTC-ETH,LTC-BTC,EOS-BTC,LINK-ETH,IOST-BTC,YEE-BTC,HT-USDT,RUFF-BTC,RDN-BTC,LUN-BTC,GNX-BTC,ELA-BTC,LET-ETH,EVX-ETH,AST-BTC,ACT-ETH,BCH-USDT,DASH-USDT,ICX-ETH,BCX-BTC,PROPY-ETH,DGD-BTC,XRP-USDT,ZIL-ETH,ZRX-BTC,THETA-ETH,ETH-BTC,SNC-ETH,DBC-ETH,REQ-ETH,WICC-ETH,SMT-BTC,LSK-ETH,RPX-BTC,TNT-ETH,SRN-ETH,ETH-USDT,ITC-BTC,OMG-BTC,PAY-ETH,STK-BTC,VEN-USDT,MDS-BTC,ADX-BTC,ETC-BTC,AIDOC-BTC,KNC-BTC,HSR-USDT,QTUM-BTC,CVC-BTC,QSP-BTC,QSP-ETH,BTG-BTC,BAT-BTC,ZLA-ETH,QASH-BTC,ITC-ETH,XEM-BTC,MANA-ETH,GAS-BTC,CHAT-BTC,BT1-BTC,ZLA-BTC,OMG-ETH,RCN-ETH,UTK-ETH,TOPC-BTC,MTL-BTC,GNT-USDT,APPC-ETH,PROPY-BTC,WICC-BTC,RDN-ETH,ELF-USDT,YEE-ETH", + "AvailablePairs": "OMG-USDT,LINK-BTC,NAS-ETH,EOS-ETH,SWFTC-BTC,XEM-USDT,ZEC-USDT,DASH-BTC,PAY-BTC,EVX-BTC,MDS-ETH,TNT-BTC,QASH-ETH,SMT-ETH,TRX-ETH,THETA-USDT,LUN-ETH,RUFF-ETH,BCH-BTC,ELA-ETH,IOST-ETH,TNB-BTC,GNX-ETH,THETA-BTC,SNT-USDT,DAT-BTC,SOC-ETH,EOS-USDT,CHAT-ETH,MANA-BTC,SMT-USDT,XRP-BTC,LTC-USDT,QTUM-USDT,LET-BTC,BCD-BTC,SNT-BTC,CVC-USDT,ELF-ETH,GNT-ETH,UTK-BTC,SBTC-BTC,NEO-USDT,MCO-BTC,OST-ETH,HT-BTC,RCN-BTC,BT2-BTC,QUN-BTC,HSR-ETH,TOPC-ETH,SALT-ETH,AIDOC-ETH,WAX-BTC,CVC-ETH,DTA-ETH,BTC-USDT,MEE-ETH,POWR-ETH,GAS-ETH,ADX-ETH,NEO-BTC,SALT-BTC,LET-USDT,BTM-BTC,EKO-ETH,BAT-ETH,EKO-BTC,SRN-BTC,APPC-BTC,OCN-ETH,CMT-BTC,VEN-ETH,QTUM-ETH,REQ-BTC,BIFI-BTC,BTM-ETH,ICX-BTC,OCN-BTC,ZEC-BTC,ACT-BTC,DGD-ETH,DAT-ETH,ETC-USDT,OST-BTC,IOST-USDT,STK-ETH,MCO-ETH,HT-ETH,STORJ-BTC,HSR-BTC,QUN-ETH,SOC-BTC,ELF-BTC,CMT-ETH,VEN-BTC,GNT-BTC,DBC-BTC,STORJ-USDT,WAX-ETH,TRX-BTC,POWR-BTC,DTA-USDT,DTA-BTC,SNC-BTC,ZIL-BTC,MEE-BTC,LSK-BTC,NAS-BTC,TNB-ETH,SWFTC-ETH,LTC-BTC,EOS-BTC,LINK-ETH,IOST-BTC,YEE-BTC,HT-USDT,RUFF-BTC,RDN-BTC,LUN-BTC,GNX-BTC,ELA-BTC,LET-ETH,EVX-ETH,AST-BTC,ACT-ETH,BCH-USDT,DASH-USDT,ICX-ETH,BCX-BTC,MTN-ETH,PROPY-ETH,DGD-BTC,XRP-USDT,ZIL-ETH,ZRX-BTC,THETA-ETH,ETH-BTC,SNC-ETH,DBC-ETH,REQ-ETH,WICC-ETH,SMT-BTC,LSK-ETH,RPX-BTC,TNT-ETH,SRN-ETH,ETH-USDT,ITC-BTC,OMG-BTC,PAY-ETH,STK-BTC,VEN-USDT,MDS-BTC,ADX-BTC,ETC-BTC,AIDOC-BTC,KNC-BTC,HSR-USDT,QTUM-BTC,CVC-BTC,QSP-BTC,QSP-ETH,BTG-BTC,BAT-BTC,ZLA-ETH,QASH-BTC,ITC-ETH,XEM-BTC,MANA-ETH,GAS-BTC,MTN-BTC,CHAT-BTC,BT1-BTC,ZLA-BTC,OMG-ETH,RCN-ETH,UTK-ETH,TOPC-BTC,MTL-BTC,GNT-USDT,APPC-ETH,PROPY-BTC,WICC-BTC,RDN-ETH,ELF-USDT,YEE-ETH", "EnabledPairs": "BTC-USDT", "BaseCurrencies": "USD", "AssetTypes": "SPOT",