Repository: freqtrade/freqtrade
Pull request: Remove hardcoded exchange parameters
## Summary
We should not hardcode timeframes nor stake-currencies. THey vary by exchange, so we should use what the exchange gives us.

Closes #2757
closes #1861 

## Quick changelog

- Use timeframes from the exchange
- Use quote-currencies from the exchange
- Limit freqtrade to work on timeframes >1m (lower than that is very risky and does currently produce errors since we assume minute in certain places (for example when resampling). While these could be easily fixed, i don't think trading <1m with freqtrade is something that can be profitable.
- Remove some occurences of empty `MagicMock()` in `mocker.patch()` calls (it defaults to MagicMock() anyway).



Problem statement:
Support USDC as a stake
I would like to use USDC as a stake trading with binance. I am currently using USDT but due to controversy that surround it would be nice if we can use another stable coin as a stake. It wont be a pleasure to wake up one morning and see that USDT have crashed -50% or something like that...


Issues hardcoded timeframes 15s timeframe - FTX
## Step 1: Have you search for this issue before posting it?

yes

## Step 2: Describe your environment

  * Operating system: ubuntu
  * Python Version: python3
  * CCXT version: latest
  * Branch:  Develop
  * Last Commit ID: latest
  
## Step 3: Describe the problem:

*when using `freqtrade list-timeframes --exchange ftx` it will return "Timeframes available for the exchange `FTX`: 15s, 1m, 5m, 15m, 1h, 4h, 24h
"
then when trying to to download 15s then the error comes:
"freqtrade download-data: error: argument -t/--timeframes: invalid choice: '15s' (choose from '1m', '3m', '5m', '15m', '30m', '1h', '2h', '4h', '6h', '8h', '12h', '1d', '3d', '1w')"

### Steps to reproduce:

  1. list available timeframes for ftx
  2. get 15s timeframes
  3. cant download 15s timeframe when trying to use `freqtrade download-data -c ftx.json --days 360 --timeframes 15s`
  
### Observed Results:

*it was explained above*

### Relevant code exceptions or logs:

  ```
freqtrade download-data: error: argument -t/--timeframes: invalid choice: '15s' (choose from '1m', '3m', '5m', '15m', '30m', '1h', '2h', '4h', '6h', '8h', '12h', '1d', '3d', '1w')  ```

Hints from issue discussion:
@xmatthias could the static list of stake currencies be changed to dynamic exchange specific one?
i.e., we could implement get_quote_symbols() (and possibly get_symbols(), get_base_symbols() for completness) methods in the Exchange class and use the returned list for checking the stake_currency to be in it instead of the static hardcoded list.

Do you see any pitfalls for it? If not, I could implement the approach and issue the PR...

Any currency on the "2nd part" of a pair-combination can be used as stake currency (BTC/USD, BTC/USDX, HOT/XRP, ...). 
The only thing that may not work is `/balance`, which tends to fail for currencies where there is no correspoinding `pair/BTC` combination.
 
Apart from the `/balance` problem, there is nothing to stop you from using USDC.
You could even use XRP - which is also now a valid stake on Binace (for a limited amount of pairs ...).


^^ @xmatthias was that comment addressed to me or to the author of the issue?

He cannot "even use XRP" now since stake_currency is checked against hardcoded list set in the config json schema.
hm, didn't think about `constants.py` - but after that nothing is blocking the use of other stake currencies, as long as the pairs exist on the exchange.
okay, assigning the issue to myself if you don't mind...
thanks for input
as an idea - i think we can remove this list completely, and replace it with a function 
 to find valid stake-currencies to the exchange class and parse the markets dict ... 
Amost identical to #1861 - but with another exchange-provided field.
While i agree that the timeframes should not be hardcoded, i think running on <1m tickers does not make much sense - and will result in almost exclusive losses.

If you think about it - assume you use only one pair - which takes ~1s to download history, and ~2s to calculate the indicators for - so now you're 3s into the candle (assuming process_trottle is set to 0 - so the bot continuously runs).

Creating the trade will require another 2 calls (one to get the price, and one to create the order). Unless you're on a VERY fast network, you'll buy 4-5s into the candle - which for a 15s candle is >30%.


On a side note: by removing the 15s limitation on startup, there are a ton of other points where this could fail, since all of freqtrade assumes a minimum of 1m ticker interval.

So while removing the hardcoded timeframe list - i will add a limitation for <1m timeframes (mainly to prevent people from losing unnecessarily).

Commit message: Add test for failing stake_validation

Patch to review:
diff --git a/freqtrade/configuration/config_validation.py b/freqtrade/configuration/config_validation.py
index 54941043d20..5183ad0b4ef 100644
--- a/freqtrade/configuration/config_validation.py
+++ b/freqtrade/configuration/config_validation.py
@@ -48,11 +48,6 @@ def validate_config_schema(conf: Dict[str, Any]) -> Dict[str, Any]:
         conf_schema['required'] = constants.SCHEMA_TRADE_REQUIRED
     else:
         conf_schema['required'] = constants.SCHEMA_MINIMAL_REQUIRED
-        # Dynamically allow empty stake-currency
-        # Since the minimal config specifies this too.
-        # It's not allowed for Dry-run or live modes
-        conf_schema['properties']['stake_currency']['enum'] += ['']  # type: ignore
-
     try:
         FreqtradeValidator(conf_schema).validate(conf)
         return conf
diff --git a/freqtrade/constants.py b/freqtrade/constants.py
index e9c103db917..53bc4af5342 100644
--- a/freqtrade/constants.py
+++ b/freqtrade/constants.py
@@ -33,12 +33,6 @@
     'strategy_analysis_example.ipynb': 'notebooks',
 }
 
