diff --git a/freqtrade/exchange/__init__.py b/freqtrade/exchange/__init__.py index acfefdad4..972ff49ca 100644 --- a/freqtrade/exchange/__init__.py +++ b/freqtrade/exchange/__init__.py @@ -70,6 +70,10 @@ class Exchange(object): # Check if all pairs are available self.validate_pairs(config['exchange']['pair_whitelist']) + if config.get('ticker_interval'): + # Check if timeframe is available + self.validate_timeframes(config['ticker_interval']) + def _init_ccxt(self, exchange_config: dict) -> ccxt.Exchange: """ Initialize ccxt with given config and return valid @@ -128,6 +132,15 @@ class Exchange(object): raise OperationalException( f'Pair {pair} is not available at {self.name}') + def validate_timeframes(self, timeframe: List[str]) -> None: + """ + Checks if ticker interval from config is a supported timeframe on the exchange + """ + timeframes = self._api.timeframes + if timeframe not in timeframes: + raise OperationalException( + f'Invalid ticker {timeframe}, this Exchange supports {timeframes}') + def exchange_has(self, endpoint: str) -> bool: """ Checks if exchange implements a specific API endpoint. diff --git a/freqtrade/tests/conftest.py b/freqtrade/tests/conftest.py index 9c86d1ece..ec435ab09 100644 --- a/freqtrade/tests/conftest.py +++ b/freqtrade/tests/conftest.py @@ -29,6 +29,7 @@ def log_has(line, logs): def patch_exchange(mocker, api_mock=None) -> None: mocker.patch('freqtrade.exchange.Exchange.validate_pairs', MagicMock()) + mocker.patch('freqtrade.exchange.Exchange.validate_timeframes', MagicMock()) if api_mock: mocker.patch('freqtrade.exchange.Exchange._init_ccxt', MagicMock(return_value=api_mock)) else: diff --git a/freqtrade/tests/exchange/test_exchange.py b/freqtrade/tests/exchange/test_exchange.py index 3ddec0ded..282d8ef01 100644 --- a/freqtrade/tests/exchange/test_exchange.py +++ b/freqtrade/tests/exchange/test_exchange.py @@ -61,6 +61,7 @@ def test_validate_pairs(default_conf, mocker): 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()) Exchange(default_conf) @@ -68,6 +69,7 @@ def test_validate_pairs_not_available(default_conf, mocker): api_mock = MagicMock() api_mock.load_markets = MagicMock(return_value={}) mocker.patch('freqtrade.exchange.Exchange._init_ccxt', MagicMock(return_value=api_mock)) + mocker.patch('freqtrade.exchange.Exchange.validate_timeframes', MagicMock()) with pytest.raises(OperationalException, match=r'not available'): Exchange(default_conf) @@ -81,7 +83,7 @@ def test_validate_pairs_not_compatible(default_conf, mocker): conf = deepcopy(default_conf) conf['stake_currency'] = 'ETH' mocker.patch('freqtrade.exchange.Exchange._init_ccxt', MagicMock(return_value=api_mock)) - + mocker.patch('freqtrade.exchange.Exchange.validate_timeframes', MagicMock()) with pytest.raises(OperationalException, match=r'not compatible'): Exchange(conf) @@ -93,6 +95,7 @@ def test_validate_pairs_exception(default_conf, mocker, caplog): api_mock.load_markets = MagicMock(return_value={}) mocker.patch('freqtrade.exchange.Exchange._init_ccxt', api_mock) + mocker.patch('freqtrade.exchange.Exchange.validate_timeframes', MagicMock()) with pytest.raises(OperationalException, match=r'Pair ETH/BTC is not available at Binance'): Exchange(default_conf) @@ -112,6 +115,7 @@ def test_validate_pairs_stake_exception(default_conf, mocker, caplog): api_mock = MagicMock() api_mock.name = MagicMock(return_value='binance') mocker.patch('freqtrade.exchange.Exchange._init_ccxt', api_mock) + mocker.patch('freqtrade.exchange.Exchange.validate_timeframes', MagicMock()) with pytest.raises( OperationalException, @@ -120,6 +124,55 @@ def test_validate_pairs_stake_exception(default_conf, mocker, caplog): Exchange(conf) +def test_validate_timeframes(default_conf, mocker): + default_conf["ticker_interval"] = "5m" + api_mock = MagicMock() + id_mock = PropertyMock(return_value='test_exchange') + type(api_mock).id = id_mock + timeframes = PropertyMock(return_value={'1m': '1m', + '5m': '5m', + '15m': '15m', + '1h': '1h'}) + type(api_mock).timeframes = timeframes + + mocker.patch('freqtrade.exchange.Exchange._init_ccxt', MagicMock(return_value=api_mock)) + mocker.patch('freqtrade.exchange.Exchange.validate_pairs', MagicMock()) + Exchange(default_conf) + + +def test_validate_timeframes_failed(default_conf, mocker): + default_conf["ticker_interval"] = "3m" + api_mock = MagicMock() + id_mock = PropertyMock(return_value='test_exchange') + type(api_mock).id = id_mock + timeframes = PropertyMock(return_value={'1m': '1m', + '5m': '5m', + '15m': '15m', + '1h': '1h'}) + type(api_mock).timeframes = timeframes + + mocker.patch('freqtrade.exchange.Exchange._init_ccxt', MagicMock(return_value=api_mock)) + mocker.patch('freqtrade.exchange.Exchange.validate_pairs', MagicMock()) + with pytest.raises(OperationalException, match=r'Invalid ticker 3m, this Exchange supports.*'): + Exchange(default_conf) + + +def test_validate_timeframes_not_in_config(default_conf, mocker): + del default_conf["ticker_interval"] + api_mock = MagicMock() + id_mock = PropertyMock(return_value='test_exchange') + type(api_mock).id = id_mock + timeframes = PropertyMock(return_value={'1m': '1m', + '5m': '5m', + '15m': '15m', + '1h': '1h'}) + type(api_mock).timeframes = timeframes + + mocker.patch('freqtrade.exchange.Exchange._init_ccxt', MagicMock(return_value=api_mock)) + mocker.patch('freqtrade.exchange.Exchange.validate_pairs', MagicMock()) + Exchange(default_conf) + + def test_exchangehas(default_conf, mocker): exchange = get_patched_exchange(mocker, default_conf) assert not exchange.exchange_has('ASDFASDF')