Repository: freqtrade/freqtrade

Issue:
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')  ```

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...



Discussion:
Amost identical to #1861 - but with another exchange-provided field.
@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 ... 

Resolving patch:
diff --git a/freqtrade/configuration/config_validation.py b/freqtrade/configuration/config_validation.py
--- 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
--- 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
--- 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,13 @@ 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
+        return sorted(set([x['quote'] for _, x in markets.items()]))
+
     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 +267,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 +339,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


Test patch:
diff --git a/tests/conftest.py b/tests/conftest.py
--- 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
--- 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