-TIMEFRAMES = [
-    '1m', '3m', '5m', '15m', '30m',
-    '1h', '2h', '4h', '6h', '8h', '12h',
-    '1d', '3d', '1w',
-]
-
 SUPPORTED_FIAT = [
     "AUD", "BRL", "CAD", "CHF", "CLP", "CNY", "CZK", "DKK",
     "EUR", "GBP", "HKD", "HUF", "IDR", "ILS", "INR", "JPY",
@@ -66,8 +60,8 @@
     'type': 'object',
     'properties': {
         'max_open_trades': {'type': ['integer', 'number'], 'minimum': -1},
-        'ticker_interval': {'type': 'string', 'enum': TIMEFRAMES},
-        'stake_currency': {'type': 'string', 'enum': ['BTC', 'XBT', 'ETH', 'USDT', 'EUR', 'USD']},
+        'ticker_interval': {'type': 'string'},
+        'stake_currency': {'type': 'string'},
         'stake_amount': {
             'type': ['number', 'string'],
             'minimum': 0.0001,
diff --git a/freqtrade/exchange/exchange.py b/freqtrade/exchange/exchange.py
index 3ef32db62ae..3735dac8a98 100644
--- a/freqtrade/exchange/exchange.py
+++ b/freqtrade/exchange/exchange.py
@@ -116,6 +116,7 @@ def __init__(self, config: dict, validate: bool = True) -> None:
             self._load_markets()
 
             # Check if all pairs are available
+            self.validate_stakecurrency(config['stake_currency'])
             self.validate_pairs(config['exchange']['pair_whitelist'])
             self.validate_ordertypes(config.get('order_types', {}))
             self.validate_order_time_in_force(config.get('order_time_in_force', {}))
@@ -210,6 +211,15 @@ def get_markets(self, base_currencies: List[str] = None, quote_currencies: List[
             markets = {k: v for k, v in markets.items() if market_is_active(v)}
         return markets
 
+    def get_quote_currencies(self) -> List[str]:
+        """
+        Return a list of supported quote currencies
+        """
+        markets = self.markets
+        currencies = list(set([x.get('quote') for _, x in markets.items()]))
+        currencies.sort()
+        return currencies
+
     def klines(self, pair_interval: Tuple[str, str], copy=True) -> DataFrame:
         if pair_interval in self._klines:
             return self._klines[pair_interval].copy() if copy else self._klines[pair_interval]
@@ -259,11 +269,23 @@ def _reload_markets(self) -> None:
         except ccxt.BaseError:
             logger.exception("Could not reload markets.")
 
+    def validate_stakecurrency(self, stake_currency) -> None:
+        """
+        Checks stake-currency against available currencies on the exchange.
+        :param stake_currency: Stake-currency to validate
+        :raise: OperationalException if stake-currency is not available.
+        """
+        quote_currencies = self.get_quote_currencies()
+        if stake_currency not in quote_currencies:
+            raise OperationalException(
+                    f"{stake_currency} is not available as stake on {self.name}. "
+                    f"Available currencies are: {', '.join(quote_currencies)}")
+
     def validate_pairs(self, pairs: List[str]) -> None:
         """
         Checks if all given pairs are tradable on the current exchange.
-        Raises OperationalException if one pair is not available.
         :param pairs: list of pairs
+        :raise: OperationalException if one pair is not available
         :return: None
         """
 
@@ -319,6 +341,10 @@ def validate_timeframes(self, timeframe: Optional[str]) -> None:
             raise OperationalException(
                 f"Invalid ticker interval '{timeframe}'. This exchange supports: {self.timeframes}")
 
+        if timeframe and timeframe_to_minutes(timeframe) < 1:
+            raise OperationalException(
+                f"Timeframes < 1m are currently not supported by Freqtrade.")
+
     def validate_ordertypes(self, order_types: Dict) -> None:
         """
         Checks if order-types configured in strategy/config are supported
diff --git a/tests/conftest.py b/tests/conftest.py
index 7bb4cf4c9a9..ac1b102841d 100644
--- a/tests/conftest.py
+++ b/tests/conftest.py
@@ -60,6 +60,7 @@ def patch_exchange(mocker, api_mock=None, id='bittrex', mock_markets=True) -> No
     mocker.patch('freqtrade.exchange.Exchange.validate_pairs', MagicMock())
     mocker.patch('freqtrade.exchange.Exchange.validate_timeframes', MagicMock())
     mocker.patch('freqtrade.exchange.Exchange.validate_ordertypes', MagicMock())
+    mocker.patch('freqtrade.exchange.Exchange.validate_stakecurrency', MagicMock())
     mocker.patch('freqtrade.exchange.Exchange.id', PropertyMock(return_value=id))
     mocker.patch('freqtrade.exchange.Exchange.name', PropertyMock(return_value=id.title()))
     if mock_markets:
diff --git a/tests/exchange/test_exchange.py b/tests/exchange/test_exchange.py
index cb40bdbd998..150ba3ea7c8 100644
--- a/tests/exchange/test_exchange.py
+++ b/tests/exchange/test_exchange.py
@@ -73,6 +73,7 @@ def test_init(default_conf, mocker, caplog):
 
 def test_init_ccxt_kwargs(default_conf, mocker, caplog):
     mocker.patch('freqtrade.exchange.Exchange._load_markets', MagicMock(return_value={}))
+    mocker.patch('freqtrade.exchange.Exchange.validate_stakecurrency')
     caplog.set_level(logging.INFO)
     conf = copy.deepcopy(default_conf)
     conf['exchange']['ccxt_async_config'] = {'aiohttp_trust_env': True}
@@ -121,9 +122,10 @@ def test_init_exception(default_conf, mocker):
 
 def test_exchange_resolver(default_conf, mocker, caplog):
     mocker.patch('freqtrade.exchange.Exchange._init_ccxt', MagicMock(return_value=MagicMock()))
-    mocker.patch('freqtrade.exchange.Exchange._load_async_markets', MagicMock())
-    mocker.patch('freqtrade.exchange.Exchange.validate_pairs', MagicMock())
-    mocker.patch('freqtrade.exchange.Exchange.validate_timeframes', MagicMock())
+    mocker.patch('freqtrade.exchange.Exchange._load_async_markets')
+    mocker.patch('freqtrade.exchange.Exchange.validate_pairs')
+    mocker.patch('freqtrade.exchange.Exchange.validate_timeframes')
+    mocker.patch('freqtrade.exchange.Exchange.validate_stakecurrency')
     exchange = ExchangeResolver.load_exchange('Bittrex', default_conf)
     assert isinstance(exchange, Exchange)
     assert log_has_re(r"No .* specific subclass found. Using the generic class instead.", caplog)
@@ -257,9 +259,10 @@ def test__load_markets(default_conf, mocker, caplog):
     api_mock = MagicMock()
     api_mock.load_markets = MagicMock(side_effect=ccxt.BaseError("SomeError"))
     mocker.patch('freqtrade.exchange.Exchange._init_ccxt', MagicMock(return_value=api_mock))
-    mocker.patch('freqtrade.exchange.Exchange.validate_pairs', MagicMock())
-    mocker.patch('freqtrade.exchange.Exchange.validate_timeframes', MagicMock())
-    mocker.patch('freqtrade.exchange.Exchange._load_async_markets', MagicMock())
+    mocker.patch('freqtrade.exchange.Exchange.validate_pairs')
+    mocker.patch('freqtrade.exchange.Exchange.validate_timeframes')
+    mocker.patch('freqtrade.exchange.Exchange._load_async_markets')
+    mocker.patch('freqtrade.exchange.Exchange.validate_stakecurrency')
     Exchange(default_conf)
     assert log_has('Unable to initialize markets. Reason: SomeError', caplog)
 
@@ -315,6 +318,44 @@ def test__reload_markets_exception(default_conf, mocker, caplog):
     assert log_has_re(r"Could not reload markets.*", caplog)
 
 
+@pytest.mark.parametrize("stake_currency", ['ETH', 'BTC', 'USDT'])
+def test_validate_stake_currency(default_conf, stake_currency, mocker, caplog):
+    default_conf['stake_currency'] = stake_currency
+    api_mock = MagicMock()
+    type(api_mock).markets = PropertyMock(return_value={
+        'ETH/BTC': {'quote': 'BTC'}, 'LTC/BTC': {'quote': 'BTC'},
+        'XRP/ETH': {'quote': 'ETH'}, 'NEO/USDT': {'quote': 'USDT'},
+    })
+    mocker.patch('freqtrade.exchange.Exchange._init_ccxt', MagicMock(return_value=api_mock))
+    mocker.patch('freqtrade.exchange.Exchange.validate_pairs')
+    mocker.patch('freqtrade.exchange.Exchange.validate_timeframes')
+    mocker.patch('freqtrade.exchange.Exchange._load_async_markets')
+    Exchange(default_conf)
+
+
+def test_validate_stake_currency_error(default_conf, mocker, caplog):
+    default_conf['stake_currency'] = 'XRP'
+    api_mock = MagicMock()
+    type(api_mock).markets = PropertyMock(return_value={
+        'ETH/BTC': {'quote': 'BTC'}, 'LTC/BTC': {'quote': 'BTC'},
+        'XRP/ETH': {'quote': 'ETH'}, 'NEO/USDT': {'quote': 'USDT'},
+    })
+    mocker.patch('freqtrade.exchange.Exchange._init_ccxt', MagicMock(return_value=api_mock))
+    mocker.patch('freqtrade.exchange.Exchange.validate_pairs')
+    mocker.patch('freqtrade.exchange.Exchange.validate_timeframes')
+    mocker.patch('freqtrade.exchange.Exchange._load_async_markets')
+    with pytest.raises(OperationalException,
+                       match=r'XRP is not available as stake on .*'
+                       'Available currencies are: BTC, ETH, USDT'):
+        Exchange(default_conf)
+
+
+def test_get_quote_currencies(default_conf, mocker):
+    ex = get_patched_exchange(mocker, default_conf)
+
+    assert set(ex.get_quote_currencies()) == set(['USD', 'BTC', 'USDT'])
+
+
 def test_validate_pairs(default_conf, mocker):  # test exchange.validate_pairs directly
     api_mock = MagicMock()
     type(api_mock).markets = PropertyMock(return_value={
@@ -324,8 +365,9 @@ def test_validate_pairs(default_conf, mocker):  # test exchange.validate_pairs d
     type(api_mock).id = id_mock
 
     mocker.patch('freqtrade.exchange.Exchange._init_ccxt', MagicMock(return_value=api_mock))
-    mocker.patch('freqtrade.exchange.Exchange.validate_timeframes', MagicMock())
-    mocker.patch('freqtrade.exchange.Exchange._load_async_markets', MagicMock())
+    mocker.patch('freqtrade.exchange.Exchange.validate_timeframes')
+    mocker.patch('freqtrade.exchange.Exchange._load_async_markets')
+    mocker.patch('freqtrade.exchange.Exchange.validate_stakecurrency')
     Exchange(default_conf)
 
 
@@ -335,8 +377,9 @@ def test_validate_pairs_not_available(default_conf, mocker):
         'XRP/BTC': {'inactive': True}
     })
     mocker.patch('freqtrade.exchange.Exchange._init_ccxt', MagicMock(return_value=api_mock))
-    mocker.patch('freqtrade.exchange.Exchange.validate_timeframes', MagicMock())
-    mocker.patch('freqtrade.exchange.Exchange._load_async_markets', MagicMock())
+    mocker.patch('freqtrade.exchange.Exchange.validate_timeframes')
+    mocker.patch('freqtrade.exchange.Exchange.validate_stakecurrency')
+    mocker.patch('freqtrade.exchange.Exchange._load_async_markets')
 
     with pytest.raises(OperationalException, match=r'not available'):
         Exchange(default_conf)
@@ -349,8 +392,9 @@ def test_validate_pairs_exception(default_conf, mocker, caplog):
 
     type(api_mock).markets = PropertyMock(return_value={})
     mocker.patch('freqtrade.exchange.Exchange._init_ccxt', api_mock)
-    mocker.patch('freqtrade.exchange.Exchange.validate_timeframes', MagicMock())
-    mocker.patch('freqtrade.exchange.Exchange._load_async_markets', MagicMock())
+    mocker.patch('freqtrade.exchange.Exchange.validate_timeframes')
+    mocker.patch('freqtrade.exchange.Exchange.validate_stakecurrency')
+    mocker.patch('freqtrade.exchange.Exchange._load_async_markets')
 
     with pytest.raises(OperationalException, match=r'Pair ETH/BTC is not available on Binance'):
         Exchange(default_conf)
@@ -368,8 +412,9 @@ def test_validate_pairs_restricted(default_conf, mocker, caplog):
         'NEO/BTC': {'info': 'TestString'},  # info can also be a string ...
     })
     mocker.patch('freqtrade.exchange.Exchange._init_ccxt', MagicMock(return_value=api_mock))
-    mocker.patch('freqtrade.exchange.Exchange.validate_timeframes', MagicMock())
-    mocker.patch('freqtrade.exchange.Exchange._load_async_markets', MagicMock())
+    mocker.patch('freqtrade.exchange.Exchange.validate_timeframes')
+    mocker.patch('freqtrade.exchange.Exchange._load_async_markets')
+    mocker.patch('freqtrade.exchange.Exchange.validate_stakecurrency')
 
     Exchange(default_conf)
     assert log_has(f"Pair XRP/BTC is restricted for some users on this exchange."
@@ -377,8 +422,11 @@ def test_validate_pairs_restricted(default_conf, mocker, caplog):
                    f"on the exchange and eventually remove XRP/BTC from your whitelist.", caplog)
 
 
-def test_validate_timeframes(default_conf, mocker):
-    default_conf["ticker_interval"] = "5m"
+@pytest.mark.parametrize("timeframe", [
+    ('5m'), ("1m"), ("15m"), ("1h")
+])
+def test_validate_timeframes(default_conf, mocker, timeframe):
+    default_conf["ticker_interval"] = timeframe
     api_mock = MagicMock()
     id_mock = PropertyMock(return_value='test_exchange')
     type(api_mock).id = id_mock
@@ -390,7 +438,8 @@ def test_validate_timeframes(default_conf, mocker):
 
     mocker.patch('freqtrade.exchange.Exchange._init_ccxt', MagicMock(return_value=api_mock))
     mocker.patch('freqtrade.exchange.Exchange._load_markets', MagicMock(return_value={}))
-    mocker.patch('freqtrade.exchange.Exchange.validate_pairs', MagicMock())
+    mocker.patch('freqtrade.exchange.Exchange.validate_pairs')
+    mocker.patch('freqtrade.exchange.Exchange.validate_stakecurrency')
     Exchange(default_conf)
 
 
@@ -399,7 +448,8 @@ def test_validate_timeframes_failed(default_conf, mocker):
     api_mock = MagicMock()
     id_mock = PropertyMock(return_value='test_exchange')
     type(api_mock).id = id_mock
-    timeframes = PropertyMock(return_value={'1m': '1m',
+    timeframes = PropertyMock(return_value={'15s': '15s',
+                                            '1m': '1m',
                                             '5m': '5m',
                                             '15m': '15m',
                                             '1h': '1h'})
@@ -411,6 +461,11 @@ def test_validate_timeframes_failed(default_conf, mocker):
     with pytest.raises(OperationalException,
                        match=r"Invalid ticker interval '3m'. This exchange supports.*"):
         Exchange(default_conf)
+    default_conf["ticker_interval"] = "15s"
+
+    with pytest.raises(OperationalException,
+                       match=r"Timeframes < 1m are currently not supported by Freqtrade."):
+        Exchange(default_conf)
 
 
 def test_validate_timeframes_emulated_ohlcv_1(default_conf, mocker):
@@ -424,7 +479,8 @@ def test_validate_timeframes_emulated_ohlcv_1(default_conf, mocker):
 
     mocker.patch('freqtrade.exchange.Exchange._init_ccxt', MagicMock(return_value=api_mock))
     mocker.patch('freqtrade.exchange.Exchange._load_markets', MagicMock(return_value={}))
-    mocker.patch('freqtrade.exchange.Exchange.validate_pairs', MagicMock())
+    mocker.patch('freqtrade.exchange.Exchange.validate_pairs')
+    mocker.patch('freqtrade.exchange.Exchange.validate_stakecurrency')
     with pytest.raises(OperationalException,
                        match=r'The ccxt library does not provide the list of timeframes '
                              r'for the exchange ".*" and this exchange '
@@ -445,6 +501,7 @@ def test_validate_timeframes_emulated_ohlcvi_2(default_conf, mocker):
     mocker.patch('freqtrade.exchange.Exchange._load_markets',
                  MagicMock(return_value={'timeframes': None}))
     mocker.patch('freqtrade.exchange.Exchange.validate_pairs', MagicMock())
+    mocker.patch('freqtrade.exchange.Exchange.validate_stakecurrency')
     with pytest.raises(OperationalException,
                        match=r'The ccxt library does not provide the list of timeframes '
                              r'for the exchange ".*" and this exchange '
@@ -465,7 +522,8 @@ def test_validate_timeframes_not_in_config(default_conf, mocker):
 
     mocker.patch('freqtrade.exchange.Exchange._init_ccxt', MagicMock(return_value=api_mock))
     mocker.patch('freqtrade.exchange.Exchange._load_markets', MagicMock(return_value={}))
-    mocker.patch('freqtrade.exchange.Exchange.validate_pairs', MagicMock())
+    mocker.patch('freqtrade.exchange.Exchange.validate_pairs')
+    mocker.patch('freqtrade.exchange.Exchange.validate_stakecurrency')
     Exchange(default_conf)
 
 
@@ -475,8 +533,9 @@ def test_validate_order_types(default_conf, mocker):
     type(api_mock).has = PropertyMock(return_value={'createMarketOrder': True})
     mocker.patch('freqtrade.exchange.Exchange._init_ccxt', MagicMock(return_value=api_mock))
     mocker.patch('freqtrade.exchange.Exchange._load_markets', MagicMock(return_value={}))
-    mocker.patch('freqtrade.exchange.Exchange.validate_pairs', MagicMock())
-    mocker.patch('freqtrade.exchange.Exchange.validate_timeframes', MagicMock())
+    mocker.patch('freqtrade.exchange.Exchange.validate_pairs')
+    mocker.patch('freqtrade.exchange.Exchange.validate_timeframes')
+    mocker.patch('freqtrade.exchange.Exchange.validate_stakecurrency')
     mocker.patch('freqtrade.exchange.Exchange.name', 'Bittrex')
     default_conf['order_types'] = {
         'buy': 'limit',
@@ -517,8 +576,9 @@ def test_validate_order_types_not_in_config(default_conf, mocker):
     api_mock = MagicMock()
     mocker.patch('freqtrade.exchange.Exchange._init_ccxt', MagicMock(return_value=api_mock))
     mocker.patch('freqtrade.exchange.Exchange._load_markets', MagicMock(return_value={}))
-    mocker.patch('freqtrade.exchange.Exchange.validate_pairs', MagicMock())
-    mocker.patch('freqtrade.exchange.Exchange.validate_timeframes', MagicMock())
+    mocker.patch('freqtrade.exchange.Exchange.validate_pairs')
+    mocker.patch('freqtrade.exchange.Exchange.validate_timeframes')
+    mocker.patch('freqtrade.exchange.Exchange.validate_stakecurrency')
 
     conf = copy.deepcopy(default_conf)
     Exchange(conf)
@@ -529,9 +589,10 @@ def test_validate_required_startup_candles(default_conf, mocker, caplog):
     mocker.patch('freqtrade.exchange.Exchange.name', PropertyMock(return_value='Binance'))
 
     mocker.patch('freqtrade.exchange.Exchange._init_ccxt', api_mock)
-    mocker.patch('freqtrade.exchange.Exchange.validate_timeframes', MagicMock())
-    mocker.patch('freqtrade.exchange.Exchange._load_async_markets', MagicMock())
-    mocker.patch('freqtrade.exchange.Exchange.validate_pairs', MagicMock())
+    mocker.patch('freqtrade.exchange.Exchange.validate_timeframes')
+    mocker.patch('freqtrade.exchange.Exchange._load_async_markets')
+    mocker.patch('freqtrade.exchange.Exchange.validate_pairs')
+    mocker.patch('freqtrade.exchange.Exchange.validate_stakecurrency')
 
     default_conf['startup_candle_count'] = 20
     ex = Exchange(default_conf)
@@ -1665,7 +1726,9 @@ def test_merge_ft_has_dict(default_conf, mocker):
                           _init_ccxt=MagicMock(return_value=MagicMock()),
                           _load_async_markets=MagicMock(),
                           validate_pairs=MagicMock(),
-                          validate_timeframes=MagicMock())
+                          validate_timeframes=MagicMock(),
+                          validate_stakecurrency=MagicMock()
+                          )
     ex = Exchange(default_conf)
     assert ex._ft_has == Exchange._ft_has_default
 


Code review comments:
diff_hunk: @@ -216,8 +216,9 @@ def get_quote_currencies(self) -> List[str]:
         Return a list of supported quote currencies
         """
         markets = self.markets
-        currencies = set([x.get('quote') for _, x in markets.items()])
-        return list(currencies)
+        currencies = list(set([x.get('quote') for _, x in markets.items()]))
+        currencies.sort()
+        return currencies
path: freqtrade/exchange/exchange.py
text: @user1:
* Proposing small improvement:
`return sorted(set([x.get('quote') for _, x in self.markets.items()]))`

* Do you really think .get() is needed here? If so, this should probably (not in this PR) be fixed in get_markets() as well...