New package provider for foreign exchange rates.

Fixes issue: https://github.com/thrasher-/gocryptotrader/issues/131
Supersedes: https://github.com/thrasher-/gocryptotrader/pull/123
This commit is contained in:
Ryan O'Hara-Reid
2018-05-04 10:49:29 +10:00
committed by Adrian Gallagher
parent 443d378f92
commit 58051b89c7
29 changed files with 1982 additions and 522 deletions

View File

@@ -13,6 +13,8 @@ import (
"github.com/thrasher-/gocryptotrader/common"
"github.com/thrasher-/gocryptotrader/currency"
"github.com/thrasher-/gocryptotrader/currency/forexprovider"
"github.com/thrasher-/gocryptotrader/currency/forexprovider/base"
"github.com/thrasher-/gocryptotrader/currency/pair"
"github.com/thrasher-/gocryptotrader/portfolio"
"github.com/thrasher-/gocryptotrader/smsglobal"
@@ -95,17 +97,17 @@ type CurrencyPairFormatConfig struct {
// Config is the overarching object that holds all the information for
// prestart management of portfolio, SMSGlobal, webserver and enabled exchange
type Config struct {
Name string
EncryptConfig int
Cryptocurrencies string
CurrencyExchangeProvider string
CurrencyPairFormat *CurrencyPairFormatConfig `json:"CurrencyPairFormat"`
FiatDisplayCurrency string
GlobalHTTPTimeout time.Duration
Portfolio portfolio.Base `json:"PortfolioAddresses"`
SMS SMSGlobalConfig `json:"SMSGlobal"`
Webserver WebserverConfig `json:"Webserver"`
Exchanges []ExchangeConfig `json:"Exchanges"`
Name string
EncryptConfig int
Cryptocurrencies string `json:"Cryptocurrencies,omitempty"`
Currency CurrencyConfig `json:"CurrencyConfig,omitempty"`
CurrencyPairFormat *CurrencyPairFormatConfig `json:"CurrencyPairFormat,omitempty"`
FiatDisplayCurrency string `json:"FiatDispayCurrency,omitempty"`
GlobalHTTPTimeout time.Duration
Portfolio portfolio.Base `json:"PortfolioAddresses"`
SMS SMSGlobalConfig `json:"SMSGlobal"`
Webserver WebserverConfig `json:"Webserver"`
Exchanges []ExchangeConfig `json:"Exchanges"`
}
// ExchangeConfig holds all the information needed for each enabled Exchange.
@@ -131,6 +133,19 @@ type ExchangeConfig struct {
RequestCurrencyPairFormat *CurrencyPairFormatConfig `json:"RequestCurrencyPairFormat"`
}
// 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
}
// GetCurrencyConfig returns currency configurations
func (c *Config) GetCurrencyConfig() CurrencyConfig {
return c.Currency
}
// SupportsPair returns true or not whether the exchange supports the supplied
// pair
func (c *Config) SupportsPair(exchName string, p pair.CurrencyPair) (bool, error) {
@@ -222,7 +237,14 @@ func (c *Config) GetRequestCurrencyPairFormat(exchName string) (*CurrencyPairFor
// GetCurrencyPairDisplayConfig retrieves the currency pair display preference
func (c *Config) GetCurrencyPairDisplayConfig() *CurrencyPairFormatConfig {
return c.CurrencyPairFormat
return c.Currency.CurrencyPairFormat
}
// GetAllExchangeConfigs returns all exchange configurations
func (c *Config) GetAllExchangeConfigs() []ExchangeConfig {
m.Lock()
defer m.Unlock()
return c.Exchanges
}
// GetExchangeConfig returns exchange configurations by its indivdual name
@@ -237,6 +259,30 @@ func (c *Config) GetExchangeConfig(name string) (ExchangeConfig, error) {
return ExchangeConfig{}, fmt.Errorf(ErrExchangeNotFound, name)
}
// GetForexProviderConfig returns a forex provider configuration by its name
func (c *Config) GetForexProviderConfig(name string) (base.Settings, error) {
m.Lock()
defer m.Unlock()
for i := range c.Currency.ForexProviders {
if c.Currency.ForexProviders[i].Name == name {
return c.Currency.ForexProviders[i], nil
}
}
return base.Settings{}, errors.New("provider not found")
}
// GetPrimaryForexProvider returns the primary forex provider
func (c *Config) GetPrimaryForexProvider() string {
m.Lock()
defer m.Unlock()
for i := range c.Currency.ForexProviders {
if c.Currency.ForexProviders[i].PrimaryProvider {
return c.Currency.ForexProviders[i].Name
}
}
return ""
}
// UpdateExchangeConfig updates exchange configurations
func (c *Config) UpdateExchangeConfig(e ExchangeConfig) error {
m.Lock()
@@ -274,10 +320,6 @@ func (c *Config) CheckSMSGlobalConfigValues() error {
// CheckExchangeConfigValues returns configuation values for all enabled
// exchanges
func (c *Config) CheckExchangeConfigValues() error {
if c.Cryptocurrencies == "" {
return errors.New(ErrCryptocurrenciesEmpty)
}
exchanges := 0
for i, exch := range c.Exchanges {
if exch.Enabled {
@@ -357,6 +399,90 @@ func (c *Config) CheckWebserverConfigValues() error {
return nil
}
// CheckCurrencyConfigValues checks to see if the currency config values are correct or not
func (c *Config) CheckCurrencyConfigValues() error {
if len(c.Currency.ForexProviders) == 0 {
if len(forexprovider.GetAvailableForexProviders()) == 0 {
return errors.New("no forex providers available")
}
var providers []base.Settings
availProviders := forexprovider.GetAvailableForexProviders()
for x := range availProviders {
providers = append(providers,
base.Settings{
Name: availProviders[x],
Enabled: false,
Verbose: false,
RESTPollingDelay: 600,
APIKey: "Key",
APIKeyLvl: -1,
PrimaryProvider: false,
},
)
}
c.Currency.ForexProviders = providers
}
count := 0
for i := range c.Currency.ForexProviders {
if c.Currency.ForexProviders[i].Enabled == true {
if c.Currency.ForexProviders[i].APIKey == "Key" {
log.Printf("WARNING -- %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
continue
}
if c.Currency.ForexProviders[i].APIKeyLvl == -1 {
log.Printf("WARNING -- %s APIKey Level not set, functions limited. Please set this in your config.json file",
c.Currency.ForexProviders[i].Name)
}
count++
}
}
if count == 0 {
for x := range c.Currency.ForexProviders {
if c.Currency.ForexProviders[x].Name == "CurrencyConverter" {
c.Currency.ForexProviders[x].Enabled = true
c.Currency.ForexProviders[x].APIKey = ""
c.Currency.ForexProviders[x].PrimaryProvider = true
log.Printf("WARNING -- No forex providers set, defaulting to free provider CurrencyConverterAPI.")
}
}
}
if len(c.Currency.Cryptocurrencies) == 0 {
if len(c.Cryptocurrencies) != 0 {
c.Currency.Cryptocurrencies = c.Cryptocurrencies
c.Cryptocurrencies = ""
} else {
c.Currency.Cryptocurrencies = currency.DefaultCryptoCurrencies
}
}
if c.Currency.CurrencyPairFormat == nil {
if c.CurrencyPairFormat != nil {
c.Currency.CurrencyPairFormat = c.CurrencyPairFormat
c.CurrencyPairFormat = nil
} else {
c.Currency.CurrencyPairFormat = &CurrencyPairFormatConfig{
Delimiter: "-",
Uppercase: true,
}
}
}
if c.Currency.FiatDisplayCurrency == "" {
if c.FiatDisplayCurrency != "" {
c.Currency.FiatDisplayCurrency = c.FiatDisplayCurrency
c.FiatDisplayCurrency = ""
} else {
c.Currency.FiatDisplayCurrency = "USD"
}
}
return nil
}
// RetrieveConfigCurrencyPairs splits, assigns and verifies enabled currency
// pairs either cryptoCurrencies or fiatCurrencies
func (c *Config) RetrieveConfigCurrencyPairs(enabledOnly bool) error {
@@ -583,24 +709,9 @@ func (c *Config) CheckConfig() error {
}
}
if c.CurrencyExchangeProvider == "" {
c.CurrencyExchangeProvider = FXProviderFixer
} else {
if c.CurrencyExchangeProvider != "yahoo" && c.CurrencyExchangeProvider != FXProviderFixer {
log.Println(WarningCurrencyExchangeProvider)
c.CurrencyExchangeProvider = FXProviderFixer
}
}
if c.CurrencyPairFormat == nil {
c.CurrencyPairFormat = &CurrencyPairFormatConfig{
Delimiter: "-",
Uppercase: true,
}
}
if c.FiatDisplayCurrency == "" {
c.FiatDisplayCurrency = "USD"
err = c.CheckCurrencyConfigValues()
if err != nil {
return err
}
if c.GlobalHTTPTimeout <= 0 {
@@ -630,10 +741,7 @@ func (c *Config) UpdateConfig(configPath string, newCfg Config) error {
c.Name = newCfg.Name
c.EncryptConfig = newCfg.EncryptConfig
c.Cryptocurrencies = newCfg.Cryptocurrencies
c.CurrencyExchangeProvider = newCfg.CurrencyExchangeProvider
c.CurrencyPairFormat = newCfg.CurrencyPairFormat
c.FiatDisplayCurrency = newCfg.FiatDisplayCurrency
c.Currency = newCfg.Currency
c.GlobalHTTPTimeout = newCfg.GlobalHTTPTimeout
c.Portfolio = newCfg.Portfolio
c.SMS = newCfg.SMS

View File

@@ -591,9 +591,9 @@ func TestUpdateConfig(t *testing.T) {
t.Fatalf("Test failed. Error should of been thrown for invalid path")
}
newCfg.Cryptocurrencies = ""
newCfg.Currency.Cryptocurrencies = ""
err = c.UpdateConfig("", newCfg)
if err == nil {
t.Fatalf("Test failed. Error should of been thrown for empty cryptocurrencies")
if len(c.Currency.Cryptocurrencies) == 0 {
t.Fatalf("Test failed. Cryptocurrencies should have been repopulated")
}
}

File diff suppressed because one or more lines are too long

View File

@@ -1,154 +1,115 @@
package currency
import (
"errors"
"fmt"
"log"
"net/url"
"strings"
"time"
"github.com/thrasher-/gocryptotrader/common"
"github.com/thrasher-/gocryptotrader/currency/forexprovider"
"github.com/thrasher-/gocryptotrader/currency/pair"
)
// Rate holds the current exchange rates for the currency pair.
type Rate struct {
ID string `json:"id"`
Name string `json:"Name"`
Rate float64 `json:",string"`
Date string `json:"Date"`
Time string `json:"Time"`
Ask float64 `json:",string"`
Bid float64 `json:",string"`
}
// YahooJSONResponseInfo is a sub type that holds JSON response info
type YahooJSONResponseInfo struct {
Count int `json:"count"`
Created time.Time `json:"created"`
Lang string `json:"lang"`
}
// YahooJSONResponse holds Yahoo API responses
type YahooJSONResponse struct {
Query struct {
YahooJSONResponseInfo
Results struct {
Rate []Rate `json:"rate"`
}
}
}
// FixerResponse contains the data fields for the Fixer API response
type FixerResponse struct {
Base string `json:"base"`
Date string `json:"date"`
Rates map[string]float64 `json:"rates"`
}
const (
maxCurrencyPairsPerRequest = 350
yahooYQLURL = "https://query.yahooapis.com/v1/public/yql?"
yahooDatabase = "store://datatables.org/alltableswithkeys"
fixerAPI = "http://api.fixer.io/latest"
// DefaultBaseCurrency is the base currency used for conversion
DefaultBaseCurrency = "USD"
// DefaultCurrencies has the default minimum of FIAT values
DefaultCurrencies = "USD,AUD,EUR,CNY"
// DefaultCryptoCurrencies has the default minimum of crytpocurrency values
DefaultCryptoCurrencies = "BTC,LTC,ETH,DOGE,DASH,XRP,XMR"
)
// Variables for package which includes base error strings & exportable
// queries
// Manager is the overarching type across this package
var (
CurrencyStore map[string]Rate
CurrencyStoreFixer map[string]float64
BaseCurrencies []string
CryptoCurrencies []string
ErrCurrencyDataNotFetched = errors.New("yahoo currency data has not been fetched yet")
ErrCurrencyNotFound = "unable to find specified currency"
ErrQueryingYahoo = errors.New("unable to query Yahoo currency values")
ErrQueryingYahooZeroCount = errors.New("yahoo returned zero currency data")
YahooEnabled = false
FXRates map[string]float64
FiatCurrencies []string
CryptoCurrencies []string
BaseCurrency string
FXProviders *forexprovider.ForexProviders
)
// SetProvider sets the currency exchange service used by the currency
// converter
func SetProvider(yahooEnabled bool) {
if yahooEnabled {
YahooEnabled = true
// SetDefaults sets the default currency provider and settings for
// currency conversion used outside of the bot setting
func SetDefaults() {
FXRates = make(map[string]float64)
BaseCurrency = DefaultBaseCurrency
FXProviders = forexprovider.NewDefaultFXProvider()
err := SeedCurrencyData(DefaultCurrencies)
if err != nil {
log.Printf("Failed to seed currency data. Err: %s", err)
return
}
YahooEnabled = false
}
// SwapProvider swaps the currency exchange service used by the curency
// converter
func SwapProvider() {
if YahooEnabled {
YahooEnabled = false
return
// SeedCurrencyData returns rates correlated with suported currencies
func SeedCurrencyData(currencies string) error {
if FXRates == nil {
FXRates = make(map[string]float64)
}
YahooEnabled = true
}
// GetProvider returns the currency exchange service used by the currency
// converter
func GetProvider() string {
if YahooEnabled {
return "yahoo"
if FXProviders == nil {
FXProviders = forexprovider.NewDefaultFXProvider()
}
return "fixer"
newRates, err := FXProviders.GetCurrencyData(BaseCurrency, currencies)
if err != nil {
return err
}
for key, value := range newRates {
FXRates[key] = value
}
return nil
}
// IsDefaultCurrency checks if the currency passed in matches the default
// FIAT currency
// GetExchangeRates returns the currency exchange rates
func GetExchangeRates() map[string]float64 {
return FXRates
}
// IsDefaultCurrency checks if the currency passed in matches the default fiat
// currency
func IsDefaultCurrency(currency string) bool {
defaultCurrencies := common.SplitStrings(DefaultCurrencies, ",")
return common.StringDataCompare(defaultCurrencies, common.StringToUpper(currency))
}
// IsDefaultCryptocurrency checks if the currency passed in matches the default
// CRYPTO currency
// cryptocurrency
func IsDefaultCryptocurrency(currency string) bool {
cryptoCurrencies := common.SplitStrings(DefaultCryptoCurrencies, ",")
return common.StringDataCompare(cryptoCurrencies, common.StringToUpper(currency))
}
// IsFiatCurrency checks if the currency passed is an enabled FIAT currency
// IsFiatCurrency checks if the currency passed is an enabled fiat currency
func IsFiatCurrency(currency string) bool {
if len(BaseCurrencies) == 0 {
log.Println("IsFiatCurrency: BaseCurrencies string variable not populated")
return false
}
return common.StringDataCompare(BaseCurrencies, common.StringToUpper(currency))
return common.StringDataCompare(FiatCurrencies, common.StringToUpper(currency))
}
// IsCryptocurrency checks if the currency passed is an enabled CRYPTO currency.
func IsCryptocurrency(currency string) bool {
if len(CryptoCurrencies) == 0 {
log.Println(
"IsCryptocurrency: CryptoCurrencies string variable not populated",
)
return false
}
return common.StringDataCompare(CryptoCurrencies, common.StringToUpper(currency))
}
// IsCryptoPair checks to see if the pair is a crypto pair. For example, BTCLTC
// IsCryptoPair checks to see if the pair is a crypto pair e.g. BTCLTC
func IsCryptoPair(p pair.CurrencyPair) bool {
return IsCryptocurrency(p.FirstCurrency.String()) && IsCryptocurrency(p.SecondCurrency.String())
return IsCryptocurrency(p.FirstCurrency.String()) &&
IsCryptocurrency(p.SecondCurrency.String())
}
// IsCryptoFiatPair checks to see if the pair is a crypto fiat pair. For example, BTCUSD
// IsCryptoFiatPair checks to see if the pair is a crypto fiat pair e.g. BTCUSD
func IsCryptoFiatPair(p pair.CurrencyPair) bool {
return IsCryptocurrency(p.FirstCurrency.String()) && !IsCryptocurrency(p.SecondCurrency.String()) ||
!IsCryptocurrency(p.FirstCurrency.String()) && IsCryptocurrency(p.SecondCurrency.String())
}
// IsFiatPair checks to see if the pair is a fiar pair. For example. EURUSD
// IsFiatPair checks to see if the pair is a fiat pair e.g. EURUSD
func IsFiatPair(p pair.CurrencyPair) bool {
return IsFiatCurrency(p.FirstCurrency.String()) && IsFiatCurrency(p.SecondCurrency.String())
return IsFiatCurrency(p.FirstCurrency.String()) &&
IsFiatCurrency(p.SecondCurrency.String())
}
// Update updates the local crypto currency or base currency store
@@ -159,47 +120,27 @@ func Update(input []string, cryptos bool) {
CryptoCurrencies = append(CryptoCurrencies, common.StringToUpper(input[x]))
}
} else {
if !common.StringDataCompare(BaseCurrencies, input[x]) {
BaseCurrencies = append(BaseCurrencies, common.StringToUpper(input[x]))
if !common.StringDataCompare(FiatCurrencies, input[x]) {
FiatCurrencies = append(FiatCurrencies, common.StringToUpper(input[x]))
}
}
}
}
// SeedCurrencyData takes the desired FIAT currency string, if not defined the
// function will assign it the default values. The function will query
// yahoo for the currency values and will seed currency data.
func SeedCurrencyData(fiatCurrencies string) error {
if fiatCurrencies == "" {
fiatCurrencies = DefaultCurrencies
func extractBaseCurrency() string {
for k := range FXRates {
return k[0:3]
}
if YahooEnabled {
return QueryYahooCurrencyValues(fiatCurrencies)
}
return FetchFixerCurrencyData()
}
// MakecurrencyPairs takes all supported currency and turns them into pairs.
func MakecurrencyPairs(supportedCurrencies string) string {
currencies := common.SplitStrings(supportedCurrencies, ",")
var pairs []string
count := len(currencies)
for i := 0; i < count; i++ {
currency := currencies[i]
for j := 0; j < count; j++ {
if currency != currencies[j] {
pairs = append(pairs, currency+currencies[j])
}
}
}
return common.JoinStrings(pairs, ",")
return ""
}
// ConvertCurrency for example converts $1 USD to the equivalent Japanese Yen
// or vice versa.
func ConvertCurrency(amount float64, from, to string) (float64, error) {
if FXProviders == nil {
SetDefaults()
}
from = common.StringToUpper(from)
to = common.StringToUpper(to)
@@ -215,131 +156,46 @@ func ConvertCurrency(amount float64, from, to string) (float64, error) {
to = "RUB"
}
if YahooEnabled {
currency := from + to
_, ok := CurrencyStore[currency]
if !ok {
err := SeedCurrencyData(currency[:len(from)] + "," + currency[len(to):])
if err != nil {
return 0, err
}
}
result, ok := CurrencyStore[currency]
if !ok {
return 0, errors.New(ErrCurrencyNotFound + " currency not found in CurrencyStore " + currency)
}
return amount * result.Rate, nil
if len(FXRates) == 0 {
SeedCurrencyData(from + "," + to)
}
if len(CurrencyStoreFixer) == 0 {
err := FetchFixerCurrencyData()
if err != nil {
return 0, err
}
}
// Need to extract the base currency to see if we actually got it from the Forex API
// Fixer free API sets the base currency to EUR
baseCurr := extractBaseCurrency()
var resultFrom float64
var resultTo float64
// First check if we're converting to USD, USD doesn't exist in the rates map
if to == "USD" {
resultFrom, ok := CurrencyStoreFixer[from]
// check to see if we're converting from the base currency
if to == baseCurr {
resultFrom, ok := FXRates[baseCurr+from]
if !ok {
return 0, errors.New(ErrCurrencyNotFound + " " + from + " in CurrencyStoreFixer with pair " + from + to)
return 0, fmt.Errorf("Currency conversion failed. Unable to find %s in currency map [%s -> %s]", from, from, to)
}
return amount / resultFrom, nil
}
// Check to see if we're converting from USD
if from == "USD" {
resultTo, ok := CurrencyStoreFixer[to]
// Check to see if we're converting from the base currency
if from == baseCurr {
resultTo, ok := FXRates[baseCurr+to]
if !ok {
return 0, errors.New(ErrCurrencyNotFound + " " + to + " in CurrencyStoreFixer with pair " + from + to)
return 0, fmt.Errorf("Currency conversion failed. Unable to find %s in currency map [%s -> %s]", to, from, to)
}
return resultTo * amount, nil
}
// Otherwise convert to USD, then to the target currency
resultFrom, ok := CurrencyStoreFixer[from]
// Otherwise convert to base currency, then to the target currency
resultFrom, ok := FXRates[baseCurr+from]
if !ok {
return 0, errors.New(ErrCurrencyNotFound + " " + from + " in CurrencyStoreFixer with pair " + from + to)
return 0, fmt.Errorf("Currency conversion failed. Unable to find %s in currency map [%s -> %s]", from, from, to)
}
converted := amount / resultFrom
resultTo, ok = CurrencyStoreFixer[to]
resultTo, ok = FXRates[baseCurr+to]
if !ok {
return 0, errors.New(ErrCurrencyNotFound + " " + to + " in CurrencyStoreFixer with pair " + from + to)
return 0, fmt.Errorf("Currency conversion failed. Unable to find %s in currency map [%s -> %s]", to, from, to)
}
return converted * resultTo, nil
}
// FetchFixerCurrencyData seeds the variable C
func FetchFixerCurrencyData() error {
var result FixerResponse
values := url.Values{}
values.Set("base", "USD")
url := common.EncodeURLValues(fixerAPI, values)
CurrencyStoreFixer = make(map[string]float64)
err := common.SendHTTPGetRequest(url, true, false, &result)
if err != nil {
return err
}
CurrencyStoreFixer = result.Rates
return nil
}
// FetchYahooCurrencyData seeds the variable CurrencyStore; this is a
// map[string]Rate
func FetchYahooCurrencyData(currencyPairs []string) error {
values := url.Values{}
values.Set(
"q", fmt.Sprintf("SELECT * from yahoo.finance.xchange WHERE pair in (\"%s\")",
common.JoinStrings(currencyPairs, ",")),
)
values.Set("format", "json")
values.Set("env", yahooDatabase)
headers := make(map[string]string)
headers["Content-Type"] = "application/x-www-form-urlencoded"
resp, err := common.SendHTTPRequest(
"POST", yahooYQLURL, headers, strings.NewReader(values.Encode()),
)
if err != nil {
return err
}
log.Printf("Currency recv: %s", resp)
yahooResp := YahooJSONResponse{}
err = common.JSONDecode([]byte(resp), &yahooResp)
if err != nil {
return err
}
if yahooResp.Query.Count == 0 {
return ErrQueryingYahooZeroCount
}
for i := 0; i < yahooResp.Query.YahooJSONResponseInfo.Count; i++ {
CurrencyStore[yahooResp.Query.Results.Rate[i].ID] = yahooResp.Query.Results.Rate[i]
}
return nil
}
// QueryYahooCurrencyValues takes in desired currencies, creates pairs then
// uses FetchYahooCurrencyData to seed CurrencyStore
func QueryYahooCurrencyValues(currencies string) error {
CurrencyStore = make(map[string]Rate)
currencyPairs := common.SplitStrings(MakecurrencyPairs(currencies), ",")
log.Printf(
"%d fiat currency pairs generated. Fetching Yahoo currency data (this may take a minute)..\n",
len(currencyPairs),
)
return FetchYahooCurrencyData(currencyPairs)
}

View File

@@ -1,70 +1,49 @@
package currency
import (
"reflect"
"testing"
"github.com/thrasher-/gocryptotrader/common"
"github.com/thrasher-/gocryptotrader/currency/pair"
)
func TestSetProvider(t *testing.T) {
defaultVal := YahooEnabled
expected := "yahoo"
SetProvider(true)
actual := GetProvider()
if expected != actual {
t.Errorf("Test failed. TestGetProvider expected %s got %s", expected, actual)
func TestSetDefaults(t *testing.T) {
FXRates = nil
BaseCurrency = "BLAH"
FXProviders = nil
SetDefaults()
if FXRates == nil {
t.Fatal("Expected FXRates to be non-nil")
}
SetProvider(false)
expected = "fixer"
actual = GetProvider()
if expected != actual {
t.Errorf("Test failed. TestGetProvider expected %s got %s", expected, actual)
if BaseCurrency != DefaultBaseCurrency {
t.Fatal("Expected BaseCurrency to be 'USD'")
}
SetProvider(defaultVal)
if FXProviders == nil {
t.Fatal("Expected FXRates to be non-nil")
}
}
func TestSwapProvider(t *testing.T) {
defaultVal := YahooEnabled
expected := "fixer"
SetProvider(true)
SwapProvider()
actual := GetProvider()
if expected != actual {
t.Errorf("Test failed. TestGetProvider expected %s got %s", expected, actual)
func TestSeedCurrencyData(t *testing.T) {
err := SeedCurrencyData("AUD")
if err != nil {
t.Fatal(err)
}
SetProvider(false)
SwapProvider()
expected = "yahoo"
actual = GetProvider()
if expected != actual {
t.Errorf("Test failed. TestGetProvider expected %s got %s", expected, actual)
}
SetProvider(defaultVal)
}
func TestGetProvider(t *testing.T) {
defaultVal := YahooEnabled
SetProvider(true)
expected := "yahoo"
actual := GetProvider()
if expected != actual {
t.Errorf("Test failed. TestGetProvider expected %s got %s", expected, actual)
func TestGetExchangeRates(t *testing.T) {
result := GetExchangeRates()
backup := FXRates
FXRates = nil
result = GetExchangeRates()
if result != nil {
t.Fatal("Expected nil map")
}
SetProvider(false)
expected = "fixer"
actual = GetProvider()
if expected != actual {
t.Errorf("Test failed. TestGetProvider expected %s got %s", expected, actual)
}
SetProvider(defaultVal)
FXRates = backup
}
func TestIsDefaultCurrency(t *testing.T) {
@@ -120,7 +99,7 @@ func TestIsFiatCurrency(t *testing.T) {
t.Error("Test failed. TestIsFiatCurrency returned true on an empty string")
}
BaseCurrencies = []string{"USD", "AUD"}
FiatCurrencies = []string{"USD", "AUD"}
var str1, str2, str3 string = "BTC", "USD", "birds123"
if IsFiatCurrency(str1) {
@@ -171,7 +150,7 @@ func TestIsCryptoPair(t *testing.T) {
}
CryptoCurrencies = []string{"BTC", "LTC", "DASH"}
BaseCurrencies = []string{"USD"}
FiatCurrencies = []string{"USD"}
if !IsCryptoPair(pair.NewCurrencyPair("BTC", "LTC")) {
t.Error("Test Failed. TestIsCryptoPair. Expected true result")
@@ -188,7 +167,7 @@ func TestIsCryptoFiatPair(t *testing.T) {
}
CryptoCurrencies = []string{"BTC", "LTC", "DASH"}
BaseCurrencies = []string{"USD"}
FiatCurrencies = []string{"USD"}
if !IsCryptoFiatPair(pair.NewCurrencyPair("BTC", "USD")) {
t.Error("Test Failed. TestIsCryptoPair. Expected true result")
@@ -201,7 +180,7 @@ func TestIsCryptoFiatPair(t *testing.T) {
func TestIsFiatPair(t *testing.T) {
CryptoCurrencies = []string{"BTC", "LTC", "DASH"}
BaseCurrencies = []string{"USD", "AUD", "EUR"}
FiatCurrencies = []string{"USD", "AUD", "EUR"}
if !IsFiatPair(pair.NewCurrencyPair("AUD", "USD")) {
t.Error("Test Failed. TestIsFiatPair. Expected true result")
@@ -214,7 +193,7 @@ func TestIsFiatPair(t *testing.T) {
func TestUpdate(t *testing.T) {
CryptoCurrencies = []string{"BTC", "LTC", "DASH"}
BaseCurrencies = []string{"USD", "AUD"}
FiatCurrencies = []string{"USD", "AUD"}
Update([]string{"ETH"}, true)
Update([]string{"JPY"}, false)
@@ -232,163 +211,46 @@ func TestUpdate(t *testing.T) {
}
}
func TestSeedCurrencyData(t *testing.T) {
// SetProvider(true)
if YahooEnabled {
currencyRequestDefault := ""
currencyRequestUSDAUD := "USD,AUD"
currencyRequestObtuse := "WigWham"
func TestExtractBaseCurrency(t *testing.T) {
backup := FXRates
FXRates = nil
FXRates = make(map[string]float64)
err := SeedCurrencyData(currencyRequestDefault)
if err != nil {
t.Errorf(
"Test Failed. SeedCurrencyData: Error %s with currency as %s.",
err, currencyRequestDefault,
)
}
err2 := SeedCurrencyData(currencyRequestUSDAUD)
if err2 != nil {
t.Errorf(
"Test Failed. SeedCurrencyData: Error %s with currency as %s.",
err2, currencyRequestUSDAUD,
)
}
err3 := SeedCurrencyData(currencyRequestObtuse)
if err3 == nil {
t.Errorf(
"Test Failed. SeedCurrencyData: Error %s with currency as %s.",
err3, currencyRequestObtuse,
)
}
if extractBaseCurrency() != "" {
t.Fatalf("Test failed. Expected '' as base currency")
}
//SetProvider(false)
err := SeedCurrencyData("")
if err != nil {
t.Errorf("Test failed. SeedCurrencyData via Fixer. Error: %s", err)
FXRates["USDAUD"] = 120
if extractBaseCurrency() != "USD" {
t.Fatalf("Test failed. Expected 'USD' as base currency")
}
FXRates = backup
}
func TestMakecurrencyPairs(t *testing.T) {
t.Parallel()
lengthDefault := len(common.SplitStrings(DefaultCurrencies, ","))
fiatPairsLength := len(
common.SplitStrings(MakecurrencyPairs(DefaultCurrencies), ","),
)
if lengthDefault*(lengthDefault-1) > fiatPairsLength {
t.Error("Test Failed. MakecurrencyPairs: Error, mismatched length")
}
}
func TestConvertCurrency(t *testing.T) {
// SetProvider(true)
if YahooEnabled {
fiatCurrencies := DefaultCurrencies
for _, currencyFrom := range common.SplitStrings(fiatCurrencies, ",") {
for _, currencyTo := range common.SplitStrings(fiatCurrencies, ",") {
floatyMcfloat, err := ConvertCurrency(1000, currencyFrom, currencyTo)
if err != nil {
t.Errorf(
"Test Failed. ConvertCurrency: Error %s with return: %.2f Currency 1: %s Currency 2: %s",
err, floatyMcfloat, currencyFrom, currencyTo,
)
}
if reflect.TypeOf(floatyMcfloat).String() != "float64" {
t.Error("Test Failed. ConvertCurrency: Error, incorrect return type")
}
if floatyMcfloat <= 0 {
t.Error(
"Test Failed. ConvertCurrency: Error, negative return or a serious issue with current fiat",
)
}
}
}
}
// SetProvider(false)
_, err := ConvertCurrency(1000, "USD", "AUD")
_, err := ConvertCurrency(100, "AUD", "USD")
if err != nil {
t.Errorf("Test failed. ConvertCurrency USD -> AUD. Error %s", err)
t.Fatal(err)
}
_, err = ConvertCurrency(1000, "AUD", "USD")
_, err = ConvertCurrency(100, "USD", "AUD")
if err != nil {
t.Errorf("Test failed. ConvertCurrency AUD -> AUD. Error %s", err)
t.Fatal(err)
}
_, err = ConvertCurrency(1000, "CNY", "AUD")
_, err = ConvertCurrency(100, "CNY", "AUD")
if err != nil {
t.Errorf("Test failed. ConvertCurrency USD -> AUD. Error %s", err)
t.Fatal(err)
}
// Test non-existent currencies
_, err = ConvertCurrency(1000, "ASDF", "USD")
_, err = ConvertCurrency(100, "meow", "USD")
if err == nil {
t.Errorf("Test failed. ConvertCurrency non-existent currency -> USD. Error %s", err)
t.Fatal("Expected err on non-existent currency")
}
_, err = ConvertCurrency(1000, "USD", "ASDF")
_, err = ConvertCurrency(100, "USD", "meow")
if err == nil {
t.Errorf("Test failed. ConvertCurrency USD -> non-existent currency. Error %s", err)
t.Fatal("Expected err on non-existent currency")
}
_, err = ConvertCurrency(1000, "CNY", "UAHF")
if err == nil {
t.Errorf("Test failed. ConvertCurrency non-USD currency CNY -> non-existent currency. Error %s", err)
}
_, err = ConvertCurrency(1000, "UASF", "UAHF")
if err == nil {
t.Errorf("Test failed. ConvertCurrency non-existent currency -> non-existent currency. Error %s", err)
}
}
func TestFetchFixerCurrencyData(t *testing.T) {
err := FetchFixerCurrencyData()
if err != nil {
t.Errorf("Test failed. FetchFixerCurrencyData returned %s", err)
}
}
func TestFetchYahooCurrencyData(t *testing.T) {
if !YahooEnabled {
t.Skip()
}
t.Parallel()
var fetchData []string
fiatCurrencies := DefaultCurrencies
for _, currencyOne := range common.SplitStrings(fiatCurrencies, ",") {
for _, currencyTwo := range common.SplitStrings(fiatCurrencies, ",") {
if currencyOne == currencyTwo {
continue
} else {
fetchData = append(fetchData, currencyOne+currencyTwo)
}
}
}
err := FetchYahooCurrencyData(fetchData)
if err != nil {
t.Errorf("Test Failed. FetchYahooCurrencyData: Error %s", err)
}
}
func TestQueryYahooCurrencyValues(t *testing.T) {
if !YahooEnabled {
t.Skip()
}
err := QueryYahooCurrencyValues(DefaultCurrencies)
if err != nil {
t.Errorf("Test Failed. QueryYahooCurrencyValues: Error, %s", err)
}
err = QueryYahooCurrencyValues(DefaultCryptoCurrencies)
if err == nil {
t.Errorf("Test Failed. QueryYahooCurrencyValues: Error, %s", err)
}
}

View File

@@ -0,0 +1,36 @@
package base
import (
"time"
)
// Settings enforces standard variables across the provider packages
type Settings struct {
Name string
Enabled bool
Verbose bool
RESTPollingDelay time.Duration
APIKey string
APIKeyLvl int
PrimaryProvider bool
}
// Base enforces standard variables across the provider packages
type Base struct {
Settings
}
// GetName returns name of provider
func (b *Base) GetName() string {
return b.Name
}
// IsEnabled returns true if enabled
func (b *Base) IsEnabled() bool {
return b.Enabled
}
// IsPrimaryProvider returns true if primary provider
func (b *Base) IsPrimaryProvider() bool {
return b.PrimaryProvider
}

View File

@@ -0,0 +1,44 @@
package base
import (
"errors"
"log"
)
// IFXProviders contains an array of foreign exchange interfaces
type IFXProviders []IFXProvider
// IFXProvider enforces standard functions for all foreign exchange providers
// supported in GoCryptoTrader
type IFXProvider interface {
Setup(config Settings)
GetRates(baseCurrency, symbols string) (map[string]float64, error)
GetName() string
IsEnabled() bool
IsPrimaryProvider() bool
}
// GetCurrencyData returns currency data from enabled FX providers
func (fxp IFXProviders) GetCurrencyData(baseCurrency, symbols string) (map[string]float64, error) {
for x := range fxp {
if fxp[x].IsPrimaryProvider() && fxp[x].IsEnabled() {
rates, err := fxp[x].GetRates(baseCurrency, symbols)
if err != nil {
log.Println(err)
for y := range fxp {
if !fxp[y].IsPrimaryProvider() && fxp[x].IsEnabled() {
rates, err = fxp[y].GetRates(baseCurrency, symbols)
if err != nil {
log.Println(err)
continue
}
return rates, nil
}
}
return nil, errors.New("ForexProvider error GetCurrencyData() failed to aquire data")
}
return rates, nil
}
}
return nil, errors.New("ForexProvider error GetCurrencyData() no providers enabled")
}

View File

@@ -0,0 +1 @@
package base

View File

@@ -0,0 +1,168 @@
package currencyconverter
import (
"errors"
"fmt"
"log"
"net/url"
"github.com/thrasher-/gocryptotrader/common"
"github.com/thrasher-/gocryptotrader/currency/forexprovider/base"
)
// const declarations consist of endpoints
const (
APIEndpointURL = "https://currencyconverterapi.com/api/"
APIEndpointFreeURL = "https://free.currencyconverterapi.com/api/"
APIEndpointVersion = "v5"
APIEndpointConvert = "convert"
APIEndpointCurrencies = "currencies"
APIEndpointCountries = "countries"
APIEndpointUsage = "usage"
)
// CurrencyConverter stores the struct for the CurrencyConverter API
type CurrencyConverter struct {
base.Base
}
// Setup sets appropriate values for CurrencyLayer
func (c *CurrencyConverter) Setup(config base.Settings) {
c.Name = config.Name
c.APIKey = config.APIKey
c.APIKeyLvl = config.APIKeyLvl
c.Enabled = config.Enabled
c.RESTPollingDelay = config.RESTPollingDelay
c.Verbose = config.Verbose
c.PrimaryProvider = config.PrimaryProvider
}
// GetRates is a wrapper function to return rates
func (c *CurrencyConverter) GetRates(baseCurrency, symbols string) (map[string]float64, error) {
splitSymbols := common.SplitStrings(symbols, ",")
if len(splitSymbols) == 1 {
return c.Convert(baseCurrency, symbols)
}
var completedStrings []string
for x := range splitSymbols {
completedStrings = append(completedStrings, baseCurrency+"_"+splitSymbols[x])
}
if (c.APIKey != "" && c.APIKey != "Key") || len(completedStrings) == 2 {
return c.ConvertMany(completedStrings)
}
rates := make(map[string]float64)
processBatch := func(length int) {
for i := 0; i < length; i += 2 {
batch := completedStrings[i : i+2]
result, err := c.ConvertMany(batch)
if err != nil {
log.Printf("Failed to get batch err: %s", err)
continue
}
for k, v := range result {
rates[common.ReplaceString(k, "_", "", -1)] = v
}
}
}
currLen := len(completedStrings)
mod := currLen % 2
if mod == 0 {
processBatch(currLen)
return rates, nil
}
processBatch(currLen - 1)
result, err := c.ConvertMany(completedStrings[currLen-1:])
if err != nil {
return nil, err
}
for k, v := range result {
rates[common.ReplaceString(k, "_", "", -1)] = v
}
return rates, nil
}
// ConvertMany takes 2 or more currencies depending on if using the free
// or paid API
func (c *CurrencyConverter) ConvertMany(currencies []string) (map[string]float64, error) {
if len(currencies) > 2 && (c.APIKey == "" || c.APIKey == "Key") {
return nil, errors.New("currency fetching is limited to two currencies per request")
}
result := make(map[string]float64)
v := url.Values{}
joined := common.JoinStrings(currencies, ",")
v.Set("q", joined)
v.Set("compact", "ultra")
err := c.SendHTTPRequest(APIEndpointConvert, v, &result)
if err != nil {
return nil, err
}
return result, nil
}
// Convert gets the conversion rate for the supplied currencies
func (c *CurrencyConverter) Convert(from, to string) (map[string]float64, error) {
result := make(map[string]float64)
v := url.Values{}
v.Set("q", from+"_"+to)
v.Set("compact", "ultra")
err := c.SendHTTPRequest(APIEndpointConvert, v, &result)
if err != nil {
return nil, err
}
return result, nil
}
// GetCurrencies returns a list of the supported currencies
func (c *CurrencyConverter) GetCurrencies() (map[string]CurrencyItem, error) {
var result Currencies
err := c.SendHTTPRequest(APIEndpointCurrencies, url.Values{}, &result)
if err != nil {
return nil, err
}
return result.Results, nil
}
// GetCountries returns a list of the supported countries and
// their symbols
func (c *CurrencyConverter) GetCountries() (map[string]CountryItem, error) {
var result Countries
err := c.SendHTTPRequest(APIEndpointCountries, url.Values{}, &result)
if err != nil {
return nil, err
}
return result.Results, nil
}
// SendHTTPRequest sends a HTTP request, if account is not free it automatically
// upgrades request to SSL.
func (c *CurrencyConverter) SendHTTPRequest(endPoint string, values url.Values, result interface{}) error {
var path string
if c.APIKey == "" || c.APIKey == "Key" {
path = fmt.Sprintf("%s%s/%s?", APIEndpointFreeURL, APIEndpointVersion, endPoint)
} else {
path = fmt.Sprintf("%s%s%s?", APIEndpointURL, APIEndpointVersion, endPoint)
values.Set("apiKey", c.APIKey)
}
path = path + values.Encode()
return common.SendHTTPGetRequest(path, true, c.Verbose, &result)
}

View File

@@ -0,0 +1,79 @@
package currencyconverter
import (
"testing"
)
var c CurrencyConverter
func TestGetRates(t *testing.T) {
result, err := c.GetRates("USD", "AUD")
if err != nil {
t.Error("Test Error. CurrencyConverter GetRates() error", err)
}
if len(result) != 1 {
t.Fatal("Test error. Expected 2 rates")
}
result, err = c.GetRates("USD", "AUD,EUR")
if err != nil {
t.Error("Test Error. CurrencyConverter GetRates() error", err)
}
if len(result) != 2 {
t.Fatal("Test error. Expected 2 rates")
}
result, err = c.GetRates("USD", "AUD,EUR,GBP")
if err != nil {
t.Error("Test Error. CurrencyConverter GetRates() error", err)
}
if len(result) != 3 {
t.Fatal("Test error. Expected 3 rates")
}
result, err = c.GetRates("USD", "AUD,EUR,GBP,CNY")
if err != nil {
t.Error("Test Error. CurrencyConverter GetRates() error", err)
}
if len(result) != 4 {
t.Fatal("Test error. Expected 4 rates")
}
}
func TestConvertMany(t *testing.T) {
currencies := []string{"USD_AUD", "USD_EUR"}
_, err := c.ConvertMany(currencies)
if err != nil {
t.Fatal(err)
}
currencies = []string{"USD_AUD", "USD_EUR", "USD_GBP"}
_, err = c.ConvertMany(currencies)
if err == nil {
t.Fatal("non error on supplying 3 or more currencies using the free API")
}
}
func TestConvert(t *testing.T) {
_, err := c.Convert("AUD", "USD")
if err != nil {
t.Fatal(err)
}
}
func TestGetCurrencies(t *testing.T) {
_, err := c.GetCurrencies()
if err != nil {
t.Fatal(err)
}
}
func TestGetCountries(t *testing.T) {
_, err := c.GetCountries()
if err != nil {
t.Fatal(err)
}
}

View File

@@ -0,0 +1,34 @@
package currencyconverter
// Error stores the error message
type Error struct {
Status int `json:"status"`
Error string `json:"error"`
}
// CurrencyItem stores variables related to the currency response
type CurrencyItem struct {
CurrencyName string `json:"currencyName"`
CurrencySymbol string `json:"currencySymbol"`
ID string `json:"ID"`
}
// Currencies stores the currency result data
type Currencies struct {
Results map[string]CurrencyItem
}
// CountryItem stores variables related to the country response
type CountryItem struct {
Alpha3 string `json:"alpha3"`
CurrencyID string `json:"currencyId"`
CurrencyName string `json:"currencyName"`
CurrencySymbol string `json:"currencySymbol"`
ID string `json:"ID"`
Name string `json:"Name"`
}
// Countries stores the country result data
type Countries struct {
Results map[string]CountryItem
}

View File

@@ -0,0 +1,209 @@
// Currencylayer provides a simple REST API with real-time and historical
// exchange rates for 168 world currencies, delivering currency pairs in
// universally usable JSON format - compatible with any of your applications.
// Spot exchange rate data is retrieved from several major forex data providers
// in real-time, validated, processed and delivered hourly, every 10 minutes, or
// even within the 60-second market window.
// Providing the most representative forex market value available
// ("midpoint" value) for every API request, the currencylayer API powers
// currency converters, mobile applications, financial software components and
// back-office systems all around the world.
package currencylayer
import (
"errors"
"fmt"
"net/url"
"strconv"
"github.com/thrasher-/gocryptotrader/common"
"github.com/thrasher-/gocryptotrader/currency/forexprovider/base"
)
// const declarations consist of endpoints and APIKey privileges
const (
AccountFree = iota
AccountBasic
AccountPro
AccountEnterprise
APIEndpointURL = "http://apilayer.net/api/"
APIEndpointURLSSL = "https://apilayer.net/api/"
APIEndpointList = "list"
APIEndpointLive = "live"
APIEndpointHistorical = "historical"
APIEndpointConversion = "convert"
APIEndpointTimeframe = "timeframe"
APIEndpointChange = "change"
)
// CurrencyLayer is a foreign exchange rate provider at
// https://currencylayer.com NOTE default base currency is USD when using a free
// account. Has automatic upgrade to a SSL connection.
type CurrencyLayer struct {
base.Base
}
// Setup sets appropriate values for CurrencyLayer
func (c *CurrencyLayer) Setup(config base.Settings) {
c.Name = config.Name
c.APIKey = config.APIKey
c.APIKeyLvl = config.APIKeyLvl
c.Enabled = config.Enabled
c.RESTPollingDelay = config.RESTPollingDelay
c.Verbose = config.Verbose
c.PrimaryProvider = config.PrimaryProvider
}
// GetRates is a wrapper function to return rates for GoCryptoTrader
func (c *CurrencyLayer) GetRates(baseCurrency, symbols string) (map[string]float64, error) {
return c.GetliveData(symbols, baseCurrency)
}
// GetSupportedCurrencies returns supported currencies
func (c *CurrencyLayer) GetSupportedCurrencies() (map[string]string, error) {
var resp SupportedCurrencies
if err := c.SendHTTPRequest(APIEndpointList, url.Values{}, &resp); err != nil {
return nil, err
}
if !resp.Success {
return nil, errors.New(resp.Error.Info)
}
return resp.Currencies, nil
}
// GetliveData returns live quotes for foreign exchange currencies
func (c *CurrencyLayer) GetliveData(currencies, source string) (map[string]float64, error) {
var resp LiveRates
v := url.Values{}
v.Set("currencies", currencies)
v.Set("source", source)
err := c.SendHTTPRequest(APIEndpointLive, v, &resp)
if err != nil {
return nil, err
}
if !resp.Success {
return nil, errors.New(resp.Error.Info)
}
return resp.Quotes, nil
}
// GetHistoricalData returns historical exchange rate data for every past day of
// the last 16 years.
func (c *CurrencyLayer) GetHistoricalData(date string, currencies []string, source string) (map[string]float64, error) {
var resp HistoricalRates
v := url.Values{}
v.Set("currencies", common.JoinStrings(currencies, ","))
v.Set("source", source)
v.Set("date", date)
err := c.SendHTTPRequest(APIEndpointHistorical, v, &resp)
if err != nil {
return nil, err
}
if !resp.Success {
return nil, errors.New(resp.Error.Info)
}
return resp.Quotes, nil
}
// Convert converts one currency amount to another currency amount.
func (c *CurrencyLayer) Convert(from, to, date string, amount float64) (float64, error) {
if c.APIKeyLvl >= AccountBasic {
return 0, errors.New("insufficient API privileges, upgrade to basic to use this function")
}
var resp ConversionRate
v := url.Values{}
v.Set("from", from)
v.Set("to", to)
v.Set("amount", strconv.FormatFloat(amount, 'f', -1, 64))
v.Set("date", date)
err := c.SendHTTPRequest(APIEndpointConversion, v, &resp)
if err != nil {
return resp.Result, err
}
if !resp.Success {
return resp.Result, errors.New(resp.Error.Info)
}
return resp.Result, nil
}
// QueryTimeFrame returns historical exchange rates for a time-period.
// (maximum range: 365 days)
func (c *CurrencyLayer) QueryTimeFrame(startDate, endDate, base string, currencies []string) (map[string]interface{}, error) {
if c.APIKeyLvl >= AccountPro {
return nil, errors.New("insufficient API privileges, upgrade to basic to use this function")
}
var resp TimeFrame
v := url.Values{}
v.Set("start_date", startDate)
v.Set("end_date", endDate)
v.Set("base", base)
v.Set("currencies", common.JoinStrings(currencies, ","))
err := c.SendHTTPRequest(APIEndpointTimeframe, v, &resp)
if err != nil {
return nil, err
}
if !resp.Success {
return nil, errors.New(resp.Error.Info)
}
return resp.Quotes, nil
}
// QueryCurrencyChange returns the change (both margin and percentage) of one or
// more currencies, relative to a Source Currency, within a specific
// time-frame (optional).
func (c *CurrencyLayer) QueryCurrencyChange(startDate, endDate, base string, currencies []string) (map[string]Changes, error) {
if c.APIKeyLvl != AccountEnterprise {
return nil, errors.New("insufficient API privileges, upgrade to basic to use this function")
}
var resp ChangeRate
v := url.Values{}
v.Set("start_date", startDate)
v.Set("end_date", endDate)
v.Set("base", base)
v.Set("currencies", common.JoinStrings(currencies, ","))
err := c.SendHTTPRequest(APIEndpointChange, v, &resp)
if err != nil {
return nil, err
}
if !resp.Success {
return nil, errors.New(resp.Error.Info)
}
return resp.Quotes, nil
}
// SendHTTPRequest sends a HTTP request, if account is not free it automatically
// upgrades request to SSL.
func (c *CurrencyLayer) SendHTTPRequest(endPoint string, values url.Values, result interface{}) error {
var path string
values.Set("access_key", c.APIKey)
if c.APIKeyLvl == AccountFree {
path = fmt.Sprintf("%s%s%s", APIEndpointURL, endPoint, "?")
} else {
path = fmt.Sprintf("%s%s%s", APIEndpointURLSSL, endPoint, "?")
}
path = path + values.Encode()
return common.SendHTTPGetRequest(path, true, c.Verbose, result)
}

View File

@@ -0,0 +1,63 @@
package currencylayer
import (
"testing"
)
var c CurrencyLayer
// please set your API key here for due diligence testing NOTE be aware you will
// minimize your API calls using this test.
const (
APIkey = ""
Apilevel = 3
)
func TestGetRates(t *testing.T) {
_, err := c.GetRates("USD", "AUD")
if err == nil {
t.Error("test error - currencylayer GetRates() error", err)
}
}
func TestGetSupportedCurrencies(t *testing.T) {
_, err := c.GetSupportedCurrencies()
if err == nil {
t.Error("test error - currencylayer GetSupportedCurrencies() error", err)
}
}
func TestGetliveData(t *testing.T) {
_, err := c.GetliveData("AUD", "USD")
if err == nil {
t.Error("test error - currencylayer GetliveData() error", err)
}
}
func TestGetHistoricalData(t *testing.T) {
_, err := c.GetHistoricalData("2016-12-15", []string{"AUD"}, "USD")
if err == nil {
t.Error("test error - currencylayer GetHistoricalData() error", err)
}
}
func TestConvert(t *testing.T) {
_, err := c.Convert("USD", "AUD", "", 1)
if err == nil {
t.Error("test error - currencylayer Convert() error")
}
}
func TestQueryTimeFrame(t *testing.T) {
_, err := c.QueryTimeFrame("2010-12-0", "2010-12-5", "USD", []string{"AUD"})
if err == nil {
t.Error("test error - currencylayer QueryTimeFrame() error")
}
}
func TestQueryCurrencyChange(t *testing.T) {
_, err := c.QueryCurrencyChange("2010-12-0", "2010-12-5", "USD", []string{"AUD"})
if err == nil {
t.Error("test error - currencylayer QueryCurrencyChange() error")
}
}

View File

@@ -0,0 +1,106 @@
package currencylayer
// LiveRates is a response type holding rates priced now.
type LiveRates struct {
Success bool `json:"success"`
Error struct {
Code int `json:"code"`
Info string `json:"info"`
} `json:"error"`
Terms string `json:"terms"`
Privacy string `json:"privacy"`
Timestamp int64 `json:"timestamp"`
Source string `json:"source"`
Quotes map[string]float64 `json:"quotes"`
}
// SupportedCurrencies holds supported currency information
type SupportedCurrencies struct {
Success bool `json:"success"`
Error struct {
Code int `json:"code"`
Info string `json:"info"`
} `json:"error"`
Terms string `json:"terms"`
Privacy string `json:"privacy"`
Currencies map[string]string `json:"currencies"`
}
// HistoricalRates is a response type holding rates priced from the past.
type HistoricalRates struct {
Success bool `json:"success"`
Error struct {
Code int `json:"code"`
Info string `json:"info"`
} `json:"error"`
Terms string `json:"terms"`
Privacy string `json:"privacy"`
Historical bool `json:"historical"`
Date string `json:"date"`
Timestamp int64 `json:"timestamp"`
Source string `json:"source"`
Quotes map[string]float64 `json:"quotes"`
}
// ConversionRate is a response type holding a converted rate.
type ConversionRate struct {
Success bool `json:"success"`
Error struct {
Code int `json:"code"`
Info string `json:"info"`
} `json:"error"`
Privacy string `json:"privacy"`
Terms string `json:"terms"`
Query struct {
From string `json:"from"`
To string `json:"to"`
Amount float64 `json:"amount"`
} `json:"query"`
Info struct {
Timestamp int64 `json:"timestamp"`
Quote float64 `json:"quote"`
} `json:"info"`
Historical bool `json:"historical"`
Date string `json:"date"`
Result float64 `json:"result"`
}
// TimeFrame is a response type holding exchange rates for a time period
type TimeFrame struct {
Success bool `json:"success"`
Error struct {
Code int `json:"code"`
Info string `json:"info"`
} `json:"error"`
Terms string `json:"terms"`
Privacy string `json:"privacy"`
Timeframe bool `json:"timeframe"`
StartDate string `json:"start_date"`
EndDate string `json:"end_date"`
Source string `json:"source"`
Quotes map[string]interface{} `json:"quotes"`
}
// ChangeRate is the response type that holds rate change data.
type ChangeRate struct {
Success bool `json:"success"`
Error struct {
Code int `json:"code"`
Info string `json:"info"`
} `json:"error"`
Terms string `json:"terms"`
Privacy string `json:"privacy"`
Change bool `json:"change"`
StartDate string `json:"start_date"`
EndDate string `json:"end_date"`
Source string `json:"source"`
Quotes map[string]Changes `json:"quotes"`
}
// Changes is a sub-type of ChangeRate that holds the actual changes of rates.
type Changes struct {
StartRate float64 `json:"start_rate"`
EndRate float64 `json:"end_rate"`
Change float64 `json:"change"`
ChangePCT float64 `json:"change_pct"`
}

View File

@@ -0,0 +1,212 @@
// Powered by 15+ exchange rate data sources, the Fixer API is capable of
// delivering real-time exchange rate data for 170 world currencies. The API
// comes with multiple endpoints, each serving a different use case. Endpoint
// functionalities include getting the latest exchange rate data for all or a
// specific set of currencies, converting amounts from one currency to another,
// retrieving Time-Series data for one or multiple currencies and querying the
// API for daily fluctuation data.
package fixer
import (
"errors"
"net/url"
"strconv"
"github.com/thrasher-/gocryptotrader/common"
"github.com/thrasher-/gocryptotrader/currency/forexprovider/base"
)
const (
fixerAPIFree = iota
fixerAPIBasic
fixerAPIProfessional
fixerAPIProfessionalPlus
fixerAPIEnterprise
fixerAPI = "http://data.fixer.io/api/"
fixerAPISSL = "https://data.fixer.io/api/"
fixerAPILatest = "latest"
fixerAPIConvert = "convert"
fixerAPITimeSeries = "timeseries"
fixerAPIFluctuation = "fluctuation"
)
// Fixer is a foreign exchange rate provider at https://fixer.io/
// NOTE DEFAULT BASE CURRENCY IS EUR upgrade to basic to change
type Fixer struct {
base.Base
}
// Setup sets appropriate values for fixer object
func (f *Fixer) Setup(config base.Settings) {
f.APIKey = config.APIKey
f.APIKeyLvl = config.APIKeyLvl
f.Enabled = config.Enabled
f.Name = config.Name
f.RESTPollingDelay = config.RESTPollingDelay
f.Verbose = config.Verbose
f.PrimaryProvider = config.PrimaryProvider
}
// GetRates is a wrapper function to return rates
func (f *Fixer) GetRates(baseCurrency, symbols string) (map[string]float64, error) {
rates, err := f.GetLatestRates(baseCurrency, symbols)
if err != nil {
return nil, err
}
if f.APIKeyLvl == fixerAPIFree {
baseCurrency = "EUR"
}
standardisedRates := make(map[string]float64)
for k, v := range rates {
curr := baseCurrency + k
standardisedRates[curr] = v
}
return standardisedRates, nil
}
// GetLatestRates returns real-time exchange rate data for all available or a
// specific set of currencies. NOTE DEFAULT BASE CURRENCY IS EUR
func (f *Fixer) GetLatestRates(base, symbols string) (map[string]float64, error) {
var resp Rates
v := url.Values{}
if f.APIKeyLvl > fixerAPIFree {
v.Add("base", base)
}
v.Add("symbols", symbols)
err := f.SendOpenHTTPRequest(fixerAPILatest, v, &resp)
if err != nil {
return resp.Rates, err
}
if !resp.Success {
return resp.Rates, errors.New(resp.Error.Type + resp.Error.Info)
}
return resp.Rates, nil
}
// GetHistoricalRates returns historical exchange rate data for all available or
// a specific set of currencies.
// date - YYYY-MM-DD [required] A date in the past
func (f *Fixer) GetHistoricalRates(date, base string, symbols []string) (map[string]float64, error) {
var resp Rates
v := url.Values{}
v.Set("symbols", common.JoinStrings(symbols, ","))
err := f.SendOpenHTTPRequest(date, v, &resp)
if err != nil {
return resp.Rates, err
}
if !resp.Success {
return resp.Rates, errors.New(resp.Error.Type + resp.Error.Info)
}
return resp.Rates, nil
}
// ConvertCurrency allows for conversion of any amount from one currency to
// another.
// from - The three-letter currency code of the currency you would like to
// convert from.
// to - The three-letter currency code of the currency you would like to convert
// to.
// amount - The amount to be converted.
// date - [optional] Specify a date (format YYYY-MM-DD) to use historical rates
// for this conversion.
func (f *Fixer) ConvertCurrency(from, to, date string, amount float64) (float64, error) {
if f.APIKeyLvl < fixerAPIBasic {
return 0, errors.New("insufficient API privileges, upgrade to basic to use this function")
}
var resp Conversion
v := url.Values{}
v.Set("from", from)
v.Set("to", to)
v.Set("amount", strconv.FormatFloat(amount, 'f', -1, 64))
v.Set("date", date)
err := f.SendOpenHTTPRequest(fixerAPIConvert, v, &resp)
if err != nil {
return resp.Result, err
}
if !resp.Success {
return resp.Result, errors.New(resp.Error.Type + resp.Error.Info)
}
return resp.Result, nil
}
// GetTimeSeriesData returns daily historical exchange rate data between two
// specified dates for all available or a specific set of currencies.
func (f *Fixer) GetTimeSeriesData(startDate, endDate, base string, symbols []string) (map[string]interface{}, error) {
if f.APIKeyLvl < fixerAPIProfessional {
return nil, errors.New("insufficient API privileges, upgrade to professional to use this function")
}
var resp TimeSeries
v := url.Values{}
v.Set("start_date", startDate)
v.Set("end_date", endDate)
v.Set("base", base)
v.Set("symbols", common.JoinStrings(symbols, ","))
err := f.SendOpenHTTPRequest(fixerAPITimeSeries, v, &resp)
if err != nil {
return resp.Rates, err
}
if !resp.Success {
return resp.Rates, errors.New(resp.Error.Type + resp.Error.Info)
}
return resp.Rates, nil
}
// GetFluctuationData returns fluctuation data between two specified dates for
// all available or a specific set of currencies.
func (f *Fixer) GetFluctuationData(startDate, endDate, base string, symbols []string) (map[string]Flux, error) {
if f.APIKeyLvl < fixerAPIProfessionalPlus {
return nil, errors.New("insufficient API privileges, upgrade to professional plus or enterprise to use this function")
}
var resp Fluctuation
v := url.Values{}
v.Set("start_date", startDate)
v.Set("end_date", endDate)
v.Set("base", base)
v.Set("symbols", common.JoinStrings(symbols, ","))
err := f.SendOpenHTTPRequest(fixerAPIFluctuation, v, &resp)
if err != nil {
return resp.Rates, err
}
if !resp.Success {
return resp.Rates, errors.New(resp.Error.Type + resp.Error.Info)
}
return resp.Rates, nil
}
// SendOpenHTTPRequest sends a typical get request
func (f *Fixer) SendOpenHTTPRequest(endpoint string, v url.Values, result interface{}) error {
var path string
v.Set("access_key", f.APIKey)
if f.APIKeyLvl == fixerAPIFree {
path = fixerAPI + endpoint + "?" + v.Encode()
} else {
path = fixerAPISSL + endpoint + "?" + v.Encode()
}
return common.SendHTTPGetRequest(path, true, f.Verbose, result)
}

View File

@@ -0,0 +1,57 @@
package fixer
import (
"testing"
)
// Please set API key and apikey subscription level for correct due diligence
// testing - NOTE please be aware tests will diminish your monthly API calls
const (
apikey = ""
apiKeyLvl = 3
)
var f Fixer
func TestGetRates(t *testing.T) {
_, err := f.GetRates("EUR", "AUD")
if err == nil {
t.Error("test failed - fixer GetRates() error", err)
}
}
func TestGetLatestRates(t *testing.T) {
_, err := f.GetLatestRates("EUR", "AUD")
if err == nil {
t.Error("test failed - fixer GetLatestRates() error", err)
}
}
func TestGetHistoricalRates(t *testing.T) {
_, err := f.GetHistoricalRates("2013-12-24", "EUR", []string{"AUD,KRW"})
if err == nil {
t.Error("test failed - fixer GetHistoricalRates() error", err)
}
}
func TestConvertCurrency(t *testing.T) {
_, err := f.ConvertCurrency("AUD", "EUR", "", 1337)
if err == nil {
t.Error("test failed - fixer ConvertCurrency() error", err)
}
}
func TestGetTimeSeriesData(t *testing.T) {
_, err := f.GetTimeSeriesData("2013-12-24", "2013-12-25", "EUR", []string{"AUD,KRW"})
if err == nil {
t.Error("test failed - fixer GetTimeSeriesData() error", err)
}
}
func TestGetFluctuationData(t *testing.T) {
_, err := f.GetFluctuationData("2013-12-24", "2013-12-25", "EUR", []string{"AUD,KRW"})
if err == nil {
t.Error("test failed - fixer GetFluctuationData() error", err)
}
}

View File

@@ -0,0 +1,76 @@
package fixer
// Rates contains the data fields for the currencies you have requested.
type Rates struct {
Success bool `json:"success"`
Error struct {
Code int `json:"code"`
Type string `json:"type"`
Info string `json:"info"`
} `json:"error"`
Historical bool `json:"historical"`
Timestamp int64 `json:"timestamp"`
Base string `json:"base"`
Date string `json:"date"`
Rates map[string]float64 `json:"rates"`
}
// Conversion contains data for currency conversion
type Conversion struct {
Success bool `json:"success"`
Error struct {
Code int `json:"code"`
Type string `json:"type"`
Info string `json:"info"`
} `json:"error"`
Query struct {
From string `json:"from"`
To string `json:"to"`
Amount float64 `json:"amount"`
} `json:"query"`
Info struct {
Timestamp int64 `json:"timestamp"`
Rate float64 `json:"rate"`
} `json:"info"`
Historical bool `json:"historical"`
Date string `json:"date"`
Result float64 `json:"result"`
}
// TimeSeries holds timeseries data
type TimeSeries struct {
Success bool `json:"success"`
Error struct {
Code int `json:"code"`
Type string `json:"type"`
Info string `json:"info"`
} `json:"error"`
Timeseries bool `json:"timeseries"`
StartDate string `json:"start_date"`
EndDate string `json:"end_date"`
Base string `json:"base"`
Rates map[string]interface{} `json:"rates"`
}
// Fluctuation holds fluctuation data
type Fluctuation struct {
Success bool `json:"success"`
Error struct {
Code int `json:"code"`
Type string `json:"type"`
Info string `json:"info"`
} `json:"error"`
Fluctuation bool `json:"fluctuation"`
StartDate string `json:"start_date"`
EndDate string `json:"end_date"`
Base string `json:"base"`
Rates map[string]Flux `json:"rates"`
}
// Flux is a sub type holding fluctation data
type Flux struct {
StartRate float64 `json:"start_rate"`
EndRate float64 `json:"end_rate"`
Change float64 `json:"change"`
ChangePCT float64 `json:"change_pct"`
}

View File

@@ -0,0 +1,67 @@
// Package forexprovider utilises foreign exchange API services to manage
// relational FIAT currencies
package forexprovider
import (
"log"
"github.com/thrasher-/gocryptotrader/currency/forexprovider/base"
currencyconverter "github.com/thrasher-/gocryptotrader/currency/forexprovider/currencyconverterapi"
"github.com/thrasher-/gocryptotrader/currency/forexprovider/currencylayer"
fixer "github.com/thrasher-/gocryptotrader/currency/forexprovider/fixer.io"
"github.com/thrasher-/gocryptotrader/currency/forexprovider/openexchangerates"
)
// ForexProviders is an array of foreign exchange interfaces
type ForexProviders struct {
base.IFXProviders
}
// GetAvailableForexProviders returns a list of supported forex providers
func GetAvailableForexProviders() []string {
return []string{"CurrencyConverter", "CurrencyLayer", "Fixer", "OpenExchangeRates"}
}
// NewDefaultFXProvider returns the default forex provider (currencyconverterAPI)
func NewDefaultFXProvider() *ForexProviders {
fxp := new(ForexProviders)
currencyC := new(currencyconverter.CurrencyConverter)
currencyC.PrimaryProvider = true
currencyC.Enabled = true
currencyC.Name = "CurrencyConverter"
currencyC.APIKeyLvl = 0
currencyC.Verbose = false
fxp.IFXProviders = append(fxp.IFXProviders, currencyC)
return fxp
}
// StartFXService starts the forex provider service and returns a pointer to it
func StartFXService(fxProviders []base.Settings) *ForexProviders {
fxp := new(ForexProviders)
for i := range fxProviders {
if fxProviders[i].Name == "CurrencyConverter" && fxProviders[i].Enabled {
currencyC := new(currencyconverter.CurrencyConverter)
currencyC.Setup(fxProviders[i])
fxp.IFXProviders = append(fxp.IFXProviders, currencyC)
}
if fxProviders[i].Name == "CurrencyLayer" && fxProviders[i].Enabled {
currencyLayerP := new(currencylayer.CurrencyLayer)
currencyLayerP.Setup(fxProviders[i])
fxp.IFXProviders = append(fxp.IFXProviders, currencyLayerP)
}
if fxProviders[i].Name == "Fixer" && fxProviders[i].Enabled {
fixerP := new(fixer.Fixer)
fixerP.Setup(fxProviders[i])
fxp.IFXProviders = append(fxp.IFXProviders, fixerP)
}
if fxProviders[i].Name == "OpenExchangeRates" && fxProviders[i].Enabled {
OpenExchangeRatesP := new(openexchangerates.OXR)
OpenExchangeRatesP.Setup(fxProviders[i])
fxp.IFXProviders = append(fxp.IFXProviders, OpenExchangeRatesP)
}
}
if len(fxp.IFXProviders) == 0 {
log.Fatal("No foreign exchange providers enabled")
}
return fxp
}

View File

@@ -0,0 +1 @@
package forexprovider

View File

@@ -0,0 +1,230 @@
// Open Exchange Rates provides a simple, lightweight and portable JSON API with
// live and historical foreign exchange (forex) rates, via a simple and
// easy-to-integrate API, in JSON format. Data are tracked and blended
// algorithmically from multiple reliable sources, ensuring fair and unbiased
// consistency.
// End-of-day rates are available historically for all days going back to
// 1st January, 1999.
package openexchangerates
import (
"errors"
"fmt"
"net/url"
"strconv"
"github.com/thrasher-/gocryptotrader/common"
"github.com/thrasher-/gocryptotrader/currency/forexprovider/base"
)
// These consts contain endpoint information
const (
APIDeveloperAccess = iota
APIEnterpriseAccess
APIUnlimitedAccess
APIURL = "https://openexchangerates.org/api/"
APIEndpointLatest = "latest.json"
APIEndpointHistorical = "historical/%s.json"
APIEndpointCurrencies = "currencies.json"
APIEndpointTimeSeries = "time-series.json"
APIEndpointConvert = "convert/%s/%s/%s"
APIEndpointOHLC = "ohlc.json"
APIEndpointUsage = "usage.json"
)
// OXR is a foreign exchange rate provider at https://openexchangerates.org/
// this is the overarching type across this package
// DOCs : https://docs.openexchangerates.org/docs
type OXR struct {
base.Base
}
// Setup sets values for the OXR object
func (o *OXR) Setup(config base.Settings) {
o.APIKey = config.APIKey
o.APIKeyLvl = config.APIKeyLvl
o.Enabled = config.Enabled
o.Name = config.Name
o.RESTPollingDelay = config.RESTPollingDelay
o.Verbose = config.Verbose
o.PrimaryProvider = config.PrimaryProvider
}
// GetRates is a wrapper function to return rates
func (o *OXR) GetRates(baseCurrency, symbols string) (map[string]float64, error) {
rates, err := o.GetLatest(baseCurrency, symbols, false, false)
if err != nil {
return nil, err
}
standardisedRates := make(map[string]float64)
for k, v := range rates {
curr := baseCurrency + k
standardisedRates[curr] = v
}
return standardisedRates, nil
}
// GetLatest returns the latest exchange rates available from the Open Exchange
// Rates
func (o *OXR) GetLatest(base, symbols string, prettyPrint, showAlternative bool) (map[string]float64, error) {
var resp Latest
v := url.Values{}
v.Set("base", base)
v.Set("symbols", symbols)
v.Set("prettyprint", strconv.FormatBool(prettyPrint))
v.Set("show_alternative", strconv.FormatBool(showAlternative))
if err := o.SendHTTPRequest(APIEndpointLatest, v, &resp); err != nil {
return nil, err
}
if resp.Error {
return nil, errors.New(resp.Message)
}
return resp.Rates, nil
}
// GetHistoricalRates returns historical exchange rates for any date available
// from the Open Exchange Rates API.
func (o *OXR) GetHistoricalRates(date, base string, symbols []string, prettyPrint, showAlternative bool) (map[string]float64, error) {
var resp Latest
v := url.Values{}
v.Set("base", base)
v.Set("symbols", common.JoinStrings(symbols, ","))
v.Set("prettyprint", strconv.FormatBool(prettyPrint))
v.Set("show_alternative", strconv.FormatBool(showAlternative))
endpoint := fmt.Sprintf(APIEndpointHistorical, date)
if err := o.SendHTTPRequest(endpoint, v, &resp); err != nil {
return nil, err
}
if resp.Error {
return nil, errors.New(resp.Message)
}
return resp.Rates, nil
}
// GetCurrencies returns a list of all currency symbols available from the Open
// Exchange Rates API,
func (o *OXR) GetCurrencies(showInactive, prettyPrint, showAlternative bool) (map[string]string, error) {
resp := make(map[string]string)
v := url.Values{}
v.Set("show_inactive", strconv.FormatBool(showInactive))
v.Set("prettyprint", strconv.FormatBool(prettyPrint))
v.Set("show_alternative", strconv.FormatBool(showAlternative))
return resp, o.SendHTTPRequest(APIEndpointCurrencies, v, &resp)
}
// GetTimeSeries returns historical exchange rates for a given time period,
// where available.
func (o *OXR) GetTimeSeries(base, startDate, endDate string, symbols []string, prettyPrint, showAlternative bool) (map[string]interface{}, error) {
if o.APIKeyLvl < APIEnterpriseAccess {
return nil, errors.New("upgrade account, insufficient access")
}
var resp TimeSeries
v := url.Values{}
v.Set("base", base)
v.Set("start", startDate)
v.Set("end", endDate)
v.Set("symbols", common.JoinStrings(symbols, ","))
v.Set("prettyprint", strconv.FormatBool(prettyPrint))
v.Set("show_alternative", strconv.FormatBool(showAlternative))
if err := o.SendHTTPRequest(APIEndpointTimeSeries, v, &resp); err != nil {
return nil, err
}
if resp.Error {
return nil, errors.New(resp.Message)
}
return resp.Rates, nil
}
// ConvertCurrency converts any money value from one currency to another at the
// latest API rates
func (o *OXR) ConvertCurrency(amount float64, from, to string) (float64, error) {
if o.APIKeyLvl < APIUnlimitedAccess {
return 0, errors.New("upgrade account, insufficient access")
}
var resp Convert
endPoint := fmt.Sprintf(APIEndpointConvert, strconv.FormatFloat(amount, 'f', -1, 64), from, to)
if err := o.SendHTTPRequest(endPoint, url.Values{}, &resp); err != nil {
return 0, err
}
if resp.Error {
return 0, errors.New(resp.Message)
}
return resp.Response, nil
}
// GetOHLC returns historical Open, High Low, Close (OHLC) and Average exchange
// rates for a given time period, ranging from 1 month to 1 minute, where
// available.
func (o *OXR) GetOHLC(startTime, period, base string, symbols []string, prettyPrint bool) (map[string]interface{}, error) {
if o.APIKeyLvl < APIUnlimitedAccess {
return nil, errors.New("upgrade account, insufficient access")
}
var resp OHLC
v := url.Values{}
v.Set("start_time", startTime)
v.Set("period", period)
v.Set("base", base)
v.Set("symbols", common.JoinStrings(symbols, ","))
v.Set("prettyprint", strconv.FormatBool(prettyPrint))
if err := o.SendHTTPRequest(APIEndpointOHLC, v, &resp); err != nil {
return nil, err
}
if resp.Error {
return nil, errors.New(resp.Message)
}
return resp.Rates, nil
}
// GetUsageStats returns basic plan information and usage statistics for an Open
// Exchange Rates App ID
func (o *OXR) GetUsageStats(prettyPrint bool) (Usage, error) {
var resp Usage
v := url.Values{}
v.Set("prettyprint", strconv.FormatBool(prettyPrint))
if err := o.SendHTTPRequest(APIEndpointUsage, v, &resp); err != nil {
return resp, err
}
if resp.Error {
return resp, errors.New(resp.Message)
}
return resp, nil
}
// SendHTTPRequest sends a HTTP request
func (o *OXR) SendHTTPRequest(endpoint string, values url.Values, result interface{}) error {
headers := make(map[string]string)
headers["Authorization"] = "Token " + o.APIKey
path := APIURL + endpoint + "?" + values.Encode()
resp, err := common.SendHTTPRequest("GET", path, headers, nil)
if err != nil {
return err
}
return common.JSONDecode([]byte(resp), result)
}

View File

@@ -0,0 +1,70 @@
package openexchangerates
import (
"testing"
)
// please set apikey for due diligence testing NOTE testing uses your allocated
// API request quota
const (
apikey = ""
apilvl = 2
)
var o OXR
func TestGetRates(t *testing.T) {
_, err := o.GetRates("USD", "AUD")
if err == nil {
t.Error("test failed - GetRates() error", err)
}
}
func TestGetLatest(t *testing.T) {
_, err := o.GetLatest("USD", "AUD", false, false)
if err == nil {
t.Error("test failed - GetLatest() error", err)
}
}
func TestGetHistoricalRates(t *testing.T) {
_, err := o.GetHistoricalRates("2017-12-01", "USD", []string{"CNH", "AUD", "ANG"}, false, false)
if err == nil {
t.Error("test failed - GetRates() error", err)
}
}
func TestGetCurrencies(t *testing.T) {
_, err := o.GetCurrencies(true, true, true)
if err != nil {
t.Error("test failed - GetCurrencies() error", err)
}
}
func TestGetTimeSeries(t *testing.T) {
_, err := o.GetTimeSeries("USD", "2017-12-01", "2017-12-02", []string{"CNH", "AUD", "ANG"}, false, false)
if err == nil {
t.Error("test failed - GetTimeSeries() error", err)
}
}
func TestConvertCurrency(t *testing.T) {
_, err := o.ConvertCurrency(1337, "USD", "AUD")
if err == nil {
t.Error("test failed - ConvertCurrency() error", err)
}
}
func TestGetOHLC(t *testing.T) {
_, err := o.GetOHLC("2017-07-17T08:30:00Z", "1m", "USD", []string{"AUD"}, false)
if err == nil {
t.Error("test failed - GetOHLC() error", err)
}
}
func TestGetUsageStats(t *testing.T) {
_, err := o.GetUsageStats(false)
if err == nil {
t.Error("test failed - GetUsageStats() error", err)
}
}

View File

@@ -0,0 +1,108 @@
package openexchangerates
// Latest holds latest rate data
type Latest struct {
Disclaimer string `json:"disclaimer"`
License string `json:"license"`
Timestamp int64 `json:"timestamp"`
Base string `json:"base"`
Rates map[string]float64 `json:"rates"`
Error bool `json:"error"`
Status int `json:"status"`
Message string `json:"message"`
Description string `json:"description"`
}
// Historical holds historic rate data
type Historical struct {
Disclaimer string `json:"disclaimer"`
License string `json:"license"`
Timestamp int64 `json:"timestamp"`
Base string `json:"base"`
Rates map[string]float64 `json:"rates"`
Error bool `json:"error"`
Status int `json:"status"`
Message string `json:"message"`
Description string `json:"description"`
}
// TimeSeries holds historic rate data
type TimeSeries struct {
Disclaimer string `json:"disclaimer"`
License string `json:"license"`
StartDate string `json:"start_date"`
EndDate string `json:"end_date"`
Base string `json:"base"`
Rates map[string]interface{} `json:"rates"`
Error bool `json:"error"`
Status int `json:"status"`
Message string `json:"message"`
Description string `json:"description"`
}
// Convert holds historic rate data
type Convert struct {
Disclaimer string `json:"disclaimer"`
License string `json:"license"`
Request struct {
Query string `json:"query"`
Amount float64 `json:"amount"`
From string `json:"from"`
To string `json:"to"`
} `json:"request"`
Meta struct {
Timestamp int64 `json:"timestamp"`
Rate float64 `json:"rate"`
}
Response float64 `json:"response"`
Error bool `json:"error"`
Status int `json:"status"`
Message string `json:"message"`
Description string `json:"description"`
}
// OHLC holds open high low close values
type OHLC struct {
Disclaimer string `json:"disclaimer"`
License string `json:"license"`
StartDate string `json:"start_date"`
EndDate string `json:"end_date"`
Base string `json:"base"`
Rates map[string]interface{} `json:"rates"`
Error bool `json:"error"`
Status int `json:"status"`
Message string `json:"message"`
Description string `json:"description"`
}
// Usage holds usage statistical data
type Usage struct {
Status int `json:"status"`
Data struct {
AppID string `json:"app_id"`
Status string `json:"status"`
Plan struct {
Name string `json:"name"`
Quota string `json:"quota"`
UpdateFrequency string `json:"update_frequency"`
Features struct {
Base bool `json:"base"`
Symbols bool `json:"symbols"`
Experimental bool `json:"experimental"`
Timeseries bool `json:"time-series"`
Convert bool `json:"convert"`
} `json:"features"`
} `json:"plaab"`
} `json:"data"`
Usages struct {
Requests int64 `json:"requests"`
RequestQuota int `json:"requests_quota"`
RequestsRemaining int `json:"requests_remaining"`
DaysElapsed int `json:"days_elapsed"`
DaysRemaining int `json:"days_remaining"`
DailyAverage int `json:"daily_average"`
}
Error bool `json:"error"`
Message string `json:"message"`
Description string `json:"description"`
}

View File

@@ -365,8 +365,8 @@ func FormatExchangeCurrency(exchName string, p pair.CurrencyPair) pair.CurrencyI
// based on the user currency display preferences
func FormatCurrency(p pair.CurrencyPair) pair.CurrencyItem {
cfg := config.GetConfig()
return p.Display(cfg.CurrencyPairFormat.Delimiter,
cfg.CurrencyPairFormat.Uppercase)
return p.Display(cfg.Currency.CurrencyPairFormat.Delimiter,
cfg.Currency.CurrencyPairFormat.Uppercase)
}
// SetEnabled is a method that sets if the exchange is enabled

View File

@@ -48,7 +48,9 @@ func GetSpecificAvailablePairs(enabledExchangesOnly, fiatPairs, includeUSDT, cry
for x := range supportedPairs {
if fiatPairs {
if currency.IsCryptoFiatPair(supportedPairs[x]) && !pair.ContainsCurrency(supportedPairs[x], "USDT") || (includeUSDT && pair.ContainsCurrency(supportedPairs[x], "USDT") && currency.IsCryptoPair(supportedPairs[x])) {
if currency.IsCryptoFiatPair(supportedPairs[x]) &&
!pair.ContainsCurrency(supportedPairs[x], "USDT") ||
(includeUSDT && pair.ContainsCurrency(supportedPairs[x], "USDT") && currency.IsCryptoPair(supportedPairs[x])) {
if pair.Contains(pairList, supportedPairs[x], false) {
continue
}
@@ -160,7 +162,7 @@ func GetRelatableCryptocurrencies(p pair.CurrencyPair) []pair.CurrencyPair {
// incOrig includes the supplied pair if desired
func GetRelatableFiatCurrencies(p pair.CurrencyPair) []pair.CurrencyPair {
var pairs []pair.CurrencyPair
fiatCurrencies := currency.BaseCurrencies
fiatCurrencies := currency.FiatCurrencies
for x := range fiatCurrencies {
newPair := pair.NewCurrencyPair(p.FirstCurrency.String(), fiatCurrencies[x])

View File

@@ -18,7 +18,9 @@ const (
TestConfig = "./testdata/configtest.json"
)
var helperTestLoaded = false
var (
helperTestLoaded = false
)
func SetupTestHelpers(t *testing.T) {
if !helperTestLoaded {
@@ -30,8 +32,7 @@ func SetupTestHelpers(t *testing.T) {
}
testSetup = true
}
err := bot.config.RetrieveConfigCurrencyPairs(false)
err := bot.config.RetrieveConfigCurrencyPairs(true)
if err != nil {
t.Fatalf("Failed to retrieve config currency pairs. %s", err)
}
@@ -43,7 +44,6 @@ func TestGetSpecificAvailablePairs(t *testing.T) {
SetupTestHelpers(t)
result := GetSpecificAvailablePairs(true, true, true, false)
log.Println(result)
if !pair.Contains(result, pair.NewCurrencyPair("BTC", "USD"), true) {
t.Fatal("Unexpected result")
}
@@ -177,15 +177,15 @@ func TestGetRelatableFiatCurrencies(t *testing.T) {
t.Fatal("Unexpected result")
}
backup := currency.BaseCurrencies
currency.BaseCurrencies = append(currency.BaseCurrencies, "USD")
backup := currency.FiatCurrencies
currency.FiatCurrencies = append(currency.FiatCurrencies, "USD")
p = GetRelatableFiatCurrencies(pair.NewCurrencyPair("BTC", "USD"))
if !pair.Contains(p, pair.NewCurrencyPair("BTC", "ZAR"), true) {
t.Fatal("Unexpected result")
}
currency.BaseCurrencies = backup
currency.FiatCurrencies = backup
}
func TestMapCurrenciesByExchange(t *testing.T) {

42
main.go
View File

@@ -11,9 +11,11 @@ import (
"strconv"
"syscall"
"github.com/thrasher-/gocryptotrader/currency"
"github.com/thrasher-/gocryptotrader/currency/forexprovider"
"github.com/thrasher-/gocryptotrader/common"
"github.com/thrasher-/gocryptotrader/config"
"github.com/thrasher-/gocryptotrader/currency"
"github.com/thrasher-/gocryptotrader/exchanges"
"github.com/thrasher-/gocryptotrader/portfolio"
"github.com/thrasher-/gocryptotrader/smsglobal"
@@ -32,12 +34,12 @@ type Bot struct {
}
const banner = `
______ ______ __ ______ __
______ ______ __ ______ __
/ ____/____ / ____/_____ __ __ ____ / /_ ____ /_ __/_____ ______ ____/ /___ _____
/ / __ / __ \ / / / ___// / / // __ \ / __// __ \ / / / ___// __ // __ // _ \ / ___/
/ /_/ // /_/ // /___ / / / /_/ // /_/ // /_ / /_/ // / / / / /_/ // /_/ // __// /
\____/ \____/ \____//_/ \__, // .___/ \__/ \____//_/ /_/ \__,_/ \__,_/ \___//_/
/____//_/
/ /_/ // /_/ // /___ / / / /_/ // /_/ // /_ / /_/ // / / / / /_/ // /_/ // __// /
\____/ \____/ \____//_/ \__, // .___/ \__/ \____//_/ /_/ \__,_/ \__,_/ \___//_/
/____//_/
`
var bot Bot
@@ -73,12 +75,11 @@ func main() {
err = bot.config.LoadConfig(bot.configFile)
if err != nil {
log.Fatal(err)
log.Fatalf("Failed to load config. Err: %s", err)
}
AdjustGoMaxProcs()
log.Printf("Bot '%s' started.\n", bot.config.Name)
log.Printf("Fiat display currency: %s.", bot.config.FiatDisplayCurrency)
log.Printf("Bot dry run mode: %v.\n", common.IsEnabled(bot.dryRun))
if bot.config.SMS.Enabled {
@@ -105,25 +106,20 @@ func main() {
log.Fatalf("No exchanges were able to be loaded. Exiting")
}
if bot.config.CurrencyExchangeProvider == "yahoo" {
currency.SetProvider(true)
} else {
currency.SetProvider(false)
}
log.Printf("Currency exchange provider: %s.", bot.config.CurrencyExchangeProvider)
bot.config.RetrieveConfigCurrencyPairs(true)
err = currency.SeedCurrencyData(common.JoinStrings(currency.BaseCurrencies, ","))
log.Printf("Fiat display currency: %s.", bot.config.Currency.FiatDisplayCurrency)
currency.BaseCurrency = bot.config.Currency.FiatDisplayCurrency
currency.FXProviders = forexprovider.StartFXService(bot.config.GetCurrencyConfig().ForexProviders)
log.Printf("Primary forex conversion provider: %s.\n", bot.config.GetPrimaryForexProvider())
err = bot.config.RetrieveConfigCurrencyPairs(true)
if err != nil {
currency.SwapProvider()
log.Printf("'%s' currency exchange provider failed, swapping to %s and testing..",
bot.config.CurrencyExchangeProvider, currency.GetProvider())
err = currency.SeedCurrencyData(common.JoinStrings(currency.BaseCurrencies, ","))
if err != nil {
log.Fatalf("Fatal error retrieving config currencies. Error: %s", err)
}
log.Fatalf("Failed to retrieve config currency pairs. Error: %s", err)
}
log.Println("Successfully retrieved config currencies.")
log.Println("Fetching currency data from forex provider..")
err = currency.SeedCurrencyData(common.JoinStrings(currency.FiatCurrencies, ","))
if err != nil {
log.Fatalf("Unable to fetch forex data. Error: %s", err)
}
bot.portfolio = &portfolio.Portfolio
bot.portfolio.SeedPortfolio(bot.config.Portfolio)

View File

@@ -6,8 +6,8 @@ import (
"sync"
"time"
"github.com/thrasher-/gocryptotrader/currency"
"github.com/thrasher-/gocryptotrader/currency/pair"
"github.com/thrasher-/gocryptotrader/currency"
"github.com/thrasher-/gocryptotrader/currency/symbol"
exchange "github.com/thrasher-/gocryptotrader/exchanges"
"github.com/thrasher-/gocryptotrader/exchanges/orderbook"
@@ -16,7 +16,7 @@ import (
)
func printCurrencyFormat(price float64) string {
displaySymbol, err := symbol.GetSymbolByCurrencyName(bot.config.FiatDisplayCurrency)
displaySymbol, err := symbol.GetSymbolByCurrencyName(bot.config.Currency.FiatDisplayCurrency)
if err != nil {
log.Printf("Failed to get display symbol: %s", err)
}
@@ -25,7 +25,7 @@ func printCurrencyFormat(price float64) string {
}
func printConvertCurrencyFormat(origCurrency string, origPrice float64) string {
displayCurrency := bot.config.FiatDisplayCurrency
displayCurrency := bot.config.Currency.FiatDisplayCurrency
conv, err := currency.ConvertCurrency(origPrice, origCurrency, displayCurrency)
if err != nil {
log.Printf("Failed to convert currency: %s", err)
@@ -61,7 +61,7 @@ func printTickerSummary(result ticker.Price, p pair.CurrencyPair, assetType, exc
}
stats.Add(exchangeName, p, assetType, result.Last, result.Volume)
if currency.IsFiatCurrency(p.SecondCurrency.String()) && p.SecondCurrency.String() != bot.config.FiatDisplayCurrency {
if currency.IsFiatCurrency(p.SecondCurrency.String()) && p.SecondCurrency.String() != bot.config.Currency.FiatDisplayCurrency {
origCurrency := p.SecondCurrency.Upper().String()
log.Printf("%s %s %s: TICKER: Last %s Ask %s Bid %s High %s Low %s Volume %.8f",
exchangeName,
@@ -74,7 +74,7 @@ func printTickerSummary(result ticker.Price, p pair.CurrencyPair, assetType, exc
printConvertCurrencyFormat(origCurrency, result.Low),
result.Volume)
} else {
if currency.IsFiatCurrency(p.SecondCurrency.String()) && p.SecondCurrency.Upper().String() == bot.config.FiatDisplayCurrency {
if currency.IsFiatCurrency(p.SecondCurrency.String()) && p.SecondCurrency.Upper().String() == bot.config.Currency.FiatDisplayCurrency {
log.Printf("%s %s %s: TICKER: Last %s Ask %s Bid %s High %s Low %s Volume %.8f",
exchangeName,
exchange.FormatCurrency(p).String(),
@@ -111,7 +111,7 @@ func printOrderbookSummary(result orderbook.Base, p pair.CurrencyPair, assetType
bidsAmount, bidsValue := result.CalculateTotalBids()
asksAmount, asksValue := result.CalculateTotalAsks()
if currency.IsFiatCurrency(p.SecondCurrency.String()) && p.SecondCurrency.String() != bot.config.FiatDisplayCurrency {
if currency.IsFiatCurrency(p.SecondCurrency.String()) && p.SecondCurrency.String() != bot.config.Currency.FiatDisplayCurrency {
origCurrency := p.SecondCurrency.Upper().String()
log.Printf("%s %s %s: ORDERBOOK: Bids len: %d Amount: %f %s. Total value: %s Asks len: %d Amount: %f %s. Total value: %s",
exchangeName,
@@ -127,7 +127,7 @@ func printOrderbookSummary(result orderbook.Base, p pair.CurrencyPair, assetType
printConvertCurrencyFormat(origCurrency, asksValue),
)
} else {
if currency.IsFiatCurrency(p.SecondCurrency.String()) && p.SecondCurrency.Upper().String() == bot.config.FiatDisplayCurrency {
if currency.IsFiatCurrency(p.SecondCurrency.String()) && p.SecondCurrency.Upper().String() == bot.config.Currency.FiatDisplayCurrency {
log.Printf("%s %s %s: ORDERBOOK: Bids len: %d Amount: %f %s. Total value: %s Asks len: %d Amount: %f %s. Total value: %s",
exchangeName,
exchange.FormatCurrency(p).String(),

View File

@@ -1,13 +1,52 @@
{
"Name": "Skynet",
"EncryptConfig": 0,
"Cryptocurrencies": "BTC,LTC,ETH,XRP,NMC,NVC,PPC,XBT,DOGE,DASH",
"CurrencyExchangeProvider": "fixer",
"CurrencyPairFormat": {
"Uppercase": true,
"Delimiter": "-"
"CurrencyConfig": {
"ForexProviders": [
{
"Name": "CurrencyConverter",
"Enabled": true,
"Verbose": false,
"RESTPollingDelay": 600,
"APIKey": "",
"APIKeyLvl": 0,
"PrimaryProvider": true
},
{
"Name": "CurrencyLayer",
"Enabled": false,
"Verbose": false,
"RESTPollingDelay": 600,
"APIKey": "Key",
"APIKeyLvl": -1,
"PrimaryProvider": false
},
{
"Name": "Fixer",
"Enabled": false,
"Verbose": false,
"RESTPollingDelay": 600,
"APIKey": "Key",
"APIKeyLvl": -1,
"PrimaryProvider": false
},
{
"Name": "OpenExchangeRates",
"Enabled": false,
"Verbose": false,
"RESTPollingDelay": 600,
"APIKey": "Key",
"APIKeyLvl": -1,
"PrimaryProvider": false
}
],
"Cryptocurrencies": "BTC,LTC,ETH,DOGE,DASH,XRP,XMR",
"CurrencyPairFormat": {
"Uppercase": true,
"Delimiter": "-"
},
"FiatDisplayCurrency": "USD"
},
"FiatDisplayCurrency": "USD",
"GlobalHTTPTimeout": 15000000000,
"PortfolioAddresses": {
"Addresses": [
@@ -55,6 +94,7 @@
"AdminPassword": "Password",
"ListenAddress": ":9050",
"WebsocketConnectionLimit": 1,
"WebsocketMaxAuthFailures": 3,
"WebsocketAllowInsecureOrigin": false
},
"Exchanges": [

View File

@@ -431,11 +431,7 @@ func wsGetExchangeRates(client *WebsocketClient, data interface{}) error {
wsResp := WebsocketEventResponse{
Event: "GetExchangeRates",
}
if currency.YahooEnabled {
wsResp.Data = currency.CurrencyStore
} else {
wsResp.Data = currency.CurrencyStoreFixer
}
wsResp.Data = currency.GetExchangeRates()
return client.SendWebsocketMessage(wsResp)
}