From 82a622294c30f044c30a932948fac496240085dc Mon Sep 17 00:00:00 2001 From: Ryan O'Hara-Reid Date: Thu, 31 Jan 2019 16:11:42 +1100 Subject: [PATCH] Coinmarketcap implementation (#243) * Updates requester package to allow unpacking of zipped files and defaults to warn if no JSON is present * Initial addition of coinmarketcap functionality * fix requested changes * Fix issue with displaying false positive in request.go && reorder plan list * Rename CurrencyProvider -> CryptocurrencyProvider Skip seeding currency data if not enabled Rm line in main.go * Update test procedures and relevant json files * Fix const issue within config.go --- config/config.go | 78 +- config/config_test.go | 25 + config_example.json | 7 + currency/coinmarketcap/coinmarketcap.go | 764 ++++++++++++++++++ currency/coinmarketcap/coinmarketcap_test.go | 415 ++++++++++ currency/coinmarketcap/coinmarketcap_types.go | 363 +++++++++ currency/currency.go | 117 +++ exchanges/request/request.go | 27 +- main.go | 26 + testdata/configtest.json | 7 + 10 files changed, 1819 insertions(+), 10 deletions(-) create mode 100644 currency/coinmarketcap/coinmarketcap.go create mode 100644 currency/coinmarketcap/coinmarketcap_test.go create mode 100644 currency/coinmarketcap/coinmarketcap_types.go diff --git a/config/config.go b/config/config.go index fc3eca36..c5c9ce1d 100644 --- a/config/config.go +++ b/config/config.go @@ -56,8 +56,16 @@ const ( WarningExchangeAuthAPIDefaultOrEmptyValues = "WARNING -- Exchange %s: Authenticated API support disabled due to default/empty APIKey/Secret/ClientID values." WarningCurrencyExchangeProvider = "WARNING -- Currency exchange provider invalid valid. Reset to Fixer." WarningPairsLastUpdatedThresholdExceeded = "WARNING -- Exchange %s: Last manual update of available currency pairs has exceeded %d days. Manual update required!" - APIURLNonDefaultMessage = "NON_DEFAULT_HTTP_LINK_TO_EXCHANGE_API" - WebsocketURLNonDefaultMessage = "NON_DEFAULT_HTTP_LINK_TO_WEBSOCKET_EXCHANGE_API" +) + +// Constants here define unset default values displayed in the config.json +// file +const ( + APIURLNonDefaultMessage = "NON_DEFAULT_HTTP_LINK_TO_EXCHANGE_API" + WebsocketURLNonDefaultMessage = "NON_DEFAULT_HTTP_LINK_TO_WEBSOCKET_EXCHANGE_API" + DefaultUnsetAPIKey = "Key" + DefaultUnsetAPISecret = "Secret" + DefaultUnsetAccountPlan = "accountPlan" ) // Variables here are used for configuration @@ -169,10 +177,20 @@ type BankTransaction struct { // CurrencyConfig holds all the information needed for currency related manipulation type CurrencyConfig struct { - ForexProviders []base.Settings `json:"forexProviders"` - Cryptocurrencies string `json:"cryptocurrencies"` - CurrencyPairFormat *CurrencyPairFormatConfig `json:"currencyPairFormat"` - FiatDisplayCurrency string `json:"fiatDisplayCurrency"` + ForexProviders []base.Settings `json:"forexProviders"` + CryptocurrencyProvider CryptocurrencyProvider `json:"cryptocurrencyProvider"` + Cryptocurrencies string `json:"cryptocurrencies"` + CurrencyPairFormat *CurrencyPairFormatConfig `json:"currencyPairFormat"` + FiatDisplayCurrency string `json:"fiatDisplayCurrency"` +} + +// CryptocurrencyProvider defines coinmarketcap tools +type CryptocurrencyProvider struct { + Name string `json:"name"` + Enabled bool `json:"enabled"` + Verbose bool `json:"verbose"` + APIkey string `json:"apiKey"` + AccountPlan string `json:"accountPlan"` } // CommunicationsConfig holds all the information needed for each @@ -365,6 +383,20 @@ func (c *Config) UpdateCommunicationsConfig(config CommunicationsConfig) { m.Unlock() } +// GetCryptocurrencyProviderConfig returns the communications configuration +func (c *Config) GetCryptocurrencyProviderConfig() CryptocurrencyProvider { + m.Lock() + defer m.Unlock() + return c.Currency.CryptocurrencyProvider +} + +// UpdateCryptocurrencyProviderConfig returns the communications configuration +func (c *Config) UpdateCryptocurrencyProviderConfig(config CryptocurrencyProvider) { + m.Lock() + c.Currency.CryptocurrencyProvider = config + m.Unlock() +} + // CheckCommunicationsConfig checks to see if the variables are set correctly // from config.json func (c *Config) CheckCommunicationsConfig() { @@ -732,7 +764,9 @@ func (c *Config) CheckExchangeConfigValues() error { return fmt.Errorf(ErrExchangeBaseCurrenciesEmpty, exch.Name) } if exch.AuthenticatedAPISupport { // non-fatal error - if exch.APIKey == "" || exch.APISecret == "" || exch.APIKey == "Key" || exch.APISecret == "Secret" { + if exch.APIKey == "" || exch.APISecret == "" || + exch.APIKey == DefaultUnsetAPIKey || + exch.APISecret == DefaultUnsetAPISecret { c.Exchanges[i].AuthenticatedAPISupport = false log.Warn(WarningExchangeAuthAPIDefaultOrEmptyValues, exch.Name) } else if exch.Name == "ITBIT" || exch.Name == "Bitstamp" || exch.Name == "COINUT" || exch.Name == "CoinbasePro" { @@ -844,7 +878,7 @@ func (c *Config) CheckCurrencyConfigValues() error { Enabled: false, Verbose: false, RESTPollingDelay: 600, - APIKey: "Key", + APIKey: DefaultUnsetAPIKey, APIKeyLvl: -1, PrimaryProvider: false, }, @@ -856,7 +890,7 @@ func (c *Config) CheckCurrencyConfigValues() error { count := 0 for i := range c.Currency.ForexProviders { if c.Currency.ForexProviders[i].Enabled { - if c.Currency.ForexProviders[i].APIKey == "Key" { + if c.Currency.ForexProviders[i].APIKey == DefaultUnsetAPIKey { log.Warnf("%s forex provider API key not set. Please set this in your config.json file", c.Currency.ForexProviders[i].Name) c.Currency.ForexProviders[i].Enabled = false c.Currency.ForexProviders[i].PrimaryProvider = false @@ -881,6 +915,32 @@ func (c *Config) CheckCurrencyConfigValues() error { } } + if c.Currency.CryptocurrencyProvider == (CryptocurrencyProvider{}) { + c.Currency.CryptocurrencyProvider.Name = "CoinMarketCap" + c.Currency.CryptocurrencyProvider.Enabled = false + c.Currency.CryptocurrencyProvider.Verbose = false + c.Currency.CryptocurrencyProvider.AccountPlan = DefaultUnsetAccountPlan + c.Currency.CryptocurrencyProvider.APIkey = DefaultUnsetAPIKey + } + + if c.Currency.CryptocurrencyProvider.Enabled { + if c.Currency.CryptocurrencyProvider.APIkey == "" || + c.Currency.CryptocurrencyProvider.APIkey == DefaultUnsetAPIKey { + log.Warnf("CryptocurrencyProvider enabled but api key is unset please set this in your config.json file") + } + if c.Currency.CryptocurrencyProvider.AccountPlan == "" || + c.Currency.CryptocurrencyProvider.AccountPlan == DefaultUnsetAccountPlan { + log.Warnf("CryptocurrencyProvider enabled but account plan is unset please set this in your config.json file") + } + } else { + if c.Currency.CryptocurrencyProvider.APIkey == "" { + c.Currency.CryptocurrencyProvider.APIkey = DefaultUnsetAPIKey + } + if c.Currency.CryptocurrencyProvider.AccountPlan == "" { + c.Currency.CryptocurrencyProvider.AccountPlan = DefaultUnsetAccountPlan + } + } + if len(c.Currency.Cryptocurrencies) == 0 { if len(c.Cryptocurrencies) != 0 { c.Currency.Cryptocurrencies = c.Cryptocurrencies diff --git a/config/config_test.go b/config/config_test.go index 2f457bb8..80c7b365 100644 --- a/config/config_test.go +++ b/config/config_test.go @@ -181,6 +181,31 @@ func TestUpdateCommunicationsConfig(t *testing.T) { } } +func TestGetCryptocurrencyProviderConfig(t *testing.T) { + cfg := GetConfig() + err := cfg.LoadConfig(ConfigTestFile) + if err != nil { + t.Error("Test failed. GetCryptocurrencyProviderConfig LoadConfig error", err) + } + _ = cfg.GetCryptocurrencyProviderConfig() +} + +func TestUpdateCryptocurrencyProviderConfig(t *testing.T) { + cfg := GetConfig() + err := cfg.LoadConfig(ConfigTestFile) + if err != nil { + t.Error("Test failed. UpdateCryptocurrencyProviderConfig LoadConfig error", err) + } + + orig := cfg.GetCryptocurrencyProviderConfig() + cfg.UpdateCryptocurrencyProviderConfig(CryptocurrencyProvider{Name: "SERIOUS TESTING PROCEDURE!"}) + if cfg.Currency.CryptocurrencyProvider.Name != "SERIOUS TESTING PROCEDURE!" { + t.Error("Test failed. UpdateCurrencyProviderConfig LoadConfig error") + } + + cfg.UpdateCryptocurrencyProviderConfig(orig) +} + func TestCheckCommunicationsConfig(t *testing.T) { cfg := GetConfig() err := cfg.LoadConfig(ConfigTestFile) diff --git a/config_example.json b/config_example.json index a380372a..250d4b6e 100644 --- a/config_example.json +++ b/config_example.json @@ -48,6 +48,13 @@ "primaryProvider": false } ], + "cryptocurrencyProvider": { + "name": "CoinMarketCap", + "enabled": false, + "verbose": false, + "apiKey": "Key", + "accountPlan": "accountPlan" + }, "cryptocurrencies": "BTC,LTC,ETH,XRP,NMC,NVC,PPC,XBT,DOGE,DASH", "currencyPairFormat": { "uppercase": true, diff --git a/currency/coinmarketcap/coinmarketcap.go b/currency/coinmarketcap/coinmarketcap.go new file mode 100644 index 00000000..c03978e2 --- /dev/null +++ b/currency/coinmarketcap/coinmarketcap.go @@ -0,0 +1,764 @@ +// Package coinmarketcap connects to a suite of high-performance RESTful JSON +// endpoints that are specifically designed to meet the mission-critical demands +// of application developers, data scientists, and enterprise business +// platforms. Please see https://coinmarketcap.com/api/documentation/v1/# for +// API documentation +package coinmarketcap + +import ( + "errors" + "fmt" + "log" + "net/url" + "strconv" + "strings" + "time" + + "github.com/thrasher-/gocryptotrader/common" + "github.com/thrasher-/gocryptotrader/exchanges/request" +) + +// Coinmarketcap account plan bitmasks, url and enpoint consts +const ( + Basic uint8 = 1 << iota + Hobbyist + Startup + Standard + Professional + Enterprise + + baseURL = "https://pro-api.coinmarketcap.com" + sandboxURL = "https://sandbox-api.coinmarketcap.com" + version = "/v1/" + + endpointCryptocurrencyInfo = "cryptocurrency/info" + endpointCryptocurrencyMap = "cryptocurrency/map" + endpointCryptocurrencyHistoricalListings = "cryptocurrency/listings/historical" + endpointCryptocurrencyLatestListings = "cryptocurrency/listings/latest" + endpointCryptocurrencyMarketPairs = "cryptocurrency/market-pairs/latest" + endpointOHLCVHistorical = "cryptocurrency/ohlcv/historical" + endpointOHLCVLatest = "cryptocurrency/ohlcv/latest" + endpointGetMarketQuotesHistorical = "cryptocurrency/quotes/historical" + endpointGetMarketQuotesLatest = "cryptocurrency/quotes/latest" + endpointExchangeInfo = "exchange/info" + endpointExchangeMap = "exchange/map" + endpointExchangeMarketPairsLatest = "exchange/market-pairs/latest" + endpointExchangeMarketQuoteHistorical = "exchange/quotes/historical" + endpointExchangeMarketQuoteLatest = "exchange/quotes/latest" + endpointGlobalQuoteHistorical = "global-metrics/quotes/historical" + endpointGlobalQuoteLatest = "global-metrics/quotes/latest" + endpointPriceConversion = "tools/price-conversion" + + authrate = 0 + defaultTimeOut = time.Second * 15 +) + +// Coinmarketcap is the overarching type across this package +type Coinmarketcap struct { + Verbose bool + Enabled bool + Name string + APIkey string + APIUrl string + APIVersion string + Plan uint8 + Requester *request.Requester +} + +// SetDefaults sets default values for the exchange +func (c *Coinmarketcap) SetDefaults() { + c.Name = "CoinMarketCap" + c.Enabled = false + c.Verbose = false + c.APIUrl = baseURL + c.APIVersion = version + c.Requester = request.New(c.Name, + request.NewRateLimit(time.Second*10, authrate), + request.NewRateLimit(time.Second*10, authrate), + common.NewHTTPClientWithTimeout(defaultTimeOut)) +} + +// Setup sets user configuration +func (c *Coinmarketcap) Setup(conf Settings) { + if !conf.Enabled { + c.Enabled = false + } else { + c.Enabled = true + c.Verbose = conf.Verbose + c.APIkey = conf.APIkey + err := c.SetAccountPlan(conf.AccountPlan) + if err != nil { + log.Fatal(err) + } + } +} + +// GetCryptocurrencyInfo returns all static metadata for one or more +// cryptocurrencies including name, symbol, logo, and its various registered +// URLs +// +// currencyID = digit code generated by coinmarketcap +func (c *Coinmarketcap) GetCryptocurrencyInfo(currencyID ...int64) (CryptoCurrencyInfo, error) { + resp := struct { + Data CryptoCurrencyInfo `json:"data"` + Status Status `json:"status"` + }{} + + err := c.CheckAccountPlan(Basic) + if err != nil { + return resp.Data, err + } + + var currStr []string + for _, d := range currencyID { + currStr = append(currStr, strconv.FormatInt(d, 10)) + } + + val := url.Values{} + val.Set("id", strings.Join(currStr, ",")) + + err = c.SendHTTPRequest("GET", endpointCryptocurrencyInfo, val, &resp) + if err != nil { + return resp.Data, err + } + + if resp.Status.ErrorCode != 0 { + return resp.Data, errors.New(resp.Status.ErrorMessage) + } + + return resp.Data, nil +} + +// GetCryptocurrencyIDMap returns a paginated list of all cryptocurrencies by +// CoinMarketCap ID. +func (c *Coinmarketcap) GetCryptocurrencyIDMap() ([]CryptoCurrencyMap, error) { + resp := struct { + Data []CryptoCurrencyMap `json:"data"` + Status Status `json:"status"` + }{} + + err := c.CheckAccountPlan(Basic) + if err != nil { + return resp.Data, err + } + + err = c.SendHTTPRequest("GET", endpointCryptocurrencyMap, nil, &resp) + if err != nil { + return resp.Data, err + } + + if resp.Status.ErrorCode != 0 { + return resp.Data, errors.New(resp.Status.ErrorMessage) + } + + return resp.Data, nil +} + +// GetCryptocurrencyHistoricalListings returns a paginated list of all +// cryptocurrencies with market data for a given historical time. +func (c *Coinmarketcap) GetCryptocurrencyHistoricalListings() ([]CryptocurrencyHistoricalListings, error) { + return nil, errors.New("this endpoint is not yet available") + // NOTE unreachable code but will be utilised at a later date + // resp := struct { + // Data []CryptocurrencyHistoricalListings `json:"data"` + // Status Status `json:"status"` + // }{} + + // err := c.CheckAccountPlan(0) + // if err != nil { + // return resp.Data, err + // } + + // err = c.SendHTTPRequest("GET", endpointCryptocurrencyHistoricalListings, nil, &resp) + // if err != nil { + // return resp.Data, err + // } + + // if resp.Status.ErrorCode != 0 { + // return resp.Data, errors.New(resp.Status.ErrorMessage) + // } + + // return resp.Data, nil +} + +// GetCryptocurrencyLatestListing returns a paginated list of all +// cryptocurrencies with latest market data. +// +// Start - optionally offsets the paginated items +// limit - optionally sets return limit on items [1..5000] +func (c *Coinmarketcap) GetCryptocurrencyLatestListing(start, limit int64) ([]CryptocurrencyLatestListings, error) { + resp := struct { + Data []CryptocurrencyLatestListings `json:"data"` + Status Status `json:"status"` + }{} + + err := c.CheckAccountPlan(Basic) + if err != nil { + return resp.Data, err + } + + val := url.Values{} + if start >= 1 { + val.Set("start", strconv.FormatInt(start, 10)) + } + + if limit > 0 { + val.Set("limit", strconv.FormatInt(limit, 10)) + } + + err = c.SendHTTPRequest("GET", endpointCryptocurrencyLatestListings, val, &resp) + if err != nil { + return resp.Data, err + } + + if resp.Status.ErrorCode != 0 { + return resp.Data, errors.New(resp.Status.ErrorMessage) + } + + return resp.Data, nil +} + +// GetCryptocurrencyLatestMarketPairs returns all market pairs across all +// exchanges for the specified cryptocurrency with associated stats. +// +// currencyID - refers to the coinmarketcap currency id +// Start - optionally offsets the paginated items +// limit - optionally sets return limit on items [1..5000] +func (c *Coinmarketcap) GetCryptocurrencyLatestMarketPairs(currencyID, start, limit int64) (CryptocurrencyLatestMarketPairs, error) { + resp := struct { + Data CryptocurrencyLatestMarketPairs `json:"data"` + Status Status `json:"status"` + }{} + + err := c.CheckAccountPlan(Standard) + if err != nil { + return resp.Data, err + } + + val := url.Values{} + val.Set("id", strconv.FormatInt(currencyID, 10)) + + if start >= 1 { + val.Set("start", strconv.FormatInt(start, 10)) + } + + if limit > 0 { + val.Set("limit", strconv.FormatInt(limit, 10)) + } + + err = c.SendHTTPRequest("GET", endpointCryptocurrencyMarketPairs, val, &resp) + if err != nil { + return resp.Data, err + } + + if resp.Status.ErrorCode != 0 { + return resp.Data, errors.New(resp.Status.ErrorMessage) + } + + return resp.Data, nil +} + +// GetCryptocurrencyOHLCHistorical return an interval of historic OHLCV +// (Open, High, Low, Close, Volume) market quotes for a cryptocurrency. +// Currently daily and hourly OHLCV periods are supported. +// +// currencyID - refers to the coinmarketcap currency id +// tStart - refers to the start time of historic value +// tEnd - refers to the end of the time block if zero will default to time.Now() +func (c *Coinmarketcap) GetCryptocurrencyOHLCHistorical(currencyID int64, tStart, tEnd time.Time) (CryptocurrencyOHLCHistorical, error) { + resp := struct { + Data CryptocurrencyOHLCHistorical `json:"data"` + Status Status `json:"status"` + }{} + + err := c.CheckAccountPlan(Standard) + if err != nil { + return resp.Data, err + } + + val := url.Values{} + val.Set("id", strconv.FormatInt(currencyID, 10)) + val.Set("time_start", strconv.FormatInt(tStart.Unix(), 10)) + + if !tEnd.IsZero() { + val.Set("time_end", strconv.FormatInt(tEnd.Unix(), 10)) + } + + err = c.SendHTTPRequest("GET", endpointOHLCVHistorical, val, &resp) + if err != nil { + return resp.Data, err + } + + if resp.Status.ErrorCode != 0 { + return resp.Data, errors.New(resp.Status.ErrorMessage) + } + + return resp.Data, nil +} + +// GetCryptocurrencyOHLCLatest return the latest OHLCV +// (Open, High, Low, Close, Volume) market values for one or more +// cryptocurrencies in the currently UTC day. Since the current UTC day is still +// active these values are updated frequently. You can find the final calculated +// OHLCV values for the last completed UTC day along with all historic days +// using /cryptocurrency/ohlcv/historical. +// +// currencyID - refers to the coinmarketcap currency id +func (c *Coinmarketcap) GetCryptocurrencyOHLCLatest(currencyID int64) (CryptocurrencyOHLCLatest, error) { + resp := struct { + Data CryptocurrencyOHLCLatest `json:"data"` + Status Status `json:"status"` + }{} + + err := c.CheckAccountPlan(Startup) + if err != nil { + return resp.Data, err + } + + val := url.Values{} + val.Set("id", strconv.FormatInt(currencyID, 10)) + + err = c.SendHTTPRequest("GET", endpointOHLCVLatest, val, &resp) + if err != nil { + return resp.Data, err + } + + if resp.Status.ErrorCode != 0 { + return resp.Data, errors.New(resp.Status.ErrorMessage) + } + + return resp.Data, nil +} + +// GetCryptocurrencyLatestQuotes returns the latest market quote for 1 or more +// cryptocurrencies. +// +// currencyID - refers to the coinmarketcap currency id +func (c *Coinmarketcap) GetCryptocurrencyLatestQuotes(currencyID ...int64) (CryptocurrencyLatestQuotes, error) { + resp := struct { + Data CryptocurrencyLatestQuotes `json:"data"` + Status Status `json:"status"` + }{} + + err := c.CheckAccountPlan(Basic) + if err != nil { + return resp.Data, err + } + + var currStr []string + for _, d := range currencyID { + currStr = append(currStr, strconv.FormatInt(d, 10)) + } + + val := url.Values{} + val.Set("id", strings.Join(currStr, ",")) + + err = c.SendHTTPRequest("GET", endpointGetMarketQuotesLatest, val, &resp) + if err != nil { + return resp.Data, err + } + + if resp.Status.ErrorCode != 0 { + return resp.Data, errors.New(resp.Status.ErrorMessage) + } + + return resp.Data, nil +} + +// GetCryptocurrencyHistoricalQuotes returns an interval of historic market +// quotes for any cryptocurrency based on time and interval parameters. +// +// currencyID - refers to the coinmarketcap currency id +// tStart - refers to the start time of historic value +// tEnd - refers to the end of the time block if zero will default to time.Now() +func (c *Coinmarketcap) GetCryptocurrencyHistoricalQuotes(currencyID int64, tStart, tEnd time.Time) (CryptocurrencyHistoricalQuotes, error) { + resp := struct { + Data CryptocurrencyHistoricalQuotes `json:"data"` + Status Status `json:"status"` + }{} + + err := c.CheckAccountPlan(Standard) + if err != nil { + return resp.Data, err + } + + val := url.Values{} + val.Set("id", strconv.FormatInt(currencyID, 10)) + val.Set("time_start", strconv.FormatInt(tStart.Unix(), 10)) + + if !tEnd.IsZero() { + val.Set("time_end", strconv.FormatInt(tEnd.Unix(), 10)) + } + + err = c.SendHTTPRequest("GET", endpointGetMarketQuotesHistorical, val, &resp) + if err != nil { + return resp.Data, err + } + + if resp.Status.ErrorCode != 0 { + return resp.Data, errors.New(resp.Status.ErrorMessage) + } + + return resp.Data, nil +} + +// GetExchangeInfo returns all static metadata for one or more exchanges +// including logo and homepage URL. +// +// exchangeID - refers to coinmarketcap exchange id +func (c *Coinmarketcap) GetExchangeInfo(exchangeID ...int64) (ExchangeInfo, error) { + resp := struct { + Data ExchangeInfo `json:"data"` + Status Status `json:"status"` + }{} + + err := c.CheckAccountPlan(Startup) + if err != nil { + return resp.Data, err + } + + var exchStr []string + for _, d := range exchangeID { + exchStr = append(exchStr, strconv.FormatInt(d, 10)) + } + + val := url.Values{} + val.Set("id", strings.Join(exchStr, ",")) + + err = c.SendHTTPRequest("GET", endpointExchangeInfo, val, &resp) + if err != nil { + return resp.Data, err + } + + if resp.Status.ErrorCode != 0 { + return resp.Data, errors.New(resp.Status.ErrorMessage) + } + + return resp.Data, nil +} + +// GetExchangeMap returns a paginated list of all cryptocurrency exchanges by +// CoinMarketCap ID. Recommend using this convenience endpoint to lookup and +// utilize the unique exchange id across all endpoints as typical exchange +// identifiers may change over time. ie huobi -> hadax -> global -> who knows +// what else +// +// Start - optionally offsets the paginated items +// limit - optionally sets return limit on items [1..5000] +func (c *Coinmarketcap) GetExchangeMap(start, limit int64) ([]ExchangeMap, error) { + resp := struct { + Data []ExchangeMap `json:"data"` + Status Status `json:"status"` + }{} + + err := c.CheckAccountPlan(Startup) + if err != nil { + return resp.Data, err + } + + val := url.Values{} + if start >= 1 { + val.Set("start", strconv.FormatInt(start, 10)) + } + + if limit != 0 { + val.Set("limit", strconv.FormatInt(start, 10)) + } + + err = c.SendHTTPRequest("GET", endpointExchangeMap, val, &resp) + if err != nil { + return resp.Data, err + } + + if resp.Status.ErrorCode != 0 { + return resp.Data, errors.New(resp.Status.ErrorMessage) + } + + return resp.Data, nil +} + +// GetExchangeHistoricalListings returns a paginated list of all cryptocurrency +// exchanges with historical market data for a given point in time. +func (c *Coinmarketcap) GetExchangeHistoricalListings() ([]ExchangeHistoricalListings, error) { + resp := struct { + Data []ExchangeHistoricalListings `json:"data"` + Status Status `json:"status"` + }{} + + return resp.Data, errors.New("this endpoint is not yet available") +} + +// GetExchangeLatestListings returns a paginated list of all cryptocurrency +// exchanges with historical market data for a given point in time. +func (c *Coinmarketcap) GetExchangeLatestListings() ([]ExchangeLatestListings, error) { + resp := struct { + Data []ExchangeLatestListings `json:"data"` + Status Status `json:"status"` + }{} + + return resp.Data, errors.New("this endpoint is not yet available") +} + +// GetExchangeLatestMarketPairs returns a list of active market pairs for an +// exchange. Active means the market pair is open for trading. +// +// exchangeID - refers to coinmarketcap exchange id +// Start - optionally offsets the paginated items +// limit - optionally sets return limit on items [1..5000] +func (c *Coinmarketcap) GetExchangeLatestMarketPairs(exchangeID, start, limit int64) (ExchangeLatestMarketPairs, error) { + resp := struct { + Data ExchangeLatestMarketPairs `json:"data"` + Status Status `json:"status"` + }{} + + err := c.CheckAccountPlan(Standard) + if err != nil { + return resp.Data, err + } + + val := url.Values{} + val.Set("id", strconv.FormatInt(exchangeID, 10)) + + if start >= 1 { + val.Set("start", strconv.FormatInt(start, 10)) + } + + if limit != 0 { + val.Set("limit", strconv.FormatInt(start, 10)) + } + + err = c.SendHTTPRequest("GET", endpointExchangeMarketPairsLatest, val, &resp) + if err != nil { + return resp.Data, err + } + + if resp.Status.ErrorCode != 0 { + return resp.Data, errors.New(resp.Status.ErrorMessage) + } + + return resp.Data, nil +} + +// GetExchangeLatestQuotes returns the latest aggregate market data for 1 or +// more exchanges. +// +// exchangeID - refers to coinmarketcap exchange id +func (c *Coinmarketcap) GetExchangeLatestQuotes(exchangeID ...int64) (ExchangeLatestQuotes, error) { + resp := struct { + Data ExchangeLatestQuotes `json:"data"` + Status Status `json:"status"` + }{} + + err := c.CheckAccountPlan(Standard) + if err != nil { + return resp.Data, err + } + + var exchStr []string + for _, d := range exchangeID { + exchStr = append(exchStr, strconv.FormatInt(d, 10)) + } + + val := url.Values{} + val.Set("id", strings.Join(exchStr, ",")) + + err = c.SendHTTPRequest("GET", endpointExchangeMarketQuoteLatest, val, &resp) + if err != nil { + return resp.Data, err + } + + if resp.Status.ErrorCode != 0 { + return resp.Data, errors.New(resp.Status.ErrorMessage) + } + + return resp.Data, nil +} + +// GetExchangeHistoricalQuotes returns an interval of historic quotes for any +// exchange based on time and interval parameters. +// +// exchangeID - refers to coinmarketcap exchange id +// tStart - refers to the start time of historic value +// tEnd - refers to the end of the time block if zero will default to time.Now() +func (c *Coinmarketcap) GetExchangeHistoricalQuotes(exchangeID int64, tStart, tEnd time.Time) (ExchangeHistoricalQuotes, error) { + resp := struct { + Data ExchangeHistoricalQuotes `json:"data"` + Status Status `json:"status"` + }{} + + err := c.CheckAccountPlan(Standard) + if err != nil { + return resp.Data, err + } + + val := url.Values{} + val.Set("id", strconv.FormatInt(exchangeID, 10)) + val.Set("time_start", strconv.FormatInt(tStart.Unix(), 10)) + + if !tEnd.IsZero() { + val.Set("time_end", strconv.FormatInt(tEnd.Unix(), 10)) + } + + err = c.SendHTTPRequest("GET", endpointExchangeMarketQuoteHistorical, val, &resp) + if err != nil { + return resp.Data, err + } + + if resp.Status.ErrorCode != 0 { + return resp.Data, errors.New(resp.Status.ErrorMessage) + } + + return resp.Data, nil +} + +// GetGlobalMeticLatestQuotes returns the latest quote of aggregate market +// metrics. +func (c *Coinmarketcap) GetGlobalMeticLatestQuotes() (GlobalMeticLatestQuotes, error) { + resp := struct { + Data GlobalMeticLatestQuotes `json:"data"` + Status Status `json:"status"` + }{} + + err := c.CheckAccountPlan(Basic) + if err != nil { + return resp.Data, err + } + + err = c.SendHTTPRequest("GET", endpointGlobalQuoteLatest, nil, &resp) + if err != nil { + return resp.Data, err + } + + if resp.Status.ErrorCode != 0 { + return resp.Data, errors.New(resp.Status.ErrorMessage) + } + + return resp.Data, nil +} + +// GetGlobalMeticHistoricalQuotes returns an interval of aggregate 24 hour +// volume and market cap data globally based on time and interval parameters. +// +// tStart - refers to the start time of historic value +// tEnd - refers to the end of the time block if zero will default to time.Now() +func (c *Coinmarketcap) GetGlobalMeticHistoricalQuotes(tStart, tEnd time.Time) (GlobalMeticHistoricalQuotes, error) { + resp := struct { + Data GlobalMeticHistoricalQuotes `json:"data"` + Status Status `json:"status"` + }{} + + err := c.CheckAccountPlan(Standard) + if err != nil { + return resp.Data, err + } + + val := url.Values{} + val.Set("time_start", strconv.FormatInt(tStart.Unix(), 10)) + + if !tEnd.IsZero() { + val.Set("time_end", strconv.FormatInt(tEnd.Unix(), 10)) + } + + err = c.SendHTTPRequest("GET", endpointGlobalQuoteHistorical, val, &resp) + if err != nil { + return resp.Data, err + } + + if resp.Status.ErrorCode != 0 { + return resp.Data, errors.New(resp.Status.ErrorMessage) + } + + return resp.Data, nil +} + +// GetPriceConversion converts an amount of one currency into multiple +// cryptocurrencies or fiat currencies at the same time using the latest market +// averages. Optionally pass a historical timestamp to convert values based on +// historic averages. +// +// amount - An amount of currency to convert. Example: 10.43 +// currencyID - refers to the coinmarketcap currency id +// atHistoricTime - [Optional] timestamp to reference historical pricing during +// conversion. +func (c *Coinmarketcap) GetPriceConversion(amount float64, currencyID int64, atHistoricTime time.Time) (PriceConversion, error) { + resp := struct { + Data PriceConversion `json:"data"` + Status + }{} + + err := c.CheckAccountPlan(Hobbyist) + if err != nil { + return resp.Data, err + } + + val := url.Values{} + val.Set("amount", strconv.FormatFloat(amount, 'f', -1, 64)) + val.Set("id", strconv.FormatInt(currencyID, 10)) + + if !atHistoricTime.IsZero() { + val.Set("time", strconv.FormatInt(atHistoricTime.Unix(), 10)) + } + + err = c.SendHTTPRequest("GET", endpointPriceConversion, val, &resp) + if err != nil { + return resp.Data, err + } + + if resp.Status.ErrorCode != 0 { + return resp.Data, errors.New(resp.Status.ErrorMessage) + } + + return resp.Data, nil +} + +// SendHTTPRequest sends a valid HTTP request +func (c *Coinmarketcap) SendHTTPRequest(method, endpoint string, v url.Values, result interface{}) error { + headers := make(map[string]string) + headers["Accept"] = "application/json" + headers["Accept-Encoding"] = "deflate, gzip" + headers["X-CMC_PRO_API_KEY"] = c.APIkey + + path := c.APIUrl + c.APIVersion + endpoint + if v != nil { + path = path + "?" + v.Encode() + } + + return c.Requester.SendPayload(method, + path, + headers, + strings.NewReader(""), + result, + false, + c.Verbose) +} + +// CheckAccountPlan checks your current account plan to the minimal account +// needed to send http request, this is used to minimize requests for lower +// account privileges +func (c *Coinmarketcap) CheckAccountPlan(minAllowable uint8) error { + if c.Plan < minAllowable { + return errors.New("function use not allowed, higher plan needed") + } + return nil +} + +// SetAccountPlan sets account plan +func (c *Coinmarketcap) SetAccountPlan(s string) error { + switch s { + case "basic": + c.Plan = Basic + case "hobbyist": + c.Plan = Hobbyist + case "startup": + c.Plan = Startup + case "standard": + c.Plan = Standard + case "professional": + c.Plan = Professional + case "enterprise": + c.Plan = Enterprise + default: + return fmt.Errorf("account plan %s not found", s) + } + return nil +} diff --git a/currency/coinmarketcap/coinmarketcap_test.go b/currency/coinmarketcap/coinmarketcap_test.go new file mode 100644 index 00000000..67d35f49 --- /dev/null +++ b/currency/coinmarketcap/coinmarketcap_test.go @@ -0,0 +1,415 @@ +package coinmarketcap + +import ( + "testing" + "time" + + log "github.com/thrasher-/gocryptotrader/logger" +) + +var c Coinmarketcap + +// Please set API keys to test endpoint +const ( + apikey = "" + apiAccountPlanLevel = "" +) + +// Checks credentials but also checks to see if the function can take the +// required account plan level +func areAPICredtionalsSet(minAllowable uint8) bool { + if apiAccountPlanLevel != "" && apikey != "" { + if err := c.CheckAccountPlan(minAllowable); err != nil { + log.Warn("coinmarketpcap test suite - account plan not allowed for function, please review or upgrade plan to test") + return false + } + return true + } + return false +} + +func TestSetDefaults(t *testing.T) { + c.SetDefaults() +} + +func TestSetup(t *testing.T) { + c.SetDefaults() + + cfg := Settings{} + cfg.APIkey = apikey + cfg.AccountPlan = apiAccountPlanLevel + cfg.Enabled = true + cfg.AccountPlan = "basic" + + c.Setup(cfg) +} + +func TestCheckAccountPlan(t *testing.T) { + c.SetDefaults() + TestSetup(t) + + if areAPICredtionalsSet(Basic) { + err := c.CheckAccountPlan(Enterprise) + if err == nil { + t.Error("Test Failed - CheckAccountPlan() error cannot be nil") + } + + err = c.CheckAccountPlan(Professional) + if err == nil { + t.Error("Test Failed - CheckAccountPlan() error cannot be nil") + } + + err = c.CheckAccountPlan(Standard) + if err == nil { + t.Error("Test Failed - CheckAccountPlan() error cannot be nil") + } + + err = c.CheckAccountPlan(Hobbyist) + if err == nil { + t.Error("Test Failed - CheckAccountPlan() error cannot be nil") + } + + err = c.CheckAccountPlan(Startup) + if err == nil { + t.Error("Test Failed - CheckAccountPlan() error cannot be nil") + } + + err = c.CheckAccountPlan(Basic) + if err != nil { + t.Error("Test Failed - CheckAccountPlan() error", err) + } + } +} + +func TestGetCryptocurrencyInfo(t *testing.T) { + c.SetDefaults() + TestSetup(t) + _, err := c.GetCryptocurrencyInfo(1) + if areAPICredtionalsSet(Basic) { + if err != nil { + t.Error("Test Failed - GetCryptocurrencyInfo() error", err) + } + } else { + if err == nil { + t.Error("Test Failed - GetCryptocurrencyInfo() error cannot be nil") + } + } +} + +func TestGetCryptocurrencyIDMap(t *testing.T) { + c.SetDefaults() + TestSetup(t) + _, err := c.GetCryptocurrencyIDMap() + if areAPICredtionalsSet(Basic) { + if err != nil { + t.Error("Test Failed - GetCryptocurrencyIDMap() error", err) + } + } else { + if err == nil { + t.Error("Test Failed - GetCryptocurrencyIDMap() error cannot be nil") + } + } +} + +func TestGetCryptocurrencyHistoricalListings(t *testing.T) { + c.SetDefaults() + TestSetup(t) + _, err := c.GetCryptocurrencyHistoricalListings() + if err == nil { + t.Error("Test Failed - GetCryptocurrencyHistoricalListings() error cannot be nil") + } +} + +func TestGetCryptocurrencyLatestListing(t *testing.T) { + c.SetDefaults() + TestSetup(t) + _, err := c.GetCryptocurrencyLatestListing(0, 0) + if areAPICredtionalsSet(Basic) { + if err != nil { + t.Error("Test Failed - GetCryptocurrencyLatestListing() error", err) + } + } else { + if err == nil { + t.Error("Test Failed - GetCryptocurrencyLatestListing() error cannot be nil") + } + } +} + +func TestGetCryptocurrencyLatestMarketPairs(t *testing.T) { + c.SetDefaults() + TestSetup(t) + _, err := c.GetCryptocurrencyLatestMarketPairs(1, 0, 0) + if areAPICredtionalsSet(Standard) { + if err != nil { + t.Error("Test Failed - GetCryptocurrencyLatestMarketPairs() error", + err) + } + } else { + if err == nil { + t.Error("Test Failed - GetCryptocurrencyLatestMarketPairs() error cannot be nil") + } + } +} + +func TestGetCryptocurrencyOHLCHistorical(t *testing.T) { + c.SetDefaults() + TestSetup(t) + _, err := c.GetCryptocurrencyOHLCHistorical(1, time.Now(), time.Now()) + if areAPICredtionalsSet(Standard) { + if err != nil { + t.Error("Test Failed - GetCryptocurrencyOHLCHistorical() error", + err) + } + } else { + if err == nil { + t.Error("Test Failed - GetCryptocurrencyOHLCHistorical() error cannot be nil") + } + } +} + +func TestGetCryptocurrencyOHLCLatest(t *testing.T) { + c.SetDefaults() + TestSetup(t) + _, err := c.GetCryptocurrencyOHLCLatest(1) + if areAPICredtionalsSet(Startup) { + if err != nil { + t.Error("Test Failed - GetCryptocurrencyOHLCLatest() error", + err) + } + } else { + if err == nil { + t.Error("Test Failed - GetCryptocurrencyOHLCLatest() error cannot be nil") + } + } +} + +func TestGetCryptocurrencyLatestQuotes(t *testing.T) { + c.SetDefaults() + TestSetup(t) + _, err := c.GetCryptocurrencyLatestQuotes(1) + if areAPICredtionalsSet(Basic) { + if err != nil { + t.Error("Test Failed - GetCryptocurrencyLatestQuotes() error", + err) + } + } else { + if err == nil { + t.Error("Test Failed - GetCryptocurrencyLatestQuotes() error cannot be nil") + } + } +} + +func TestGetCryptocurrencyHistoricalQuotes(t *testing.T) { + c.SetDefaults() + TestSetup(t) + _, err := c.GetCryptocurrencyHistoricalQuotes(1, time.Now(), time.Now()) + if areAPICredtionalsSet(Standard) { + if err != nil { + t.Error("Test Failed - GetCryptocurrencyHistoricalQuotes() error", + err) + } + } else { + if err == nil { + t.Error("Test Failed - GetCryptocurrencyHistoricalQuotes() error cannot be nil") + } + } +} + +func TestGetExchangeInfo(t *testing.T) { + c.SetDefaults() + TestSetup(t) + _, err := c.GetExchangeInfo(1) + if areAPICredtionalsSet(Startup) { + if err != nil { + t.Error("Test Failed - GetExchangeInfo() error", + err) + } + } else { + if err == nil { + t.Error("Test Failed - GetExchangeInfo() error cannot be nil") + } + } +} + +func TestGetExchangeMap(t *testing.T) { + c.SetDefaults() + TestSetup(t) + _, err := c.GetExchangeMap(0, 0) + if areAPICredtionalsSet(Startup) { + if err != nil { + t.Error("Test Failed - GetExchangeMap() error", + err) + } + } else { + if err == nil { + t.Error("Test Failed - GetExchangeMap() error cannot be nil") + } + } +} + +func TestGetExchangeHistoricalListings(t *testing.T) { + c.SetDefaults() + TestSetup(t) + _, err := c.GetExchangeHistoricalListings() + if areAPICredtionalsSet(Basic) { + if err == nil { + t.Error("Test Failed - GetExchangeHistoricalListings() error cannot be nil") + } + } else { + if err == nil { + t.Error("Test Failed - GetExchangeHistoricalListings() error cannot be nil") + } + } +} + +func TestGetExchangeLatestListings(t *testing.T) { + c.SetDefaults() + TestSetup(t) + _, err := c.GetExchangeLatestListings() + if areAPICredtionalsSet(Basic) { + if err == nil { + t.Error("Test Failed - GetExchangeLatestListings() error cannot be nil") + } + } else { + if err == nil { + t.Error("Test Failed - GetExchangeLatestListings() error cannot be nil") + } + } +} + +func TestGetExchangeLatestMarketPairs(t *testing.T) { + c.SetDefaults() + TestSetup(t) + _, err := c.GetExchangeLatestMarketPairs(1, 0, 0) + if areAPICredtionalsSet(Standard) { + if err != nil { + t.Error("Test Failed - GetExchangeLatestMarketPairs() error", + err) + } + } else { + if err == nil { + t.Error("Test Failed - GetExchangeLatestMarketPairs() error cannot be nil") + } + } +} + +func TestGetExchangeLatestQuotes(t *testing.T) { + c.SetDefaults() + TestSetup(t) + _, err := c.GetExchangeLatestQuotes(1) + if areAPICredtionalsSet(Standard) { + if err != nil { + t.Error("Test Failed - GetExchangeLatestQuotes() error", + err) + } + } else { + if err == nil { + t.Error("Test Failed - GetExchangeLatestQuotes() error cannot be nil") + } + } +} + +func TestGetExchangeHistoricalQuotes(t *testing.T) { + c.SetDefaults() + TestSetup(t) + _, err := c.GetExchangeHistoricalQuotes(1, time.Now(), time.Now()) + if areAPICredtionalsSet(Standard) { + if err != nil { + t.Error("Test Failed - GetExchangeHistoricalQuotes() error", + err) + } + } else { + if err == nil { + t.Error("Test Failed - GetExchangeHistoricalQuotes() error cannot be nil") + } + } +} + +func TestGetGlobalMeticLatestQuotes(t *testing.T) { + c.SetDefaults() + TestSetup(t) + _, err := c.GetGlobalMeticLatestQuotes() + if areAPICredtionalsSet(Basic) { + if err != nil { + t.Error("Test Failed - GetGlobalMeticLatestQuotes() error", + err) + } + } else { + if err == nil { + t.Error("Test Failed - GetGlobalMeticLatestQuotes() error cannot be nil") + } + } +} + +func TestGetGlobalMeticHistoricalQuotes(t *testing.T) { + c.SetDefaults() + TestSetup(t) + _, err := c.GetGlobalMeticHistoricalQuotes(time.Now(), time.Now()) + if areAPICredtionalsSet(Standard) { + if err != nil { + t.Error("Test Failed - GetGlobalMeticHistoricalQuotes() error", + err) + } + } else { + if err == nil { + t.Error("Test Failed - GetGlobalMeticHistoricalQuotes() error cannot be nil") + } + } +} + +func TestGetPriceConversion(t *testing.T) { + c.SetDefaults() + TestSetup(t) + _, err := c.GetPriceConversion(0, 1, time.Now()) + if areAPICredtionalsSet(Hobbyist) { + if err != nil { + t.Error("Test Failed - GetPriceConversion() error", + err) + } + } else { + if err == nil { + t.Error("Test Failed - GetPriceConversion() error cannot be nil") + } + } +} + +func TestSetAccountPlan(t *testing.T) { + accPlans := []string{"basic", "startup", "hobbyist", "standard", "professional", "enterprise"} + for _, plan := range accPlans { + err := c.SetAccountPlan(plan) + if err != nil { + t.Error("Test Failed - SetAccountPlan() error", err) + } + + switch plan { + case "basic": + if c.Plan != Basic { + t.Error("Test Failed - SetAccountPlan() error basic plan not set correctly") + } + case "startup": + if c.Plan != Startup { + t.Error("Test Failed - SetAccountPlan() error startup plan not set correctly") + } + case "hobbyist": + if c.Plan != Hobbyist { + t.Error("Test Failed - SetAccountPlan() error hobbyist plan not set correctly") + } + case "standard": + if c.Plan != Standard { + t.Error("Test Failed - SetAccountPlan() error standard plan not set correctly") + } + case "professional": + if c.Plan != Professional { + t.Error("Test Failed - SetAccountPlan() error professional plan not set correctly") + } + case "enterprise": + if c.Plan != Enterprise { + t.Error("Test Failed - SetAccountPlan() error enterprise plan not set correctly") + } + } + } + + if err := c.SetAccountPlan("bra"); err == nil { + t.Error("Test Failed - SetAccountPlan() error cannot be nil") + } +} diff --git a/currency/coinmarketcap/coinmarketcap_types.go b/currency/coinmarketcap/coinmarketcap_types.go new file mode 100644 index 00000000..074369d9 --- /dev/null +++ b/currency/coinmarketcap/coinmarketcap_types.go @@ -0,0 +1,363 @@ +package coinmarketcap + +import "time" + +// Settings defines the current settings from configuration file +type Settings struct { + Name string `json:"name"` + Enabled bool `json:"enabled"` + Verbose bool `json:"verbose"` + APIkey string `json:"apiKey"` + AccountPlan string `json:"accountPlan"` +} + +// Status defines a response status JSON struct that is received with every +// HTTP request +type Status struct { + Timestamp string `json:"timestamp"` + ErrorCode int64 `json:"error_code"` + ErrorMessage string `json:"error_message"` + Elapsed int64 `json:"elapsed"` + CreditCount int64 `json:"credit_count"` +} + +// Currency defines a generic sub type to capture currency data +type Currency struct { + Price float64 `json:"price"` + Volume24H float64 `json:"volume_24h"` + Volume24HAdjusted float64 `json:"volume_24h_adjusted"` + Volume7D float64 `json:"volume_7d"` + Volume30D float64 `json:"volume_30d"` + PercentChange1H float64 `json:"percent_change_1h"` + PercentChangeVolume24H float64 `json:"percent_change_volume_24h"` + PercentChangeVolume7D float64 `json:"percent_change_volume_7d"` + PercentChangeVolume30D float64 `json:"percent_change_volume_30d"` + MarketCap float64 `json:"market_cap"` + TotalMarketCap float64 `json:"total_market_cap"` + LastUpdated time.Time `json:"last_updated"` +} + +// OHLC defines a generic sub type for OHLC currency data +type OHLC struct { + Open float64 `json:"open"` + High float64 `json:"high"` + Low float64 `json:"low"` + Close float64 `json:"close"` + Volume float64 `json:"volume"` + Timestamp time.Time `json:"timestamp"` +} + +// CryptoCurrencyInfo defines cryptocurrency information +type CryptoCurrencyInfo map[string]struct { + ID int `json:"id"` + Name string `json:"name"` + Symbol string `json:"symbol"` + Category string `json:"category"` + Slug string `json:"slug"` + Logo string `json:"logo"` + Tags []string `json:"tags"` + Platform interface{} `json:"platform"` + Urls struct { + Website []string `json:"website"` + Explorer []string `json:"explorer"` + SourceCode []string `json:"source_code"` + MessageBoard []string `json:"message_board"` + Chat []interface{} `json:"chat"` + Announcement []interface{} `json:"announcement"` + Reddit []string `json:"reddit"` + Twitter []string `json:"twitter"` + } `json:"urls"` +} + +// CryptoCurrencyMap defines a cryptocurrency struct +type CryptoCurrencyMap struct { + ID int `json:"id"` + Name string `json:"name"` + Symbol string `json:"symbol"` + Slug string `json:"slug"` + IsActive int `json:"is_active"` + FirstHistoricalData time.Time `json:"first_historical_data"` + LastHistoricalData time.Time `json:"last_historical_data"` + Platform interface{} `json:"platform"` +} + +// CryptocurrencyHistoricalListings defines a historical listing data +type CryptocurrencyHistoricalListings struct { + ID int `json:"id"` + Name string `json:"name"` + Symbol string `json:"symbol"` + Slug string `json:"slug"` + CmcRank int `json:"cmc_rank"` + NumMarketPairs int `json:"num_market_pairs"` + CirculatingSupply float64 `json:"circulating_supply"` + TotalSupply float64 `json:"total_supply"` + MaxSupply float64 `json:"max_supply"` + LastUpdated time.Time `json:"last_updated"` + Quote struct { + USD Currency `json:"USD"` + BTC Currency `json:"BTC"` + } `json:"quote"` +} + +// CryptocurrencyLatestListings defines latest cryptocurrency listing data +type CryptocurrencyLatestListings struct { + ID int `json:"id"` + Name string `json:"name"` + Symbol string `json:"symbol"` + Slug string `json:"slug"` + CmcRank int `json:"cmc_rank"` + NumMarketPairs int `json:"num_market_pairs"` + CirculatingSupply float64 `json:"circulating_supply"` + TotalSupply float64 `json:"total_supply"` + MaxSupply float64 `json:"max_supply"` + LastUpdated time.Time `json:"last_updated"` + DateAdded time.Time `json:"date_added"` + Tags []string `json:"tags"` + Platform interface{} `json:"platform"` + Quote struct { + USD Currency `json:"USD"` + BTC Currency `json:"BTC"` + } `json:"quote"` +} + +// CryptocurrencyLatestMarketPairs defines the latest cryptocurrency pairs +type CryptocurrencyLatestMarketPairs struct { + ID int `json:"id"` + Name string `json:"name"` + Symbol string `json:"symbol"` + NumMarketPairs int `json:"num_market_pairs"` + MarketPairs []struct { + Exchange struct { + ID int `json:"id"` + Name string `json:"name"` + Slug string `json:"slug"` + } `json:"exchange"` + MarketPair string `json:"market_pair"` + MarketPairBase struct { + CurrencyID int `json:"currency_id"` + CurrencySymbol string `json:"currency_symbol"` + CurrencyType string `json:"currency_type"` + } `json:"market_pair_base"` + MarketPairQuote struct { + CurrencyID int `json:"currency_id"` + CurrencySymbol string `json:"currency_symbol"` + CurrencyType string `json:"currency_type"` + } `json:"market_pair_quote"` + Quote struct { + ExchangeReported struct { + Price float64 `json:"price"` + Volume24HBase float64 `json:"volume_24h_base"` + Volume24HQuote float64 `json:"volume_24h_quote"` + LastUpdated time.Time `json:"last_updated"` + } `json:"exchange_reported"` + USD Currency `json:"USD"` + } `json:"quote"` + } `json:"market_pairs"` +} + +// CryptocurrencyOHLCHistorical defines open high low close historical data +type CryptocurrencyOHLCHistorical struct { + ID int `json:"id"` + Name string `json:"name"` + Symbol string `json:"symbol"` + Quotes []struct { + TimeOpen time.Time `json:"time_open"` + TimeClose time.Time `json:"time_close"` + Quote struct { + USD OHLC `json:"USD"` + } `json:"quote"` + } `json:"quotes"` +} + +// CryptocurrencyOHLCLatest defines open high low close latest data +type CryptocurrencyOHLCLatest map[string]struct { + ID int `json:"id"` + Name string `json:"name"` + Symbol string `json:"symbol"` + LastUpdated time.Time `json:"last_updated"` + TimeOpen time.Time `json:"time_open"` + TimeClose interface{} `json:"time_close"` + Quote struct { + USD OHLC `json:"USD"` + } `json:"quote"` +} + +// CryptocurrencyLatestQuotes defines latest cryptocurrency quotation data +type CryptocurrencyLatestQuotes map[string]struct { + ID int `json:"id"` + Name string `json:"name"` + Symbol string `json:"symbol"` + Slug string `json:"slug"` + CirculatingSupply float64 `json:"circulating_supply"` + TotalSupply float64 `json:"total_supply"` + MaxSupply float64 `json:"max_supply"` + DateAdded time.Time `json:"date_added"` + NumMarketPairs int `json:"num_market_pairs"` + CmcRank int `json:"cmc_rank"` + LastUpdated time.Time `json:"last_updated"` + Tags []string `json:"tags"` + Platform interface{} `json:"platform"` + Quote struct { + USD Currency `json:"USD"` + } `json:"quote"` +} + +// CryptocurrencyHistoricalQuotes defines historical cryptocurrency quotation +// data +type CryptocurrencyHistoricalQuotes struct { + ID int `json:"id"` + Name string `json:"name"` + Symbol string `json:"symbol"` + Quotes []struct { + Timestamp time.Time `json:"timestamp"` + Quote struct { + USD Currency `json:"USD"` + } `json:"quote"` + } `json:"quotes"` +} + +// ExchangeInfo defines exchange information +type ExchangeInfo map[string]struct { + Urls struct { + Website []string `json:"website"` + Twitter []string `json:"twitter"` + Blog []interface{} `json:"blog"` + Chat []string `json:"chat"` + Fee []string `json:"fee"` + } `json:"urls"` + Logo string `json:"logo"` + ID int `json:"id"` + Name string `json:"name"` + Slug string `json:"slug"` +} + +// ExchangeMap defines a data for an exchange +type ExchangeMap struct { + ID int `json:"id"` + Name string `json:"name"` + Slug string `json:"slug"` + IsActive int `json:"is_active"` + FirstHistoricalData time.Time `json:"first_historical_data"` + LastHistoricalData time.Time `json:"last_historical_data"` +} + +// ExchangeHistoricalListings defines historical exchange listings +type ExchangeHistoricalListings struct { + ID int `json:"id"` + Name string `json:"name"` + Slug string `json:"slug"` + CmcRank int `json:"cmc_rank"` + NumMarketPairs int `json:"num_market_pairs"` + Timestamp time.Time `json:"timestamp"` + Quote struct { + USD Currency `json:"USD"` + } `json:"quote"` +} + +// ExchangeLatestListings defines latest exchange listings +type ExchangeLatestListings struct { + ID int `json:"id"` + Name string `json:"name"` + Slug string `json:"slug"` + NumMarketPairs int `json:"num_market_pairs"` + LastUpdated time.Time `json:"last_updated"` + Quote struct { + USD Currency `json:"USD"` + } `json:"quote"` +} + +// ExchangeLatestMarketPairs defines latest market pairs +type ExchangeLatestMarketPairs struct { + ID int `json:"id"` + Name string `json:"name"` + Slug string `json:"slug"` + NumMarketPairs int `json:"num_market_pairs"` + MarketPairs []struct { + MarketPair string `json:"market_pair"` + MarketPairBase struct { + CurrencyID int `json:"currency_id"` + CurrencySymbol string `json:"currency_symbol"` + CurrencyType string `json:"currency_type"` + } `json:"market_pair_base"` + MarketPairQuote struct { + CurrencyID int `json:"currency_id"` + CurrencySymbol string `json:"currency_symbol"` + CurrencyType string `json:"currency_type"` + } `json:"market_pair_quote"` + Quote struct { + ExchangeReported struct { + Price float64 `json:"price"` + Volume24HBase float64 `json:"volume_24h_base"` + Volume24HQuote float64 `json:"volume_24h_quote"` + LastUpdated time.Time `json:"last_updated"` + } `json:"exchange_reported"` + USD Currency `json:"USD"` + } `json:"quote"` + } `json:"market_pairs"` +} + +// ExchangeLatestQuotes defines latest exchange quotations +type ExchangeLatestQuotes struct { + Binance struct { + ID int `json:"id"` + Name string `json:"name"` + Slug string `json:"slug"` + NumMarketPairs int `json:"num_market_pairs"` + LastUpdated time.Time `json:"last_updated"` + Quote struct { + USD Currency `json:"USD"` + } `json:"quote"` + } `json:"binance"` +} + +// ExchangeHistoricalQuotes defines historical exchange quotations +type ExchangeHistoricalQuotes struct { + ID int `json:"id"` + Name string `json:"name"` + Slug string `json:"slug"` + Quotes []struct { + Timestamp time.Time `json:"timestamp"` + Quote struct { + USD Currency `json:"USD"` + } `json:"quote"` + NumMarketPairs int `json:"num_market_pairs"` + } `json:"quotes"` +} + +// GlobalMeticLatestQuotes defines latest global metric quotations +type GlobalMeticLatestQuotes struct { + BtcDominance float64 `json:"btc_dominance"` + EthDominance float64 `json:"eth_dominance"` + ActiveCryptocurrencies int `json:"active_cryptocurrencies"` + ActiveMarketPairs int `json:"active_market_pairs"` + ActiveExchanges int `json:"active_exchanges"` + LastUpdated time.Time `json:"last_updated"` + Quote struct { + USD Currency `json:"USD"` + } `json:"quote"` +} + +// GlobalMeticHistoricalQuotes defines historical global metric quotations +type GlobalMeticHistoricalQuotes struct { + Quotes []struct { + Timestamp time.Time `json:"timestamp"` + BtcDominance float64 `json:"btc_dominance"` + Quote struct { + USD Currency `json:"USD"` + } `json:"quote"` + } `json:"quotes"` +} + +// PriceConversion defines price conversion data +type PriceConversion struct { + Symbol string `json:"symbol"` + ID string `json:"id"` + Name string `json:"name"` + Amount float64 `json:"amount"` + LastUpdated time.Time `json:"last_updated"` + Quote struct { + GBP Currency `json:"GBP"` + LTC Currency `json:"LTC"` + USD Currency `json:"USD"` + } `json:"quote"` +} diff --git a/currency/currency.go b/currency/currency.go index c63573d6..2f4813eb 100644 --- a/currency/currency.go +++ b/currency/currency.go @@ -1,9 +1,12 @@ package currency import ( + "errors" "fmt" + "time" "github.com/thrasher-/gocryptotrader/common" + "github.com/thrasher-/gocryptotrader/currency/coinmarketcap" "github.com/thrasher-/gocryptotrader/currency/forexprovider" "github.com/thrasher-/gocryptotrader/currency/pair" log "github.com/thrasher-/gocryptotrader/logger" @@ -27,6 +30,10 @@ var ( BaseCurrency string FXProviders *forexprovider.ForexProviders + + CryptocurrencyProvider *coinmarketcap.Coinmarketcap + TotalCryptocurrencies []Data + TotalExchanges []Data ) // SetDefaults sets the default currency provider and settings for @@ -201,3 +208,113 @@ func ConvertCurrency(amount float64, from, to string) (float64, error) { return converted * resultTo, nil } + +// Data defines information pertaining to exchange or a cryptocurrency from +// coinmarketcap +type Data struct { + ID int + Name string + Symbol string `json:",omitempty"` + Slug string + Active bool + LastUpdated time.Time +} + +// SeedCryptocurrencyMarketData seeds cryptocurrency market data +func SeedCryptocurrencyMarketData(settings coinmarketcap.Settings) error { + if !settings.Enabled { + return errors.New("not enabled please set in config.json with apikey and account levels") + } + + if CryptocurrencyProvider == nil { + err := setupCryptoProvider(settings) + if err != nil { + return err + } + } + + cryptoData, err := CryptocurrencyProvider.GetCryptocurrencyIDMap() + if err != nil { + return err + } + + for _, data := range cryptoData { + var active bool + if data.IsActive == 1 { + active = true + } + + TotalCryptocurrencies = append(TotalCryptocurrencies, Data{ + ID: data.ID, + Name: data.Name, + Symbol: data.Symbol, + Slug: data.Slug, + Active: active, + LastUpdated: time.Now(), + }) + } + + return nil +} + +// SeedExchangeMarketData seeds exchange market data +func SeedExchangeMarketData(settings coinmarketcap.Settings) error { + if !settings.Enabled { + return errors.New("not enabled please set in config.json with apikey and account levels") + } + + if CryptocurrencyProvider == nil { + err := setupCryptoProvider(settings) + if err != nil { + return err + } + } + + exchangeData, err := CryptocurrencyProvider.GetExchangeMap(0, 0) + if err != nil { + return err + } + + for _, data := range exchangeData { + var active bool + if data.IsActive == 1 { + active = true + } + + TotalExchanges = append(TotalExchanges, Data{ + ID: data.ID, + Name: data.Name, + Slug: data.Slug, + Active: active, + LastUpdated: time.Now(), + }) + } + + return nil +} + +func setupCryptoProvider(settings coinmarketcap.Settings) error { + if settings.APIkey == "" || + settings.APIkey == "key" || + settings.AccountPlan == "" || + settings.AccountPlan == "accountPlan" { + return errors.New("currencyprovider error api key or plan not set in config.json") + } + + CryptocurrencyProvider = new(coinmarketcap.Coinmarketcap) + CryptocurrencyProvider.SetDefaults() + CryptocurrencyProvider.Setup(settings) + + return nil +} + +// GetTotalMarketCryptocurrencies returns the total seeded market +// cryptocurrencies +func GetTotalMarketCryptocurrencies() []Data { + return TotalCryptocurrencies +} + +// GetTotalMarketExchanges returns the total seeded market exchanges +func GetTotalMarketExchanges() []Data { + return TotalExchanges +} diff --git a/exchanges/request/request.go b/exchanges/request/request.go index 3fdac63d..48f448af 100644 --- a/exchanges/request/request.go +++ b/exchanges/request/request.go @@ -1,6 +1,7 @@ package request import ( + "compress/gzip" "errors" "fmt" "io" @@ -290,7 +291,31 @@ func (r *Requester) DoRequest(req *http.Request, method, path string, headers ma return errors.New("resp is nil") } - contents, err := ioutil.ReadAll(resp.Body) + var reader io.ReadCloser + switch resp.Header.Get("Content-Encoding") { + case "gzip": + reader, err = gzip.NewReader(resp.Body) + defer reader.Close() + if err != nil { + return err + } + + case "json": + reader = resp.Body + + default: + switch { + case common.StringContains(resp.Header.Get("Content-Type"), "application/json"): + reader = resp.Body + + default: + log.Warnf("encoding is not JSON for request response but receieved %v", + resp.Header.Get("Content-Type")) + reader = resp.Body + } + } + + contents, err := ioutil.ReadAll(reader) if err != nil { return err } diff --git a/main.go b/main.go index 92e21351..f4251032 100644 --- a/main.go +++ b/main.go @@ -14,6 +14,7 @@ import ( "github.com/thrasher-/gocryptotrader/communications" "github.com/thrasher-/gocryptotrader/config" "github.com/thrasher-/gocryptotrader/currency" + "github.com/thrasher-/gocryptotrader/currency/coinmarketcap" "github.com/thrasher-/gocryptotrader/currency/forexprovider" exchange "github.com/thrasher-/gocryptotrader/exchanges" log "github.com/thrasher-/gocryptotrader/logger" @@ -117,6 +118,31 @@ func main() { bot.comms = communications.NewComm(bot.config.GetCommunicationsConfig()) bot.comms.GetEnabledCommunicationMediums() + if bot.config.GetCryptocurrencyProviderConfig().Enabled { + log.Debug("Seeding full market data...") + err = currency.SeedCryptocurrencyMarketData(coinmarketcap.Settings(bot.config.GetCryptocurrencyProviderConfig())) + if err != nil { + log.Warnf("Failure seeding cryptocurrency market data %s", err) + } else { + if *verbosity { + log.Debugf("Total market cryptocurrencies: %d", + len(currency.GetTotalMarketCryptocurrencies())) + } + } + + err = currency.SeedExchangeMarketData(coinmarketcap.Settings(bot.config.GetCryptocurrencyProviderConfig())) + if err != nil { + log.Warnf("Failure seeding exchange market data %s", err) + } else { + if *verbosity { + log.Debugf("Total market exchanges: %d", + len(currency.GetTotalMarketExchanges())) + } + } + } else { + log.Debug("Cryptocurrency provider not enabled.") + } + log.Debugf("Fiat display currency: %s.", bot.config.Currency.FiatDisplayCurrency) currency.BaseCurrency = bot.config.Currency.FiatDisplayCurrency currency.FXProviders = forexprovider.StartFXService(bot.config.GetCurrencyConfig().ForexProviders) diff --git a/testdata/configtest.json b/testdata/configtest.json index 6ddc6281..bcc054bf 100644 --- a/testdata/configtest.json +++ b/testdata/configtest.json @@ -48,6 +48,13 @@ "primaryProvider": false } ], + "cryptocurrencyProvider": { + "name": "CoinMarketCap", + "enabled": false, + "verbose": false, + "apiKey": "Key", + "accountPlan": "accountPlan" + }, "cryptocurrencies": "BTC,LTC,ETH,DOGE,DASH,XRP,XMR", "currencyPairFormat": { "uppercase": true,