Engine: Remove Default Forex Provider (#1395)

* Currency: Do not use a default forex provider

exchangerate.host now requires an API key.
Instead of finding a new Free (for now) default, this change simply
disables the currency exchange when nothing is enabled.

* SyncManager: Report ?.?? for an unknown forex amount

In a situation where we thought forex was available but we got an error,
this avoids showing 0.00 when there was actually an error.

* Currency: Tests for no default forex

* Currency: Use mock provider for tests

* Currency: Add API key to exchangerate.host

* Currency: Remove Exchangerate.host

Exchangerate.host was bought by apilayer, the old API deprecated, and
replaced with a proxy to the apilayer api.
We already have currencylayer support, so ther's no reason to keep exh.

Worth noting: New ERH keys actually work on currencylayer

* Currencies: Add test coverage for currency layer

* fixup! Currency: Tests for no default forex

Remove duplicate assignment

Fixes [review comment](https://github.com/thrasher-corp/gocryptotrader/pull/1395#discussion_r1395178513)

* fixup! Currency: Add API key to exchangerate.host

Remove unused ErrVar

Fixes [review
comment](https://github.com/thrasher-corp/gocryptotrader/pull/1395#discussion_r1396647418)

* fixup! Currency: Tests for no default forex

Fix spelling of override in test

Fixes [review comment](https://github.com/thrasher-corp/gocryptotrader/pull/1395#discussion_r1396701476)

* fixup! SyncManager: Report ?.?? for an unknown forex amount

Fix display of non-positive currency conversions.

Fixes [review comment](https://github.com/thrasher-corp/gocryptotrader/pull/1395/files#r1398527134)
This commit is contained in:
Gareth Kirwan
2023-11-24 03:50:01 +01:00
committed by GitHub
parent f23cc004fd
commit 88182ec414
22 changed files with 281 additions and 984 deletions

View File

@@ -2,8 +2,8 @@
Thanks to the following contributors:
thrasher- | https://github.com/thrasher-
shazbert | https://github.com/shazbert
gloriousCode | https://github.com/gloriousCode
dependabot[bot] | https://github.com/apps/dependabot
gloriousCode | https://github.com/gloriousCode
dependabot-preview[bot] | https://github.com/apps/dependabot-preview
xtda | https://github.com/xtda
gbjk | https://github.com/gbjk
@@ -13,13 +13,13 @@ vazha | https://github.com/vazha
ydm | https://github.com/ydm
ermalguni | https://github.com/ermalguni
MadCozBadd | https://github.com/MadCozBadd
Beadko | https://github.com/Beadko
vadimzhukck | https://github.com/vadimzhukck
140am | https://github.com/140am
marcofranssen | https://github.com/marcofranssen
geseq | https://github.com/geseq
Beadko | https://github.com/Beadko
TaltaM | https://github.com/TaltaM
samuael | https://github.com/samuael
TaltaM | https://github.com/TaltaM
dackroyd | https://github.com/dackroyd
cranktakular | https://github.com/cranktakular
khcchiu | https://github.com/khcchiu

View File

@@ -144,25 +144,25 @@ Binaries will be published once the codebase reaches a stable condition.
|User|Contribution Amount|
|--|--|
| [thrasher-](https://github.com/thrasher-) | 683 |
| [shazbert](https://github.com/shazbert) | 301 |
| [gloriousCode](https://github.com/gloriousCode) | 219 |
| [dependabot[bot]](https://github.com/apps/dependabot) | 207 |
| [shazbert](https://github.com/shazbert) | 313 |
| [dependabot[bot]](https://github.com/apps/dependabot) | 227 |
| [gloriousCode](https://github.com/gloriousCode) | 224 |
| [dependabot-preview[bot]](https://github.com/apps/dependabot-preview) | 88 |
| [xtda](https://github.com/xtda) | 47 |
| [gbjk](https://github.com/gbjk) | 35 |
| [gbjk](https://github.com/gbjk) | 40 |
| [lrascao](https://github.com/lrascao) | 27 |
| [Rots](https://github.com/Rots) | 15 |
| [vazha](https://github.com/vazha) | 15 |
| [ydm](https://github.com/ydm) | 15 |
| [ermalguni](https://github.com/ermalguni) | 14 |
| [MadCozBadd](https://github.com/MadCozBadd) | 13 |
| [Beadko](https://github.com/Beadko) | 10 |
| [vadimzhukck](https://github.com/vadimzhukck) | 10 |
| [140am](https://github.com/140am) | 8 |
| [marcofranssen](https://github.com/marcofranssen) | 8 |
| [geseq](https://github.com/geseq) | 8 |
| [Beadko](https://github.com/Beadko) | 6 |
| [samuael](https://github.com/samuael) | 7 |
| [TaltaM](https://github.com/TaltaM) | 6 |
| [samuael](https://github.com/samuael) | 6 |
| [dackroyd](https://github.com/dackroyd) | 5 |
| [cranktakular](https://github.com/cranktakular) | 5 |
| [khcchiu](https://github.com/khcchiu) | 5 |

View File

@@ -7,7 +7,6 @@
+ Exchange Rates support
+ Fixer.io support
+ Open Exchange Rates support
+ ExchangeRate.host support
### Please click GoDocs chevron above to view current GoDoc information for this package
{{template "contributions"}}

View File

@@ -1,35 +0,0 @@
{{define "currency forexprovider exchangerate.host" -}}
{{template "header" .}}
## Current Features for {{.Name}}
+ Fetches up to date currency data from [ExchangeRate.host API]("https://exchangerate.host")
### How to enable
+ [Enable via configuration](https://github.com/thrasher-corp/gocryptotrader/tree/master/config#enable-currency-via-config-example)
+ Individual package example below:
```go
import (
"github.com/thrasher-corp/gocryptotrader/currency/forexprovider/base"
"github.com/thrasher-corp/gocryptotrader/currency/forexprovider/exchangerate.host"
)
var c exchangeratehost.ExchangeRateHost
// Define configuration
newSettings := base.Settings{
Name: "ExchangeRateHost",
// ...
}
c.Setup(newSettings)
rates, err := c.GetRates("USD", "EUR,AUD")
// Handle error
```
### Please click GoDocs chevron above to view current GoDoc information for this package
{{template "contributions"}}
{{template "donations" .}}
{{- end}}

View File

@@ -68,12 +68,11 @@ const (
// 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"
DefaultForexProviderExchangeRatesAPI = "ExchangeRateHost"
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

View File

@@ -2,32 +2,25 @@ package currency
import (
"fmt"
"strings"
"testing"
"github.com/stretchr/testify/assert"
)
func TestNewConversionFromString(t *testing.T) {
expected := "AUDUSD"
conv, err := NewConversionFromString(expected)
if err != nil {
t.Error(err)
}
if conv.String() != expected {
t.Errorf("NewConversion() error expected %s but received %s",
expected,
conv)
}
conv, err := NewConversionFromString("AUDUSD")
assert.NoError(t, err, "NewConversionFromString should not error")
assert.Equal(t, "AUDUSD", conv.String(), "Should provide correct conversion currency")
r, err := conv.GetRate()
assert.NoError(t, err, "GetRate should not error")
assert.Positive(t, r, "Should provide correct conversion rate")
newexpected := strings.ToLower(expected)
conv, err = NewConversionFromString(newexpected)
if err != nil {
t.Error(err)
}
if conv.String() != newexpected {
t.Errorf("NewConversion() error expected %s but received %s",
newexpected,
conv)
}
conv, err = NewConversionFromString("audusd")
assert.NoError(t, err, "NewConversionFromString should not error")
assert.Equal(t, "audusd", conv.String(), "Should provide correct conversion for lowercase")
r, err = conv.GetRate()
assert.NoError(t, err, "GetRate should not error")
assert.Positive(t, r, "Should provide correct conversion rate")
}
func TestNewConversionFromStrings(t *testing.T) {

View File

@@ -34,7 +34,6 @@ type BotOverrides struct {
ExchangeRates bool
Fixer bool
OpenExchangeRates bool
ExchangeRateHost bool
}
// CoinmarketcapSettings refers to settings

View File

@@ -25,7 +25,6 @@ Join our slack to discuss all things related to GoCryptoTrader! [GoCryptoTrader
+ Exchange Rates support
+ Fixer.io support
+ Open Exchange Rates support
+ ExchangeRate.host support
### Please click GoDocs chevron above to view current GoDoc information for this package

View File

@@ -1,137 +1,129 @@
package currencylayer
import (
"log"
"os"
"strings"
"testing"
"github.com/stretchr/testify/assert"
"github.com/thrasher-corp/gocryptotrader/currency/forexprovider/base"
)
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 = 0
// Either set your API key here or in env var for integration testing
var (
apiKey = ""
apiKeyLevel = 0
)
var isSet bool
func setup() error {
if !isSet {
defaultCfg := base.Settings{
Name: "CurrencyLayer",
Enabled: true,
}
if APIkey != "" {
defaultCfg.APIKey = APIkey
}
if Apilevel > -2 && Apilevel < 4 {
defaultCfg.APIKeyLvl = Apilevel
}
err := c.Setup(defaultCfg)
if err != nil {
return err
}
isSet = true
func TestMain(m *testing.M) {
if apiKey == "" {
apiKey = os.Getenv("CURRENCYLAYER_APIKEY")
}
return nil
cfg := base.Settings{
Name: "CurrencyLayer",
Enabled: true,
APIKeyLvl: apiKeyLevel,
APIKey: apiKey,
}
if err := c.Setup(cfg); err != nil {
log.Fatal(err)
}
os.Exit(m.Run())
}
func areAPIKeysSet() bool {
return APIkey != "" && Apilevel != -1
func skipIfNoAPIKey(tb testing.TB) {
tb.Helper()
if apiKey == "" {
tb.Skip("No API Key configured. Set env var CURRENCYLAYER_APIKEY")
}
}
func TestGetRates(t *testing.T) {
err := setup()
if err != nil {
t.Skip("CurrencyLayer GetRates error", err)
}
_, err = c.GetRates("USD", "AUD")
if areAPIKeysSet() && err != nil {
t.Error("test error - currencylayer GetRates() error", err)
} else if !areAPIKeysSet() && err == nil {
t.Error("test error - currencylayer GetRates() error cannot be nil")
}
func TestNoAPIKey(t *testing.T) {
t.Parallel()
n := c
n.APIKey = ""
_, err := n.GetSupportedCurrencies()
assert.ErrorContains(t, err, "You have not supplied an API Access Key", "Should error APIKeyRequired")
}
func TestGetSupportedCurrencies(t *testing.T) {
err := setup()
if err != nil {
t.Fatal("CurrencyLayer GetSupportedCurrencies error", err)
t.Parallel()
skipIfNoAPIKey(t)
currs, err := c.GetSupportedCurrencies()
if assert.NoError(t, err, "GetSupportedCurrencies should not error") {
assert.Contains(t, currs, "AUD", "AUD is a valid currency")
assert.Contains(t, currs, "USD", "USD is a valid currency") // Might fail in the near future
}
_, err = c.GetSupportedCurrencies()
if areAPIKeysSet() && err != nil {
t.Error("test error - currencylayer GetSupportedCurrencies() error", err)
} else if !areAPIKeysSet() && err == nil {
t.Error("test error - currencylayer GetSupportedCurrencies() error cannot be nil")
}
func TestGetRates(t *testing.T) {
t.Parallel()
skipIfNoAPIKey(t)
r, err := c.GetRates("USD", "AUD")
if assert.NoError(t, err, "GetRates should not error") {
assert.Contains(t, r, "USDAUD", "Should find a USDAUD rate")
assert.Positive(t, r["USDAUD"], "Rate should be positive")
}
}
func TestGetliveData(t *testing.T) {
err := setup()
if err != nil {
t.Fatal("CurrencyLayer GetliveData error", err)
}
_, err = c.GetliveData("AUD", "USD")
if areAPIKeysSet() && err != nil {
t.Error("test error - currencylayer GetliveData() error", err)
} else if !areAPIKeysSet() && err == nil {
t.Error("test error - currencylayer GetliveData() error cannot be nil")
t.Parallel()
skipIfNoAPIKey(t)
r, err := c.GetliveData("EUR", "GBP")
if assert.NoError(t, err, "GetliveData should not error") {
assert.Contains(t, r, "GBPEUR", "Should find rate")
assert.Positive(t, r["GBPEUR"], "Rate should be positive")
}
}
func TestGetHistoricalData(t *testing.T) {
err := setup()
if err != nil {
t.Fatal("CurrencyLayer GetHistoricalData error", err)
}
_, err = c.GetHistoricalData("2016-12-15", []string{"AUD"}, "USD")
if areAPIKeysSet() && err != nil {
t.Error("test error - currencylayer GetHistoricalData() error", err)
} else if !areAPIKeysSet() && err == nil {
t.Error("test error - currencylayer GetHistoricalData() error cannot be nil")
t.Parallel()
skipIfNoAPIKey(t)
r, err := c.GetHistoricalData("2022-09-26", []string{"USD"}, "EUR")
if assert.NoError(t, err, "GetHistoricalData should not error") {
assert.Contains(t, r, "EURUSD", "Should find rate")
assert.Equal(t, r["EURUSD"], 0.962232, "Rate should be exactly correct")
}
}
func TestConvert(t *testing.T) {
err := setup()
if err != nil {
t.Fatal("CurrencyLayer Convert error", err)
}
_, err = c.Convert("USD", "AUD", "", 1)
if areAPIKeysSet() && err != nil && c.APIKeyLvl >= AccountBasic {
t.Error("test error - currencylayer Convert() error", err)
} else if !areAPIKeysSet() && err == nil {
t.Error("test error - currencylayer Convert() error cannot be nil")
t.Parallel()
skipIfNoAPIKey(t)
r, err := c.Convert("USD", "AUD", "", 1)
if assert.NoError(t, err, "Convert should not error") {
assert.Positive(t, r, "Should get a positive rate")
}
}
func TestQueryTimeFrame(t *testing.T) {
err := setup()
if err != nil {
t.Fatal("CurrencyLayer QueryTimeFrame error", err)
}
_, err = c.QueryTimeFrame("2010-12-0", "2010-12-5", "USD", []string{"AUD"})
if areAPIKeysSet() && err != nil && c.APIKeyLvl >= AccountPro {
t.Error("test error - currencylayer QueryTimeFrame() error", err)
} else if !areAPIKeysSet() && err == nil {
t.Error("test error - currencylayer QueryTimeFrame() error cannot be nil")
t.Parallel()
skipIfNoAPIKey(t)
r, err := c.QueryTimeFrame("2020-03-12", "2020-03-16", "USD", []string{"AUD"})
if assert.NoError(t, err, "QueryTimeFrame should not error") {
assert.Len(t, r, 5, "Should get correct number of days")
a, ok := r["2020-03-16"].(map[string]any)
assert.True(t, ok, "Has final date entry")
assert.Equal(t, a["USDAUD"], 1.6397, "And it was a bad week")
}
}
func TestQueryCurrencyChange(t *testing.T) {
err := setup()
if err != nil {
t.Fatal("CurrencyLayer QueryCurrencyChange() error", err)
}
_, err = c.QueryCurrencyChange("2010-12-0", "2010-12-5", "USD", []string{"AUD"})
if areAPIKeysSet() && err != nil && c.APIKeyLvl == AccountEnterprise {
t.Error("test error - currencylayer QueryCurrencyChange() error", err)
} else if !areAPIKeysSet() && err == nil {
t.Error("test error - currencylayer QueryCurrencyChange() error cannot be nil")
t.Parallel()
skipIfNoAPIKey(t)
r, err := c.QueryCurrencyChange("2030-03-12", "2030-03-16", "USD", []string{"AUD"})
switch {
case err != nil && strings.Contains(err.Error(), "insufficient API privileges, upgrade to basic to use this function"):
t.Skip("Upgrade to Basic API plan to test Currency Change")
case assert.NoError(t, err, "QueryCurrencyChange should not error"):
assert.Contains(t, r, "USDAUD", "Should find change")
assert.Positive(t, r["USDAUD"].Change, "Change should be positive")
assert.Positive(t, r["USDAUD"].ChangePCT, "Change PCT should be positive")
}
}

View File

@@ -33,6 +33,7 @@ type CurrencyLayer struct {
// Error Defines the response error if an error occurred
type Error struct {
Code int `json:"code"`
Type string `json:"type"`
Info string `json:"info"`
}

View File

@@ -1,69 +0,0 @@
# GoCryptoTrader package Exchangerate.host
<img src="/common/gctlogo.png?raw=true" width="350px" height="350px" hspace="70">
[![Build Status](https://github.com/thrasher-corp/gocryptotrader/actions/workflows/tests.yml/badge.svg?branch=master)](https://github.com/thrasher-corp/gocryptotrader/actions/workflows/tests.yml)
[![Software License](https://img.shields.io/badge/License-MIT-orange.svg?style=flat-square)](https://github.com/thrasher-corp/gocryptotrader/blob/master/LICENSE)
[![GoDoc](https://godoc.org/github.com/thrasher-corp/gocryptotrader?status.svg)](https://godoc.org/github.com/thrasher-corp/gocryptotrader/currency/forexprovider/exchangerate.host)
[![Coverage Status](http://codecov.io/github/thrasher-corp/gocryptotrader/coverage.svg?branch=master)](http://codecov.io/github/thrasher-corp/gocryptotrader?branch=master)
[![Go Report Card](https://goreportcard.com/badge/github.com/thrasher-corp/gocryptotrader)](https://goreportcard.com/report/github.com/thrasher-corp/gocryptotrader)
This exchangerate.host package is part of the GoCryptoTrader codebase.
## This is still in active development
You can track ideas, planned features and what's in progress on this Trello board: [https://trello.com/b/ZAhMhpOy/gocryptotrader](https://trello.com/b/ZAhMhpOy/gocryptotrader).
Join our slack to discuss all things related to GoCryptoTrader! [GoCryptoTrader Slack](https://join.slack.com/t/gocryptotrader/shared_invite/enQtNTQ5NDAxMjA2Mjc5LTc5ZDE1ZTNiOGM3ZGMyMmY1NTAxYWZhODE0MWM5N2JlZDk1NDU0YTViYzk4NTk3OTRiMDQzNGQ1YTc4YmRlMTk)
## Current Features for exchangerate.host
+ Fetches up to date currency data from [ExchangeRate.host API]("https://exchangerate.host")
### How to enable
+ [Enable via configuration](https://github.com/thrasher-corp/gocryptotrader/tree/master/config#enable-currency-via-config-example)
+ Individual package example below:
```go
import (
"github.com/thrasher-corp/gocryptotrader/currency/forexprovider/base"
"github.com/thrasher-corp/gocryptotrader/currency/forexprovider/exchangerate.host"
)
var c exchangeratehost.ExchangeRateHost
// Define configuration
newSettings := base.Settings{
Name: "ExchangeRateHost",
// ...
}
c.Setup(newSettings)
rates, err := c.GetRates("USD", "EUR,AUD")
// Handle error
```
### Please click GoDocs chevron above to view current GoDoc information for this package
## Contribution
Please feel free to submit any pull requests or suggest any desired features to be added.
When submitting a PR, please abide by our coding guidelines:
+ Code must adhere to the official Go [formatting](https://golang.org/doc/effective_go.html#formatting) guidelines (i.e. uses [gofmt](https://golang.org/cmd/gofmt/)).
+ Code must be documented adhering to the official Go [commentary](https://golang.org/doc/effective_go.html#commentary) guidelines.
+ Code must adhere to our [coding style](https://github.com/thrasher-corp/gocryptotrader/blob/master/doc/coding_style.md).
+ Pull requests need to be based on and opened against the `master` branch.
## Donations
<img src="https://github.com/thrasher-corp/gocryptotrader/blob/master/web/src/assets/donate.png?raw=true" hspace="70">
If this framework helped you in any way, or you would like to support the developers working on it, please donate Bitcoin to:
***bc1qk0jareu4jytc0cfrhr5wgshsq8282awpavfahc***

View File

@@ -1,265 +0,0 @@
package exchangeratehost
import (
"context"
"errors"
"net/http"
"net/url"
"strconv"
"time"
"github.com/thrasher-corp/gocryptotrader/common"
"github.com/thrasher-corp/gocryptotrader/currency/forexprovider/base"
"github.com/thrasher-corp/gocryptotrader/exchanges/request"
)
// A client for the exchangerate.host API. NOTE: The format and callback
// parameters aren't supported as they're not needed for this implementation.
// Furthermore, the source is set to "ECB" as default
const (
timeLayout = "2006-01-02"
exchangeRateHostURL = "https://api.exchangerate.host"
)
var (
// DefaultSource uses the ecb for forex rates
DefaultSource = "ecb"
)
// Setup sets up the ExchangeRateHost config
func (e *ExchangeRateHost) Setup(config base.Settings) error {
e.Name = config.Name
e.Enabled = config.Enabled
e.Verbose = config.Verbose
e.PrimaryProvider = config.PrimaryProvider
var err error
e.Requester, err = request.New(e.Name,
common.NewHTTPClientWithTimeout(base.DefaultTimeOut))
return err
}
// GetLatestRates returns a list of forex rates based on the supplied params
func (e *ExchangeRateHost) GetLatestRates(baseCurrency, symbols string, amount float64, places int64, source string) (*LatestRates, error) {
v := url.Values{}
if baseCurrency != "" {
v.Set("base", baseCurrency)
}
if symbols != "" {
v.Set("symbols", symbols)
}
if amount != 0 {
v.Set("amount", strconv.FormatFloat(amount, 'f', -1, 64))
}
if places != 0 {
v.Set("places", strconv.FormatInt(places, 10))
}
targetSource := DefaultSource
if source != "" {
targetSource = source
}
v.Set("source", targetSource)
var l LatestRates
return &l, e.SendHTTPRequest("latest", v, &l)
}
// ConvertCurrency converts a currency based on the supplied params
func (e *ExchangeRateHost) ConvertCurrency(from, to, baseCurrency, symbols, source string, date time.Time, amount float64, places int64) (*ConvertCurrency, error) {
v := url.Values{}
if from != "" {
v.Set("from", from)
}
if to != "" {
v.Set("to", to)
}
if !date.IsZero() {
v.Set("date", date.UTC().Format(timeLayout))
}
if baseCurrency != "" {
v.Set("base", baseCurrency)
}
if symbols != "" {
v.Set("symbols", symbols)
}
if amount != 0 {
v.Set("amount", strconv.FormatFloat(amount, 'f', -1, 64))
}
if places != 0 {
v.Set("places", strconv.FormatInt(places, 10))
}
targetSource := DefaultSource
if source != "" {
targetSource = source
}
v.Set("source", targetSource)
var c ConvertCurrency
return &c, e.SendHTTPRequest("convert", v, &c)
}
// GetHistoricalRates returns a list of historical rates based on the supplied params
func (e *ExchangeRateHost) GetHistoricalRates(date time.Time, baseCurrency, symbols string, amount float64, places int64, source string) (*HistoricRates, error) {
v := url.Values{}
if date.IsZero() {
date = time.Now()
}
fmtDate := date.UTC().Format(timeLayout)
v.Set("date", fmtDate)
if baseCurrency != "" {
v.Set("base", baseCurrency)
}
if symbols != "" {
v.Set("symbols", symbols)
}
if amount != 0 {
v.Set("amount", strconv.FormatFloat(amount, 'f', -1, 64))
}
if places != 0 {
v.Set("places", strconv.FormatInt(places, 10))
}
targetSource := DefaultSource
if source != "" {
targetSource = source
}
v.Set("source", targetSource)
var h HistoricRates
return &h, e.SendHTTPRequest(fmtDate, v, &h)
}
// GetTimeSeries returns time series forex data based on the supplied params
func (e *ExchangeRateHost) GetTimeSeries(startDate, endDate time.Time, baseCurrency, symbols string, amount float64, places int64, source string) (*TimeSeries, error) {
if startDate.IsZero() || endDate.IsZero() {
return nil, errors.New("startDate and endDate must be set")
}
if startDate.After(endDate) || startDate.Equal(endDate) {
return nil, errors.New("startDate and endDate must be set correctly")
}
v := url.Values{}
v.Set("start_date", startDate.UTC().Format(timeLayout))
v.Set("end_date", endDate.UTC().Format(timeLayout))
if baseCurrency != "" {
v.Set("base", baseCurrency)
}
if symbols != "" {
v.Set("symbols", symbols)
}
if amount != 0 {
v.Set("amount", strconv.FormatFloat(amount, 'f', -1, 64))
}
if places != 0 {
v.Set("places", strconv.FormatInt(places, 10))
}
targetSource := DefaultSource
if source != "" {
targetSource = source
}
v.Set("source", targetSource)
var t TimeSeries
return &t, e.SendHTTPRequest("timeseries", v, &t)
}
// GetFluctuations returns a list of forex price fluctuations based on the supplied params
func (e *ExchangeRateHost) GetFluctuations(startDate, endDate time.Time, baseCurrency, symbols string, amount float64, places int64, source string) (*Fluctuations, error) {
if startDate.IsZero() || endDate.IsZero() {
return nil, errors.New("startDate and endDate must be set")
}
if startDate.After(endDate) || startDate.Equal(endDate) {
return nil, errors.New("startDate and endDate must be set correctly")
}
v := url.Values{}
v.Set("start_date", startDate.UTC().Format(timeLayout))
v.Set("end_date", endDate.UTC().Format(timeLayout))
if baseCurrency != "" {
v.Set("base", baseCurrency)
}
if symbols != "" {
v.Set("symbols", symbols)
}
if amount != 0 {
v.Set("amount", strconv.FormatFloat(amount, 'f', -1, 64))
}
if places != 0 {
v.Set("places", strconv.FormatInt(places, 10))
}
targetSource := DefaultSource
if source != "" {
targetSource = source
}
v.Set("source", targetSource)
var f Fluctuations
return &f, e.SendHTTPRequest("fluctuation", v, &f)
}
// GetSupportedSymbols returns a list of supported symbols
func (e *ExchangeRateHost) GetSupportedSymbols() (*SupportedSymbols, error) {
var s SupportedSymbols
return &s, e.SendHTTPRequest("symbols", url.Values{}, &s)
}
// GetSupportedCurrencies returns a list of supported currencies
func (e *ExchangeRateHost) GetSupportedCurrencies() ([]string, error) {
s, err := e.GetSupportedSymbols()
if err != nil {
return nil, err
}
symbols := make([]string, 0, len(s.Symbols))
for x := range s.Symbols {
symbols = append(symbols, x)
}
return symbols, nil
}
// GetRates returns the forex rates based on the supplied base currency and symbols
func (e *ExchangeRateHost) GetRates(baseCurrency, symbols string) (map[string]float64, error) {
l, err := e.GetLatestRates(baseCurrency, symbols, 0, 0, "")
if err != nil {
return nil, err
}
rates := make(map[string]float64)
for k, v := range l.Rates {
rates[baseCurrency+k] = v
}
return rates, nil
}
// SendHTTPRequest sends a typical get request
func (e *ExchangeRateHost) SendHTTPRequest(endpoint string, v url.Values, result interface{}) error {
path := common.EncodeURLValues(exchangeRateHostURL+"/"+endpoint, v)
item := &request.Item{
Method: http.MethodGet,
Path: path,
Result: &result,
Verbose: e.Verbose,
}
return e.Requester.SendPayload(context.TODO(), request.Unset, func() (*request.Item, error) {
return item, nil
}, request.UnauthenticatedRequest)
}

View File

@@ -1,110 +0,0 @@
package exchangeratehost
import (
"log"
"os"
"testing"
"time"
"github.com/thrasher-corp/gocryptotrader/currency/forexprovider/base"
)
var (
e ExchangeRateHost
testCurrencies = "USD,EUR,CZK"
)
func TestMain(t *testing.M) {
err := e.Setup(base.Settings{
Name: "ExchangeRateHost",
})
if err != nil {
log.Fatal(err)
}
os.Exit(t.Run())
}
func TestGetLatestRates(t *testing.T) {
_, err := e.GetLatestRates("USD", testCurrencies, 1200, 2, "")
if err != nil {
t.Error(err)
}
}
func TestConvertCurrency(t *testing.T) {
_, err := e.ConvertCurrency("USD", "EUR", "", testCurrencies, "", time.Now(), 1200, 2)
if err != nil {
t.Error(err)
}
}
func TestGetHistoricRates(t *testing.T) {
_, err := e.GetHistoricalRates(time.Time{}, "AUD", testCurrencies, 1200, 2, "")
if err != nil {
t.Error(err)
}
}
func TestGetTimeSeriesRates(t *testing.T) {
_, err := e.GetTimeSeries(time.Time{}, time.Now(), "USD", testCurrencies, 1200, 2, "")
if err == nil {
t.Error("empty start time show throw an error")
}
tmNow := time.Now()
_, err = e.GetTimeSeries(tmNow, tmNow, "USD", testCurrencies, 1200, 2, "")
if err == nil {
t.Error("equal times show throw an error")
}
tmStart := tmNow.AddDate(0, -3, 0)
_, err = e.GetTimeSeries(tmStart, tmNow, "USD", testCurrencies, 1200, 2, "")
if err != nil {
t.Error(err)
}
}
func TestGetFluctuationData(t *testing.T) {
_, err := e.GetFluctuations(time.Time{}, time.Now(), "USD", testCurrencies, 1200, 2, "")
if err == nil {
t.Error("empty start time show throw an error")
}
tmNow := time.Now()
_, err = e.GetFluctuations(tmNow, tmNow, "USD", testCurrencies, 1200, 2, "")
if err == nil {
t.Error("equal times show throw an error")
}
tmStart := tmNow.AddDate(0, -3, 0)
_, err = e.GetFluctuations(tmStart, tmNow, "USD", testCurrencies, 1200, 2, "")
if err != nil {
t.Error(err)
}
}
func TestGetSupportedSymbols(t *testing.T) {
r, err := e.GetSupportedSymbols()
if err != nil {
t.Fatal(err)
}
if _, ok := r.Symbols["AUD"]; !ok {
t.Error("should contain AUD")
}
}
func TestGetGetSupportedCurrencies(t *testing.T) {
s, err := e.GetSupportedCurrencies()
if err != nil {
t.Fatal(err)
}
if len(s) == 0 {
t.Error("supported currencies should be greater than 0")
}
}
func TestGetRates(t *testing.T) {
r, err := e.GetRates("USD", "")
if err != nil {
t.Fatal(err)
}
if rate := r["USDAUD"]; rate == 0 {
t.Error("rate of USDAUD should be set")
}
}

View File

@@ -1,91 +0,0 @@
package exchangeratehost
import (
"github.com/thrasher-corp/gocryptotrader/currency/forexprovider/base"
"github.com/thrasher-corp/gocryptotrader/exchanges/request"
)
// ExchangeRateHost stores the struct for the exchangerate.host API
type ExchangeRateHost struct {
base.Base
Requester *request.Requester
}
// MessageOfTheDay stores the message of the day
type MessageOfTheDay struct {
Message string `json:"msg"`
DonationURL string `json:"url"`
}
// LatestRates stores the latest forex rates
type LatestRates struct {
MessageOfTheDay MessageOfTheDay `json:"motd"`
Success bool `json:"success"`
Base string `json:"base"`
Date string `json:"date"`
Rates map[string]float64 `json:"rates"`
}
// ConvertCurrency stores currency conversion data
type ConvertCurrency struct {
MessageOfTheDay MessageOfTheDay `json:"motd"`
Query struct {
From string `json:"from"`
To string `json:"to"`
Amount float64 `json:"amount"`
} `json:"query"`
Info struct {
Rate float64 `json:"rate"`
} `json:"info"`
Historical bool `json:"historical"`
Date string `json:"date"`
Result float64 `json:"result"`
}
// HistoricRates stores the hostoric rates
type HistoricRates struct {
LatestRates
Historical bool `json:"historical"`
}
// TimeSeries stores time series data
type TimeSeries struct {
MessageOfTheDay MessageOfTheDay `json:"motd"`
Success bool `json:"success"`
TimeSeries bool `json:"timeseries"`
Base string `json:"base"`
StartDate string `json:"start_date"`
EndDate string `json:"end_date"`
Rates map[string]map[string]float64 `json:"rates"`
}
// Fluctuation stores an individual rate flucutation
type Fluctuation struct {
StartRate float64 `json:"start_rate"`
EndRate float64 `json:"end_rate"`
Change float64 `json:"change"`
ChangePercentage float64 `json:"change_pct"`
}
// Fluctuations stores a collection of rate fluctuations
type Fluctuations struct {
MessageOfTheDay MessageOfTheDay `json:"motd"`
Success bool `json:"success"`
Flucutation bool `json:"fluctuation"`
StartDate string `json:"start_date"`
EndDate string `json:"end_date"`
Rates map[string]Fluctuation `json:"rate"`
}
// Symbol stores an individual symbol
type Symbol struct {
Description string `json:"description"`
Code string `json:"code"`
}
// SupportedSymbols store a collection of supported symbols
type SupportedSymbols struct {
MessageOfTheDay MessageOfTheDay `json:"motd"`
Success bool `json:"success"`
Symbols map[string]Symbol `json:"symbols"`
}

View File

@@ -9,7 +9,6 @@ import (
"github.com/thrasher-corp/gocryptotrader/currency/forexprovider/base"
currencyconverter "github.com/thrasher-corp/gocryptotrader/currency/forexprovider/currencyconverterapi"
"github.com/thrasher-corp/gocryptotrader/currency/forexprovider/currencylayer"
exchangeratehost "github.com/thrasher-corp/gocryptotrader/currency/forexprovider/exchangerate.host"
exchangerates "github.com/thrasher-corp/gocryptotrader/currency/forexprovider/exchangeratesapi.io"
fixer "github.com/thrasher-corp/gocryptotrader/currency/forexprovider/fixer.io"
"github.com/thrasher-corp/gocryptotrader/currency/forexprovider/openexchangerates"
@@ -33,36 +32,9 @@ func GetSupportedForexProviders() []string {
"ExchangeRates",
"Fixer",
"OpenExchangeRates",
"ExchangeRateHost",
}
}
// NewDefaultFXProvider returns the default forex provider (currencyconverterAPI)
func NewDefaultFXProvider() *ForexProviders {
handler := new(ForexProviders)
provider := new(exchangeratehost.ExchangeRateHost)
err := provider.Setup(base.Settings{
PrimaryProvider: true,
Enabled: true,
Name: "ExchangeRateHost",
})
if err != nil {
panic(err)
}
currencies, _ := provider.GetSupportedCurrencies()
providerBase := base.Provider{
Provider: provider,
SupportedCurrencies: currencies,
}
handler.FXHandler = base.FXHandler{
Primary: providerBase,
}
return handler
}
// SetProvider sets provider to the FX handler
func (f *ForexProviders) SetProvider(b base.IFXProvider) error {
currencies, err := b.GetSupportedCurrencies()
@@ -102,8 +74,6 @@ func StartFXService(fxProviders []base.Settings) (*ForexProviders, error) {
provider = new(fixer.Fixer)
case "OpenExchangeRates":
provider = new(openexchangerates.OXR)
case "ExchangeRateHost":
provider = new(exchangeratehost.ExchangeRateHost)
default:
return nil, fmt.Errorf("%s %w", fxProviders[i].Name,
errUnhandledForeignExchangeProvider)

View File

@@ -0,0 +1,40 @@
package currency
import (
"math/rand"
"strings"
"github.com/thrasher-corp/gocryptotrader/currency/forexprovider"
"github.com/thrasher-corp/gocryptotrader/currency/forexprovider/base"
)
type MockProvider struct{}
func newMockProvider() *forexprovider.ForexProviders {
p := &MockProvider{}
c, _ := p.GetSupportedCurrencies()
return &forexprovider.ForexProviders{
FXHandler: base.FXHandler{
Primary: base.Provider{
Provider: p,
SupportedCurrencies: c,
},
},
}
}
func (m *MockProvider) GetName() string { return "MockProvider" }
func (m *MockProvider) Setup(base.Settings) error { return nil }
func (m *MockProvider) IsEnabled() bool { return true }
func (m *MockProvider) IsPrimaryProvider() bool { return true }
func (m *MockProvider) GetSupportedCurrencies() ([]string, error) {
return storage.defaultFiatCurrencies.Strings(), nil
}
func (m *MockProvider) GetRates(baseCurrency, symbols string) (map[string]float64, error) {
c := map[string]float64{}
for _, s := range strings.Split(symbols, ",") {
// The year is 2027; The USD is nearly worthless. The world reserve currency is eggs.
c[baseCurrency+s] = 1 / (1 + rand.Float64()) //nolint:gosec // Doesn't need to be a strong random number
}
return c, nil
}

View File

@@ -26,10 +26,9 @@ func init() {
// CurrencyFileUpdateDelay defines the rate at which the currency.json file is
// updated
const (
DefaultCurrencyFileDelay = 168 * time.Hour
DefaultForeignExchangeDelay = 1 * time.Minute
DefaultStorageFile = "currency.json"
DefaultForexProviderExchangeRatesAPI = "ExchangeRateHost"
DefaultCurrencyFileDelay = 168 * time.Hour
DefaultForeignExchangeDelay = 1 * time.Minute
DefaultStorageFile = "currency.json"
)
var (
@@ -74,7 +73,12 @@ func (s *Storage) SetDefaults() {
log.Errorf(log.Currency, "Currency Storage: Setting default cryptocurrencies error: %s", err)
}
s.SetupConversionRates()
s.fiatExchangeMarkets = forexprovider.NewDefaultFXProvider()
s.fiatExchangeMarkets = nil
}
// ForexEnabled returns whether the currency system has any available forex providers enabled
func ForexEnabled() bool {
return storage.fiatExchangeMarkets != nil
}
// RunUpdater runs the foreign exchange updater service. This will set up a JSON
@@ -103,6 +107,10 @@ func (s *Storage) RunUpdater(overrides BotOverrides, settings *Config, filePath
}
s.mtx.Lock()
// Ensure the forex provider is unset in cases we exit early
s.fiatExchangeMarkets = nil
s.shutdown = make(chan struct{})
s.baseCurrency = settings.FiatDisplayCurrency
s.path = filepath.Join(filePath, DefaultStorageFile)
@@ -132,26 +140,23 @@ func (s *Storage) RunUpdater(overrides BotOverrides, settings *Config, filePath
(settings.ForexProviders[i].Name == "CurrencyLayer" && overrides.CurrencyLayer) ||
(settings.ForexProviders[i].Name == "Fixer" && overrides.Fixer) ||
(settings.ForexProviders[i].Name == "OpenExchangeRates" && overrides.OpenExchangeRates) ||
(settings.ForexProviders[i].Name == "ExchangeRates" && overrides.ExchangeRates) ||
(settings.ForexProviders[i].Name == "ExchangeRateHost" && overrides.ExchangeRateHost)
(settings.ForexProviders[i].Name == "ExchangeRates" && overrides.ExchangeRates)
if !enabled {
continue
}
if settings.ForexProviders[i].Name != DefaultForexProviderExchangeRatesAPI {
if settings.ForexProviders[i].APIKey == "" || settings.ForexProviders[i].APIKey == "Key" {
log.Warnf(log.Currency, "%s forex provider API key not set, disabling. Please set this in your config.json file\n",
settings.ForexProviders[i].Name)
settings.ForexProviders[i].Enabled = false
settings.ForexProviders[i].PrimaryProvider = false
continue
}
if settings.ForexProviders[i].APIKey == "" || settings.ForexProviders[i].APIKey == "Key" {
log.Warnf(log.Currency, "%s forex provider API key not set, disabling. Please set this in your config.json file\n",
settings.ForexProviders[i].Name)
settings.ForexProviders[i].Enabled = false
settings.ForexProviders[i].PrimaryProvider = false
continue
}
if settings.ForexProviders[i].APIKeyLvl == -1 && settings.ForexProviders[i].Name != "ExchangeRates" {
log.Warnf(log.Currency, "%s APIKey level not set, functionality is limited. Please review this in your config.json file\n",
settings.ForexProviders[i].Name)
}
if settings.ForexProviders[i].APIKeyLvl == -1 && settings.ForexProviders[i].Name != "ExchangeRates" {
log.Warnf(log.Currency, "%s APIKey level not set, functionality is limited. Please review this in your config.json file\n",
settings.ForexProviders[i].Name)
}
if settings.ForexProviders[i].PrimaryProvider {
@@ -166,23 +171,10 @@ func (s *Storage) RunUpdater(overrides BotOverrides, settings *Config, filePath
fxSettings = append(fxSettings, base.Settings(settings.ForexProviders[i]))
}
if len(fxSettings) == 0 {
log.Warnln(log.Currency, "No foreign exchange providers enabled, setting default provider...")
for x := range settings.ForexProviders {
if settings.ForexProviders[x].Name != DefaultForexProviderExchangeRatesAPI {
continue
}
settings.ForexProviders[x].Enabled = true
settings.ForexProviders[x].PrimaryProvider = true
primaryProvider = true
log.Warnf(log.Currency, "No valid foreign exchange providers configured. Defaulting to %s.", DefaultForexProviderExchangeRatesAPI)
fxSettings = append(fxSettings, base.Settings(settings.ForexProviders[x]))
}
}
if len(fxSettings) == 0 {
s.mtx.Unlock()
return errNoForeignExchangeProvidersEnabled
log.Warnln(log.Currency, "No foreign exchange providers enabled, currency conversion will not be available")
return nil
}
if !primaryProvider {
@@ -313,8 +305,7 @@ func (s *Storage) ForeignExchangeUpdater() {
log.Errorln(log.Currency, err)
}
// Unlock main rate retrieval mutex so all routines waiting can get access
// to data
// Unlock main rate retrieval mutex so all routines waiting can get access to data
s.mtx.Unlock()
// Set tickers to client defined rates or defaults
@@ -516,6 +507,9 @@ func (s *Storage) UpdateCurrencies() error {
func (s *Storage) SeedForeignExchangeRatesByCurrencies(c Currencies) error {
s.fxRates.mtx.Lock()
defer s.fxRates.mtx.Unlock()
if s.fiatExchangeMarkets == nil {
return nil
}
rates, err := s.fiatExchangeMarkets.GetCurrencyData(s.baseCurrency.String(),
c.Strings())
if err != nil {
@@ -526,6 +520,9 @@ func (s *Storage) SeedForeignExchangeRatesByCurrencies(c Currencies) error {
// SeedForeignExchangeRate returns a singular exchange rate
func (s *Storage) SeedForeignExchangeRate(from, to Code) (map[string]float64, error) {
if s.fiatExchangeMarkets == nil {
return nil, nil
}
return s.fiatExchangeMarkets.GetCurrencyData(from.String(),
[]string{to.String()})
}
@@ -546,6 +543,9 @@ func (s *Storage) GetDefaultForeignExchangeRates() (Conversions, error) {
func (s *Storage) SeedDefaultForeignExchangeRates() error {
s.fxRates.mtx.Lock()
defer s.fxRates.mtx.Unlock()
if s.fiatExchangeMarkets == nil {
return errNoForeignExchangeProvidersEnabled
}
rates, err := s.fiatExchangeMarkets.GetCurrencyData(
s.defaultBaseCurrency.String(),
s.defaultFiatCurrencies.Strings())
@@ -566,11 +566,13 @@ func (s *Storage) GetExchangeRates() (Conversions, error) {
return s.fxRates.GetFullRates(), nil
}
// SeedForeignExchangeRates seeds the foreign exchange rates from storage config
// currencies
// SeedForeignExchangeRates seeds the foreign exchange rates from storage config currencies
func (s *Storage) SeedForeignExchangeRates() error {
s.fxRates.mtx.Lock()
defer s.fxRates.mtx.Unlock()
if s.fiatExchangeMarkets == nil {
return errNoForeignExchangeProvidersEnabled
}
rates, err := s.fiatExchangeMarkets.GetCurrencyData(s.baseCurrency.String(),
s.fiatCurrencies.Strings())
if err != nil {
@@ -695,6 +697,9 @@ func (s *Storage) UpdateEnabledFiatCurrencies(c Currencies) {
// ConvertCurrency for example converts $1 USD to the equivalent Japanese Yen
// or vice versa.
func (s *Storage) ConvertCurrency(amount float64, from, to Code) (float64, error) {
if s.fiatExchangeMarkets == nil {
return 0, errNoForeignExchangeProvidersEnabled
}
if amount <= 0 {
return 0, fmt.Errorf("%f %w", amount, errInvalidAmount)
}

View File

@@ -1,11 +1,11 @@
package currency
import (
"errors"
"fmt"
"os"
"testing"
"github.com/stretchr/testify/assert"
"github.com/thrasher-corp/gocryptotrader/database/testhelpers"
)
@@ -17,6 +17,8 @@ func TestMain(m *testing.M) {
os.Exit(1)
}
storage.fiatExchangeMarkets = newMockProvider()
t := m.Run()
err = os.RemoveAll(testhelpers.TempDir)
@@ -30,219 +32,88 @@ func TestMain(m *testing.M) {
func TestRunUpdater(t *testing.T) {
var newStorage Storage
emptyMainConfig := Config{}
err := newStorage.RunUpdater(BotOverrides{}, &emptyMainConfig, "")
if err == nil {
t.Fatal("storage RunUpdater() error cannot be nil")
}
err := newStorage.RunUpdater(BotOverrides{}, &Config{}, "")
assert.ErrorIs(t, err, errFiatDisplayCurrencyUnset, "No currency should error correctly")
mainConfig := Config{}
err = newStorage.RunUpdater(BotOverrides{}, &mainConfig, "")
if !errors.Is(err, errFiatDisplayCurrencyUnset) {
t.Fatalf("received: '%v' but expected: '%v'", err, errFiatDisplayCurrencyUnset)
}
err = newStorage.RunUpdater(BotOverrides{}, &Config{FiatDisplayCurrency: BTC}, "")
assert.ErrorIs(t, err, ErrFiatDisplayCurrencyIsNotFiat, "Crypto currency should error as not fiat")
mainConfig.FiatDisplayCurrency = BTC
err = newStorage.RunUpdater(BotOverrides{}, &mainConfig, "")
if !errors.Is(err, ErrFiatDisplayCurrencyIsNotFiat) {
t.Fatalf("received: '%v' but expected: '%v'", err, ErrFiatDisplayCurrencyIsNotFiat)
}
mainConfig.FiatDisplayCurrency = AUD
err = newStorage.RunUpdater(BotOverrides{}, &mainConfig, "")
if !errors.Is(err, errNoFilePathSet) {
t.Fatalf("received: '%v' but expected: '%v'", err, errNoFilePathSet)
}
c := &Config{FiatDisplayCurrency: AUD}
err = newStorage.RunUpdater(BotOverrides{}, c, "")
assert.ErrorIs(t, err, errNoFilePathSet, "Should error with no path set")
tempDir := testhelpers.TempDir
err = newStorage.RunUpdater(BotOverrides{}, c, tempDir)
assert.ErrorIs(t, err, errInvalidCurrencyFileUpdateDuration, "Should error invalid file update duration")
err = newStorage.RunUpdater(BotOverrides{}, &mainConfig, tempDir)
if !errors.Is(err, errInvalidCurrencyFileUpdateDuration) {
t.Fatalf("received: '%v' but expected: '%v'", err, errInvalidCurrencyFileUpdateDuration)
}
c.CurrencyFileUpdateDuration = DefaultCurrencyFileDelay
err = newStorage.RunUpdater(BotOverrides{}, c, tempDir)
assert.ErrorIs(t, err, errInvalidForeignExchangeUpdateDuration, "Should error invalid forex update duration")
mainConfig.CurrencyFileUpdateDuration = DefaultCurrencyFileDelay
err = newStorage.RunUpdater(BotOverrides{}, &mainConfig, tempDir)
if !errors.Is(err, errInvalidForeignExchangeUpdateDuration) {
t.Fatalf("received: '%v' but expected: '%v'", err, errInvalidForeignExchangeUpdateDuration)
}
c.ForeignExchangeUpdateDuration = DefaultForeignExchangeDelay
err = newStorage.RunUpdater(BotOverrides{}, c, tempDir)
mainConfig.ForeignExchangeUpdateDuration = DefaultForeignExchangeDelay
err = newStorage.RunUpdater(BotOverrides{}, &mainConfig, tempDir)
if !errors.Is(err, errNoForeignExchangeProvidersEnabled) {
t.Fatalf("received: '%v' but expected: '%v'", err, errNoForeignExchangeProvidersEnabled)
}
settings := FXSettings{
Name: "Fixer",
Enabled: true,
APIKey: "wo",
}
mainConfig.ForexProviders = AllFXSettings{settings}
err = newStorage.RunUpdater(BotOverrides{Fixer: true}, &mainConfig, tempDir)
if errors.Is(err, nil) {
t.Fatalf("received: '%v' but expected: '%v'", err, "an error")
}
assert.NoError(t, err, "Storage should not error with no forex providers enabled")
assert.Nil(t, newStorage.fiatExchangeMarkets, "Forex should not be enabled with no providers") // Proxy for testing ForexEnabled
err = newStorage.Shutdown()
if err != nil {
t.Fatal(err)
assert.NoError(t, err, "Shutdown should not error evne though it silently aborted the RunUpdater early")
// Exchanges which reject a bad APIKey
for _, n := range []string{"Fixer", "CurrencyConverter", "CurrencyLayer", "ExchangeRates"} {
c.ForexProviders = AllFXSettings{{Name: n, Enabled: true, APIKey: ""}}
err = newStorage.RunUpdater(overrideForProvider(n), c, tempDir)
assert.NoErrorf(t, err, "%s should not error and silently exit without running with no api keys", n)
assert.Falsef(t, c.ForexProviders[0].Enabled, "%s should not be marked enabled with no api keys", n)
assert.Nil(t, newStorage.fiatExchangeMarkets, "Forex should not be enabled with no providers")
c.ForexProviders = AllFXSettings{{Name: n, Enabled: true, APIKey: "sudo shazam!"}}
err = newStorage.RunUpdater(overrideForProvider(n), c, tempDir)
assert.Errorf(t, err, "%s should throw some provider originating error with a (hopefully) invalid api key", n)
assert.Truef(t, c.ForexProviders[0].Enabled, "%s should still be enabled after being chosen but failing", n)
assert.Nil(t, newStorage.fiatExchangeMarkets, "Forex should not be enabled when provider errored during startup")
err = newStorage.Shutdown()
assert.NoError(t, err, "Shutdown should not error")
}
settings.Name = "CurrencyConverter"
mainConfig.ForexProviders = AllFXSettings{settings}
err = newStorage.RunUpdater(BotOverrides{CurrencyConverter: true}, &mainConfig, tempDir)
if errors.Is(err, nil) {
t.Fatalf("received: '%v' but expected: '%v'", err, "an error")
// Exchanges which do not error with a bad APIKey on startup
for _, n := range []string{"OpenExchangeRates"} {
c.ForexProviders = AllFXSettings{{Name: n, Enabled: true, APIKey: ""}}
err = newStorage.RunUpdater(overrideForProvider(n), c, tempDir)
assert.NoErrorf(t, err, "%s should not error and silently exit without running with no api keys", n)
assert.Nil(t, newStorage.fiatExchangeMarkets, "Forex should not be enabled with no providers")
c.ForexProviders = AllFXSettings{{Name: n, Enabled: true, APIKey: "sudo shazam!"}}
err = newStorage.RunUpdater(overrideForProvider(n), c, tempDir)
assert.NoError(t, err, "%s should not error on Setup with a bad apikey", n)
assert.NotNil(t, newStorage.fiatExchangeMarkets, "Forex should be enabled now we have a provider with a key")
err = newStorage.Shutdown()
assert.NoError(t, err, "Shutdown should not error")
}
err = newStorage.Shutdown()
if err != nil {
t.Fatal(err)
c.ForexProviders = AllFXSettings{
{Name: "ExchangeRates"}, // Old Default
{Name: "OpenExchangeRates", APIKey: "shazam?"},
}
settings.Name = "CurrencyLayer"
mainConfig.ForexProviders = AllFXSettings{settings}
err = newStorage.RunUpdater(BotOverrides{CurrencyLayer: true}, &mainConfig, tempDir)
if errors.Is(err, nil) {
t.Fatalf("received: '%v' but expected: '%v'", err, "an error")
}
err = newStorage.Shutdown()
if err != nil {
t.Fatal(err)
}
settings.Name = "OpenExchangeRates"
mainConfig.ForexProviders = AllFXSettings{settings}
err = newStorage.RunUpdater(BotOverrides{OpenExchangeRates: true}, &mainConfig, tempDir)
if !errors.Is(err, nil) {
t.Fatalf("received: '%v' but expected: '%v'", err, nil)
}
err = newStorage.Shutdown()
if err != nil {
t.Fatal(err)
}
settings.Name = "ExchangeRates"
mainConfig.ForexProviders = AllFXSettings{settings}
err = newStorage.RunUpdater(BotOverrides{ExchangeRates: true}, &mainConfig, tempDir)
if errors.Is(err, nil) {
t.Fatalf("received: '%v' but expected: '%v'", err, "an error")
}
err = newStorage.Shutdown()
if err != nil {
t.Fatal(err)
}
settings.Name = "ExchangeRateHost"
mainConfig.ForexProviders = AllFXSettings{settings}
err = newStorage.RunUpdater(BotOverrides{ExchangeRateHost: true}, &mainConfig, tempDir)
if !errors.Is(err, nil) {
t.Fatalf("received: '%v' but expected: '%v'", err, nil)
}
err = newStorage.Shutdown()
if err != nil {
t.Fatal(err)
}
// old config where two providers enabled
oldDefault := FXSettings{
Name: "ExchangeRates",
Enabled: true,
APIKey: "", // old default provider which did not need api keys.
PrimaryProvider: true,
}
other := FXSettings{
Name: "OpenExchangeRates",
Enabled: true,
APIKey: "enabled-keys", // Has keys enabled and will fall over to primary
}
defaultProvider := FXSettings{
// From config this will be included but not necessarily enabled.
Name: "ExchangeRateHost",
Enabled: false,
}
mainConfig.ForexProviders = AllFXSettings{oldDefault, other, defaultProvider}
err = newStorage.RunUpdater(BotOverrides{ExchangeRates: true, OpenExchangeRates: true}, &mainConfig, tempDir)
if !errors.Is(err, nil) {
t.Fatalf("received: '%v' but expected: '%v'", err, nil)
}
if mainConfig.ForexProviders[0].Enabled {
t.Fatal("old default provider should not be enabled due to unset keys")
}
if mainConfig.ForexProviders[0].PrimaryProvider {
t.Fatal("old default provider should not be a primary provider anymore")
}
if !mainConfig.ForexProviders[1].Enabled {
t.Fatal("open exchange rates provider with keys set should be enabled")
}
if !mainConfig.ForexProviders[1].PrimaryProvider {
t.Fatal("open exchange rates provider with keys set should be set as primary provider")
}
if mainConfig.ForexProviders[2].Enabled {
t.Fatal("actual default provider should not be enabled")
}
if mainConfig.ForexProviders[2].PrimaryProvider {
t.Fatal("actual default provider should not be designated as primary provider")
}
err = newStorage.Shutdown()
if err != nil {
t.Fatal(err)
}
// old config where two providers enabled
oldDefault = FXSettings{
Name: "ExchangeRates",
Enabled: true,
APIKey: "", // old default provider which did not need api keys.
PrimaryProvider: true,
}
other = FXSettings{
Name: "OpenExchangeRates",
Enabled: true,
APIKey: "", // Has no keys enabled will fall over to new default provider.
}
mainConfig.ForexProviders = AllFXSettings{oldDefault, other, defaultProvider}
err = newStorage.RunUpdater(BotOverrides{ExchangeRates: true, OpenExchangeRates: true}, &mainConfig, tempDir)
if !errors.Is(err, nil) {
t.Fatalf("received: '%v' but expected: '%v'", err, nil)
}
if mainConfig.ForexProviders[0].Enabled {
t.Fatal("old default provider should not be enabled due to unset keys")
}
if mainConfig.ForexProviders[0].PrimaryProvider {
t.Fatal("old default provider should not be a primary provider anymore")
}
if mainConfig.ForexProviders[1].Enabled {
t.Fatal("open exchange rates provider with keys unset should not be enabled")
}
if mainConfig.ForexProviders[1].PrimaryProvider {
t.Fatal("open exchange rates provider with keys unset should not be set as primary provider")
}
if !mainConfig.ForexProviders[2].Enabled {
t.Fatal("actual default provider should not be disabled")
}
if !mainConfig.ForexProviders[2].PrimaryProvider {
t.Fatal("actual default provider should be designated as primary provider")
}
// Regression test for old defaults which were enabled when in settings and nothing else was enabled and configured
err = newStorage.RunUpdater(BotOverrides{}, c, tempDir)
assert.NoError(t, err, "RunUpdater should not error")
assert.Nil(t, newStorage.fiatExchangeMarkets, "Forex should not be enabled with no providers") // Proxy for testing ForexEnabled
assert.False(t, c.ForexProviders[0].Enabled, "Old Default ExchangeRates should not have defaulted to enabled with no enabled overrides")
}
func overrideForProvider(n string) BotOverrides {
b := BotOverrides{}
switch n {
case "Fixer":
b.Fixer = true
case "CurrencyConverter":
b.CurrencyConverter = true
case "CurrencyLayer":
b.CurrencyLayer = true
case "OpenExchangeRates":
b.OpenExchangeRates = true
case "ExchangeRates":
b.ExchangeRates = true
}
return b
}

View File

@@ -173,7 +173,6 @@ func validateSettings(b *Engine, s *Settings, flagSet FlagSet) {
flagSet.WithBool("exchangerates", &b.Settings.EnableExchangeRates, b.Config.Currency.ForexProviders.IsEnabled("exchangerates"))
flagSet.WithBool("fixer", &b.Settings.EnableFixer, b.Config.Currency.ForexProviders.IsEnabled("fixer"))
flagSet.WithBool("openexchangerates", &b.Settings.EnableOpenExchangeRates, b.Config.Currency.ForexProviders.IsEnabled("openexchangerates"))
flagSet.WithBool("exchangeratehost", &b.Settings.EnableExchangeRateHost, b.Config.Currency.ForexProviders.IsEnabled("exchangeratehost"))
flagSet.WithBool("datahistorymanager", &b.Settings.EnableDataHistoryManager, b.Config.DataHistoryManager.Enabled)
flagSet.WithBool("currencystatemanager", &b.Settings.EnableCurrencyStateManager, b.Config.CurrencyStateManager.Enabled != nil && *b.Config.CurrencyStateManager.Enabled)
@@ -403,12 +402,11 @@ func (bot *Engine) Start() error {
ExchangeRates: bot.Settings.EnableExchangeRates,
Fixer: bot.Settings.EnableFixer,
OpenExchangeRates: bot.Settings.EnableOpenExchangeRates,
ExchangeRateHost: bot.Settings.EnableExchangeRateHost,
},
&bot.Config.Currency,
bot.Settings.DataDir,
); err != nil {
gctlog.Errorf(gctlog.Global, "ExchangeSettings updater system failed to start %s", err)
gctlog.Errorf(gctlog.Global, "Currency Converter system failed to start %s", err)
}
if bot.Settings.EnableGRPC {
@@ -677,7 +675,7 @@ func (bot *Engine) Stop() {
err = currency.ShutdownStorageUpdater()
if err != nil {
gctlog.Errorf(gctlog.Global, "ExchangeSettings storage system. Error: %v", err)
gctlog.Errorf(gctlog.Global, "Currency Converter unable to stop. Error: %v", err)
}
if !bot.Settings.EnableDryRun {

View File

@@ -80,7 +80,6 @@ type ForexSettings struct {
EnableExchangeRates bool
EnableFixer bool
EnableOpenExchangeRates bool
EnableExchangeRateHost bool
}
// ExchangeTuningSettings defines settings related to an exchange

View File

@@ -697,13 +697,15 @@ func printCurrencyFormat(price float64, displayCurrency currency.Code) string {
}
func printConvertCurrencyFormat(origPrice float64, origCurrency, displayCurrency currency.Code) string {
var conv float64
var conv string
if origPrice > 0 {
var err error
conv, err = currency.ConvertFiat(origPrice, origCurrency, displayCurrency)
if err != nil {
log.Errorf(log.SyncMgr, "Failed to convert currency: %s", err)
if convFloat, err := currency.ConvertFiat(origPrice, origCurrency, displayCurrency); err != nil {
conv = "?.??"
} else {
conv = fmt.Sprintf("%.2f", convFloat)
}
} else {
conv = "0.00"
}
displaySymbol, err := currency.GetSymbolByCurrencyName(displayCurrency)
@@ -718,7 +720,7 @@ func printConvertCurrencyFormat(origPrice float64, origCurrency, displayCurrency
err)
}
return fmt.Sprintf("%s%.2f %s (%s%.2f %s)",
return fmt.Sprintf("%s%s %s (%s%.2f %s)",
displaySymbol,
conv,
displayCurrency,
@@ -755,7 +757,8 @@ func (m *SyncManager) PrintTickerSummary(result *ticker.Price, protocol string,
return
}
if result.Pair.Quote.IsFiatCurrency() &&
if currency.ForexEnabled() &&
result.Pair.Quote.IsFiatCurrency() &&
!result.Pair.Quote.Equal(m.fiatDisplayCurrency) &&
!m.fiatDisplayCurrency.IsEmpty() {
origCurrency := result.Pair.Quote.Upper()
@@ -854,7 +857,7 @@ func (m *SyncManager) PrintOrderbookSummary(result *orderbook.Base, protocol str
var bidValueResult, askValueResult string
switch {
case result.Pair.Quote.IsFiatCurrency() && !result.Pair.Quote.Equal(m.fiatDisplayCurrency) && !m.fiatDisplayCurrency.IsEmpty():
case currency.ForexEnabled() && result.Pair.Quote.IsFiatCurrency() && !result.Pair.Quote.Equal(m.fiatDisplayCurrency) && !m.fiatDisplayCurrency.IsEmpty():
origCurrency := result.Pair.Quote.Upper()
if bidsValue > 0 {
bidValueResult = printConvertCurrencyFormat(bidsValue, origCurrency, m.fiatDisplayCurrency)

View File

@@ -79,7 +79,6 @@ func main() {
flag.BoolVar(&settings.EnableExchangeRates, "exchangerates", false, "overrides config and sets up foreign exchange exchangeratesapi.io")
flag.BoolVar(&settings.EnableFixer, "fixer", false, "overrides config and sets up foreign exchange Fixer.io")
flag.BoolVar(&settings.EnableOpenExchangeRates, "openexchangerates", false, "overrides config and sets up foreign exchange Open Exchange Rates")
flag.BoolVar(&settings.EnableExchangeRateHost, "exchangeratehost", false, "overrides config and sets up foreign exchange ExchangeRate.host")
// Exchange tuning settings
flag.BoolVar(&settings.EnableExchangeAutoPairUpdates, "exchangeautopairupdates", false, "enables automatic available currency pair updates for supported exchanges")