From 54226b45b1913383da8cc2922421ce01f83ffeac Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 7 Jun 2020 16:02:08 +0200 Subject: [PATCH 01/41] Add test verifying failure --- tests/optimize/test_backtesting.py | 27 +++++++++++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/tests/optimize/test_backtesting.py b/tests/optimize/test_backtesting.py index 40c106975..b1e9dec56 100644 --- a/tests/optimize/test_backtesting.py +++ b/tests/optimize/test_backtesting.py @@ -401,6 +401,33 @@ def test_backtesting_no_pair_left(default_conf, mocker, caplog, testdatadir) -> Backtesting(default_conf) + +def test_backtesting_pairlist_list(default_conf, mocker, caplog, testdatadir, tickers) -> None: + mocker.patch('freqtrade.exchange.Exchange.exchange_has', MagicMock(return_value=True)) + mocker.patch('freqtrade.exchange.Exchange.get_tickers', tickers) + mocker.patch('freqtrade.exchange.Exchange.price_to_precision', lambda s, x, y: y) + mocker.patch('freqtrade.data.history.get_timerange', get_timerange) + patch_exchange(mocker) + mocker.patch('freqtrade.optimize.backtesting.Backtesting.backtest') + mocker.patch('freqtrade.pairlist.pairlistmanager.PairListManager.whitelist', + PropertyMock(return_value=['XRP/BTC'])) + mocker.patch('freqtrade.pairlist.pairlistmanager.PairListManager.refresh_pairlist') + + default_conf['ticker_interval'] = "1m" + default_conf['datadir'] = testdatadir + default_conf['export'] = None + # Use stoploss from strategy + del default_conf['stoploss'] + default_conf['timerange'] = '20180101-20180102' + + default_conf['pairlists'] = [{"method": "VolumePairList", "number_assets": 5}] + with pytest.raises(OperationalException, match='VolumePairList not allowed for backtesting.'): + Backtesting(default_conf) + + default_conf['pairlists'] = [{"method": "StaticPairList"}, {"method": "PrecisionFilter"}, ] + Backtesting(default_conf) + + def test_backtest(default_conf, fee, mocker, testdatadir) -> None: default_conf['ask_strategy']['use_sell_signal'] = False mocker.patch('freqtrade.exchange.Exchange.get_fee', fee) From 72ae4b15002801a47ad4b3ae0576b414d37020f4 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 7 Jun 2020 16:06:20 +0200 Subject: [PATCH 02/41] Load pairlist after strategy to use strategy-config fail in certain conditions when using strategy-list Fix #3363 --- freqtrade/optimize/backtesting.py | 33 +++++++++++++++++------------- tests/optimize/test_backtesting.py | 7 ++++++- 2 files changed, 25 insertions(+), 15 deletions(-) diff --git a/freqtrade/optimize/backtesting.py b/freqtrade/optimize/backtesting.py index b47b38ea4..0c5bb1a0c 100644 --- a/freqtrade/optimize/backtesting.py +++ b/freqtrade/optimize/backtesting.py @@ -65,20 +65,6 @@ class Backtesting: self.strategylist: List[IStrategy] = [] self.exchange = ExchangeResolver.load_exchange(self.config['exchange']['name'], self.config) - self.pairlists = PairListManager(self.exchange, self.config) - if 'VolumePairList' in self.pairlists.name_list: - raise OperationalException("VolumePairList not allowed for backtesting.") - - self.pairlists.refresh_pairlist() - - if len(self.pairlists.whitelist) == 0: - raise OperationalException("No pair in whitelist.") - - if config.get('fee'): - self.fee = config['fee'] - else: - self.fee = self.exchange.get_fee(symbol=self.pairlists.whitelist[0]) - if self.config.get('runmode') != RunMode.HYPEROPT: self.dataprovider = DataProvider(self.config, self.exchange) IStrategy.dp = self.dataprovider @@ -101,6 +87,25 @@ class Backtesting: self.timeframe = str(self.config.get('ticker_interval')) self.timeframe_min = timeframe_to_minutes(self.timeframe) + self.pairlists = PairListManager(self.exchange, self.config) + if 'VolumePairList' in self.pairlists.name_list: + raise OperationalException("VolumePairList not allowed for backtesting.") + + if len(self.strategylist) > 1 and 'PrecisionFilter' in self.pairlists.name_list: + raise OperationalException( + "PrecisionFilter not allowed for backtesting multiple strategies." + ) + + self.pairlists.refresh_pairlist() + + if len(self.pairlists.whitelist) == 0: + raise OperationalException("No pair in whitelist.") + + if config.get('fee'): + self.fee = config['fee'] + else: + self.fee = self.exchange.get_fee(symbol=self.pairlists.whitelist[0]) + # Get maximum required startup period self.required_startup = max([strat.startup_candle_count for strat in self.strategylist]) # Load one (first) strategy diff --git a/tests/optimize/test_backtesting.py b/tests/optimize/test_backtesting.py index b1e9dec56..fc03223d2 100644 --- a/tests/optimize/test_backtesting.py +++ b/tests/optimize/test_backtesting.py @@ -401,7 +401,6 @@ def test_backtesting_no_pair_left(default_conf, mocker, caplog, testdatadir) -> Backtesting(default_conf) - def test_backtesting_pairlist_list(default_conf, mocker, caplog, testdatadir, tickers) -> None: mocker.patch('freqtrade.exchange.Exchange.exchange_has', MagicMock(return_value=True)) mocker.patch('freqtrade.exchange.Exchange.get_tickers', tickers) @@ -427,6 +426,12 @@ def test_backtesting_pairlist_list(default_conf, mocker, caplog, testdatadir, ti default_conf['pairlists'] = [{"method": "StaticPairList"}, {"method": "PrecisionFilter"}, ] Backtesting(default_conf) + # Multiple strategies + default_conf['strategy_list'] = ['DefaultStrategy', 'TestStrategyLegacy'] + with pytest.raises(OperationalException, + match='PrecisionFilter not allowed for backtesting multiple strategies.'): + Backtesting(default_conf) + def test_backtest(default_conf, fee, mocker, testdatadir) -> None: default_conf['ask_strategy']['use_sell_signal'] = False From ab0003f56517a51e358e24245f53ea054032d151 Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 9 Jun 2020 14:33:57 +0200 Subject: [PATCH 03/41] fix #3463 by explicitly failing if no stoploss is defined --- freqtrade/pairlist/PrecisionFilter.py | 6 +++++- tests/pairlist/test_pairlist.py | 11 +++++++++++ 2 files changed, 16 insertions(+), 1 deletion(-) diff --git a/freqtrade/pairlist/PrecisionFilter.py b/freqtrade/pairlist/PrecisionFilter.py index 0331347be..45baf656c 100644 --- a/freqtrade/pairlist/PrecisionFilter.py +++ b/freqtrade/pairlist/PrecisionFilter.py @@ -5,7 +5,7 @@ import logging from typing import Any, Dict from freqtrade.pairlist.IPairList import IPairList - +from freqtrade.exceptions import OperationalException logger = logging.getLogger(__name__) @@ -17,6 +17,10 @@ class PrecisionFilter(IPairList): pairlist_pos: int) -> None: super().__init__(exchange, pairlistmanager, config, pairlistconfig, pairlist_pos) + if 'stoploss' not in self._config: + raise OperationalException( + 'PrecisionFilter can only work with stoploss defined. Please add the ' + 'stoploss key to your configuration (overwrites eventual strategy settings).') self._stoploss = self._config['stoploss'] self._enabled = self._stoploss != 0 diff --git a/tests/pairlist/test_pairlist.py b/tests/pairlist/test_pairlist.py index 421f06911..07f853342 100644 --- a/tests/pairlist/test_pairlist.py +++ b/tests/pairlist/test_pairlist.py @@ -362,6 +362,17 @@ def test_VolumePairList_whitelist_gen(mocker, whitelist_conf, shitcoinmarkets, t assert not log_has(logmsg, caplog) +def test_PrecisionFilter_error(mocker, whitelist_conf, tickers) -> None: + whitelist_conf['pairlists'] = [{"method": "StaticPairList"}, {"method": "PrecisionFilter"}] + del whitelist_conf['stoploss'] + + mocker.patch('freqtrade.exchange.Exchange.exchange_has', MagicMock(return_value=True)) + + with pytest.raises(OperationalException, + match=r"PrecisionFilter can only work with stoploss defined\..*"): + PairListManager(MagicMock, whitelist_conf) + + def test_gen_pair_whitelist_not_supported(mocker, default_conf, tickers) -> None: default_conf['pairlists'] = [{'method': 'VolumePairList', 'number_assets': 10}] From dbf14ccf13d00be1687d4d3b45af252384bbec7f Mon Sep 17 00:00:00 2001 From: "dependabot-preview[bot]" <27856297+dependabot-preview[bot]@users.noreply.github.com> Date: Mon, 22 Jun 2020 09:13:36 +0000 Subject: [PATCH 04/41] Bump mypy from 0.780 to 0.781 Bumps [mypy](https://github.com/python/mypy) from 0.780 to 0.781. - [Release notes](https://github.com/python/mypy/releases) - [Commits](https://github.com/python/mypy/compare/v0.780...v0.781) Signed-off-by: dependabot-preview[bot] --- requirements-dev.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements-dev.txt b/requirements-dev.txt index 840eff15f..91b33c573 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -7,7 +7,7 @@ coveralls==2.0.0 flake8==3.8.3 flake8-type-annotations==0.1.0 flake8-tidy-imports==4.1.0 -mypy==0.780 +mypy==0.781 pytest==5.4.3 pytest-asyncio==0.12.0 pytest-cov==2.10.0 From 993333a61cdc99afbc04aa7e96ffcb582e181776 Mon Sep 17 00:00:00 2001 From: "dependabot-preview[bot]" <27856297+dependabot-preview[bot]@users.noreply.github.com> Date: Mon, 22 Jun 2020 09:14:25 +0000 Subject: [PATCH 05/41] Bump pandas from 1.0.4 to 1.0.5 Bumps [pandas](https://github.com/pandas-dev/pandas) from 1.0.4 to 1.0.5. - [Release notes](https://github.com/pandas-dev/pandas/releases) - [Changelog](https://github.com/pandas-dev/pandas/blob/master/RELEASE.md) - [Commits](https://github.com/pandas-dev/pandas/compare/v1.0.4...v1.0.5) Signed-off-by: dependabot-preview[bot] --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 2c68b8f2c..8376e41aa 100644 --- a/requirements.txt +++ b/requirements.txt @@ -2,4 +2,4 @@ -r requirements-common.txt numpy==1.18.5 -pandas==1.0.4 +pandas==1.0.5 From 432c1b54bfc8332aab70f1a08ba491d08972b7a8 Mon Sep 17 00:00:00 2001 From: "dependabot-preview[bot]" <27856297+dependabot-preview[bot]@users.noreply.github.com> Date: Mon, 22 Jun 2020 09:14:57 +0000 Subject: [PATCH 06/41] Bump arrow from 0.15.6 to 0.15.7 Bumps [arrow](https://github.com/crsmithdev/arrow) from 0.15.6 to 0.15.7. - [Release notes](https://github.com/crsmithdev/arrow/releases) - [Changelog](https://github.com/crsmithdev/arrow/blob/master/CHANGELOG.rst) - [Commits](https://github.com/crsmithdev/arrow/compare/0.15.6...0.15.7) Signed-off-by: dependabot-preview[bot] --- requirements-common.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements-common.txt b/requirements-common.txt index a6420d76a..767e993de 100644 --- a/requirements-common.txt +++ b/requirements-common.txt @@ -3,7 +3,7 @@ ccxt==1.30.2 SQLAlchemy==1.3.17 python-telegram-bot==12.7 -arrow==0.15.6 +arrow==0.15.7 cachetools==4.1.0 requests==2.23.0 urllib3==1.25.9 From dcc95d09339a104977a347499ada458f1855fba7 Mon Sep 17 00:00:00 2001 From: "dependabot-preview[bot]" <27856297+dependabot-preview[bot]@users.noreply.github.com> Date: Mon, 22 Jun 2020 09:15:33 +0000 Subject: [PATCH 07/41] Bump mkdocs-material from 5.3.0 to 5.3.2 Bumps [mkdocs-material](https://github.com/squidfunk/mkdocs-material) from 5.3.0 to 5.3.2. - [Release notes](https://github.com/squidfunk/mkdocs-material/releases) - [Changelog](https://github.com/squidfunk/mkdocs-material/blob/master/CHANGELOG) - [Commits](https://github.com/squidfunk/mkdocs-material/compare/5.3.0...5.3.2) Signed-off-by: dependabot-preview[bot] --- docs/requirements-docs.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/requirements-docs.txt b/docs/requirements-docs.txt index 666cf5ac4..7ddfc1dfb 100644 --- a/docs/requirements-docs.txt +++ b/docs/requirements-docs.txt @@ -1,2 +1,2 @@ -mkdocs-material==5.3.0 +mkdocs-material==5.3.2 mdx_truly_sane_lists==1.2 From b29f12bfad2e526c4dd3ae2b1bc0fcebe5bc4a9f Mon Sep 17 00:00:00 2001 From: "dependabot-preview[bot]" <27856297+dependabot-preview[bot]@users.noreply.github.com> Date: Mon, 22 Jun 2020 09:16:00 +0000 Subject: [PATCH 08/41] Bump scipy from 1.4.1 to 1.5.0 Bumps [scipy](https://github.com/scipy/scipy) from 1.4.1 to 1.5.0. - [Release notes](https://github.com/scipy/scipy/releases) - [Commits](https://github.com/scipy/scipy/compare/v1.4.1...v1.5.0) Signed-off-by: dependabot-preview[bot] --- requirements-hyperopt.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements-hyperopt.txt b/requirements-hyperopt.txt index e1b3fef4f..aedfc0eaa 100644 --- a/requirements-hyperopt.txt +++ b/requirements-hyperopt.txt @@ -2,7 +2,7 @@ -r requirements.txt # Required for hyperopt -scipy==1.4.1 +scipy==1.5.0 scikit-learn==0.23.1 scikit-optimize==0.7.4 filelock==3.0.12 From 6d82e41dd1bcf966d89d4ae6d8d7104b8cb14809 Mon Sep 17 00:00:00 2001 From: "dependabot-preview[bot]" <27856297+dependabot-preview[bot]@users.noreply.github.com> Date: Mon, 22 Jun 2020 09:17:07 +0000 Subject: [PATCH 09/41] Bump ccxt from 1.30.2 to 1.30.31 Bumps [ccxt](https://github.com/ccxt/ccxt) from 1.30.2 to 1.30.31. - [Release notes](https://github.com/ccxt/ccxt/releases) - [Changelog](https://github.com/ccxt/ccxt/blob/master/doc/exchanges-by-country.rst) - [Commits](https://github.com/ccxt/ccxt/compare/1.30.2...1.30.31) Signed-off-by: dependabot-preview[bot] --- requirements-common.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements-common.txt b/requirements-common.txt index a6420d76a..2e90c6cc1 100644 --- a/requirements-common.txt +++ b/requirements-common.txt @@ -1,6 +1,6 @@ # requirements without requirements installable via conda # mainly used for Raspberry pi installs -ccxt==1.30.2 +ccxt==1.30.31 SQLAlchemy==1.3.17 python-telegram-bot==12.7 arrow==0.15.6 From 9af1dae53e40f35beb4fc41069a655c95449f52c Mon Sep 17 00:00:00 2001 From: "dependabot-preview[bot]" <27856297+dependabot-preview[bot]@users.noreply.github.com> Date: Mon, 22 Jun 2020 09:48:54 +0000 Subject: [PATCH 10/41] Bump numpy from 1.18.5 to 1.19.0 Bumps [numpy](https://github.com/numpy/numpy) from 1.18.5 to 1.19.0. - [Release notes](https://github.com/numpy/numpy/releases) - [Changelog](https://github.com/numpy/numpy/blob/master/doc/HOWTO_RELEASE.rst.txt) - [Commits](https://github.com/numpy/numpy/compare/v1.18.5...v1.19.0) Signed-off-by: dependabot-preview[bot] --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 8376e41aa..1e61d165f 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,5 +1,5 @@ # Load common requirements -r requirements-common.txt -numpy==1.18.5 +numpy==1.19.0 pandas==1.0.5 From 1854e3053890552e3892757568c359219c6a3106 Mon Sep 17 00:00:00 2001 From: "dependabot-preview[bot]" <27856297+dependabot-preview[bot]@users.noreply.github.com> Date: Mon, 22 Jun 2020 11:56:51 +0000 Subject: [PATCH 11/41] Bump requests from 2.23.0 to 2.24.0 Bumps [requests](https://github.com/psf/requests) from 2.23.0 to 2.24.0. - [Release notes](https://github.com/psf/requests/releases) - [Changelog](https://github.com/psf/requests/blob/master/HISTORY.md) - [Commits](https://github.com/psf/requests/compare/v2.23.0...v2.24.0) Signed-off-by: dependabot-preview[bot] --- requirements-common.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements-common.txt b/requirements-common.txt index 767e993de..3519eda69 100644 --- a/requirements-common.txt +++ b/requirements-common.txt @@ -5,7 +5,7 @@ SQLAlchemy==1.3.17 python-telegram-bot==12.7 arrow==0.15.7 cachetools==4.1.0 -requests==2.23.0 +requests==2.24.0 urllib3==1.25.9 wrapt==1.12.1 jsonschema==3.2.0 From f2807143c65c7bb3e9f46566cb1e3d2f43952a22 Mon Sep 17 00:00:00 2001 From: "dependabot-preview[bot]" <27856297+dependabot-preview[bot]@users.noreply.github.com> Date: Mon, 22 Jun 2020 12:10:52 +0000 Subject: [PATCH 12/41] Bump ccxt from 1.30.2 to 1.30.34 Bumps [ccxt](https://github.com/ccxt/ccxt) from 1.30.2 to 1.30.34. - [Release notes](https://github.com/ccxt/ccxt/releases) - [Changelog](https://github.com/ccxt/ccxt/blob/master/doc/exchanges-by-country.rst) - [Commits](https://github.com/ccxt/ccxt/compare/1.30.2...1.30.34) Signed-off-by: dependabot-preview[bot] --- requirements-common.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements-common.txt b/requirements-common.txt index 767e993de..1c981aee3 100644 --- a/requirements-common.txt +++ b/requirements-common.txt @@ -1,6 +1,6 @@ # requirements without requirements installable via conda # mainly used for Raspberry pi installs -ccxt==1.30.2 +ccxt==1.30.34 SQLAlchemy==1.3.17 python-telegram-bot==12.7 arrow==0.15.7 From 3624aec059f0bfc5f8aa587c18fcc97cf7a5cd8b Mon Sep 17 00:00:00 2001 From: gambcl Date: Wed, 24 Jun 2020 15:21:28 +0100 Subject: [PATCH 13/41] Typos --- freqtrade/pairlist/IPairList.py | 2 +- freqtrade/pairlist/PrecisionFilter.py | 2 +- freqtrade/pairlist/PriceFilter.py | 2 +- freqtrade/pairlist/ShuffleFilter.py | 2 +- freqtrade/pairlist/SpreadFilter.py | 2 +- freqtrade/pairlist/StaticPairList.py | 2 +- freqtrade/pairlist/VolumePairList.py | 2 +- 7 files changed, 7 insertions(+), 7 deletions(-) diff --git a/freqtrade/pairlist/IPairList.py b/freqtrade/pairlist/IPairList.py index fd25e0766..1cca00eba 100644 --- a/freqtrade/pairlist/IPairList.py +++ b/freqtrade/pairlist/IPairList.py @@ -68,7 +68,7 @@ class IPairList(ABC): def needstickers(self) -> bool: """ Boolean property defining if tickers are necessary. - If no Pairlist requries tickers, an empty List is passed + If no Pairlist requires tickers, an empty List is passed as tickers argument to filter_pairlist """ diff --git a/freqtrade/pairlist/PrecisionFilter.py b/freqtrade/pairlist/PrecisionFilter.py index 0331347be..120f7c4e0 100644 --- a/freqtrade/pairlist/PrecisionFilter.py +++ b/freqtrade/pairlist/PrecisionFilter.py @@ -27,7 +27,7 @@ class PrecisionFilter(IPairList): def needstickers(self) -> bool: """ Boolean property defining if tickers are necessary. - If no Pairlist requries tickers, an empty List is passed + If no Pairlist requires tickers, an empty List is passed as tickers argument to filter_pairlist """ return True diff --git a/freqtrade/pairlist/PriceFilter.py b/freqtrade/pairlist/PriceFilter.py index b85d68269..29dd88a76 100644 --- a/freqtrade/pairlist/PriceFilter.py +++ b/freqtrade/pairlist/PriceFilter.py @@ -24,7 +24,7 @@ class PriceFilter(IPairList): def needstickers(self) -> bool: """ Boolean property defining if tickers are necessary. - If no Pairlist requries tickers, an empty List is passed + If no Pairlist requires tickers, an empty List is passed as tickers argument to filter_pairlist """ return True diff --git a/freqtrade/pairlist/ShuffleFilter.py b/freqtrade/pairlist/ShuffleFilter.py index ba3792213..eb4f6dcc3 100644 --- a/freqtrade/pairlist/ShuffleFilter.py +++ b/freqtrade/pairlist/ShuffleFilter.py @@ -25,7 +25,7 @@ class ShuffleFilter(IPairList): def needstickers(self) -> bool: """ Boolean property defining if tickers are necessary. - If no Pairlist requries tickers, an empty List is passed + If no Pairlist requires tickers, an empty List is passed as tickers argument to filter_pairlist """ return False diff --git a/freqtrade/pairlist/SpreadFilter.py b/freqtrade/pairlist/SpreadFilter.py index 0147c0068..2527a3131 100644 --- a/freqtrade/pairlist/SpreadFilter.py +++ b/freqtrade/pairlist/SpreadFilter.py @@ -24,7 +24,7 @@ class SpreadFilter(IPairList): def needstickers(self) -> bool: """ Boolean property defining if tickers are necessary. - If no Pairlist requries tickers, an empty List is passed + If no Pairlist requires tickers, an empty List is passed as tickers argument to filter_pairlist """ return True diff --git a/freqtrade/pairlist/StaticPairList.py b/freqtrade/pairlist/StaticPairList.py index b5c1bc767..aa6268ba3 100644 --- a/freqtrade/pairlist/StaticPairList.py +++ b/freqtrade/pairlist/StaticPairList.py @@ -28,7 +28,7 @@ class StaticPairList(IPairList): def needstickers(self) -> bool: """ Boolean property defining if tickers are necessary. - If no Pairlist requries tickers, an empty List is passed + If no Pairlist requires tickers, an empty List is passed as tickers argument to filter_pairlist """ return False diff --git a/freqtrade/pairlist/VolumePairList.py b/freqtrade/pairlist/VolumePairList.py index d32be3dc9..35dce93eb 100644 --- a/freqtrade/pairlist/VolumePairList.py +++ b/freqtrade/pairlist/VolumePairList.py @@ -54,7 +54,7 @@ class VolumePairList(IPairList): def needstickers(self) -> bool: """ Boolean property defining if tickers are necessary. - If no Pairlist requries tickers, an empty List is passed + If no Pairlist requires tickers, an empty List is passed as tickers argument to filter_pairlist """ return True From 676006b99c0610fcbb975325b4544597bc0fcaa7 Mon Sep 17 00:00:00 2001 From: Matthias Date: Wed, 24 Jun 2020 17:40:23 +0200 Subject: [PATCH 14/41] --dl-trades should also support increasing download span (by downloading the whole dataset again to avoid missing data in the middle). --- freqtrade/data/history/history_utils.py | 5 +++++ tests/conftest.py | 2 +- tests/data/test_history.py | 22 ++++++++++++++++++++++ 3 files changed, 28 insertions(+), 1 deletion(-) diff --git a/freqtrade/data/history/history_utils.py b/freqtrade/data/history/history_utils.py index 4f3f75a87..58bd752ea 100644 --- a/freqtrade/data/history/history_utils.py +++ b/freqtrade/data/history/history_utils.py @@ -270,6 +270,11 @@ def _download_trades_history(exchange: Exchange, # DEFAULT_TRADES_COLUMNS: 0 -> timestamp # DEFAULT_TRADES_COLUMNS: 1 -> id + if trades and since < trades[0][0]: + # since is before the first trade + logger.info(f"Start earlier than available data. Redownloading trades for {pair}...") + trades = [] + from_id = trades[-1][1] if trades else None if trades and since < trades[-1][0]: # Reset since to the last available point diff --git a/tests/conftest.py b/tests/conftest.py index a4106c767..f2143e60e 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -1424,7 +1424,7 @@ def trades_for_order(): @pytest.fixture(scope="function") def trades_history(): - return [[1565798399463, '126181329', None, 'buy', 0.019627, 0.04, 0.00078508], + return [[1565798389463, '126181329', None, 'buy', 0.019627, 0.04, 0.00078508], [1565798399629, '126181330', None, 'buy', 0.019627, 0.244, 0.004788987999999999], [1565798399752, '126181331', None, 'sell', 0.019626, 0.011, 0.00021588599999999999], [1565798399862, '126181332', None, 'sell', 0.019626, 0.011, 0.00021588599999999999], diff --git a/tests/data/test_history.py b/tests/data/test_history.py index c52163bbc..c2eb2d715 100644 --- a/tests/data/test_history.py +++ b/tests/data/test_history.py @@ -557,6 +557,7 @@ def test_download_trades_history(trades_history, mocker, default_conf, testdatad assert ght_mock.call_count == 1 # Check this in seconds - since we had to convert to seconds above too. assert int(ght_mock.call_args_list[0][1]['since'] // 1000) == since_time2 - 5 + assert ght_mock.call_args_list[0][1]['from_id'] is not None # clean files freshly downloaded _clean_test_file(file1) @@ -568,6 +569,27 @@ def test_download_trades_history(trades_history, mocker, default_conf, testdatad pair='ETH/BTC') assert log_has_re('Failed to download historic trades for pair: "ETH/BTC".*', caplog) + file2 = testdatadir / 'XRP_ETH-trades.json.gz' + + _backup_file(file2, True) + + ght_mock.reset_mock() + mocker.patch('freqtrade.exchange.Exchange.get_historic_trades', + ght_mock) + # Since before first start date + since_time = int(trades_history[0][0] // 1000) - 500 + timerange = TimeRange('date', None, since_time, 0) + + assert _download_trades_history(data_handler=data_handler, exchange=exchange, + pair='XRP/ETH', timerange=timerange) + + assert ght_mock.call_count == 1 + + assert int(ght_mock.call_args_list[0][1]['since'] // 1000) == since_time + assert ght_mock.call_args_list[0][1]['from_id'] is None + assert log_has_re(r'Start earlier than available data. Redownloading trades for.*', caplog) + _clean_test_file(file2) + def test_convert_trades_to_ohlcv(mocker, default_conf, testdatadir, caplog): From b77a105778842ee012a122ac5f9a8f218fbc3716 Mon Sep 17 00:00:00 2001 From: Matthias Date: Wed, 24 Jun 2020 20:32:19 +0200 Subject: [PATCH 15/41] Add CORS_origins key to configuration --- config.json.example | 1 + config_binance.json.example | 1 + config_full.json.example | 1 + config_kraken.json.example | 1 + docs/rest-api.md | 24 ++++++++++++++++++++++++ freqtrade/constants.py | 2 ++ freqtrade/rpc/api_server.py | 4 +++- freqtrade/templates/base_config.json.j2 | 1 + 8 files changed, 34 insertions(+), 1 deletion(-) diff --git a/config.json.example b/config.json.example index 9e3daa2b5..77a147d0c 100644 --- a/config.json.example +++ b/config.json.example @@ -82,6 +82,7 @@ "listen_port": 8080, "verbosity": "info", "jwt_secret_key": "somethingrandom", + "CORS_origins": [], "username": "", "password": "" }, diff --git a/config_binance.json.example b/config_binance.json.example index b45e69bba..82943749d 100644 --- a/config_binance.json.example +++ b/config_binance.json.example @@ -87,6 +87,7 @@ "listen_port": 8080, "verbosity": "info", "jwt_secret_key": "somethingrandom", + "CORS_origins": [], "username": "", "password": "" }, diff --git a/config_full.json.example b/config_full.json.example index 1fd1b44a5..3a8667da4 100644 --- a/config_full.json.example +++ b/config_full.json.example @@ -123,6 +123,7 @@ "listen_port": 8080, "verbosity": "info", "jwt_secret_key": "somethingrandom", + "CORS_origins": [], "username": "freqtrader", "password": "SuperSecurePassword" }, diff --git a/config_kraken.json.example b/config_kraken.json.example index 7e4001ff3..fb983a4a3 100644 --- a/config_kraken.json.example +++ b/config_kraken.json.example @@ -93,6 +93,7 @@ "listen_port": 8080, "verbosity": "info", "jwt_secret_key": "somethingrandom", + "CORS_origins": [], "username": "", "password": "" }, diff --git a/docs/rest-api.md b/docs/rest-api.md index 33f62f884..630c952b4 100644 --- a/docs/rest-api.md +++ b/docs/rest-api.md @@ -13,6 +13,7 @@ Sample configuration: "listen_port": 8080, "verbosity": "info", "jwt_secret_key": "somethingrandom", + "CORS_origins": [], "username": "Freqtrader", "password": "SuperSecret1!" }, @@ -232,3 +233,26 @@ Since the access token has a short timeout (15 min) - the `token/refresh` reques > curl -X POST --header "Authorization: Bearer ${refresh_token}"http://localhost:8080/api/v1/token/refresh {"access_token":"eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpYXQiOjE1ODkxMTk5NzQsIm5iZiI6MTU4OTExOTk3NCwianRpIjoiMDBjNTlhMWUtMjBmYS00ZTk0LTliZjAtNWQwNTg2MTdiZDIyIiwiZXhwIjoxNTg5MTIwODc0LCJpZGVudGl0eSI6eyJ1IjoiRnJlcXRyYWRlciJ9LCJmcmVzaCI6ZmFsc2UsInR5cGUiOiJhY2Nlc3MifQ.1seHlII3WprjjclY6DpRhen0rqdF4j6jbvxIhUFaSbs"} ``` + +## CORS + +All web-based frontends are subject to [CORS](https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS) - Cross-Origin Resource Sharing. +Since most request to the Freqtrade API must be authenticated, a proper CORS policy is key to avoid security problems. +Also, the Standard disallows `*` CORS policies for requests with credentials, so this setting must be done appropriately. + +Users can configure this themselfs via the `CORS_origins` configuration setting. +It consists of a list of allowed sites that are allowed to consume resources from the bot's API. + +Assuming your Application would be deployed as `https://frequi.freqtrade.io/home/` - this would mean that the following configuration becomes necessary: + +```jsonc +{ + //... + "jwt_secret_key": "somethingrandom", + "CORS_origins": ["https://frequi.freqtrade.io"], + //... +} +``` + +!!! Note + We strongly recommend to also set `jwt_secret_key` to something random and known only to yourself to avoid unauthorized access to your bot. diff --git a/freqtrade/constants.py b/freqtrade/constants.py index 6741e0605..1f8944ed9 100644 --- a/freqtrade/constants.py +++ b/freqtrade/constants.py @@ -221,6 +221,8 @@ CONF_SCHEMA = { }, 'username': {'type': 'string'}, 'password': {'type': 'string'}, + 'jwt_secret_key': {'type': 'string'}, + 'CORS_origins': {'type': 'array', 'items': {'type': 'string'}}, 'verbosity': {'type': 'string', 'enum': ['error', 'info']}, }, 'required': ['enabled', 'listen_ip_address', 'listen_port', 'username', 'password'] diff --git a/freqtrade/rpc/api_server.py b/freqtrade/rpc/api_server.py index f424bea92..a2cef9a98 100644 --- a/freqtrade/rpc/api_server.py +++ b/freqtrade/rpc/api_server.py @@ -90,7 +90,9 @@ class ApiServer(RPC): self._config = freqtrade.config self.app = Flask(__name__) self._cors = CORS(self.app, - resources={r"/api/*": {"supports_credentials": True, }} + resources={r"/api/*": { + "supports_credentials": True, + "origins": self._config['api_server'].get('CORS_origins', [])}} ) # Setup the Flask-JWT-Extended extension diff --git a/freqtrade/templates/base_config.json.j2 b/freqtrade/templates/base_config.json.j2 index 118ae348b..b362690f9 100644 --- a/freqtrade/templates/base_config.json.j2 +++ b/freqtrade/templates/base_config.json.j2 @@ -59,6 +59,7 @@ "listen_port": 8080, "verbosity": "info", "jwt_secret_key": "somethingrandom", + "CORS_origins": [], "username": "", "password": "" }, From 5423d8588ecb6bf1c9094f9ca89b19585fe3ddaf Mon Sep 17 00:00:00 2001 From: Matthias Date: Wed, 24 Jun 2020 20:32:35 +0200 Subject: [PATCH 16/41] Test for cors settings --- tests/rpc/test_rpc_apiserver.py | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/tests/rpc/test_rpc_apiserver.py b/tests/rpc/test_rpc_apiserver.py index 8e73eacf8..0acb31282 100644 --- a/tests/rpc/test_rpc_apiserver.py +++ b/tests/rpc/test_rpc_apiserver.py @@ -24,6 +24,7 @@ def botclient(default_conf, mocker): default_conf.update({"api_server": {"enabled": True, "listen_ip_address": "127.0.0.1", "listen_port": 8080, + "CORS_origins": ['http://example.com'], "username": _TEST_USER, "password": _TEST_PASS, }}) @@ -40,13 +41,13 @@ def client_post(client, url, data={}): content_type="application/json", data=data, headers={'Authorization': _basic_auth_str(_TEST_USER, _TEST_PASS), - 'Origin': 'example.com'}) + 'Origin': 'http://example.com'}) def client_get(client, url): # Add fake Origin to ensure CORS kicks in return client.get(url, headers={'Authorization': _basic_auth_str(_TEST_USER, _TEST_PASS), - 'Origin': 'example.com'}) + 'Origin': 'http://example.com'}) def assert_response(response, expected_code=200, needs_cors=True): @@ -54,6 +55,7 @@ def assert_response(response, expected_code=200, needs_cors=True): assert response.content_type == "application/json" if needs_cors: assert ('Access-Control-Allow-Credentials', 'true') in response.headers._list + assert ('Access-Control-Allow-Origin', 'http://example.com') in response.headers._list def test_api_not_found(botclient): @@ -110,7 +112,7 @@ def test_api_token_login(botclient): rc = client.get(f"{BASE_URI}/count", content_type="application/json", headers={'Authorization': f'Bearer {rc.json["access_token"]}', - 'Origin': 'example.com'}) + 'Origin': 'http://example.com'}) assert_response(rc) @@ -122,7 +124,7 @@ def test_api_token_refresh(botclient): content_type="application/json", data=None, headers={'Authorization': f'Bearer {rc.json["refresh_token"]}', - 'Origin': 'example.com'}) + 'Origin': 'http://example.com'}) assert_response(rc) assert 'access_token' in rc.json assert 'refresh_token' not in rc.json From ab7f5a2bcf41e3e0c988b5e66d8f02d6ed39a4b4 Mon Sep 17 00:00:00 2001 From: gambcl Date: Wed, 24 Jun 2020 23:58:12 +0100 Subject: [PATCH 17/41] Added pairslist AgeFilter --- config_full.json.example | 1 + docs/configuration.md | 14 +++++- freqtrade/constants.py | 3 +- freqtrade/pairlist/AgeFilter.py | 76 +++++++++++++++++++++++++++++++++ tests/pairlist/test_pairlist.py | 76 +++++++++++++++++++++++++++++++-- 5 files changed, 164 insertions(+), 6 deletions(-) create mode 100644 freqtrade/pairlist/AgeFilter.py diff --git a/config_full.json.example b/config_full.json.example index 1fd1b44a5..5b8fa256b 100644 --- a/config_full.json.example +++ b/config_full.json.example @@ -64,6 +64,7 @@ "sort_key": "quoteVolume", "refresh_period": 1800 }, + {"method": "AgeFilter", "min_days_listed": 10}, {"method": "PrecisionFilter"}, {"method": "PriceFilter", "low_price_ratio": 0.01}, {"method": "SpreadFilter", "max_spread_ratio": 0.005} diff --git a/docs/configuration.md b/docs/configuration.md index 8438d55da..e7a79361a 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -592,7 +592,7 @@ Pairlist Handlers define the list of pairs (pairlist) that the bot should trade. In your configuration, you can use Static Pairlist (defined by the [`StaticPairList`](#static-pair-list) Pairlist Handler) and Dynamic Pairlist (defined by the [`VolumePairList`](#volume-pair-list) Pairlist Handler). -Additionaly, [`PrecisionFilter`](#precisionfilter), [`PriceFilter`](#pricefilter), [`ShuffleFilter`](#shufflefilter) and [`SpreadFilter`](#spreadfilter) act as Pairlist Filters, removing certain pairs and/or moving their positions in the pairlist. +Additionaly, [`AgeFilter`](#agefilter), [`PrecisionFilter`](#precisionfilter), [`PriceFilter`](#pricefilter), [`ShuffleFilter`](#shufflefilter) and [`SpreadFilter`](#spreadfilter) act as Pairlist Filters, removing certain pairs and/or moving their positions in the pairlist. If multiple Pairlist Handlers are used, they are chained and a combination of all Pairlist Handlers forms the resulting pairlist the bot uses for trading and backtesting. Pairlist Handlers are executed in the sequence they are configured. You should always configure either `StaticPairList` or `VolumePairList` as the starting Pairlist Handler. @@ -602,6 +602,7 @@ Inactive markets are always removed from the resulting pairlist. Explicitly blac * [`StaticPairList`](#static-pair-list) (default, if not configured differently) * [`VolumePairList`](#volume-pair-list) +* [`AgeFilter`](#agefilter) * [`PrecisionFilter`](#precisionfilter) * [`PriceFilter`](#pricefilter) * [`ShuffleFilter`](#shufflefilter) @@ -645,6 +646,16 @@ The `refresh_period` setting allows to define the period (in seconds), at which }], ``` +#### AgeFilter + +Removes pairs that have been listed on the exchange for less than `min_days_listed` days (defaults to `10`). + +When pairs are first listed on an exchange they can suffer huge price drops and volatility +in the first few days while the pair goes through its price-discovery period. Bots can often +be caught out buying before the pair has finished dropping in price. + +This filter allows freqtrade to ignore pairs until they have been listed for at least `min_days_listed` days. + #### PrecisionFilter Filters low-value coins which would not allow setting stoplosses. @@ -692,6 +703,7 @@ The below example blacklists `BNB/BTC`, uses `VolumePairList` with `20` assets, "number_assets": 20, "sort_key": "quoteVolume", }, + {"method": "AgeFilter", "min_days_listed": 10}, {"method": "PrecisionFilter"}, {"method": "PriceFilter", "low_price_ratio": 0.01}, {"method": "SpreadFilter", "max_spread_ratio": 0.005}, diff --git a/freqtrade/constants.py b/freqtrade/constants.py index 6741e0605..92824f4c4 100644 --- a/freqtrade/constants.py +++ b/freqtrade/constants.py @@ -22,7 +22,8 @@ ORDERBOOK_SIDES = ['ask', 'bid'] ORDERTYPE_POSSIBILITIES = ['limit', 'market'] ORDERTIF_POSSIBILITIES = ['gtc', 'fok', 'ioc'] AVAILABLE_PAIRLISTS = ['StaticPairList', 'VolumePairList', - 'PrecisionFilter', 'PriceFilter', 'ShuffleFilter', 'SpreadFilter'] + 'AgeFilter', 'PrecisionFilter', 'PriceFilter', + 'ShuffleFilter', 'SpreadFilter'] AVAILABLE_DATAHANDLERS = ['json', 'jsongz'] DRY_RUN_WALLET = 1000 MATH_CLOSE_PREC = 1e-14 # Precision used for float comparisons diff --git a/freqtrade/pairlist/AgeFilter.py b/freqtrade/pairlist/AgeFilter.py new file mode 100644 index 000000000..a23682599 --- /dev/null +++ b/freqtrade/pairlist/AgeFilter.py @@ -0,0 +1,76 @@ +""" +Minimum age (days listed) pair list filter +""" +import logging +import arrow +from typing import Any, Dict + +from freqtrade.misc import plural +from freqtrade.pairlist.IPairList import IPairList + + +logger = logging.getLogger(__name__) + + +class AgeFilter(IPairList): + + # Checked symbols cache (dictionary of ticker symbol => timestamp) + _symbolsChecked: Dict[str, int] = {} + + def __init__(self, exchange, pairlistmanager, + config: Dict[str, Any], pairlistconfig: Dict[str, Any], + pairlist_pos: int) -> None: + super().__init__(exchange, pairlistmanager, config, pairlistconfig, pairlist_pos) + + self._min_days_listed = pairlistconfig.get('min_days_listed', 10) + self._enabled = self._min_days_listed >= 1 + + @property + def needstickers(self) -> bool: + """ + Boolean property defining if tickers are necessary. + If no Pairlist requires tickers, an empty List is passed + as tickers argument to filter_pairlist + """ + return True + + def short_desc(self) -> str: + """ + Short whitelist method description - used for startup-messages + """ + return (f"{self.name} - Filtering pairs with age less than " + f"{self._min_days_listed} {plural(self._min_days_listed, 'day')}.") + + def _validate_pair(self, ticker: dict) -> bool: + """ + Validate age for the ticker + :param ticker: ticker dict as returned from ccxt.load_markets() + :return: True if the pair can stay, False if it should be removed + """ + + # Check symbol in cache + if ticker['symbol'] in self._symbolsChecked: + return True + + since_ms = int(arrow.utcnow() + .floor('day') + .shift(days=-self._min_days_listed) + .float_timestamp) * 1000 + + daily_candles = self._exchange.get_historic_ohlcv(pair=ticker['symbol'], + timeframe='1d', + since_ms=since_ms) + + if daily_candles is not None: + if len(daily_candles) > self._min_days_listed: + # We have fetched at least the minimum required number of daily candles + # Add to cache, store the time we last checked this symbol + self._symbolsChecked[ticker['symbol']] = int(arrow.utcnow().float_timestamp) * 1000 + return True + else: + self.log_on_refresh(logger.info, f"Removed {ticker['symbol']} from whitelist, " + f"because age is less than " + f"{self._min_days_listed} " + f"{plural(self._min_days_listed, 'day')}") + return False + return False diff --git a/tests/pairlist/test_pairlist.py b/tests/pairlist/test_pairlist.py index c67f7ae1c..87ecced21 100644 --- a/tests/pairlist/test_pairlist.py +++ b/tests/pairlist/test_pairlist.py @@ -57,6 +57,31 @@ def whitelist_conf_2(default_conf): return default_conf +@pytest.fixture(scope="function") +def whitelist_conf_3(default_conf): + default_conf['stake_currency'] = 'BTC' + default_conf['exchange']['pair_whitelist'] = [ + 'ETH/BTC', 'TKN/BTC', 'BLK/BTC', 'LTC/BTC', + 'BTT/BTC', 'HOT/BTC', 'FUEL/BTC', 'XRP/BTC' + ] + default_conf['exchange']['pair_blacklist'] = [ + 'BLK/BTC' + ] + default_conf['pairlists'] = [ + { + "method": "VolumePairList", + "number_assets": 5, + "sort_key": "quoteVolume", + "refresh_period": 0, + }, + { + "method": "AgeFilter", + "min_days_listed": 2 + } + ] + return default_conf + + @pytest.fixture(scope="function") def static_pl_conf(whitelist_conf): whitelist_conf['pairlists'] = [ @@ -220,11 +245,20 @@ def test_VolumePairList_refresh_empty(mocker, markets_empty, whitelist_conf): # No pair for ETH, all handlers ([{"method": "StaticPairList"}, {"method": "VolumePairList", "number_assets": 5, "sort_key": "quoteVolume"}, + {"method": "AgeFilter", "min_days_listed": 2}, {"method": "PrecisionFilter"}, {"method": "PriceFilter", "low_price_ratio": 0.03}, {"method": "SpreadFilter", "max_spread_ratio": 0.005}, {"method": "ShuffleFilter"}], "ETH", []), + # AgeFilter and VolumePairList (require 2 days only, all should pass age test) + ([{"method": "VolumePairList", "number_assets": 5, "sort_key": "quoteVolume"}, + {"method": "AgeFilter", "min_days_listed": 2}], + "BTC", ['ETH/BTC', 'TKN/BTC', 'LTC/BTC', 'XRP/BTC', 'HOT/BTC']), + # AgeFilter and VolumePairList (require 10 days, all should fail age test) + ([{"method": "VolumePairList", "number_assets": 5, "sort_key": "quoteVolume"}, + {"method": "AgeFilter", "min_days_listed": 10}], + "BTC", []), # Precisionfilter and quote volume ([{"method": "VolumePairList", "number_assets": 5, "sort_key": "quoteVolume"}, {"method": "PrecisionFilter"}], @@ -272,7 +306,10 @@ def test_VolumePairList_refresh_empty(mocker, markets_empty, whitelist_conf): # ShuffleFilter, no seed ([{"method": "VolumePairList", "number_assets": 5, "sort_key": "quoteVolume"}, {"method": "ShuffleFilter"}], - "USDT", 3), # whitelist_result is integer -- check only lenght of randomized pairlist + "USDT", 3), # whitelist_result is integer -- check only length of randomized pairlist + # AgeFilter only + ([{"method": "AgeFilter", "min_days_listed": 2}], + "BTC", 'filter_at_the_beginning'), # OperationalException expected # PrecisionFilter after StaticPairList ([{"method": "StaticPairList"}, {"method": "PrecisionFilter"}], @@ -307,8 +344,8 @@ def test_VolumePairList_refresh_empty(mocker, markets_empty, whitelist_conf): "BTC", 'static_in_the_middle'), ]) def test_VolumePairList_whitelist_gen(mocker, whitelist_conf, shitcoinmarkets, tickers, - pairlists, base_currency, whitelist_result, - caplog) -> None: + ohlcv_history_list, pairlists, base_currency, + whitelist_result, caplog) -> None: whitelist_conf['pairlists'] = pairlists whitelist_conf['stake_currency'] = base_currency @@ -324,8 +361,12 @@ def test_VolumePairList_whitelist_gen(mocker, whitelist_conf, shitcoinmarkets, t freqtrade = get_patched_freqtradebot(mocker, whitelist_conf) mocker.patch.multiple('freqtrade.exchange.Exchange', get_tickers=tickers, - markets=PropertyMock(return_value=shitcoinmarkets), + markets=PropertyMock(return_value=shitcoinmarkets) ) + mocker.patch.multiple( + 'freqtrade.exchange.Exchange', + get_historic_ohlcv=MagicMock(return_value=ohlcv_history_list), + ) # Set whitelist_result to None if pairlist is invalid and should produce exception if whitelist_result == 'filter_at_the_beginning': @@ -346,6 +387,10 @@ def test_VolumePairList_whitelist_gen(mocker, whitelist_conf, shitcoinmarkets, t len(whitelist) == whitelist_result for pairlist in pairlists: + if pairlist['method'] == 'AgeFilter' and pairlist['min_days_listed'] and \ + len(ohlcv_history_list) <= pairlist['min_days_listed']: + assert log_has_re(r'^Removed .* from whitelist, because age is less than ' + r'.* day.*', caplog) if pairlist['method'] == 'PrecisionFilter' and whitelist_result: assert log_has_re(r'^Removed .* from whitelist, because stop price .* ' r'would be <= stop limit.*', caplog) @@ -468,6 +513,29 @@ def test_volumepairlist_caching(mocker, markets, whitelist_conf, tickers): assert freqtrade.pairlists._pairlist_handlers[0]._last_refresh == lrf +def test_agefilter_caching(mocker, markets, whitelist_conf_3, tickers, ohlcv_history_list): + + mocker.patch.multiple('freqtrade.exchange.Exchange', + markets=PropertyMock(return_value=markets), + exchange_has=MagicMock(return_value=True), + get_tickers=tickers + ) + mocker.patch.multiple( + 'freqtrade.exchange.Exchange', + get_historic_ohlcv=MagicMock(return_value=ohlcv_history_list), + ) + + freqtrade = get_patched_freqtradebot(mocker, whitelist_conf_3) + assert freqtrade.exchange.get_historic_ohlcv.call_count == 0 + freqtrade.pairlists.refresh_pairlist() + assert freqtrade.exchange.get_historic_ohlcv.call_count > 0 + + previous_call_count = freqtrade.exchange.get_historic_ohlcv.call_count + freqtrade.pairlists.refresh_pairlist() + # Should not have increased since first call. + assert freqtrade.exchange.get_historic_ohlcv.call_count == previous_call_count + + def test_pairlistmanager_no_pairlist(mocker, markets, whitelist_conf, caplog): mocker.patch('freqtrade.exchange.Exchange.exchange_has', MagicMock(return_value=True)) From da8e87660e35e50f9d955dce703cd4cdfe9d50ca Mon Sep 17 00:00:00 2001 From: Matthias Date: Fri, 26 Jun 2020 06:39:47 +0200 Subject: [PATCH 18/41] Add missing data fillup to FAQ --- docs/faq.md | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/docs/faq.md b/docs/faq.md index 31c49171d..7e9551051 100644 --- a/docs/faq.md +++ b/docs/faq.md @@ -49,6 +49,16 @@ You can use the `/forcesell all` command from Telegram. Please look at the [advanced setup documentation Page](advanced-setup.md#running-multiple-instances-of-freqtrade). +### I'm getting "Missing data fillup" messages in the log + +This message is just a warning that the latest candles had missing candles in them. +Depending on the exchange, this can indicate that the pair didn't have a trade for `` - and the exchange does only return candles with volume. +On low volume pairs, this is a rather common occurance. + +If this happens for all pairs in the pairlist, this might indicate a recent exchange downtime. Please check your exchange's public channels for details. + +Independently of the reason, Freqtrade will fill up these candles with "empty" candles, where open, high, low and close are set to the previous candle close - and volume is empty. In a chart, this will look like a `_` - and is aligned with how exchanges usually represent 0 volume candles. + ### I'm getting the "RESTRICTED_MARKET" message in the log Currently known to happen for US Bittrex users. From 185fab7b57fc7e47e05e06022a2df1ef32f688aa Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 27 Jun 2020 15:26:55 +0200 Subject: [PATCH 19/41] Change some wordings in documentation Co-authored-by: hroff-1902 <47309513+hroff-1902@users.noreply.github.com> --- docs/rest-api.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/docs/rest-api.md b/docs/rest-api.md index 630c952b4..a8d902b53 100644 --- a/docs/rest-api.md +++ b/docs/rest-api.md @@ -237,13 +237,13 @@ Since the access token has a short timeout (15 min) - the `token/refresh` reques ## CORS All web-based frontends are subject to [CORS](https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS) - Cross-Origin Resource Sharing. -Since most request to the Freqtrade API must be authenticated, a proper CORS policy is key to avoid security problems. -Also, the Standard disallows `*` CORS policies for requests with credentials, so this setting must be done appropriately. +Since most of the requests to the Freqtrade API must be authenticated, a proper CORS policy is key to avoid security problems. +Also, the standard disallows `*` CORS policies for requests with credentials, so this setting must be set appropriately. -Users can configure this themselfs via the `CORS_origins` configuration setting. +Users can configure this themselves via the `CORS_origins` configuration setting. It consists of a list of allowed sites that are allowed to consume resources from the bot's API. -Assuming your Application would be deployed as `https://frequi.freqtrade.io/home/` - this would mean that the following configuration becomes necessary: +Assuming your application is deployed as `https://frequi.freqtrade.io/home/` - this would mean that the following configuration becomes necessary: ```jsonc { From e11d22a6a2d997da487a298d87b6de7927f33a10 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 27 Jun 2020 15:31:37 +0200 Subject: [PATCH 20/41] Apply suggestions from code review Co-authored-by: hroff-1902 <47309513+hroff-1902@users.noreply.github.com> --- docs/faq.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/faq.md b/docs/faq.md index 7e9551051..151b2c054 100644 --- a/docs/faq.md +++ b/docs/faq.md @@ -52,12 +52,12 @@ Please look at the [advanced setup documentation Page](advanced-setup.md#running ### I'm getting "Missing data fillup" messages in the log This message is just a warning that the latest candles had missing candles in them. -Depending on the exchange, this can indicate that the pair didn't have a trade for `` - and the exchange does only return candles with volume. +Depending on the exchange, this can indicate that the pair didn't have a trade for the timeframe you are using - and the exchange does only return candles with volume. On low volume pairs, this is a rather common occurance. If this happens for all pairs in the pairlist, this might indicate a recent exchange downtime. Please check your exchange's public channels for details. -Independently of the reason, Freqtrade will fill up these candles with "empty" candles, where open, high, low and close are set to the previous candle close - and volume is empty. In a chart, this will look like a `_` - and is aligned with how exchanges usually represent 0 volume candles. +Irrespectively of the reason, Freqtrade will fill up these candles with "empty" candles, where open, high, low and close are set to the previous candle close - and volume is empty. In a chart, this will look like a `_` - and is aligned with how exchanges usually represent 0 volume candles. ### I'm getting the "RESTRICTED_MARKET" message in the log From 2c45114a64c31ff1abaed6e6d22bf0fe0561e2f4 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 28 Jun 2020 11:17:06 +0200 Subject: [PATCH 21/41] Implement DDos backoff (1s) --- freqtrade/exceptions.py | 7 +++++++ freqtrade/exchange/binance.py | 7 +++++-- freqtrade/exchange/common.py | 8 +++++++- freqtrade/exchange/exchange.py | 30 ++++++++++++++++++++++++++---- freqtrade/exchange/ftx.py | 11 +++++++++-- freqtrade/exchange/kraken.py | 6 +++++- tests/exchange/test_exchange.py | 19 +++++++++++++++++-- 7 files changed, 76 insertions(+), 12 deletions(-) diff --git a/freqtrade/exceptions.py b/freqtrade/exceptions.py index 7cfed87e8..bc84f30b8 100644 --- a/freqtrade/exceptions.py +++ b/freqtrade/exceptions.py @@ -45,6 +45,13 @@ class TemporaryError(FreqtradeException): """ +class DDosProtection(TemporaryError): + """ + Temporary error caused by DDOS protection. + Bot will wait for a second and then retry. + """ + + class StrategyError(FreqtradeException): """ Errors with custom user-code deteced. diff --git a/freqtrade/exchange/binance.py b/freqtrade/exchange/binance.py index 4279f392c..3a98f161b 100644 --- a/freqtrade/exchange/binance.py +++ b/freqtrade/exchange/binance.py @@ -4,8 +4,9 @@ from typing import Dict import ccxt -from freqtrade.exceptions import (DependencyException, InvalidOrderException, - OperationalException, TemporaryError) +from freqtrade.exceptions import (DDosProtection, DependencyException, + InvalidOrderException, OperationalException, + TemporaryError) from freqtrade.exchange import Exchange logger = logging.getLogger(__name__) @@ -88,6 +89,8 @@ class Binance(Exchange): f'Could not create {ordertype} sell order on market {pair}. ' f'Tried to sell amount {amount} at rate {rate}. ' f'Message: {e}') from e + except ccxt.DDoSProtection as e: + raise DDosProtection(e) from e except (ccxt.NetworkError, ccxt.ExchangeError) as e: raise TemporaryError( f'Could not place sell order due to {e.__class__.__name__}. Message: {e}') from e diff --git a/freqtrade/exchange/common.py b/freqtrade/exchange/common.py index a10d41247..1f74412c6 100644 --- a/freqtrade/exchange/common.py +++ b/freqtrade/exchange/common.py @@ -1,6 +1,8 @@ +import asyncio import logging +import time -from freqtrade.exceptions import TemporaryError +from freqtrade.exceptions import DDosProtection, TemporaryError logger = logging.getLogger(__name__) @@ -99,6 +101,8 @@ def retrier_async(f): count -= 1 kwargs.update({'count': count}) logger.warning('retrying %s() still for %s times', f.__name__, count) + if isinstance(ex, DDosProtection): + await asyncio.sleep(1) return await wrapper(*args, **kwargs) else: logger.warning('Giving up retrying: %s()', f.__name__) @@ -117,6 +121,8 @@ def retrier(f): count -= 1 kwargs.update({'count': count}) logger.warning('retrying %s() still for %s times', f.__name__, count) + if isinstance(ex, DDosProtection): + time.sleep(1) return wrapper(*args, **kwargs) else: logger.warning('Giving up retrying: %s()', f.__name__) diff --git a/freqtrade/exchange/exchange.py b/freqtrade/exchange/exchange.py index b62410c34..09f27e638 100644 --- a/freqtrade/exchange/exchange.py +++ b/freqtrade/exchange/exchange.py @@ -18,12 +18,13 @@ from ccxt.base.decimal_to_precision import (ROUND_DOWN, ROUND_UP, TICK_SIZE, TRUNCATE, decimal_to_precision) from pandas import DataFrame +from freqtrade.constants import ListPairsWithTimeframes from freqtrade.data.converter import ohlcv_to_dataframe, trades_dict_to_list -from freqtrade.exceptions import (DependencyException, InvalidOrderException, - OperationalException, TemporaryError) +from freqtrade.exceptions import (DDosProtection, DependencyException, + InvalidOrderException, OperationalException, + TemporaryError) from freqtrade.exchange.common import BAD_EXCHANGES, retrier, retrier_async from freqtrade.misc import deep_merge_dicts, safe_value_fallback -from freqtrade.constants import ListPairsWithTimeframes CcxtModuleType = Any @@ -527,6 +528,8 @@ class Exchange: f'Could not create {ordertype} {side} order on market {pair}.' f'Tried to {side} amount {amount} at rate {rate}.' f'Message: {e}') from e + except ccxt.DDoSProtection as e: + raise DDosProtection(e) from e except (ccxt.NetworkError, ccxt.ExchangeError) as e: raise TemporaryError( f'Could not place {side} order due to {e.__class__.__name__}. Message: {e}') from e @@ -606,6 +609,8 @@ class Exchange: balances.pop("used", None) return balances + except ccxt.DDoSProtection as e: + raise DDosProtection(e) from e except (ccxt.NetworkError, ccxt.ExchangeError) as e: raise TemporaryError( f'Could not get balance due to {e.__class__.__name__}. Message: {e}') from e @@ -620,6 +625,8 @@ class Exchange: raise OperationalException( f'Exchange {self._api.name} does not support fetching tickers in batch. ' f'Message: {e}') from e + except ccxt.DDoSProtection as e: + raise DDosProtection(e) from e except (ccxt.NetworkError, ccxt.ExchangeError) as e: raise TemporaryError( f'Could not load tickers due to {e.__class__.__name__}. Message: {e}') from e @@ -633,6 +640,8 @@ class Exchange: raise DependencyException(f"Pair {pair} not available") data = self._api.fetch_ticker(pair) return data + except ccxt.DDoSProtection as e: + raise DDosProtection(e) from e except (ccxt.NetworkError, ccxt.ExchangeError) as e: raise TemporaryError( f'Could not load ticker due to {e.__class__.__name__}. Message: {e}') from e @@ -766,6 +775,8 @@ class Exchange: raise OperationalException( f'Exchange {self._api.name} does not support fetching historical ' f'candle (OHLCV) data. Message: {e}') from e + except ccxt.DDoSProtection as e: + raise DDosProtection(e) from e except (ccxt.NetworkError, ccxt.ExchangeError) as e: raise TemporaryError(f'Could not fetch historical candle (OHLCV) data ' f'for pair {pair} due to {e.__class__.__name__}. ' @@ -802,6 +813,8 @@ class Exchange: raise OperationalException( f'Exchange {self._api.name} does not support fetching historical trade data.' f'Message: {e}') from e + except ccxt.DDoSProtection as e: + raise DDosProtection(e) from e except (ccxt.NetworkError, ccxt.ExchangeError) as e: raise TemporaryError(f'Could not load trade history due to {e.__class__.__name__}. ' f'Message: {e}') from e @@ -948,6 +961,8 @@ class Exchange: except ccxt.InvalidOrder as e: raise InvalidOrderException( f'Could not cancel order. Message: {e}') from e + except ccxt.DDoSProtection as e: + raise DDosProtection(e) from e except (ccxt.NetworkError, ccxt.ExchangeError) as e: raise TemporaryError( f'Could not cancel order due to {e.__class__.__name__}. Message: {e}') from e @@ -1003,6 +1018,8 @@ class Exchange: except ccxt.InvalidOrder as e: raise InvalidOrderException( f'Tried to get an invalid order (id: {order_id}). Message: {e}') from e + except ccxt.DDoSProtection as e: + raise DDosProtection(e) from e except (ccxt.NetworkError, ccxt.ExchangeError) as e: raise TemporaryError( f'Could not get order due to {e.__class__.__name__}. Message: {e}') from e @@ -1027,6 +1044,8 @@ class Exchange: raise OperationalException( f'Exchange {self._api.name} does not support fetching order book.' f'Message: {e}') from e + except ccxt.DDoSProtection as e: + raise DDosProtection(e) from e except (ccxt.NetworkError, ccxt.ExchangeError) as e: raise TemporaryError( f'Could not get order book due to {e.__class__.__name__}. Message: {e}') from e @@ -1063,7 +1082,8 @@ class Exchange: matched_trades = [trade for trade in my_trades if trade['order'] == order_id] return matched_trades - + except ccxt.DDoSProtection as e: + raise DDosProtection(e) from e except (ccxt.NetworkError, ccxt.ExchangeError) as e: raise TemporaryError( f'Could not get trades due to {e.__class__.__name__}. Message: {e}') from e @@ -1080,6 +1100,8 @@ class Exchange: return self._api.calculate_fee(symbol=symbol, type=type, side=side, amount=amount, price=price, takerOrMaker=taker_or_maker)['rate'] + except ccxt.DDoSProtection as e: + raise DDosProtection(e) from e except (ccxt.NetworkError, ccxt.ExchangeError) as e: raise TemporaryError( f'Could not get fee info due to {e.__class__.__name__}. Message: {e}') from e diff --git a/freqtrade/exchange/ftx.py b/freqtrade/exchange/ftx.py index f16db96f5..7d0778ae5 100644 --- a/freqtrade/exchange/ftx.py +++ b/freqtrade/exchange/ftx.py @@ -4,8 +4,9 @@ from typing import Dict import ccxt -from freqtrade.exceptions import (DependencyException, InvalidOrderException, - OperationalException, TemporaryError) +from freqtrade.exceptions import (DDosProtection, DependencyException, + InvalidOrderException, OperationalException, + TemporaryError) from freqtrade.exchange import Exchange from freqtrade.exchange.common import retrier @@ -68,6 +69,8 @@ class Ftx(Exchange): f'Could not create {ordertype} sell order on market {pair}. ' f'Tried to create stoploss with amount {amount} at stoploss {stop_price}. ' f'Message: {e}') from e + except ccxt.DDoSProtection as e: + raise DDosProtection(e) from e except (ccxt.NetworkError, ccxt.ExchangeError) as e: raise TemporaryError( f'Could not place sell order due to {e.__class__.__name__}. Message: {e}') from e @@ -96,6 +99,8 @@ class Ftx(Exchange): except ccxt.InvalidOrder as e: raise InvalidOrderException( f'Tried to get an invalid order (id: {order_id}). Message: {e}') from e + except ccxt.DDoSProtection as e: + raise DDosProtection(e) from e except (ccxt.NetworkError, ccxt.ExchangeError) as e: raise TemporaryError( f'Could not get order due to {e.__class__.__name__}. Message: {e}') from e @@ -111,6 +116,8 @@ class Ftx(Exchange): except ccxt.InvalidOrder as e: raise InvalidOrderException( f'Could not cancel order. Message: {e}') from e + except ccxt.DDoSProtection as e: + raise DDosProtection(e) from e except (ccxt.NetworkError, ccxt.ExchangeError) as e: raise TemporaryError( f'Could not cancel order due to {e.__class__.__name__}. Message: {e}') from e diff --git a/freqtrade/exchange/kraken.py b/freqtrade/exchange/kraken.py index 932d82a27..7710b8e9b 100644 --- a/freqtrade/exchange/kraken.py +++ b/freqtrade/exchange/kraken.py @@ -4,7 +4,7 @@ from typing import Dict import ccxt -from freqtrade.exceptions import (DependencyException, InvalidOrderException, +from freqtrade.exceptions import (DependencyException, InvalidOrderException, DDosProtection, OperationalException, TemporaryError) from freqtrade.exchange import Exchange from freqtrade.exchange.common import retrier @@ -45,6 +45,8 @@ class Kraken(Exchange): balances[bal]['free'] = balances[bal]['total'] - balances[bal]['used'] return balances + except ccxt.DDoSProtection as e: + raise DDosProtection(e) from e except (ccxt.NetworkError, ccxt.ExchangeError) as e: raise TemporaryError( f'Could not get balance due to {e.__class__.__name__}. Message: {e}') from e @@ -93,6 +95,8 @@ class Kraken(Exchange): f'Could not create {ordertype} sell order on market {pair}. ' f'Tried to create stoploss with amount {amount} at stoploss {stop_price}. ' f'Message: {e}') from e + except ccxt.DDoSProtection as e: + raise DDosProtection(e) from e except (ccxt.NetworkError, ccxt.ExchangeError) as e: raise TemporaryError( f'Could not place sell order due to {e.__class__.__name__}. Message: {e}') from e diff --git a/tests/exchange/test_exchange.py b/tests/exchange/test_exchange.py index 700aff969..f3a3a3789 100644 --- a/tests/exchange/test_exchange.py +++ b/tests/exchange/test_exchange.py @@ -4,14 +4,14 @@ import copy import logging from datetime import datetime, timezone from random import randint -from unittest.mock import MagicMock, Mock, PropertyMock +from unittest.mock import MagicMock, Mock, PropertyMock, patch import arrow import ccxt import pytest from pandas import DataFrame -from freqtrade.exceptions import (DependencyException, InvalidOrderException, +from freqtrade.exceptions import (DependencyException, InvalidOrderException, DDosProtection, OperationalException, TemporaryError) from freqtrade.exchange import Binance, Exchange, Kraken from freqtrade.exchange.common import API_RETRY_COUNT @@ -38,6 +38,14 @@ def get_mock_coro(return_value): def ccxt_exceptionhandlers(mocker, default_conf, api_mock, exchange_name, fun, mock_ccxt_fun, **kwargs): + + with patch('freqtrade.exchange.common.time.sleep'): + with pytest.raises(DDosProtection): + api_mock.__dict__[mock_ccxt_fun] = MagicMock(side_effect=ccxt.DDoSProtection("DDos")) + exchange = get_patched_exchange(mocker, default_conf, api_mock, id=exchange_name) + getattr(exchange, fun)(**kwargs) + assert api_mock.__dict__[mock_ccxt_fun].call_count == API_RETRY_COUNT + 1 + with pytest.raises(TemporaryError): api_mock.__dict__[mock_ccxt_fun] = MagicMock(side_effect=ccxt.NetworkError("DeaDBeef")) exchange = get_patched_exchange(mocker, default_conf, api_mock, id=exchange_name) @@ -52,6 +60,13 @@ def ccxt_exceptionhandlers(mocker, default_conf, api_mock, exchange_name, async def async_ccxt_exception(mocker, default_conf, api_mock, fun, mock_ccxt_fun, **kwargs): + + with patch('freqtrade.exchange.common.asyncio.sleep'): + with pytest.raises(DDosProtection): + api_mock.__dict__[mock_ccxt_fun] = MagicMock(side_effect=ccxt.DDoSProtection("DeadBeef")) + exchange = get_patched_exchange(mocker, default_conf, api_mock) + await getattr(exchange, fun)(**kwargs) + with pytest.raises(TemporaryError): api_mock.__dict__[mock_ccxt_fun] = MagicMock(side_effect=ccxt.NetworkError("DeadBeef")) exchange = get_patched_exchange(mocker, default_conf, api_mock) From 5bd4798ed0953ab1b94fd5a2cc388cdd82eccf50 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 28 Jun 2020 11:56:29 +0200 Subject: [PATCH 22/41] Add retrier to stoploss calls (but without retrying) --- freqtrade/exchange/binance.py | 2 ++ freqtrade/exchange/common.py | 44 +++++++++++++++++++-------------- freqtrade/exchange/ftx.py | 1 + freqtrade/exchange/kraken.py | 1 + tests/exchange/test_binance.py | 15 ++++------- tests/exchange/test_exchange.py | 24 ++++++++++++------ tests/exchange/test_ftx.py | 16 ++++-------- tests/exchange/test_kraken.py | 15 +++-------- 8 files changed, 61 insertions(+), 57 deletions(-) diff --git a/freqtrade/exchange/binance.py b/freqtrade/exchange/binance.py index 3a98f161b..ee9566282 100644 --- a/freqtrade/exchange/binance.py +++ b/freqtrade/exchange/binance.py @@ -8,6 +8,7 @@ from freqtrade.exceptions import (DDosProtection, DependencyException, InvalidOrderException, OperationalException, TemporaryError) from freqtrade.exchange import Exchange +from freqtrade.exchange.common import retrier logger = logging.getLogger(__name__) @@ -40,6 +41,7 @@ class Binance(Exchange): """ return order['type'] == 'stop_loss_limit' and stop_loss > float(order['info']['stopPrice']) + @retrier(retries=0) def stoploss(self, pair: str, amount: float, stop_price: float, order_types: Dict) -> Dict: """ creates a stoploss limit order. diff --git a/freqtrade/exchange/common.py b/freqtrade/exchange/common.py index 1f74412c6..d931e1789 100644 --- a/freqtrade/exchange/common.py +++ b/freqtrade/exchange/common.py @@ -1,6 +1,7 @@ import asyncio import logging import time +from functools import wraps from freqtrade.exceptions import DDosProtection, TemporaryError @@ -110,21 +111,28 @@ def retrier_async(f): return wrapper -def retrier(f): - def wrapper(*args, **kwargs): - count = kwargs.pop('count', API_RETRY_COUNT) - try: - return f(*args, **kwargs) - except TemporaryError as ex: - logger.warning('%s() returned exception: "%s"', f.__name__, ex) - if count > 0: - count -= 1 - kwargs.update({'count': count}) - logger.warning('retrying %s() still for %s times', f.__name__, count) - if isinstance(ex, DDosProtection): - time.sleep(1) - return wrapper(*args, **kwargs) - else: - logger.warning('Giving up retrying: %s()', f.__name__) - raise ex - return wrapper +def retrier(_func=None, retries=API_RETRY_COUNT): + def decorator(f): + @wraps(f) + def wrapper(*args, **kwargs): + count = kwargs.pop('count', retries) + try: + return f(*args, **kwargs) + except TemporaryError as ex: + logger.warning('%s() returned exception: "%s"', f.__name__, ex) + if count > 0: + count -= 1 + kwargs.update({'count': count}) + logger.warning('retrying %s() still for %s times', f.__name__, count) + if isinstance(ex, DDosProtection): + time.sleep(1) + return wrapper(*args, **kwargs) + else: + logger.warning('Giving up retrying: %s()', f.__name__) + raise ex + return wrapper + # Support both @retrier and @retrier() syntax + if _func is None: + return decorator + else: + return decorator(_func) diff --git a/freqtrade/exchange/ftx.py b/freqtrade/exchange/ftx.py index 7d0778ae5..f1bd23b52 100644 --- a/freqtrade/exchange/ftx.py +++ b/freqtrade/exchange/ftx.py @@ -27,6 +27,7 @@ class Ftx(Exchange): """ return order['type'] == 'stop' and stop_loss > float(order['price']) + @retrier(retries=0) def stoploss(self, pair: str, amount: float, stop_price: float, order_types: Dict) -> Dict: """ Creates a stoploss order. diff --git a/freqtrade/exchange/kraken.py b/freqtrade/exchange/kraken.py index 7710b8e9b..01aa647b4 100644 --- a/freqtrade/exchange/kraken.py +++ b/freqtrade/exchange/kraken.py @@ -60,6 +60,7 @@ class Kraken(Exchange): """ return order['type'] == 'stop-loss' and stop_loss > float(order['price']) + @retrier(retries=0) def stoploss(self, pair: str, amount: float, stop_price: float, order_types: Dict) -> Dict: """ Creates a stoploss market order. diff --git a/tests/exchange/test_binance.py b/tests/exchange/test_binance.py index 52faa284b..72da708b4 100644 --- a/tests/exchange/test_binance.py +++ b/tests/exchange/test_binance.py @@ -5,8 +5,9 @@ import ccxt import pytest from freqtrade.exceptions import (DependencyException, InvalidOrderException, - OperationalException, TemporaryError) + OperationalException) from tests.conftest import get_patched_exchange +from tests.exchange.test_exchange import ccxt_exceptionhandlers @pytest.mark.parametrize('limitratio,expected', [ @@ -62,15 +63,9 @@ def test_stoploss_order_binance(default_conf, mocker, limitratio, expected): exchange = get_patched_exchange(mocker, default_conf, api_mock, 'binance') exchange.stoploss(pair='ETH/BTC', amount=1, stop_price=220, order_types={}) - with pytest.raises(TemporaryError): - api_mock.create_order = MagicMock(side_effect=ccxt.NetworkError("No connection")) - exchange = get_patched_exchange(mocker, default_conf, api_mock, 'binance') - exchange.stoploss(pair='ETH/BTC', amount=1, stop_price=220, order_types={}) - - with pytest.raises(OperationalException, match=r".*DeadBeef.*"): - api_mock.create_order = MagicMock(side_effect=ccxt.BaseError("DeadBeef")) - exchange = get_patched_exchange(mocker, default_conf, api_mock, 'binance') - exchange.stoploss(pair='ETH/BTC', amount=1, stop_price=220, order_types={}) + ccxt_exceptionhandlers(mocker, default_conf, api_mock, "binance", + "stoploss", "create_order", retries=1, + pair='ETH/BTC', amount=1, stop_price=220, order_types={}) def test_stoploss_order_dry_run_binance(default_conf, mocker): diff --git a/tests/exchange/test_exchange.py b/tests/exchange/test_exchange.py index f3a3a3789..15ab0d3d9 100644 --- a/tests/exchange/test_exchange.py +++ b/tests/exchange/test_exchange.py @@ -37,20 +37,20 @@ def get_mock_coro(return_value): def ccxt_exceptionhandlers(mocker, default_conf, api_mock, exchange_name, - fun, mock_ccxt_fun, **kwargs): + fun, mock_ccxt_fun, retries=API_RETRY_COUNT + 1, **kwargs): with patch('freqtrade.exchange.common.time.sleep'): with pytest.raises(DDosProtection): api_mock.__dict__[mock_ccxt_fun] = MagicMock(side_effect=ccxt.DDoSProtection("DDos")) exchange = get_patched_exchange(mocker, default_conf, api_mock, id=exchange_name) getattr(exchange, fun)(**kwargs) - assert api_mock.__dict__[mock_ccxt_fun].call_count == API_RETRY_COUNT + 1 + assert api_mock.__dict__[mock_ccxt_fun].call_count == retries with pytest.raises(TemporaryError): api_mock.__dict__[mock_ccxt_fun] = MagicMock(side_effect=ccxt.NetworkError("DeaDBeef")) exchange = get_patched_exchange(mocker, default_conf, api_mock, id=exchange_name) getattr(exchange, fun)(**kwargs) - assert api_mock.__dict__[mock_ccxt_fun].call_count == API_RETRY_COUNT + 1 + assert api_mock.__dict__[mock_ccxt_fun].call_count == retries with pytest.raises(OperationalException): api_mock.__dict__[mock_ccxt_fun] = MagicMock(side_effect=ccxt.BaseError("DeadBeef")) @@ -59,19 +59,21 @@ def ccxt_exceptionhandlers(mocker, default_conf, api_mock, exchange_name, assert api_mock.__dict__[mock_ccxt_fun].call_count == 1 -async def async_ccxt_exception(mocker, default_conf, api_mock, fun, mock_ccxt_fun, **kwargs): +async def async_ccxt_exception(mocker, default_conf, api_mock, fun, mock_ccxt_fun, + retries=API_RETRY_COUNT + 1, **kwargs): with patch('freqtrade.exchange.common.asyncio.sleep'): with pytest.raises(DDosProtection): api_mock.__dict__[mock_ccxt_fun] = MagicMock(side_effect=ccxt.DDoSProtection("DeadBeef")) exchange = get_patched_exchange(mocker, default_conf, api_mock) await getattr(exchange, fun)(**kwargs) + assert api_mock.__dict__[mock_ccxt_fun].call_count == retries with pytest.raises(TemporaryError): api_mock.__dict__[mock_ccxt_fun] = MagicMock(side_effect=ccxt.NetworkError("DeadBeef")) exchange = get_patched_exchange(mocker, default_conf, api_mock) await getattr(exchange, fun)(**kwargs) - assert api_mock.__dict__[mock_ccxt_fun].call_count == API_RETRY_COUNT + 1 + assert api_mock.__dict__[mock_ccxt_fun].call_count == retries with pytest.raises(OperationalException): api_mock.__dict__[mock_ccxt_fun] = MagicMock(side_effect=ccxt.BaseError("DeadBeef")) @@ -1142,9 +1144,10 @@ def test_get_balance_prod(default_conf, mocker, exchange_name): exchange.get_balance(currency='BTC') -def test_get_balances_dry_run(default_conf, mocker): +@pytest.mark.parametrize("exchange_name", EXCHANGES) +def test_get_balances_dry_run(default_conf, mocker, exchange_name): default_conf['dry_run'] = True - exchange = get_patched_exchange(mocker, default_conf) + exchange = get_patched_exchange(mocker, default_conf, id=exchange_name) assert exchange.get_balances() == {} @@ -2126,6 +2129,13 @@ def test_get_markets(default_conf, mocker, markets, assert sorted(pairs.keys()) == sorted(expected_keys) +def test_get_markets_error(default_conf, mocker): + ex = get_patched_exchange(mocker, default_conf) + mocker.patch('freqtrade.exchange.Exchange.markets', PropertyMock(return_value=None)) + with pytest.raises(OperationalException, match="Markets were not loaded."): + ex.get_markets('LTC', 'USDT', True, False) + + def test_timeframe_to_minutes(): assert timeframe_to_minutes("5m") == 5 assert timeframe_to_minutes("10m") == 10 diff --git a/tests/exchange/test_ftx.py b/tests/exchange/test_ftx.py index 75e98740c..1b7d68770 100644 --- a/tests/exchange/test_ftx.py +++ b/tests/exchange/test_ftx.py @@ -6,9 +6,9 @@ from unittest.mock import MagicMock import ccxt import pytest -from freqtrade.exceptions import (DependencyException, InvalidOrderException, - OperationalException, TemporaryError) +from freqtrade.exceptions import DependencyException, InvalidOrderException from tests.conftest import get_patched_exchange + from .test_exchange import ccxt_exceptionhandlers STOPLOSS_ORDERTYPE = 'stop' @@ -85,15 +85,9 @@ def test_stoploss_order_ftx(default_conf, mocker): exchange = get_patched_exchange(mocker, default_conf, api_mock, 'ftx') exchange.stoploss(pair='ETH/BTC', amount=1, stop_price=220, order_types={}) - with pytest.raises(TemporaryError): - api_mock.create_order = MagicMock(side_effect=ccxt.NetworkError("No connection")) - exchange = get_patched_exchange(mocker, default_conf, api_mock, 'ftx') - exchange.stoploss(pair='ETH/BTC', amount=1, stop_price=220, order_types={}) - - with pytest.raises(OperationalException, match=r".*DeadBeef.*"): - api_mock.create_order = MagicMock(side_effect=ccxt.BaseError("DeadBeef")) - exchange = get_patched_exchange(mocker, default_conf, api_mock, 'ftx') - exchange.stoploss(pair='ETH/BTC', amount=1, stop_price=220, order_types={}) + ccxt_exceptionhandlers(mocker, default_conf, api_mock, "ftx", + "stoploss", "create_order", retries=1, + pair='ETH/BTC', amount=1, stop_price=220, order_types={}) def test_stoploss_order_dry_run_ftx(default_conf, mocker): diff --git a/tests/exchange/test_kraken.py b/tests/exchange/test_kraken.py index 0950979cf..9451c0b9e 100644 --- a/tests/exchange/test_kraken.py +++ b/tests/exchange/test_kraken.py @@ -6,8 +6,7 @@ from unittest.mock import MagicMock import ccxt import pytest -from freqtrade.exceptions import (DependencyException, InvalidOrderException, - OperationalException, TemporaryError) +from freqtrade.exceptions import DependencyException, InvalidOrderException from tests.conftest import get_patched_exchange from tests.exchange.test_exchange import ccxt_exceptionhandlers @@ -206,15 +205,9 @@ def test_stoploss_order_kraken(default_conf, mocker): exchange = get_patched_exchange(mocker, default_conf, api_mock, 'kraken') exchange.stoploss(pair='ETH/BTC', amount=1, stop_price=220, order_types={}) - with pytest.raises(TemporaryError): - api_mock.create_order = MagicMock(side_effect=ccxt.NetworkError("No connection")) - exchange = get_patched_exchange(mocker, default_conf, api_mock, 'kraken') - exchange.stoploss(pair='ETH/BTC', amount=1, stop_price=220, order_types={}) - - with pytest.raises(OperationalException, match=r".*DeadBeef.*"): - api_mock.create_order = MagicMock(side_effect=ccxt.BaseError("DeadBeef")) - exchange = get_patched_exchange(mocker, default_conf, api_mock, 'kraken') - exchange.stoploss(pair='ETH/BTC', amount=1, stop_price=220, order_types={}) + ccxt_exceptionhandlers(mocker, default_conf, api_mock, "kraken", + "stoploss", "create_order", retries=1, + pair='ETH/BTC', amount=1, stop_price=220, order_types={}) def test_stoploss_order_dry_run_kraken(default_conf, mocker): From e74d2af85788b2e52a4025105e5f6063f29b50e9 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 28 Jun 2020 15:44:58 +0200 Subject: [PATCH 23/41] Have TemporaryError a subCategory of DependencyException so it's safe to raise out of the exchange --- freqtrade/exceptions.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/freqtrade/exceptions.py b/freqtrade/exceptions.py index bc84f30b8..1ddb2a396 100644 --- a/freqtrade/exceptions.py +++ b/freqtrade/exceptions.py @@ -37,7 +37,7 @@ class InvalidOrderException(FreqtradeException): """ -class TemporaryError(FreqtradeException): +class TemporaryError(DependencyException): """ Temporary network or exchange related error. This could happen when an exchange is congested, unavailable, or the user From bf61bc9d8329aed5ecc7fa034ccb458c1c442434 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 28 Jun 2020 16:01:40 +0200 Subject: [PATCH 24/41] Introduce ExchangeError --- freqtrade/data/dataprovider.py | 4 ++-- freqtrade/exceptions.py | 9 ++++++++- freqtrade/exchange/binance.py | 4 ++-- freqtrade/exchange/exchange.py | 12 ++++++------ freqtrade/exchange/ftx.py | 4 ++-- freqtrade/exchange/kraken.py | 7 ++++--- freqtrade/freqtradebot.py | 8 ++++---- freqtrade/rpc/rpc.py | 12 ++++++------ 8 files changed, 34 insertions(+), 26 deletions(-) diff --git a/freqtrade/data/dataprovider.py b/freqtrade/data/dataprovider.py index 058ca42da..b677f374b 100644 --- a/freqtrade/data/dataprovider.py +++ b/freqtrade/data/dataprovider.py @@ -10,7 +10,7 @@ from typing import Any, Dict, List, Optional from pandas import DataFrame from freqtrade.data.history import load_pair_history -from freqtrade.exceptions import DependencyException, OperationalException +from freqtrade.exceptions import ExchangeError, OperationalException from freqtrade.exchange import Exchange from freqtrade.state import RunMode from freqtrade.constants import ListPairsWithTimeframes @@ -105,7 +105,7 @@ class DataProvider: """ try: return self._exchange.fetch_ticker(pair) - except DependencyException: + except ExchangeError: return {} def orderbook(self, pair: str, maximum: int) -> Dict[str, List]: diff --git a/freqtrade/exceptions.py b/freqtrade/exceptions.py index 1ddb2a396..995a2cdb7 100644 --- a/freqtrade/exceptions.py +++ b/freqtrade/exceptions.py @@ -37,7 +37,14 @@ class InvalidOrderException(FreqtradeException): """ -class TemporaryError(DependencyException): +class ExchangeError(DependencyException): + """ + Error raised out of the exchange. + Has multiple Errors to determine the appropriate error. + """ + + +class TemporaryError(ExchangeError): """ Temporary network or exchange related error. This could happen when an exchange is congested, unavailable, or the user diff --git a/freqtrade/exchange/binance.py b/freqtrade/exchange/binance.py index ee9566282..08e84ee34 100644 --- a/freqtrade/exchange/binance.py +++ b/freqtrade/exchange/binance.py @@ -4,7 +4,7 @@ from typing import Dict import ccxt -from freqtrade.exceptions import (DDosProtection, DependencyException, +from freqtrade.exceptions import (DDosProtection, ExchangeError, InvalidOrderException, OperationalException, TemporaryError) from freqtrade.exchange import Exchange @@ -80,7 +80,7 @@ class Binance(Exchange): 'stop price: %s. limit: %s', pair, stop_price, rate) return order except ccxt.InsufficientFunds as e: - raise DependencyException( + raise ExchangeError( f'Insufficient funds to create {ordertype} sell order on market {pair}.' f'Tried to sell amount {amount} at rate {rate}. ' f'Message: {e}') from e diff --git a/freqtrade/exchange/exchange.py b/freqtrade/exchange/exchange.py index 09f27e638..d48e47909 100644 --- a/freqtrade/exchange/exchange.py +++ b/freqtrade/exchange/exchange.py @@ -20,7 +20,7 @@ from pandas import DataFrame from freqtrade.constants import ListPairsWithTimeframes from freqtrade.data.converter import ohlcv_to_dataframe, trades_dict_to_list -from freqtrade.exceptions import (DDosProtection, DependencyException, +from freqtrade.exceptions import (DDosProtection, ExchangeError, InvalidOrderException, OperationalException, TemporaryError) from freqtrade.exchange.common import BAD_EXCHANGES, retrier, retrier_async @@ -352,7 +352,7 @@ class Exchange: for pair in [f"{curr_1}/{curr_2}", f"{curr_2}/{curr_1}"]: if pair in self.markets and self.markets[pair].get('active'): return pair - raise DependencyException(f"Could not combine {curr_1} and {curr_2} to get a valid pair.") + raise ExchangeError(f"Could not combine {curr_1} and {curr_2} to get a valid pair.") def validate_timeframes(self, timeframe: Optional[str]) -> None: """ @@ -519,12 +519,12 @@ class Exchange: amount, rate_for_order, params) except ccxt.InsufficientFunds as e: - raise DependencyException( + raise ExchangeError( f'Insufficient funds to create {ordertype} {side} order on market {pair}.' f'Tried to {side} amount {amount} at rate {rate}.' f'Message: {e}') from e except ccxt.InvalidOrder as e: - raise DependencyException( + raise ExchangeError( f'Could not create {ordertype} {side} order on market {pair}.' f'Tried to {side} amount {amount} at rate {rate}.' f'Message: {e}') from e @@ -637,7 +637,7 @@ class Exchange: def fetch_ticker(self, pair: str) -> dict: try: if pair not in self._api.markets or not self._api.markets[pair].get('active'): - raise DependencyException(f"Pair {pair} not available") + raise ExchangeError(f"Pair {pair} not available") data = self._api.fetch_ticker(pair) return data except ccxt.DDoSProtection as e: @@ -1151,7 +1151,7 @@ class Exchange: fee_to_quote_rate = safe_value_fallback(tick, tick, 'last', 'ask') return round((order['fee']['cost'] * fee_to_quote_rate) / order['cost'], 8) - except DependencyException: + except ExchangeError: return None def extract_cost_curr_rate(self, order: Dict) -> Tuple[float, str, Optional[float]]: diff --git a/freqtrade/exchange/ftx.py b/freqtrade/exchange/ftx.py index f1bd23b52..be815d336 100644 --- a/freqtrade/exchange/ftx.py +++ b/freqtrade/exchange/ftx.py @@ -4,7 +4,7 @@ from typing import Dict import ccxt -from freqtrade.exceptions import (DDosProtection, DependencyException, +from freqtrade.exceptions import (DDosProtection, ExchangeError, InvalidOrderException, OperationalException, TemporaryError) from freqtrade.exchange import Exchange @@ -61,7 +61,7 @@ class Ftx(Exchange): 'stop price: %s.', pair, stop_price) return order except ccxt.InsufficientFunds as e: - raise DependencyException( + raise ExchangeError( f'Insufficient funds to create {ordertype} sell order on market {pair}. ' f'Tried to create stoploss with amount {amount} at stoploss {stop_price}. ' f'Message: {e}') from e diff --git a/freqtrade/exchange/kraken.py b/freqtrade/exchange/kraken.py index 01aa647b4..2ca4ba167 100644 --- a/freqtrade/exchange/kraken.py +++ b/freqtrade/exchange/kraken.py @@ -4,8 +4,9 @@ from typing import Dict import ccxt -from freqtrade.exceptions import (DependencyException, InvalidOrderException, DDosProtection, - OperationalException, TemporaryError) +from freqtrade.exceptions import (DDosProtection, ExchangeError, + InvalidOrderException, OperationalException, + TemporaryError) from freqtrade.exchange import Exchange from freqtrade.exchange.common import retrier @@ -87,7 +88,7 @@ class Kraken(Exchange): 'stop price: %s.', pair, stop_price) return order except ccxt.InsufficientFunds as e: - raise DependencyException( + raise ExchangeError( f'Insufficient funds to create {ordertype} sell order on market {pair}.' f'Tried to create stoploss with amount {amount} at stoploss {stop_price}. ' f'Message: {e}') from e diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index 289850709..2e59d915d 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -11,14 +11,14 @@ from typing import Any, Dict, List, Optional import arrow from cachetools import TTLCache -from requests.exceptions import RequestException from freqtrade import __version__, constants, persistence from freqtrade.configuration import validate_config_consistency from freqtrade.data.converter import order_book_to_dataframe from freqtrade.data.dataprovider import DataProvider from freqtrade.edge import Edge -from freqtrade.exceptions import DependencyException, InvalidOrderException, PricingError +from freqtrade.exceptions import (DependencyException, ExchangeError, + InvalidOrderException, PricingError) from freqtrade.exchange import timeframe_to_minutes, timeframe_to_next_date from freqtrade.misc import safe_value_fallback from freqtrade.pairlist.pairlistmanager import PairListManager @@ -755,7 +755,7 @@ class FreqtradeBot: logger.warning('Selling the trade forcefully') self.execute_sell(trade, trade.stop_loss, sell_reason=SellType.EMERGENCY_SELL) - except DependencyException: + except ExchangeError: trade.stoploss_order_id = None logger.exception('Unable to place a stoploss order on exchange.') return False @@ -891,7 +891,7 @@ class FreqtradeBot: if not trade.open_order_id: continue order = self.exchange.get_order(trade.open_order_id, trade.pair) - except (RequestException, DependencyException, InvalidOrderException): + except (ExchangeError, InvalidOrderException): logger.info('Cannot query order for %s due to %s', trade, traceback.format_exc()) continue diff --git a/freqtrade/rpc/rpc.py b/freqtrade/rpc/rpc.py index aeaf82662..aab3da258 100644 --- a/freqtrade/rpc/rpc.py +++ b/freqtrade/rpc/rpc.py @@ -11,7 +11,7 @@ from typing import Any, Dict, List, Optional, Tuple import arrow from numpy import NAN, mean -from freqtrade.exceptions import DependencyException, TemporaryError +from freqtrade.exceptions import ExchangeError, PricingError from freqtrade.misc import shorten_date from freqtrade.persistence import Trade from freqtrade.rpc.fiat_convert import CryptoToFiatConverter @@ -130,7 +130,7 @@ class RPC: # calculate profit and send message to user try: current_rate = self._freqtrade.get_sell_rate(trade.pair, False) - except DependencyException: + except (ExchangeError, PricingError): current_rate = NAN current_profit = trade.calc_profit_ratio(current_rate) current_profit_abs = trade.calc_profit(current_rate) @@ -174,7 +174,7 @@ class RPC: # calculate profit and send message to user try: current_rate = self._freqtrade.get_sell_rate(trade.pair, False) - except DependencyException: + except (PricingError, ExchangeError): current_rate = NAN trade_percent = (100 * trade.calc_profit_ratio(current_rate)) trade_profit = trade.calc_profit(current_rate) @@ -286,7 +286,7 @@ class RPC: # Get current rate try: current_rate = self._freqtrade.get_sell_rate(trade.pair, False) - except DependencyException: + except (PricingError, ExchangeError): current_rate = NAN profit_ratio = trade.calc_profit_ratio(rate=current_rate) @@ -352,7 +352,7 @@ class RPC: total = 0.0 try: tickers = self._freqtrade.exchange.get_tickers() - except (TemporaryError, DependencyException): + except (ExchangeError): raise RPCException('Error getting current tickers.') self._freqtrade.wallets.update(require_update=False) @@ -373,7 +373,7 @@ class RPC: if pair.startswith(stake_currency): rate = 1.0 / rate est_stake = rate * balance.total - except (TemporaryError, DependencyException): + except (ExchangeError): logger.warning(f" Could not get rate for pair {coin}.") continue total = total + (est_stake or 0) From 29d3ff1bc922690b2aae5865cc574613ec454631 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 28 Jun 2020 16:04:04 +0200 Subject: [PATCH 25/41] Adjust tests to work with ExchangeError --- tests/data/test_dataprovider.py | 6 +++--- tests/exchange/test_exchange.py | 2 +- tests/rpc/test_rpc.py | 11 ++++++----- tests/test_freqtradebot.py | 15 +++++++-------- 4 files changed, 17 insertions(+), 17 deletions(-) diff --git a/tests/data/test_dataprovider.py b/tests/data/test_dataprovider.py index def3ad535..2f91dcd38 100644 --- a/tests/data/test_dataprovider.py +++ b/tests/data/test_dataprovider.py @@ -1,11 +1,11 @@ from unittest.mock import MagicMock -from pandas import DataFrame import pytest +from pandas import DataFrame from freqtrade.data.dataprovider import DataProvider +from freqtrade.exceptions import ExchangeError, OperationalException from freqtrade.pairlist.pairlistmanager import PairListManager -from freqtrade.exceptions import DependencyException, OperationalException from freqtrade.state import RunMode from tests.conftest import get_patched_exchange @@ -164,7 +164,7 @@ def test_ticker(mocker, default_conf, tickers): assert 'symbol' in res assert res['symbol'] == 'ETH/BTC' - ticker_mock = MagicMock(side_effect=DependencyException('Pair not found')) + ticker_mock = MagicMock(side_effect=ExchangeError('Pair not found')) mocker.patch("freqtrade.exchange.Exchange.fetch_ticker", ticker_mock) exchange = get_patched_exchange(mocker, default_conf) dp = DataProvider(default_conf, exchange) diff --git a/tests/exchange/test_exchange.py b/tests/exchange/test_exchange.py index 15ab0d3d9..64ea317aa 100644 --- a/tests/exchange/test_exchange.py +++ b/tests/exchange/test_exchange.py @@ -64,7 +64,7 @@ async def async_ccxt_exception(mocker, default_conf, api_mock, fun, mock_ccxt_fu with patch('freqtrade.exchange.common.asyncio.sleep'): with pytest.raises(DDosProtection): - api_mock.__dict__[mock_ccxt_fun] = MagicMock(side_effect=ccxt.DDoSProtection("DeadBeef")) + api_mock.__dict__[mock_ccxt_fun] = MagicMock(side_effect=ccxt.DDoSProtection("Dooh")) exchange = get_patched_exchange(mocker, default_conf, api_mock) await getattr(exchange, fun)(**kwargs) assert api_mock.__dict__[mock_ccxt_fun].call_count == retries diff --git a/tests/rpc/test_rpc.py b/tests/rpc/test_rpc.py index 0ffbaa72a..45243e5e6 100644 --- a/tests/rpc/test_rpc.py +++ b/tests/rpc/test_rpc.py @@ -8,12 +8,13 @@ import pytest from numpy import isnan from freqtrade.edge import PairInfo -from freqtrade.exceptions import DependencyException, TemporaryError +from freqtrade.exceptions import ExchangeError, TemporaryError from freqtrade.persistence import Trade from freqtrade.rpc import RPC, RPCException from freqtrade.rpc.fiat_convert import CryptoToFiatConverter from freqtrade.state import State -from tests.conftest import get_patched_freqtradebot, patch_get_signal, create_mock_trades +from tests.conftest import (create_mock_trades, get_patched_freqtradebot, + patch_get_signal) # Functions for recurrent object patching @@ -106,7 +107,7 @@ def test_rpc_trade_status(default_conf, ticker, fee, mocker) -> None: } mocker.patch('freqtrade.freqtradebot.FreqtradeBot.get_sell_rate', - MagicMock(side_effect=DependencyException("Pair 'ETH/BTC' not available"))) + MagicMock(side_effect=ExchangeError("Pair 'ETH/BTC' not available"))) results = rpc._rpc_trade_status() assert isnan(results[0]['current_profit']) assert isnan(results[0]['current_rate']) @@ -209,7 +210,7 @@ def test_rpc_status_table(default_conf, ticker, fee, mocker) -> None: assert '-0.41% (-0.06)' == result[0][3] mocker.patch('freqtrade.freqtradebot.FreqtradeBot.get_sell_rate', - MagicMock(side_effect=DependencyException("Pair 'ETH/BTC' not available"))) + MagicMock(side_effect=ExchangeError("Pair 'ETH/BTC' not available"))) result, headers = rpc._rpc_status_table(default_conf['stake_currency'], 'USD') assert 'instantly' == result[0][2] assert 'ETH/BTC' in result[0][1] @@ -365,7 +366,7 @@ def test_rpc_trade_statistics(default_conf, ticker, ticker_sell_up, fee, # Test non-available pair mocker.patch('freqtrade.freqtradebot.FreqtradeBot.get_sell_rate', - MagicMock(side_effect=DependencyException("Pair 'ETH/BTC' not available"))) + MagicMock(side_effect=ExchangeError("Pair 'ETH/BTC' not available"))) stats = rpc._rpc_trade_statistics(stake_currency, fiat_display_currency) assert stats['trade_count'] == 2 assert stats['first_trade_date'] == 'just now' diff --git a/tests/test_freqtradebot.py b/tests/test_freqtradebot.py index 5d83c893e..f2967411b 100644 --- a/tests/test_freqtradebot.py +++ b/tests/test_freqtradebot.py @@ -9,13 +9,12 @@ from unittest.mock import ANY, MagicMock, PropertyMock import arrow import pytest -import requests from freqtrade.constants import (CANCEL_REASON, MATH_CLOSE_PREC, UNLIMITED_STAKE_AMOUNT) -from freqtrade.exceptions import (DependencyException, InvalidOrderException, - OperationalException, PricingError, - TemporaryError) +from freqtrade.exceptions import (DependencyException, ExchangeError, + InvalidOrderException, OperationalException, + PricingError, TemporaryError) from freqtrade.freqtradebot import FreqtradeBot from freqtrade.persistence import Trade from freqtrade.rpc import RPCMessageType @@ -1172,7 +1171,7 @@ def test_handle_stoploss_on_exchange(mocker, default_conf, fee, caplog, mocker.patch( 'freqtrade.exchange.Exchange.stoploss', - side_effect=DependencyException() + side_effect=ExchangeError() ) trade.is_open = True freqtrade.handle_stoploss_on_exchange(trade) @@ -1216,7 +1215,7 @@ def test_handle_sle_cancel_cant_recreate(mocker, default_conf, fee, caplog, sell=MagicMock(return_value={'id': limit_sell_order['id']}), get_fee=fee, get_stoploss_order=MagicMock(return_value={'status': 'canceled'}), - stoploss=MagicMock(side_effect=DependencyException()), + stoploss=MagicMock(side_effect=ExchangeError()), ) freqtrade = FreqtradeBot(default_conf) patch_get_signal(freqtrade) @@ -1442,7 +1441,7 @@ def test_handle_stoploss_on_exchange_trailing_error(mocker, default_conf, fee, c # Fail creating stoploss order caplog.clear() cancel_mock = mocker.patch("freqtrade.exchange.Exchange.cancel_stoploss_order", MagicMock()) - mocker.patch("freqtrade.exchange.Exchange.stoploss", side_effect=DependencyException()) + mocker.patch("freqtrade.exchange.Exchange.stoploss", side_effect=ExchangeError()) freqtrade.handle_trailing_stoploss_on_exchange(trade, stoploss_order_hanging) assert cancel_mock.call_count == 1 assert log_has_re(r"Could not create trailing stoploss order for pair ETH/BTC\..*", caplog) @@ -2320,7 +2319,7 @@ def test_check_handle_timedout_exception(default_conf, ticker, open_trade, mocke mocker.patch.multiple( 'freqtrade.exchange.Exchange', fetch_ticker=ticker, - get_order=MagicMock(side_effect=requests.exceptions.RequestException('Oh snap')), + get_order=MagicMock(side_effect=ExchangeError('Oh snap')), cancel_order=cancel_order_mock ) freqtrade = FreqtradeBot(default_conf) From e040c518cac73e6ee1ad551757eef93407fc6fdc Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 28 Jun 2020 16:18:39 +0200 Subject: [PATCH 26/41] Dynamic backoff on DDos errors --- freqtrade/exchange/common.py | 11 +++++++++-- tests/exchange/test_exchange.py | 14 +++++++++++++- 2 files changed, 22 insertions(+), 3 deletions(-) diff --git a/freqtrade/exchange/common.py b/freqtrade/exchange/common.py index d931e1789..b0f6b14e3 100644 --- a/freqtrade/exchange/common.py +++ b/freqtrade/exchange/common.py @@ -91,6 +91,13 @@ MAP_EXCHANGE_CHILDCLASS = { } +def calculate_backoff(retry, max_retries): + """ + Calculate backoff + """ + return retry ** 2 + 1 + + def retrier_async(f): async def wrapper(*args, **kwargs): count = kwargs.pop('count', API_RETRY_COUNT) @@ -125,13 +132,13 @@ def retrier(_func=None, retries=API_RETRY_COUNT): kwargs.update({'count': count}) logger.warning('retrying %s() still for %s times', f.__name__, count) if isinstance(ex, DDosProtection): - time.sleep(1) + time.sleep(calculate_backoff(count, retries)) return wrapper(*args, **kwargs) else: logger.warning('Giving up retrying: %s()', f.__name__) raise ex return wrapper - # Support both @retrier and @retrier() syntax + # Support both @retrier and @retrier(retries=2) syntax if _func is None: return decorator else: diff --git a/tests/exchange/test_exchange.py b/tests/exchange/test_exchange.py index 64ea317aa..358452caf 100644 --- a/tests/exchange/test_exchange.py +++ b/tests/exchange/test_exchange.py @@ -14,7 +14,7 @@ from pandas import DataFrame from freqtrade.exceptions import (DependencyException, InvalidOrderException, DDosProtection, OperationalException, TemporaryError) from freqtrade.exchange import Binance, Exchange, Kraken -from freqtrade.exchange.common import API_RETRY_COUNT +from freqtrade.exchange.common import API_RETRY_COUNT, calculate_backoff from freqtrade.exchange.exchange import (market_is_active, symbol_is_pair, timeframe_to_minutes, timeframe_to_msecs, @@ -2296,3 +2296,15 @@ def test_calculate_fee_rate(mocker, default_conf, order, expected) -> None: ex = get_patched_exchange(mocker, default_conf) assert ex.calculate_fee_rate(order) == expected + + +@pytest.mark.parametrize('retry,max_retries,expected', [ + (0, 3, 1), + (1, 3, 2), + (2, 3, 5), + (3, 3, 10), + (0, 1, 1), + (1, 1, 2), +]) +def test_calculate_backoff(retry, max_retries, expected): + assert calculate_backoff(retry, max_retries) == expected From 92c70fb903f59d9651f50efb3061f36a32d13cf3 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 28 Jun 2020 16:27:35 +0200 Subject: [PATCH 27/41] Rename get_order to fetch_order (to align to ccxt naming) --- freqtrade/exchange/exchange.py | 10 +++--- freqtrade/freqtradebot.py | 6 ++-- freqtrade/persistence.py | 2 +- freqtrade/rpc/rpc.py | 4 +-- tests/exchange/test_exchange.py | 12 +++---- tests/rpc/test_rpc.py | 8 ++--- tests/test_freqtradebot.py | 60 ++++++++++++++++----------------- 7 files changed, 51 insertions(+), 51 deletions(-) diff --git a/freqtrade/exchange/exchange.py b/freqtrade/exchange/exchange.py index d48e47909..5a19b34af 100644 --- a/freqtrade/exchange/exchange.py +++ b/freqtrade/exchange/exchange.py @@ -946,7 +946,7 @@ class Exchange: def check_order_canceled_empty(self, order: Dict) -> bool: """ Verify if an order has been cancelled without being partially filled - :param order: Order dict as returned from get_order() + :param order: Order dict as returned from fetch_order() :return: True if order has been cancelled without being filled, False otherwise. """ return order.get('status') in ('closed', 'canceled') and order.get('filled') == 0.0 @@ -983,7 +983,7 @@ class Exchange: """ Cancel order returning a result. Creates a fake result if cancel order returns a non-usable result - and get_order does not work (certain exchanges don't return cancelled orders) + and fetch_order does not work (certain exchanges don't return cancelled orders) :param order_id: Orderid to cancel :param pair: Pair corresponding to order_id :param amount: Amount to use for fake response @@ -996,7 +996,7 @@ class Exchange: except InvalidOrderException: logger.warning(f"Could not cancel order {order_id}.") try: - order = self.get_order(order_id, pair) + order = self.fetch_order(order_id, pair) except InvalidOrderException: logger.warning(f"Could not fetch cancelled order {order_id}.") order = {'fee': {}, 'status': 'canceled', 'amount': amount, 'info': {}} @@ -1004,7 +1004,7 @@ class Exchange: return order @retrier - def get_order(self, order_id: str, pair: str) -> Dict: + def fetch_order(self, order_id: str, pair: str) -> Dict: if self._config['dry_run']: try: order = self._dry_run_open_orders[order_id] @@ -1027,7 +1027,7 @@ class Exchange: raise OperationalException(e) from e # Assign method to get_stoploss_order to allow easy overriding in other classes - get_stoploss_order = get_order + get_stoploss_order = fetch_order @retrier def fetch_l2_order_book(self, pair: str, limit: int = 100) -> dict: diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index 2e59d915d..0d9c5e27c 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -890,7 +890,7 @@ class FreqtradeBot: try: if not trade.open_order_id: continue - order = self.exchange.get_order(trade.open_order_id, trade.pair) + order = self.exchange.fetch_order(trade.open_order_id, trade.pair) except (ExchangeError, InvalidOrderException): logger.info('Cannot query order for %s due to %s', trade, traceback.format_exc()) continue @@ -923,7 +923,7 @@ class FreqtradeBot: for trade in Trade.get_open_order_trades(): try: - order = self.exchange.get_order(trade.open_order_id, trade.pair) + order = self.exchange.fetch_order(trade.open_order_id, trade.pair) except (DependencyException, InvalidOrderException): logger.info('Cannot query order for %s due to %s', trade, traceback.format_exc()) continue @@ -1202,7 +1202,7 @@ class FreqtradeBot: # Update trade with order values logger.info('Found open order for %s', trade) try: - order = action_order or self.exchange.get_order(order_id, trade.pair) + order = action_order or self.exchange.fetch_order(order_id, trade.pair) except InvalidOrderException as exception: logger.warning('Unable to fetch order %s: %s', order_id, exception) return False diff --git a/freqtrade/persistence.py b/freqtrade/persistence.py index 097a2f984..a6c1de402 100644 --- a/freqtrade/persistence.py +++ b/freqtrade/persistence.py @@ -360,7 +360,7 @@ class Trade(_DECL_BASE): def update(self, order: Dict) -> None: """ Updates this entity with amount and actual open/close rates. - :param order: order retrieved by exchange.get_order() + :param order: order retrieved by exchange.fetch_order() :return: None """ order_type = order['type'] diff --git a/freqtrade/rpc/rpc.py b/freqtrade/rpc/rpc.py index aab3da258..cc5f35f0d 100644 --- a/freqtrade/rpc/rpc.py +++ b/freqtrade/rpc/rpc.py @@ -126,7 +126,7 @@ class RPC: for trade in trades: order = None if trade.open_order_id: - order = self._freqtrade.exchange.get_order(trade.open_order_id, trade.pair) + order = self._freqtrade.exchange.fetch_order(trade.open_order_id, trade.pair) # calculate profit and send message to user try: current_rate = self._freqtrade.get_sell_rate(trade.pair, False) @@ -442,7 +442,7 @@ class RPC: def _exec_forcesell(trade: Trade) -> None: # Check if there is there is an open order if trade.open_order_id: - order = self._freqtrade.exchange.get_order(trade.open_order_id, trade.pair) + order = self._freqtrade.exchange.fetch_order(trade.open_order_id, trade.pair) # Cancel open LIMIT_BUY orders and close trade if order and order['status'] == 'open' \ diff --git a/tests/exchange/test_exchange.py b/tests/exchange/test_exchange.py index 358452caf..cf38b3cd5 100644 --- a/tests/exchange/test_exchange.py +++ b/tests/exchange/test_exchange.py @@ -1865,31 +1865,31 @@ def test_cancel_stoploss_order(default_conf, mocker, exchange_name): @pytest.mark.parametrize("exchange_name", EXCHANGES) -def test_get_order(default_conf, mocker, exchange_name): +def test_fetch_order(default_conf, mocker, exchange_name): default_conf['dry_run'] = True order = MagicMock() order.myid = 123 exchange = get_patched_exchange(mocker, default_conf, id=exchange_name) exchange._dry_run_open_orders['X'] = order - assert exchange.get_order('X', 'TKN/BTC').myid == 123 + assert exchange.fetch_order('X', 'TKN/BTC').myid == 123 with pytest.raises(InvalidOrderException, match=r'Tried to get an invalid dry-run-order.*'): - exchange.get_order('Y', 'TKN/BTC') + exchange.fetch_order('Y', 'TKN/BTC') default_conf['dry_run'] = False api_mock = MagicMock() api_mock.fetch_order = MagicMock(return_value=456) exchange = get_patched_exchange(mocker, default_conf, api_mock, id=exchange_name) - assert exchange.get_order('X', 'TKN/BTC') == 456 + assert exchange.fetch_order('X', 'TKN/BTC') == 456 with pytest.raises(InvalidOrderException): api_mock.fetch_order = MagicMock(side_effect=ccxt.InvalidOrder("Order not found")) exchange = get_patched_exchange(mocker, default_conf, api_mock, id=exchange_name) - exchange.get_order(order_id='_', pair='TKN/BTC') + exchange.fetch_order(order_id='_', pair='TKN/BTC') assert api_mock.fetch_order.call_count == 1 ccxt_exceptionhandlers(mocker, default_conf, api_mock, exchange_name, - 'get_order', 'fetch_order', + 'fetch_order', 'fetch_order', order_id='_', pair='TKN/BTC') diff --git a/tests/rpc/test_rpc.py b/tests/rpc/test_rpc.py index 45243e5e6..de9327ba9 100644 --- a/tests/rpc/test_rpc.py +++ b/tests/rpc/test_rpc.py @@ -607,7 +607,7 @@ def test_rpc_forcesell(default_conf, ticker, fee, mocker) -> None: 'freqtrade.exchange.Exchange', fetch_ticker=ticker, cancel_order=cancel_order_mock, - get_order=MagicMock( + fetch_order=MagicMock( return_value={ 'status': 'closed', 'type': 'limit', @@ -653,7 +653,7 @@ def test_rpc_forcesell(default_conf, ticker, fee, mocker) -> None: trade = Trade.query.filter(Trade.id == '1').first() filled_amount = trade.amount / 2 mocker.patch( - 'freqtrade.exchange.Exchange.get_order', + 'freqtrade.exchange.Exchange.fetch_order', return_value={ 'status': 'open', 'type': 'limit', @@ -672,7 +672,7 @@ def test_rpc_forcesell(default_conf, ticker, fee, mocker) -> None: amount = trade.amount # make an limit-buy open trade, if there is no 'filled', don't sell it mocker.patch( - 'freqtrade.exchange.Exchange.get_order', + 'freqtrade.exchange.Exchange.fetch_order', return_value={ 'status': 'open', 'type': 'limit', @@ -689,7 +689,7 @@ def test_rpc_forcesell(default_conf, ticker, fee, mocker) -> None: freqtradebot.enter_positions() # make an limit-sell open trade mocker.patch( - 'freqtrade.exchange.Exchange.get_order', + 'freqtrade.exchange.Exchange.fetch_order', return_value={ 'status': 'open', 'type': 'limit', diff --git a/tests/test_freqtradebot.py b/tests/test_freqtradebot.py index f2967411b..3b00f3371 100644 --- a/tests/test_freqtradebot.py +++ b/tests/test_freqtradebot.py @@ -762,7 +762,7 @@ def test_process_trade_creation(default_conf, ticker, limit_buy_order, 'freqtrade.exchange.Exchange', fetch_ticker=ticker, buy=MagicMock(return_value={'id': limit_buy_order['id']}), - get_order=MagicMock(return_value=limit_buy_order), + fetch_order=MagicMock(return_value=limit_buy_order), get_fee=fee, ) freqtrade = FreqtradeBot(default_conf) @@ -831,7 +831,7 @@ def test_process_trade_handling(default_conf, ticker, limit_buy_order, fee, mock 'freqtrade.exchange.Exchange', fetch_ticker=ticker, buy=MagicMock(return_value={'id': limit_buy_order['id']}), - get_order=MagicMock(return_value=limit_buy_order), + fetch_order=MagicMock(return_value=limit_buy_order), get_fee=fee, ) freqtrade = FreqtradeBot(default_conf) @@ -858,7 +858,7 @@ def test_process_trade_no_whitelist_pair(default_conf, ticker, limit_buy_order, 'freqtrade.exchange.Exchange', fetch_ticker=ticker, buy=MagicMock(return_value={'id': limit_buy_order['id']}), - get_order=MagicMock(return_value=limit_buy_order), + fetch_order=MagicMock(return_value=limit_buy_order), get_fee=fee, ) freqtrade = FreqtradeBot(default_conf) @@ -1063,7 +1063,7 @@ def test_add_stoploss_on_exchange(mocker, default_conf, limit_buy_order) -> None patch_RPCManager(mocker) patch_exchange(mocker) mocker.patch('freqtrade.freqtradebot.FreqtradeBot.handle_trade', MagicMock(return_value=True)) - mocker.patch('freqtrade.exchange.Exchange.get_order', return_value=limit_buy_order) + mocker.patch('freqtrade.exchange.Exchange.fetch_order', return_value=limit_buy_order) mocker.patch('freqtrade.exchange.Exchange.get_trades_for_order', return_value=[]) mocker.patch('freqtrade.freqtradebot.FreqtradeBot.get_real_amount', return_value=limit_buy_order['amount']) @@ -1178,7 +1178,7 @@ def test_handle_stoploss_on_exchange(mocker, default_conf, fee, caplog, assert log_has('Unable to place a stoploss order on exchange.', caplog) assert trade.stoploss_order_id is None - # Fifth case: get_order returns InvalidOrder + # Fifth case: fetch_order returns InvalidOrder # It should try to add stoploss order trade.stoploss_order_id = 100 stoploss.reset_mock() @@ -1193,7 +1193,7 @@ def test_handle_stoploss_on_exchange(mocker, default_conf, fee, caplog, trade.stoploss_order_id = None trade.is_open = False stoploss.reset_mock() - mocker.patch('freqtrade.exchange.Exchange.get_order') + mocker.patch('freqtrade.exchange.Exchange.fetch_order') mocker.patch('freqtrade.exchange.Exchange.stoploss', stoploss) assert freqtrade.handle_stoploss_on_exchange(trade) is False assert stoploss.call_count == 0 @@ -1248,7 +1248,7 @@ def test_create_stoploss_order_invalid_order(mocker, default_conf, caplog, fee, buy=MagicMock(return_value={'id': limit_buy_order['id']}), sell=sell_mock, get_fee=fee, - get_order=MagicMock(return_value={'status': 'canceled'}), + fetch_order=MagicMock(return_value={'status': 'canceled'}), stoploss=MagicMock(side_effect=InvalidOrderException()), ) freqtrade = FreqtradeBot(default_conf) @@ -1588,7 +1588,7 @@ def test_exit_positions(mocker, default_conf, limit_buy_order, caplog) -> None: freqtrade = get_patched_freqtradebot(mocker, default_conf) mocker.patch('freqtrade.freqtradebot.FreqtradeBot.handle_trade', MagicMock(return_value=True)) - mocker.patch('freqtrade.exchange.Exchange.get_order', return_value=limit_buy_order) + mocker.patch('freqtrade.exchange.Exchange.fetch_order', return_value=limit_buy_order) mocker.patch('freqtrade.exchange.Exchange.get_trades_for_order', return_value=[]) mocker.patch('freqtrade.freqtradebot.FreqtradeBot.get_real_amount', return_value=limit_buy_order['amount']) @@ -1612,7 +1612,7 @@ def test_exit_positions(mocker, default_conf, limit_buy_order, caplog) -> None: def test_exit_positions_exception(mocker, default_conf, limit_buy_order, caplog) -> None: freqtrade = get_patched_freqtradebot(mocker, default_conf) - mocker.patch('freqtrade.exchange.Exchange.get_order', return_value=limit_buy_order) + mocker.patch('freqtrade.exchange.Exchange.fetch_order', return_value=limit_buy_order) trade = MagicMock() trade.open_order_id = None @@ -1633,7 +1633,7 @@ def test_update_trade_state(mocker, default_conf, limit_buy_order, caplog) -> No freqtrade = get_patched_freqtradebot(mocker, default_conf) mocker.patch('freqtrade.freqtradebot.FreqtradeBot.handle_trade', MagicMock(return_value=True)) - mocker.patch('freqtrade.exchange.Exchange.get_order', return_value=limit_buy_order) + mocker.patch('freqtrade.exchange.Exchange.fetch_order', return_value=limit_buy_order) mocker.patch('freqtrade.exchange.Exchange.get_trades_for_order', return_value=[]) mocker.patch('freqtrade.freqtradebot.FreqtradeBot.get_real_amount', return_value=limit_buy_order['amount']) @@ -1672,8 +1672,8 @@ def test_update_trade_state(mocker, default_conf, limit_buy_order, caplog) -> No def test_update_trade_state_withorderdict(default_conf, trades_for_order, limit_buy_order, fee, mocker): mocker.patch('freqtrade.exchange.Exchange.get_trades_for_order', return_value=trades_for_order) - # get_order should not be called!! - mocker.patch('freqtrade.exchange.Exchange.get_order', MagicMock(side_effect=ValueError)) + # fetch_order should not be called!! + mocker.patch('freqtrade.exchange.Exchange.fetch_order', MagicMock(side_effect=ValueError)) patch_exchange(mocker) Trade.session = MagicMock() amount = sum(x['amount'] for x in trades_for_order) @@ -1697,8 +1697,8 @@ def test_update_trade_state_withorderdict_rounding_fee(default_conf, trades_for_ limit_buy_order, mocker, caplog): trades_for_order[0]['amount'] = limit_buy_order['amount'] + 1e-14 mocker.patch('freqtrade.exchange.Exchange.get_trades_for_order', return_value=trades_for_order) - # get_order should not be called!! - mocker.patch('freqtrade.exchange.Exchange.get_order', MagicMock(side_effect=ValueError)) + # fetch_order should not be called!! + mocker.patch('freqtrade.exchange.Exchange.fetch_order', MagicMock(side_effect=ValueError)) patch_exchange(mocker) Trade.session = MagicMock() amount = sum(x['amount'] for x in trades_for_order) @@ -1723,7 +1723,7 @@ def test_update_trade_state_withorderdict_rounding_fee(default_conf, trades_for_ def test_update_trade_state_exception(mocker, default_conf, limit_buy_order, caplog) -> None: freqtrade = get_patched_freqtradebot(mocker, default_conf) - mocker.patch('freqtrade.exchange.Exchange.get_order', return_value=limit_buy_order) + mocker.patch('freqtrade.exchange.Exchange.fetch_order', return_value=limit_buy_order) trade = MagicMock() trade.open_order_id = '123' @@ -1740,7 +1740,7 @@ def test_update_trade_state_exception(mocker, default_conf, def test_update_trade_state_orderexception(mocker, default_conf, caplog) -> None: freqtrade = get_patched_freqtradebot(mocker, default_conf) - mocker.patch('freqtrade.exchange.Exchange.get_order', + mocker.patch('freqtrade.exchange.Exchange.fetch_order', MagicMock(side_effect=InvalidOrderException)) trade = MagicMock() @@ -1756,8 +1756,8 @@ def test_update_trade_state_orderexception(mocker, default_conf, caplog) -> None def test_update_trade_state_sell(default_conf, trades_for_order, limit_sell_order, mocker): mocker.patch('freqtrade.exchange.Exchange.get_trades_for_order', return_value=trades_for_order) - # get_order should not be called!! - mocker.patch('freqtrade.exchange.Exchange.get_order', MagicMock(side_effect=ValueError)) + # fetch_order should not be called!! + mocker.patch('freqtrade.exchange.Exchange.fetch_order', MagicMock(side_effect=ValueError)) wallet_mock = MagicMock() mocker.patch('freqtrade.wallets.Wallets.update', wallet_mock) @@ -1972,7 +1972,7 @@ def test_check_handle_timedout_buy_usercustom(default_conf, ticker, limit_buy_or mocker.patch.multiple( 'freqtrade.exchange.Exchange', fetch_ticker=ticker, - get_order=MagicMock(return_value=limit_buy_order_old), + fetch_order=MagicMock(return_value=limit_buy_order_old), cancel_order=cancel_order_mock, get_fee=fee ) @@ -2021,7 +2021,7 @@ def test_check_handle_timedout_buy(default_conf, ticker, limit_buy_order_old, op mocker.patch.multiple( 'freqtrade.exchange.Exchange', fetch_ticker=ticker, - get_order=MagicMock(return_value=limit_buy_order_old), + fetch_order=MagicMock(return_value=limit_buy_order_old), cancel_order_with_result=cancel_order_mock, get_fee=fee ) @@ -2051,7 +2051,7 @@ def test_check_handle_cancelled_buy(default_conf, ticker, limit_buy_order_old, o mocker.patch.multiple( 'freqtrade.exchange.Exchange', fetch_ticker=ticker, - get_order=MagicMock(return_value=limit_buy_order_old), + fetch_order=MagicMock(return_value=limit_buy_order_old), cancel_order=cancel_order_mock, get_fee=fee ) @@ -2078,7 +2078,7 @@ def test_check_handle_timedout_buy_exception(default_conf, ticker, limit_buy_ord 'freqtrade.exchange.Exchange', validate_pairs=MagicMock(), fetch_ticker=ticker, - get_order=MagicMock(side_effect=DependencyException), + fetch_order=MagicMock(side_effect=DependencyException), cancel_order=cancel_order_mock, get_fee=fee ) @@ -2104,7 +2104,7 @@ def test_check_handle_timedout_sell_usercustom(default_conf, ticker, limit_sell_ mocker.patch.multiple( 'freqtrade.exchange.Exchange', fetch_ticker=ticker, - get_order=MagicMock(return_value=limit_sell_order_old), + fetch_order=MagicMock(return_value=limit_sell_order_old), cancel_order=cancel_order_mock ) freqtrade = FreqtradeBot(default_conf) @@ -2151,7 +2151,7 @@ def test_check_handle_timedout_sell(default_conf, ticker, limit_sell_order_old, mocker.patch.multiple( 'freqtrade.exchange.Exchange', fetch_ticker=ticker, - get_order=MagicMock(return_value=limit_sell_order_old), + fetch_order=MagicMock(return_value=limit_sell_order_old), cancel_order=cancel_order_mock ) freqtrade = FreqtradeBot(default_conf) @@ -2182,7 +2182,7 @@ def test_check_handle_cancelled_sell(default_conf, ticker, limit_sell_order_old, mocker.patch.multiple( 'freqtrade.exchange.Exchange', fetch_ticker=ticker, - get_order=MagicMock(return_value=limit_sell_order_old), + fetch_order=MagicMock(return_value=limit_sell_order_old), cancel_order_with_result=cancel_order_mock ) freqtrade = FreqtradeBot(default_conf) @@ -2209,7 +2209,7 @@ def test_check_handle_timedout_partial(default_conf, ticker, limit_buy_order_old mocker.patch.multiple( 'freqtrade.exchange.Exchange', fetch_ticker=ticker, - get_order=MagicMock(return_value=limit_buy_order_old_partial), + fetch_order=MagicMock(return_value=limit_buy_order_old_partial), cancel_order_with_result=cancel_order_mock ) freqtrade = FreqtradeBot(default_conf) @@ -2237,7 +2237,7 @@ def test_check_handle_timedout_partial_fee(default_conf, ticker, open_trade, cap mocker.patch.multiple( 'freqtrade.exchange.Exchange', fetch_ticker=ticker, - get_order=MagicMock(return_value=limit_buy_order_old_partial), + fetch_order=MagicMock(return_value=limit_buy_order_old_partial), cancel_order_with_result=cancel_order_mock, get_trades_for_order=MagicMock(return_value=trades_for_order), ) @@ -2275,7 +2275,7 @@ def test_check_handle_timedout_partial_except(default_conf, ticker, open_trade, mocker.patch.multiple( 'freqtrade.exchange.Exchange', fetch_ticker=ticker, - get_order=MagicMock(return_value=limit_buy_order_old_partial), + fetch_order=MagicMock(return_value=limit_buy_order_old_partial), cancel_order_with_result=cancel_order_mock, get_trades_for_order=MagicMock(return_value=trades_for_order), ) @@ -2319,7 +2319,7 @@ def test_check_handle_timedout_exception(default_conf, ticker, open_trade, mocke mocker.patch.multiple( 'freqtrade.exchange.Exchange', fetch_ticker=ticker, - get_order=MagicMock(side_effect=ExchangeError('Oh snap')), + fetch_order=MagicMock(side_effect=ExchangeError('Oh snap')), cancel_order=cancel_order_mock ) freqtrade = FreqtradeBot(default_conf) @@ -4016,7 +4016,7 @@ def test_sync_wallet_dry_run(mocker, default_conf, ticker, fee, limit_buy_order, @pytest.mark.usefixtures("init_persistence") def test_cancel_all_open_orders(mocker, default_conf, fee, limit_buy_order, limit_sell_order): default_conf['cancel_open_orders_on_exit'] = True - mocker.patch('freqtrade.exchange.Exchange.get_order', + mocker.patch('freqtrade.exchange.Exchange.fetch_order', side_effect=[DependencyException(), limit_sell_order, limit_buy_order]) buy_mock = mocker.patch('freqtrade.freqtradebot.FreqtradeBot.handle_cancel_buy') sell_mock = mocker.patch('freqtrade.freqtradebot.FreqtradeBot.handle_cancel_sell') From cbcbb4bdb533247076b338035b640dc0f29f0495 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 28 Jun 2020 16:30:24 +0200 Subject: [PATCH 28/41] Rename get_stoploss_order to fetch_stoploss_order (align with fetch_order) --- freqtrade/exchange/exchange.py | 6 +++--- freqtrade/exchange/ftx.py | 2 +- freqtrade/freqtradebot.py | 4 ++-- tests/exchange/test_exchange.py | 12 ++++++------ tests/exchange/test_ftx.py | 14 +++++++------- tests/test_freqtradebot.py | 18 +++++++++--------- tests/test_integration.py | 2 +- 7 files changed, 29 insertions(+), 29 deletions(-) diff --git a/freqtrade/exchange/exchange.py b/freqtrade/exchange/exchange.py index 5a19b34af..daa73ca35 100644 --- a/freqtrade/exchange/exchange.py +++ b/freqtrade/exchange/exchange.py @@ -969,7 +969,7 @@ class Exchange: except ccxt.BaseError as e: raise OperationalException(e) from e - # Assign method to get_stoploss_order to allow easy overriding in other classes + # Assign method to fetch_stoploss_order to allow easy overriding in other classes cancel_stoploss_order = cancel_order def is_cancel_order_result_suitable(self, corder) -> bool: @@ -1026,8 +1026,8 @@ class Exchange: except ccxt.BaseError as e: raise OperationalException(e) from e - # Assign method to get_stoploss_order to allow easy overriding in other classes - get_stoploss_order = fetch_order + # Assign method to fetch_stoploss_order to allow easy overriding in other classes + fetch_stoploss_order = fetch_order @retrier def fetch_l2_order_book(self, pair: str, limit: int = 100) -> dict: diff --git a/freqtrade/exchange/ftx.py b/freqtrade/exchange/ftx.py index be815d336..b75f77ca4 100644 --- a/freqtrade/exchange/ftx.py +++ b/freqtrade/exchange/ftx.py @@ -79,7 +79,7 @@ class Ftx(Exchange): raise OperationalException(e) from e @retrier - def get_stoploss_order(self, order_id: str, pair: str) -> Dict: + def fetch_stoploss_order(self, order_id: str, pair: str) -> Dict: if self._config['dry_run']: try: order = self._dry_run_open_orders[order_id] diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index 0d9c5e27c..bd6bba344 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -773,8 +773,8 @@ class FreqtradeBot: try: # First we check if there is already a stoploss on exchange - stoploss_order = self.exchange.get_stoploss_order(trade.stoploss_order_id, trade.pair) \ - if trade.stoploss_order_id else None + stoploss_order = self.exchange.fetch_stoploss_order( + trade.stoploss_order_id, trade.pair) if trade.stoploss_order_id else None except InvalidOrderException as exception: logger.warning('Unable to fetch stoploss order: %s', exception) diff --git a/tests/exchange/test_exchange.py b/tests/exchange/test_exchange.py index cf38b3cd5..fc4bf490c 100644 --- a/tests/exchange/test_exchange.py +++ b/tests/exchange/test_exchange.py @@ -1894,7 +1894,7 @@ def test_fetch_order(default_conf, mocker, exchange_name): @pytest.mark.parametrize("exchange_name", EXCHANGES) -def test_get_stoploss_order(default_conf, mocker, exchange_name): +def test_fetch_stoploss_order(default_conf, mocker, exchange_name): # Don't test FTX here - that needs a seperate test if exchange_name == 'ftx': return @@ -1903,25 +1903,25 @@ def test_get_stoploss_order(default_conf, mocker, exchange_name): order.myid = 123 exchange = get_patched_exchange(mocker, default_conf, id=exchange_name) exchange._dry_run_open_orders['X'] = order - assert exchange.get_stoploss_order('X', 'TKN/BTC').myid == 123 + assert exchange.fetch_stoploss_order('X', 'TKN/BTC').myid == 123 with pytest.raises(InvalidOrderException, match=r'Tried to get an invalid dry-run-order.*'): - exchange.get_stoploss_order('Y', 'TKN/BTC') + exchange.fetch_stoploss_order('Y', 'TKN/BTC') default_conf['dry_run'] = False api_mock = MagicMock() api_mock.fetch_order = MagicMock(return_value=456) exchange = get_patched_exchange(mocker, default_conf, api_mock, id=exchange_name) - assert exchange.get_stoploss_order('X', 'TKN/BTC') == 456 + assert exchange.fetch_stoploss_order('X', 'TKN/BTC') == 456 with pytest.raises(InvalidOrderException): api_mock.fetch_order = MagicMock(side_effect=ccxt.InvalidOrder("Order not found")) exchange = get_patched_exchange(mocker, default_conf, api_mock, id=exchange_name) - exchange.get_stoploss_order(order_id='_', pair='TKN/BTC') + exchange.fetch_stoploss_order(order_id='_', pair='TKN/BTC') assert api_mock.fetch_order.call_count == 1 ccxt_exceptionhandlers(mocker, default_conf, api_mock, exchange_name, - 'get_stoploss_order', 'fetch_order', + 'fetch_stoploss_order', 'fetch_order', order_id='_', pair='TKN/BTC') diff --git a/tests/exchange/test_ftx.py b/tests/exchange/test_ftx.py index 1b7d68770..eb7d83be3 100644 --- a/tests/exchange/test_ftx.py +++ b/tests/exchange/test_ftx.py @@ -124,34 +124,34 @@ def test_stoploss_adjust_ftx(mocker, default_conf): assert not exchange.stoploss_adjust(1501, order) -def test_get_stoploss_order(default_conf, mocker): +def test_fetch_stoploss_order(default_conf, mocker): default_conf['dry_run'] = True order = MagicMock() order.myid = 123 exchange = get_patched_exchange(mocker, default_conf, id='ftx') exchange._dry_run_open_orders['X'] = order - assert exchange.get_stoploss_order('X', 'TKN/BTC').myid == 123 + assert exchange.fetch_stoploss_order('X', 'TKN/BTC').myid == 123 with pytest.raises(InvalidOrderException, match=r'Tried to get an invalid dry-run-order.*'): - exchange.get_stoploss_order('Y', 'TKN/BTC') + exchange.fetch_stoploss_order('Y', 'TKN/BTC') default_conf['dry_run'] = False api_mock = MagicMock() api_mock.fetch_orders = MagicMock(return_value=[{'id': 'X', 'status': '456'}]) exchange = get_patched_exchange(mocker, default_conf, api_mock, id='ftx') - assert exchange.get_stoploss_order('X', 'TKN/BTC')['status'] == '456' + assert exchange.fetch_stoploss_order('X', 'TKN/BTC')['status'] == '456' api_mock.fetch_orders = MagicMock(return_value=[{'id': 'Y', 'status': '456'}]) exchange = get_patched_exchange(mocker, default_conf, api_mock, id='ftx') with pytest.raises(InvalidOrderException, match=r"Could not get stoploss order for id X"): - exchange.get_stoploss_order('X', 'TKN/BTC')['status'] + exchange.fetch_stoploss_order('X', 'TKN/BTC')['status'] with pytest.raises(InvalidOrderException): api_mock.fetch_orders = MagicMock(side_effect=ccxt.InvalidOrder("Order not found")) exchange = get_patched_exchange(mocker, default_conf, api_mock, id='ftx') - exchange.get_stoploss_order(order_id='_', pair='TKN/BTC') + exchange.fetch_stoploss_order(order_id='_', pair='TKN/BTC') assert api_mock.fetch_orders.call_count == 1 ccxt_exceptionhandlers(mocker, default_conf, api_mock, 'ftx', - 'get_stoploss_order', 'fetch_orders', + 'fetch_stoploss_order', 'fetch_orders', order_id='_', pair='TKN/BTC') diff --git a/tests/test_freqtradebot.py b/tests/test_freqtradebot.py index 3b00f3371..ef48bdc34 100644 --- a/tests/test_freqtradebot.py +++ b/tests/test_freqtradebot.py @@ -1125,7 +1125,7 @@ def test_handle_stoploss_on_exchange(mocker, default_conf, fee, caplog, trade.stoploss_order_id = 100 hanging_stoploss_order = MagicMock(return_value={'status': 'open'}) - mocker.patch('freqtrade.exchange.Exchange.get_stoploss_order', hanging_stoploss_order) + mocker.patch('freqtrade.exchange.Exchange.fetch_stoploss_order', hanging_stoploss_order) assert freqtrade.handle_stoploss_on_exchange(trade) is False assert trade.stoploss_order_id == 100 @@ -1138,7 +1138,7 @@ def test_handle_stoploss_on_exchange(mocker, default_conf, fee, caplog, trade.stoploss_order_id = 100 canceled_stoploss_order = MagicMock(return_value={'status': 'canceled'}) - mocker.patch('freqtrade.exchange.Exchange.get_stoploss_order', canceled_stoploss_order) + mocker.patch('freqtrade.exchange.Exchange.fetch_stoploss_order', canceled_stoploss_order) stoploss.reset_mock() assert freqtrade.handle_stoploss_on_exchange(trade) is False @@ -1163,7 +1163,7 @@ def test_handle_stoploss_on_exchange(mocker, default_conf, fee, caplog, 'average': 2, 'amount': limit_buy_order['amount'], }) - mocker.patch('freqtrade.exchange.Exchange.get_stoploss_order', stoploss_order_hit) + mocker.patch('freqtrade.exchange.Exchange.fetch_stoploss_order', stoploss_order_hit) assert freqtrade.handle_stoploss_on_exchange(trade) is True assert log_has('STOP_LOSS_LIMIT is hit for {}.'.format(trade), caplog) assert trade.stoploss_order_id is None @@ -1182,7 +1182,7 @@ def test_handle_stoploss_on_exchange(mocker, default_conf, fee, caplog, # It should try to add stoploss order trade.stoploss_order_id = 100 stoploss.reset_mock() - mocker.patch('freqtrade.exchange.Exchange.get_stoploss_order', + mocker.patch('freqtrade.exchange.Exchange.fetch_stoploss_order', side_effect=InvalidOrderException()) mocker.patch('freqtrade.exchange.Exchange.stoploss', stoploss) freqtrade.handle_stoploss_on_exchange(trade) @@ -1214,7 +1214,7 @@ def test_handle_sle_cancel_cant_recreate(mocker, default_conf, fee, caplog, buy=MagicMock(return_value={'id': limit_buy_order['id']}), sell=MagicMock(return_value={'id': limit_sell_order['id']}), get_fee=fee, - get_stoploss_order=MagicMock(return_value={'status': 'canceled'}), + fetch_stoploss_order=MagicMock(return_value={'status': 'canceled'}), stoploss=MagicMock(side_effect=ExchangeError()), ) freqtrade = FreqtradeBot(default_conf) @@ -1331,7 +1331,7 @@ def test_handle_stoploss_on_exchange_trailing(mocker, default_conf, fee, caplog, } }) - mocker.patch('freqtrade.exchange.Exchange.get_stoploss_order', stoploss_order_hanging) + mocker.patch('freqtrade.exchange.Exchange.fetch_stoploss_order', stoploss_order_hanging) # stoploss initially at 5% assert freqtrade.handle_trade(trade) is False @@ -1431,7 +1431,7 @@ def test_handle_stoploss_on_exchange_trailing_error(mocker, default_conf, fee, c } mocker.patch('freqtrade.exchange.Exchange.cancel_stoploss_order', side_effect=InvalidOrderException()) - mocker.patch('freqtrade.exchange.Exchange.get_stoploss_order', stoploss_order_hanging) + mocker.patch('freqtrade.exchange.Exchange.fetch_stoploss_order', stoploss_order_hanging) freqtrade.handle_trailing_stoploss_on_exchange(trade, stoploss_order_hanging) assert log_has_re(r"Could not cancel stoploss order abcd for pair ETH/BTC.*", caplog) @@ -1511,7 +1511,7 @@ def test_tsl_on_exchange_compatible_with_edge(mocker, edge_conf, fee, caplog, } }) - mocker.patch('freqtrade.exchange.Exchange.get_stoploss_order', stoploss_order_hanging) + mocker.patch('freqtrade.exchange.Exchange.fetch_stoploss_order', stoploss_order_hanging) # stoploss initially at 20% as edge dictated it. assert freqtrade.handle_trade(trade) is False @@ -2773,7 +2773,7 @@ def test_may_execute_sell_after_stoploss_on_exchange_hit(default_conf, ticker, f "fee": None, "trades": None }) - mocker.patch('freqtrade.exchange.Exchange.get_stoploss_order', stoploss_executed) + mocker.patch('freqtrade.exchange.Exchange.fetch_stoploss_order', stoploss_executed) freqtrade.exit_positions(trades) assert trade.stoploss_order_id is None diff --git a/tests/test_integration.py b/tests/test_integration.py index 57960503e..168286e6d 100644 --- a/tests/test_integration.py +++ b/tests/test_integration.py @@ -62,7 +62,7 @@ def test_may_execute_sell_stoploss_on_exchange_multi(default_conf, ticker, fee, get_fee=fee, amount_to_precision=lambda s, x, y: y, price_to_precision=lambda s, x, y: y, - get_stoploss_order=stoploss_order_mock, + fetch_stoploss_order=stoploss_order_mock, cancel_stoploss_order=cancel_order_mock, ) From 6362bfc36ef843c60046a94de8184b51a09f64e7 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 28 Jun 2020 19:40:33 +0200 Subject: [PATCH 29/41] Fix calculate_backoff implementation --- freqtrade/exchange/common.py | 4 ++-- tests/exchange/test_exchange.py | 18 +++++++++--------- 2 files changed, 11 insertions(+), 11 deletions(-) diff --git a/freqtrade/exchange/common.py b/freqtrade/exchange/common.py index b0f6b14e3..d3ba8cef6 100644 --- a/freqtrade/exchange/common.py +++ b/freqtrade/exchange/common.py @@ -91,11 +91,11 @@ MAP_EXCHANGE_CHILDCLASS = { } -def calculate_backoff(retry, max_retries): +def calculate_backoff(retrycount, max_retries): """ Calculate backoff """ - return retry ** 2 + 1 + return (max_retries - retrycount) ** 2 + 1 def retrier_async(f): diff --git a/tests/exchange/test_exchange.py b/tests/exchange/test_exchange.py index fc4bf490c..8c397fe68 100644 --- a/tests/exchange/test_exchange.py +++ b/tests/exchange/test_exchange.py @@ -2298,13 +2298,13 @@ def test_calculate_fee_rate(mocker, default_conf, order, expected) -> None: assert ex.calculate_fee_rate(order) == expected -@pytest.mark.parametrize('retry,max_retries,expected', [ - (0, 3, 1), - (1, 3, 2), - (2, 3, 5), - (3, 3, 10), - (0, 1, 1), - (1, 1, 2), +@pytest.mark.parametrize('retrycount,max_retries,expected', [ + (0, 3, 10), + (1, 3, 5), + (2, 3, 2), + (3, 3, 1), + (0, 1, 2), + (1, 1, 1), ]) -def test_calculate_backoff(retry, max_retries, expected): - assert calculate_backoff(retry, max_retries) == expected +def test_calculate_backoff(retrycount, max_retries, expected): + assert calculate_backoff(retrycount, max_retries) == expected From c6124180fe14bcdf9f78676d021c2e8e6880d1e7 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 28 Jun 2020 19:45:42 +0200 Subject: [PATCH 30/41] Fix bug when fetching orders fails --- freqtrade/exceptions.py | 7 +++++++ freqtrade/exchange/common.py | 14 ++++++++------ freqtrade/exchange/exchange.py | 5 ++++- tests/exchange/test_exchange.py | 12 ++++++++++++ tests/test_freqtradebot.py | 2 +- 5 files changed, 32 insertions(+), 8 deletions(-) diff --git a/freqtrade/exceptions.py b/freqtrade/exceptions.py index 995a2cdb7..c85fccc4b 100644 --- a/freqtrade/exceptions.py +++ b/freqtrade/exceptions.py @@ -37,6 +37,13 @@ class InvalidOrderException(FreqtradeException): """ +class RetryableOrderError(InvalidOrderException): + """ + This is returned when the order is not found. + This Error will be repeated with increasing backof (in line with DDosError). + """ + + class ExchangeError(DependencyException): """ Error raised out of the exchange. diff --git a/freqtrade/exchange/common.py b/freqtrade/exchange/common.py index d3ba8cef6..9b7d8dfea 100644 --- a/freqtrade/exchange/common.py +++ b/freqtrade/exchange/common.py @@ -3,7 +3,8 @@ import logging import time from functools import wraps -from freqtrade.exceptions import DDosProtection, TemporaryError +from freqtrade.exceptions import (DDosProtection, RetryableOrderError, + TemporaryError) logger = logging.getLogger(__name__) @@ -109,8 +110,8 @@ def retrier_async(f): count -= 1 kwargs.update({'count': count}) logger.warning('retrying %s() still for %s times', f.__name__, count) - if isinstance(ex, DDosProtection): - await asyncio.sleep(1) + if isinstance(ex, DDosProtection) or isinstance(ex, RetryableOrderError): + await asyncio.sleep(calculate_backoff(count + 1, API_RETRY_COUNT)) return await wrapper(*args, **kwargs) else: logger.warning('Giving up retrying: %s()', f.__name__) @@ -125,14 +126,15 @@ def retrier(_func=None, retries=API_RETRY_COUNT): count = kwargs.pop('count', retries) try: return f(*args, **kwargs) - except TemporaryError as ex: + except (TemporaryError, RetryableOrderError) as ex: logger.warning('%s() returned exception: "%s"', f.__name__, ex) if count > 0: count -= 1 kwargs.update({'count': count}) logger.warning('retrying %s() still for %s times', f.__name__, count) - if isinstance(ex, DDosProtection): - time.sleep(calculate_backoff(count, retries)) + if isinstance(ex, DDosProtection) or isinstance(ex, RetryableOrderError): + # increasing backoff + time.sleep(calculate_backoff(count + 1, retries)) return wrapper(*args, **kwargs) else: logger.warning('Giving up retrying: %s()', f.__name__) diff --git a/freqtrade/exchange/exchange.py b/freqtrade/exchange/exchange.py index daa73ca35..a3a548176 100644 --- a/freqtrade/exchange/exchange.py +++ b/freqtrade/exchange/exchange.py @@ -22,7 +22,7 @@ from freqtrade.constants import ListPairsWithTimeframes from freqtrade.data.converter import ohlcv_to_dataframe, trades_dict_to_list from freqtrade.exceptions import (DDosProtection, ExchangeError, InvalidOrderException, OperationalException, - TemporaryError) + RetryableOrderError, TemporaryError) from freqtrade.exchange.common import BAD_EXCHANGES, retrier, retrier_async from freqtrade.misc import deep_merge_dicts, safe_value_fallback @@ -1015,6 +1015,9 @@ class Exchange: f'Tried to get an invalid dry-run-order (id: {order_id}). Message: {e}') from e try: return self._api.fetch_order(order_id, pair) + except ccxt.OrderNotFound as e: + raise RetryableOrderError( + f'Order not found (id: {order_id}). Message: {e}') from e except ccxt.InvalidOrder as e: raise InvalidOrderException( f'Tried to get an invalid order (id: {order_id}). Message: {e}') from e diff --git a/tests/exchange/test_exchange.py b/tests/exchange/test_exchange.py index 8c397fe68..66f88d82f 100644 --- a/tests/exchange/test_exchange.py +++ b/tests/exchange/test_exchange.py @@ -1888,6 +1888,18 @@ def test_fetch_order(default_conf, mocker, exchange_name): exchange.fetch_order(order_id='_', pair='TKN/BTC') assert api_mock.fetch_order.call_count == 1 + api_mock.fetch_order = MagicMock(side_effect=ccxt.OrderNotFound("Order not found")) + exchange = get_patched_exchange(mocker, default_conf, api_mock, id=exchange_name) + with patch('freqtrade.exchange.common.time.sleep') as tm: + with pytest.raises(InvalidOrderException): + exchange.fetch_order(order_id='_', pair='TKN/BTC') + # Ensure backoff is called + assert tm.call_args_list[0][0][0] == 1 + assert tm.call_args_list[1][0][0] == 2 + assert tm.call_args_list[2][0][0] == 5 + assert tm.call_args_list[3][0][0] == 10 + assert api_mock.fetch_order.call_count == API_RETRY_COUNT + 1 + ccxt_exceptionhandlers(mocker, default_conf, api_mock, exchange_name, 'fetch_order', 'fetch_order', order_id='_', pair='TKN/BTC') diff --git a/tests/test_freqtradebot.py b/tests/test_freqtradebot.py index ef48bdc34..654e6ca4f 100644 --- a/tests/test_freqtradebot.py +++ b/tests/test_freqtradebot.py @@ -2078,7 +2078,7 @@ def test_check_handle_timedout_buy_exception(default_conf, ticker, limit_buy_ord 'freqtrade.exchange.Exchange', validate_pairs=MagicMock(), fetch_ticker=ticker, - fetch_order=MagicMock(side_effect=DependencyException), + fetch_order=MagicMock(side_effect=ExchangeError), cancel_order=cancel_order_mock, get_fee=fee ) From 4d9ecf137b5d2d037e888a56eda4a0bd78925f7c Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 28 Jun 2020 20:17:03 +0200 Subject: [PATCH 31/41] Fix failing test in python 3.7 can't use Magicmock in 3.7 (works in 3.8 though). --- freqtrade/exchange/common.py | 2 +- tests/exchange/test_exchange.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/freqtrade/exchange/common.py b/freqtrade/exchange/common.py index 9b7d8dfea..cc70bb875 100644 --- a/freqtrade/exchange/common.py +++ b/freqtrade/exchange/common.py @@ -110,7 +110,7 @@ def retrier_async(f): count -= 1 kwargs.update({'count': count}) logger.warning('retrying %s() still for %s times', f.__name__, count) - if isinstance(ex, DDosProtection) or isinstance(ex, RetryableOrderError): + if isinstance(ex, DDosProtection): await asyncio.sleep(calculate_backoff(count + 1, API_RETRY_COUNT)) return await wrapper(*args, **kwargs) else: diff --git a/tests/exchange/test_exchange.py b/tests/exchange/test_exchange.py index 66f88d82f..251f257f7 100644 --- a/tests/exchange/test_exchange.py +++ b/tests/exchange/test_exchange.py @@ -62,7 +62,7 @@ def ccxt_exceptionhandlers(mocker, default_conf, api_mock, exchange_name, async def async_ccxt_exception(mocker, default_conf, api_mock, fun, mock_ccxt_fun, retries=API_RETRY_COUNT + 1, **kwargs): - with patch('freqtrade.exchange.common.asyncio.sleep'): + with patch('freqtrade.exchange.common.asyncio.sleep', get_mock_coro(None)): with pytest.raises(DDosProtection): api_mock.__dict__[mock_ccxt_fun] = MagicMock(side_effect=ccxt.DDoSProtection("Dooh")) exchange = get_patched_exchange(mocker, default_conf, api_mock) From fe0b17c70c82e429afbdf5a17e4242275548ad2f Mon Sep 17 00:00:00 2001 From: "dependabot-preview[bot]" <27856297+dependabot-preview[bot]@users.noreply.github.com> Date: Mon, 29 Jun 2020 09:01:20 +0000 Subject: [PATCH 32/41] Bump progressbar2 from 3.51.3 to 3.51.4 Bumps [progressbar2](https://github.com/WoLpH/python-progressbar) from 3.51.3 to 3.51.4. - [Release notes](https://github.com/WoLpH/python-progressbar/releases) - [Changelog](https://github.com/WoLpH/python-progressbar/blob/develop/CHANGES.rst) - [Commits](https://github.com/WoLpH/python-progressbar/compare/v3.51.3...v3.51.4) Signed-off-by: dependabot-preview[bot] --- requirements-hyperopt.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements-hyperopt.txt b/requirements-hyperopt.txt index aedfc0eaa..2784bc156 100644 --- a/requirements-hyperopt.txt +++ b/requirements-hyperopt.txt @@ -7,4 +7,4 @@ scikit-learn==0.23.1 scikit-optimize==0.7.4 filelock==3.0.12 joblib==0.15.1 -progressbar2==3.51.3 +progressbar2==3.51.4 From e06b00921416acdd8f05ab9a770ebe0a21689254 Mon Sep 17 00:00:00 2001 From: "dependabot-preview[bot]" <27856297+dependabot-preview[bot]@users.noreply.github.com> Date: Mon, 29 Jun 2020 09:02:57 +0000 Subject: [PATCH 33/41] Bump plotly from 4.8.1 to 4.8.2 Bumps [plotly](https://github.com/plotly/plotly.py) from 4.8.1 to 4.8.2. - [Release notes](https://github.com/plotly/plotly.py/releases) - [Changelog](https://github.com/plotly/plotly.py/blob/master/CHANGELOG.md) - [Commits](https://github.com/plotly/plotly.py/compare/v4.8.1...v4.8.2) Signed-off-by: dependabot-preview[bot] --- requirements-plot.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements-plot.txt b/requirements-plot.txt index cb13a59bf..ec5af3dbf 100644 --- a/requirements-plot.txt +++ b/requirements-plot.txt @@ -1,5 +1,5 @@ # Include all requirements to run the bot. -r requirements.txt -plotly==4.8.1 +plotly==4.8.2 From 4e5910afba3a72efe0735a0f257b6328ff3c8f24 Mon Sep 17 00:00:00 2001 From: "dependabot-preview[bot]" <27856297+dependabot-preview[bot]@users.noreply.github.com> Date: Mon, 29 Jun 2020 09:03:19 +0000 Subject: [PATCH 34/41] Bump mkdocs-material from 5.3.2 to 5.3.3 Bumps [mkdocs-material](https://github.com/squidfunk/mkdocs-material) from 5.3.2 to 5.3.3. - [Release notes](https://github.com/squidfunk/mkdocs-material/releases) - [Changelog](https://github.com/squidfunk/mkdocs-material/blob/master/CHANGELOG) - [Commits](https://github.com/squidfunk/mkdocs-material/compare/5.3.2...5.3.3) Signed-off-by: dependabot-preview[bot] --- docs/requirements-docs.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/requirements-docs.txt b/docs/requirements-docs.txt index 7ddfc1dfb..a0505c84b 100644 --- a/docs/requirements-docs.txt +++ b/docs/requirements-docs.txt @@ -1,2 +1,2 @@ -mkdocs-material==5.3.2 +mkdocs-material==5.3.3 mdx_truly_sane_lists==1.2 From be2b326a6e208d7f0835c7c899c6649764cdd924 Mon Sep 17 00:00:00 2001 From: "dependabot-preview[bot]" <27856297+dependabot-preview[bot]@users.noreply.github.com> Date: Mon, 29 Jun 2020 09:03:58 +0000 Subject: [PATCH 35/41] Bump pytest-asyncio from 0.12.0 to 0.14.0 Bumps [pytest-asyncio](https://github.com/pytest-dev/pytest-asyncio) from 0.12.0 to 0.14.0. - [Release notes](https://github.com/pytest-dev/pytest-asyncio/releases) - [Commits](https://github.com/pytest-dev/pytest-asyncio/compare/v0.12.0...v0.14.0) Signed-off-by: dependabot-preview[bot] --- requirements-dev.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements-dev.txt b/requirements-dev.txt index 91b33c573..7d68a0c3b 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -9,7 +9,7 @@ flake8-type-annotations==0.1.0 flake8-tidy-imports==4.1.0 mypy==0.781 pytest==5.4.3 -pytest-asyncio==0.12.0 +pytest-asyncio==0.14.0 pytest-cov==2.10.0 pytest-mock==3.1.1 pytest-random-order==1.0.4 From 9e1ce0c67ab1dbb505a35d1c3e1e24e5b946e9c7 Mon Sep 17 00:00:00 2001 From: "dependabot-preview[bot]" <27856297+dependabot-preview[bot]@users.noreply.github.com> Date: Mon, 29 Jun 2020 09:04:12 +0000 Subject: [PATCH 36/41] Bump sqlalchemy from 1.3.17 to 1.3.18 Bumps [sqlalchemy](https://github.com/sqlalchemy/sqlalchemy) from 1.3.17 to 1.3.18. - [Release notes](https://github.com/sqlalchemy/sqlalchemy/releases) - [Changelog](https://github.com/sqlalchemy/sqlalchemy/blob/master/CHANGES) - [Commits](https://github.com/sqlalchemy/sqlalchemy/commits) Signed-off-by: dependabot-preview[bot] --- requirements-common.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements-common.txt b/requirements-common.txt index 916b0e24b..06549d2bf 100644 --- a/requirements-common.txt +++ b/requirements-common.txt @@ -1,7 +1,7 @@ # requirements without requirements installable via conda # mainly used for Raspberry pi installs ccxt==1.30.34 -SQLAlchemy==1.3.17 +SQLAlchemy==1.3.18 python-telegram-bot==12.7 arrow==0.15.7 cachetools==4.1.0 From 449d4625336096658e1cbd39f8c9ed6ef9677af4 Mon Sep 17 00:00:00 2001 From: "dependabot-preview[bot]" <27856297+dependabot-preview[bot]@users.noreply.github.com> Date: Mon, 29 Jun 2020 12:12:58 +0000 Subject: [PATCH 37/41] Bump python-telegram-bot from 12.7 to 12.8 Bumps [python-telegram-bot](https://github.com/python-telegram-bot/python-telegram-bot) from 12.7 to 12.8. - [Release notes](https://github.com/python-telegram-bot/python-telegram-bot/releases) - [Changelog](https://github.com/python-telegram-bot/python-telegram-bot/blob/master/CHANGES.rst) - [Commits](https://github.com/python-telegram-bot/python-telegram-bot/compare/v12.7...v12.8) Signed-off-by: dependabot-preview[bot] --- requirements-common.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements-common.txt b/requirements-common.txt index 06549d2bf..06eddb536 100644 --- a/requirements-common.txt +++ b/requirements-common.txt @@ -2,7 +2,7 @@ # mainly used for Raspberry pi installs ccxt==1.30.34 SQLAlchemy==1.3.18 -python-telegram-bot==12.7 +python-telegram-bot==12.8 arrow==0.15.7 cachetools==4.1.0 requests==2.24.0 From c06b2802882ec70b7f725ca39818212d75e0e1d5 Mon Sep 17 00:00:00 2001 From: "dependabot-preview[bot]" <27856297+dependabot-preview[bot]@users.noreply.github.com> Date: Mon, 29 Jun 2020 12:14:27 +0000 Subject: [PATCH 38/41] Bump mypy from 0.781 to 0.782 Bumps [mypy](https://github.com/python/mypy) from 0.781 to 0.782. - [Release notes](https://github.com/python/mypy/releases) - [Commits](https://github.com/python/mypy/compare/v0.781...v0.782) Signed-off-by: dependabot-preview[bot] --- requirements-dev.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements-dev.txt b/requirements-dev.txt index 7d68a0c3b..ed4f8f713 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -7,7 +7,7 @@ coveralls==2.0.0 flake8==3.8.3 flake8-type-annotations==0.1.0 flake8-tidy-imports==4.1.0 -mypy==0.781 +mypy==0.782 pytest==5.4.3 pytest-asyncio==0.14.0 pytest-cov==2.10.0 From 8fb1683bdc1237ec55f2f5a47379b424ab6a5ea2 Mon Sep 17 00:00:00 2001 From: "dependabot-preview[bot]" <27856297+dependabot-preview[bot]@users.noreply.github.com> Date: Mon, 29 Jun 2020 12:32:42 +0000 Subject: [PATCH 39/41] Bump cachetools from 4.1.0 to 4.1.1 Bumps [cachetools](https://github.com/tkem/cachetools) from 4.1.0 to 4.1.1. - [Release notes](https://github.com/tkem/cachetools/releases) - [Changelog](https://github.com/tkem/cachetools/blob/master/CHANGELOG.rst) - [Commits](https://github.com/tkem/cachetools/compare/v4.1.0...v4.1.1) Signed-off-by: dependabot-preview[bot] --- requirements-common.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements-common.txt b/requirements-common.txt index 06eddb536..4c0f93ef7 100644 --- a/requirements-common.txt +++ b/requirements-common.txt @@ -4,7 +4,7 @@ ccxt==1.30.34 SQLAlchemy==1.3.18 python-telegram-bot==12.8 arrow==0.15.7 -cachetools==4.1.0 +cachetools==4.1.1 requests==2.24.0 urllib3==1.25.9 wrapt==1.12.1 From a9064117a5b35c7d6a2f63a0edce3c9188fa27de Mon Sep 17 00:00:00 2001 From: "dependabot-preview[bot]" <27856297+dependabot-preview[bot]@users.noreply.github.com> Date: Mon, 29 Jun 2020 12:53:58 +0000 Subject: [PATCH 40/41] Bump ccxt from 1.30.34 to 1.30.48 Bumps [ccxt](https://github.com/ccxt/ccxt) from 1.30.34 to 1.30.48. - [Release notes](https://github.com/ccxt/ccxt/releases) - [Changelog](https://github.com/ccxt/ccxt/blob/master/doc/exchanges-by-country.rst) - [Commits](https://github.com/ccxt/ccxt/compare/1.30.34...1.30.48) Signed-off-by: dependabot-preview[bot] --- requirements-common.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements-common.txt b/requirements-common.txt index 4c0f93ef7..2948b8f35 100644 --- a/requirements-common.txt +++ b/requirements-common.txt @@ -1,6 +1,6 @@ # requirements without requirements installable via conda # mainly used for Raspberry pi installs -ccxt==1.30.34 +ccxt==1.30.48 SQLAlchemy==1.3.18 python-telegram-bot==12.8 arrow==0.15.7 From b95065d70179318f1841708c64c7e333e763afb2 Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 29 Jun 2020 20:00:42 +0200 Subject: [PATCH 41/41] Log backoff --- freqtrade/exchange/common.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/freqtrade/exchange/common.py b/freqtrade/exchange/common.py index cc70bb875..0610e8447 100644 --- a/freqtrade/exchange/common.py +++ b/freqtrade/exchange/common.py @@ -111,7 +111,9 @@ def retrier_async(f): kwargs.update({'count': count}) logger.warning('retrying %s() still for %s times', f.__name__, count) if isinstance(ex, DDosProtection): - await asyncio.sleep(calculate_backoff(count + 1, API_RETRY_COUNT)) + backoff_delay = calculate_backoff(count + 1, API_RETRY_COUNT) + logger.debug(f"Applying DDosProtection backoff delay: {backoff_delay}") + await asyncio.sleep(backoff_delay) return await wrapper(*args, **kwargs) else: logger.warning('Giving up retrying: %s()', f.__name__) @@ -134,7 +136,9 @@ def retrier(_func=None, retries=API_RETRY_COUNT): logger.warning('retrying %s() still for %s times', f.__name__, count) if isinstance(ex, DDosProtection) or isinstance(ex, RetryableOrderError): # increasing backoff - time.sleep(calculate_backoff(count + 1, retries)) + backoff_delay = calculate_backoff(count + 1, retries) + logger.debug(f"Applying DDosProtection backoff delay: {backoff_delay}") + time.sleep(backoff_delay) return wrapper(*args, **kwargs) else: logger.warning('Giving up retrying: %s()', f.__name__)