From f64f2b1ad8f1f5a289e14d305b205961375a5b46 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 29 May 2022 10:34:22 +0200 Subject: [PATCH 01/95] Fix /stats Formatting issue in multi-message settings --- freqtrade/rpc/telegram.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/freqtrade/rpc/telegram.py b/freqtrade/rpc/telegram.py index f26de8b5c..4a274002e 100644 --- a/freqtrade/rpc/telegram.py +++ b/freqtrade/rpc/telegram.py @@ -785,7 +785,7 @@ class Telegram(RPCHandler): headers=['Exit Reason', 'Exits', 'Wins', 'Losses'] ) if len(exit_reasons_tabulate) > 25: - self._send_msg(exit_reasons_msg, ParseMode.MARKDOWN) + self._send_msg(f"```\n{exit_reasons_msg}```", ParseMode.MARKDOWN) exit_reasons_msg = '' durations = stats['durations'] From a875a7dc40996355cfe32bb9136b44fdea354213 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 29 May 2022 10:58:01 +0200 Subject: [PATCH 02/95] Use unified stopPrice for binance --- freqtrade/exchange/binance.py | 4 ++-- tests/exchange/test_binance.py | 1 + tests/test_freqtradebot.py | 4 +--- 3 files changed, 4 insertions(+), 5 deletions(-) diff --git a/freqtrade/exchange/binance.py b/freqtrade/exchange/binance.py index 1b6496a64..03546dcf9 100644 --- a/freqtrade/exchange/binance.py +++ b/freqtrade/exchange/binance.py @@ -53,8 +53,8 @@ class Binance(Exchange): ordertype = 'stop' if self.trading_mode == TradingMode.FUTURES else 'stop_loss_limit' return order['type'] == ordertype and ( - (side == "sell" and stop_loss > float(order['info']['stopPrice'])) or - (side == "buy" and stop_loss < float(order['info']['stopPrice'])) + (side == "sell" and stop_loss > float(order['stopPrice'])) or + (side == "buy" and stop_loss < float(order['stopPrice'])) ) def get_tickers(self, symbols: Optional[List[str]] = None, cached: bool = False) -> Dict: diff --git a/tests/exchange/test_binance.py b/tests/exchange/test_binance.py index 324be9962..45f8a3817 100644 --- a/tests/exchange/test_binance.py +++ b/tests/exchange/test_binance.py @@ -154,6 +154,7 @@ def test_stoploss_adjust_binance(mocker, default_conf, sl1, sl2, sl3, side): order = { 'type': 'stop_loss_limit', 'price': 1500, + 'stopPrice': 1500, 'info': {'stopPrice': 1500}, } assert exchange.stoploss_adjust(sl1, order, side=side) diff --git a/tests/test_freqtradebot.py b/tests/test_freqtradebot.py index 23ef4ffc2..5a5467370 100644 --- a/tests/test_freqtradebot.py +++ b/tests/test_freqtradebot.py @@ -1775,9 +1775,7 @@ def test_tsl_on_exchange_compatible_with_edge(mocker, edge_conf, fee, caplog, 'type': 'stop_loss_limit', 'price': 3, 'average': 2, - 'info': { - 'stopPrice': '2.178' - } + 'stopPrice': '2.178' }) mocker.patch('freqtrade.exchange.Exchange.fetch_stoploss_order', stoploss_order_hanging) From 1ee08d22d24973d0ec435e619ad168cc0ad7a3b6 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 29 May 2022 15:58:40 +0200 Subject: [PATCH 03/95] Delay parameter init closes #6894 --- freqtrade/optimize/backtesting.py | 3 +++ freqtrade/strategy/hyper.py | 33 ++++++++++++++++++++----------- 2 files changed, 24 insertions(+), 12 deletions(-) diff --git a/freqtrade/optimize/backtesting.py b/freqtrade/optimize/backtesting.py index f1e9b7251..43bc97f32 100755 --- a/freqtrade/optimize/backtesting.py +++ b/freqtrade/optimize/backtesting.py @@ -187,6 +187,9 @@ class Backtesting: # since a "perfect" stoploss-exit is assumed anyway # And the regular "stoploss" function would not apply to that case self.strategy.order_types['stoploss_on_exchange'] = False + if self.dataprovider.runmode == RunMode.BACKTEST: + # in hyperopt mode - don't re-init params + self.strategy.ft_load_hyper_params(False) self.strategy.ft_bot_start() def _load_protections(self, strategy: IStrategy): diff --git a/freqtrade/strategy/hyper.py b/freqtrade/strategy/hyper.py index 5c09dd862..bbd6ef5fe 100644 --- a/freqtrade/strategy/hyper.py +++ b/freqtrade/strategy/hyper.py @@ -31,7 +31,12 @@ class HyperStrategyMixin: self.ft_sell_params: List[BaseParameter] = [] self.ft_protection_params: List[BaseParameter] = [] - self._load_hyper_params(config.get('runmode') == RunMode.HYPEROPT) + params = self.load_params_from_file() + params = params.get('params', {}) + self._ft_params_from_file = params + + if config.get('runmode') != RunMode.BACKTEST: + self.ft_load_hyper_params(config.get('runmode') == RunMode.HYPEROPT) def enumerate_parameters(self, category: str = None) -> Iterator[Tuple[str, BaseParameter]]: """ @@ -80,21 +85,25 @@ class HyperStrategyMixin: return params - def _load_hyper_params(self, hyperopt: bool = False) -> None: + def ft_load_hyper_params(self, hyperopt: bool = False) -> None: """ Load Hyperoptable parameters + Prevalence: + * Parameters from parameter file + * Parameters defined in parameters objects (buy_params, sell_params, ...) + * Parameter defaults """ - params = self.load_params_from_file() - params = params.get('params', {}) - self._ft_params_from_file = params - buy_params = deep_merge_dicts(params.get('buy', {}), getattr(self, 'buy_params', {})) - sell_params = deep_merge_dicts(params.get('sell', {}), getattr(self, 'sell_params', {})) - protection_params = deep_merge_dicts(params.get('protection', {}), + + buy_params = deep_merge_dicts(self._ft_params_from_file.get('buy', {}), + getattr(self, 'buy_params', {})) + sell_params = deep_merge_dicts(self._ft_params_from_file.get('sell', {}), + getattr(self, 'sell_params', {})) + protection_params = deep_merge_dicts(self._ft_params_from_file.get('protection', {}), getattr(self, 'protection_params', {})) - self._load_params(buy_params, 'buy', hyperopt) - self._load_params(sell_params, 'sell', hyperopt) - self._load_params(protection_params, 'protection', hyperopt) + self._ft_load_params(buy_params, 'buy', hyperopt) + self._ft_load_params(sell_params, 'sell', hyperopt) + self._ft_load_params(protection_params, 'protection', hyperopt) def load_params_from_file(self) -> Dict: filename_str = getattr(self, '__file__', '') @@ -117,7 +126,7 @@ class HyperStrategyMixin: return {} - def _load_params(self, params: Dict, space: str, hyperopt: bool = False) -> None: + def _ft_load_params(self, params: Dict, space: str, hyperopt: bool = False) -> None: """ Set optimizable parameter values. :param params: Dictionary with new parameter values. From e6affcc23e01ecd8fe4a48ea41d7ca15b20f9727 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 29 May 2022 16:39:47 +0200 Subject: [PATCH 04/95] Move parameter file loading to hyper-mixin --- freqtrade/resolvers/strategy_resolver.py | 21 +-------------------- freqtrade/strategy/hyper.py | 21 +++++++++++++++++++++ 2 files changed, 22 insertions(+), 20 deletions(-) diff --git a/freqtrade/resolvers/strategy_resolver.py b/freqtrade/resolvers/strategy_resolver.py index 44d590b67..c63c133ce 100644 --- a/freqtrade/resolvers/strategy_resolver.py +++ b/freqtrade/resolvers/strategy_resolver.py @@ -47,26 +47,7 @@ class StrategyResolver(IResolver): strategy: IStrategy = StrategyResolver._load_strategy( strategy_name, config=config, extra_dir=config.get('strategy_path')) - - if strategy._ft_params_from_file: - # Set parameters from Hyperopt results file - params = strategy._ft_params_from_file - strategy.minimal_roi = params.get('roi', getattr(strategy, 'minimal_roi', {})) - - strategy.stoploss = params.get('stoploss', {}).get( - 'stoploss', getattr(strategy, 'stoploss', -0.1)) - trailing = params.get('trailing', {}) - strategy.trailing_stop = trailing.get( - 'trailing_stop', getattr(strategy, 'trailing_stop', False)) - strategy.trailing_stop_positive = trailing.get( - 'trailing_stop_positive', getattr(strategy, 'trailing_stop_positive', None)) - strategy.trailing_stop_positive_offset = trailing.get( - 'trailing_stop_positive_offset', - getattr(strategy, 'trailing_stop_positive_offset', 0)) - strategy.trailing_only_offset_is_reached = trailing.get( - 'trailing_only_offset_is_reached', - getattr(strategy, 'trailing_only_offset_is_reached', 0.0)) - + strategy.ft_load_hyper_params_from_file() # Set attributes # Check if we need to override configuration # (Attribute name, default, subkey) diff --git a/freqtrade/strategy/hyper.py b/freqtrade/strategy/hyper.py index bbd6ef5fe..c4119173b 100644 --- a/freqtrade/strategy/hyper.py +++ b/freqtrade/strategy/hyper.py @@ -85,6 +85,27 @@ class HyperStrategyMixin: return params + def ft_load_hyper_params_from_file(self) -> None: + """ Load Parameters from parameter file""" + if self._ft_params_from_file: + # Set parameters from Hyperopt results file + params = self._ft_params_from_file + self.minimal_roi = params.get('roi', getattr(self, 'minimal_roi', {})) + + self.stoploss = params.get('stoploss', {}).get( + 'stoploss', getattr(self, 'stoploss', -0.1)) + trailing = params.get('trailing', {}) + self.trailing_stop = trailing.get( + 'trailing_stop', getattr(self, 'trailing_stop', False)) + self.trailing_stop_positive = trailing.get( + 'trailing_stop_positive', getattr(self, 'trailing_stop_positive', None)) + self.trailing_stop_positive_offset = trailing.get( + 'trailing_stop_positive_offset', + getattr(self, 'trailing_stop_positive_offset', 0)) + self.trailing_only_offset_is_reached = trailing.get( + 'trailing_only_offset_is_reached', + getattr(self, 'trailing_only_offset_is_reached', 0.0)) + def ft_load_hyper_params(self, hyperopt: bool = False) -> None: """ Load Hyperoptable parameters From f65df4901e2acb3f400328950b17fa1333ce7078 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 29 May 2022 20:53:09 +0200 Subject: [PATCH 05/95] Update doc clarity --- docs/hyperopt.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/hyperopt.md b/docs/hyperopt.md index 030d73f4b..63c7a4413 100644 --- a/docs/hyperopt.md +++ b/docs/hyperopt.md @@ -680,7 +680,7 @@ class MyAwesomeStrategy(IStrategy): !!! Note Values in the configuration file will overwrite Parameter-file level parameters - and both will overwrite parameters within the strategy. - The prevalence is therefore: config > parameter file > strategy + The prevalence is therefore: config > parameter file > strategy `*_params` > parameter default ### Understand Hyperopt ROI results From b52fd0b4df21dcd04c2aed91a486e0ccad4cf72f Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 30 May 2022 03:01:56 +0000 Subject: [PATCH 06/95] Bump python-telegram-bot from 13.11 to 13.12 Bumps [python-telegram-bot](https://github.com/python-telegram-bot/python-telegram-bot) from 13.11 to 13.12. - [Release notes](https://github.com/python-telegram-bot/python-telegram-bot/releases) - [Changelog](https://github.com/python-telegram-bot/python-telegram-bot/blob/v13.12/CHANGES.rst) - [Commits](https://github.com/python-telegram-bot/python-telegram-bot/compare/v13.11...v13.12) --- updated-dependencies: - dependency-name: python-telegram-bot dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index ae1ff7a89..9c12f7cdf 100644 --- a/requirements.txt +++ b/requirements.txt @@ -7,7 +7,7 @@ ccxt==1.83.62 cryptography==37.0.2 aiohttp==3.8.1 SQLAlchemy==1.4.36 -python-telegram-bot==13.11 +python-telegram-bot==13.12 arrow==1.2.2 cachetools==4.2.2 requests==2.27.1 From e7c78529e97edc5514fc7d3c8177aef491d16a2e Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 30 May 2022 03:01:58 +0000 Subject: [PATCH 07/95] Bump types-python-dateutil from 2.8.16 to 2.8.17 Bumps [types-python-dateutil](https://github.com/python/typeshed) from 2.8.16 to 2.8.17. - [Release notes](https://github.com/python/typeshed/releases) - [Commits](https://github.com/python/typeshed/commits) --- updated-dependencies: - dependency-name: types-python-dateutil dependency-type: direct:development update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- requirements-dev.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements-dev.txt b/requirements-dev.txt index e863238bd..3823e5ebb 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -26,4 +26,4 @@ types-cachetools==5.0.1 types-filelock==3.2.6 types-requests==2.27.27 types-tabulate==0.8.9 -types-python-dateutil==2.8.16 +types-python-dateutil==2.8.17 From 9366c1d36f0bfa88f2d734e81357babf926c4921 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 30 May 2022 03:02:03 +0000 Subject: [PATCH 08/95] Bump mkdocs-material from 8.2.15 to 8.2.16 Bumps [mkdocs-material](https://github.com/squidfunk/mkdocs-material) from 8.2.15 to 8.2.16. - [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/8.2.15...8.2.16) --- updated-dependencies: - dependency-name: mkdocs-material dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[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 3fa35d80d..e7ca17c34 100644 --- a/docs/requirements-docs.txt +++ b/docs/requirements-docs.txt @@ -1,5 +1,5 @@ mkdocs==1.3.0 -mkdocs-material==8.2.15 +mkdocs-material==8.2.16 mdx_truly_sane_lists==1.2 pymdown-extensions==9.4 jinja2==3.1.2 From a937f36997b93d2f047f2174020b0c5a0f35e97b Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 30 May 2022 03:02:13 +0000 Subject: [PATCH 09/95] Bump mypy from 0.950 to 0.960 Bumps [mypy](https://github.com/python/mypy) from 0.950 to 0.960. - [Release notes](https://github.com/python/mypy/releases) - [Commits](https://github.com/python/mypy/compare/v0.950...v0.960) --- updated-dependencies: - dependency-name: mypy dependency-type: direct:development update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- requirements-dev.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements-dev.txt b/requirements-dev.txt index e863238bd..1c96108cc 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -7,7 +7,7 @@ coveralls==3.3.1 flake8==4.0.1 flake8-tidy-imports==4.8.0 -mypy==0.950 +mypy==0.960 pre-commit==2.19.0 pytest==7.1.2 pytest-asyncio==0.18.3 From 23fa00e29aca9d0da473a454201c595184a56525 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 30 May 2022 03:02:26 +0000 Subject: [PATCH 10/95] Bump ccxt from 1.83.62 to 1.84.39 Bumps [ccxt](https://github.com/ccxt/ccxt) from 1.83.62 to 1.84.39. - [Release notes](https://github.com/ccxt/ccxt/releases) - [Changelog](https://github.com/ccxt/ccxt/blob/master/exchanges.cfg) - [Commits](https://github.com/ccxt/ccxt/compare/1.83.62...1.84.39) --- updated-dependencies: - dependency-name: ccxt dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index ae1ff7a89..08425294d 100644 --- a/requirements.txt +++ b/requirements.txt @@ -2,7 +2,7 @@ numpy==1.22.4 pandas==1.4.2 pandas-ta==0.3.14b -ccxt==1.83.62 +ccxt==1.84.39 # Pin cryptography for now due to rust build errors with piwheels cryptography==37.0.2 aiohttp==3.8.1 From 41052b4e1e2cad4ad23bab66e2e2fd6764744950 Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 30 May 2022 06:28:03 +0200 Subject: [PATCH 11/95] Bump types dateutil precommit --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index d59010154..99bf35f7d 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -17,7 +17,7 @@ repos: - types-filelock==3.2.6 - types-requests==2.27.27 - types-tabulate==0.8.9 - - types-python-dateutil==2.8.16 + - types-python-dateutil==2.8.17 # stages: [push] - repo: https://github.com/pycqa/isort From ad8ff10a05912d1075579060588e7e9328d24af5 Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 30 May 2022 06:32:35 +0200 Subject: [PATCH 12/95] Minor doc changes --- docs/strategy-callbacks.md | 3 +++ freqtrade/strategy/hyper.py | 5 ++++- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/docs/strategy-callbacks.md b/docs/strategy-callbacks.md index 06e7152aa..e1e57a1f3 100644 --- a/docs/strategy-callbacks.md +++ b/docs/strategy-callbacks.md @@ -46,6 +46,9 @@ class AwesomeStrategy(IStrategy): self.cust_remote_data = requests.get('https://some_remote_source.example.com') ``` + +During hyperopt, this runs only once at startup. + ## Bot loop start A simple callback which is called once at the start of every bot throttling iteration (roughly every 5 seconds, unless configured differently). diff --git a/freqtrade/strategy/hyper.py b/freqtrade/strategy/hyper.py index c4119173b..622ad7718 100644 --- a/freqtrade/strategy/hyper.py +++ b/freqtrade/strategy/hyper.py @@ -86,7 +86,10 @@ class HyperStrategyMixin: return params def ft_load_hyper_params_from_file(self) -> None: - """ Load Parameters from parameter file""" + """ + Load Parameters from parameter file + Should/must run before config values are loaded in strategy_resolver. + """ if self._ft_params_from_file: # Set parameters from Hyperopt results file params = self._ft_params_from_file From 386d3e035337cea7cbe9e38a5a8100fa79948fbb Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 30 May 2022 06:52:44 +0200 Subject: [PATCH 13/95] Rename stop/roi loading method --- freqtrade/resolvers/strategy_resolver.py | 2 +- freqtrade/strategy/hyper.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/freqtrade/resolvers/strategy_resolver.py b/freqtrade/resolvers/strategy_resolver.py index c63c133ce..8b01980ce 100644 --- a/freqtrade/resolvers/strategy_resolver.py +++ b/freqtrade/resolvers/strategy_resolver.py @@ -47,7 +47,7 @@ class StrategyResolver(IResolver): strategy: IStrategy = StrategyResolver._load_strategy( strategy_name, config=config, extra_dir=config.get('strategy_path')) - strategy.ft_load_hyper_params_from_file() + strategy.ft_load_params_from_file() # Set attributes # Check if we need to override configuration # (Attribute name, default, subkey) diff --git a/freqtrade/strategy/hyper.py b/freqtrade/strategy/hyper.py index 622ad7718..ee62b5516 100644 --- a/freqtrade/strategy/hyper.py +++ b/freqtrade/strategy/hyper.py @@ -85,7 +85,7 @@ class HyperStrategyMixin: return params - def ft_load_hyper_params_from_file(self) -> None: + def ft_load_params_from_file(self) -> None: """ Load Parameters from parameter file Should/must run before config values are loaded in strategy_resolver. From 2b2967f34e0b83b386269d1c025fad3ee1ef95fa Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 30 May 2022 04:54:54 +0000 Subject: [PATCH 14/95] Bump types-requests from 2.27.27 to 2.27.29 Bumps [types-requests](https://github.com/python/typeshed) from 2.27.27 to 2.27.29. - [Release notes](https://github.com/python/typeshed/releases) - [Commits](https://github.com/python/typeshed/commits) --- updated-dependencies: - dependency-name: types-requests dependency-type: direct:development update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- requirements-dev.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements-dev.txt b/requirements-dev.txt index 3823e5ebb..1c8cc2352 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -24,6 +24,6 @@ nbconvert==6.5.0 # mypy types types-cachetools==5.0.1 types-filelock==3.2.6 -types-requests==2.27.27 +types-requests==2.27.29 types-tabulate==0.8.9 types-python-dateutil==2.8.17 From eaa656f859a5edfb9837707b14846aa5a44f06cc Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 30 May 2022 07:07:47 +0200 Subject: [PATCH 15/95] Hyperoptable parameters can be instance attributes --- freqtrade/optimize/backtesting.py | 4 +--- freqtrade/strategy/hyper.py | 12 ++++-------- freqtrade/strategy/interface.py | 3 +++ 3 files changed, 8 insertions(+), 11 deletions(-) diff --git a/freqtrade/optimize/backtesting.py b/freqtrade/optimize/backtesting.py index 43bc97f32..fa32666cc 100755 --- a/freqtrade/optimize/backtesting.py +++ b/freqtrade/optimize/backtesting.py @@ -187,9 +187,7 @@ class Backtesting: # since a "perfect" stoploss-exit is assumed anyway # And the regular "stoploss" function would not apply to that case self.strategy.order_types['stoploss_on_exchange'] = False - if self.dataprovider.runmode == RunMode.BACKTEST: - # in hyperopt mode - don't re-init params - self.strategy.ft_load_hyper_params(False) + self.strategy.ft_bot_start() def _load_protections(self, strategy: IStrategy): diff --git a/freqtrade/strategy/hyper.py b/freqtrade/strategy/hyper.py index ee62b5516..7f99c9b8a 100644 --- a/freqtrade/strategy/hyper.py +++ b/freqtrade/strategy/hyper.py @@ -6,7 +6,6 @@ import logging from pathlib import Path from typing import Any, Dict, Iterator, List, Tuple -from freqtrade.enums import RunMode from freqtrade.exceptions import OperationalException from freqtrade.misc import deep_merge_dicts, json_load from freqtrade.optimize.hyperopt_tools import HyperoptTools @@ -34,9 +33,7 @@ class HyperStrategyMixin: params = self.load_params_from_file() params = params.get('params', {}) self._ft_params_from_file = params - - if config.get('runmode') != RunMode.BACKTEST: - self.ft_load_hyper_params(config.get('runmode') == RunMode.HYPEROPT) + # Init/loading of parameters is done as part of ft_bot_start(). def enumerate_parameters(self, category: str = None) -> Iterator[Tuple[str, BaseParameter]]: """ @@ -56,12 +53,11 @@ class HyperStrategyMixin: for par in params: yield par.name, par - @classmethod - def detect_parameters(cls, category: str) -> Iterator[Tuple[str, BaseParameter]]: + def detect_parameters(self, category: str) -> Iterator[Tuple[str, BaseParameter]]: """ Detect all parameters for 'category' """ - for attr_name in dir(cls): + for attr_name in dir(self): if not attr_name.startswith('__'): # Ignore internals, not strictly necessary. - attr = getattr(cls, attr_name) + attr = getattr(self, attr_name) if issubclass(attr.__class__, BaseParameter): if (attr_name.startswith(category + '_') and attr.category is not None and attr.category != category): diff --git a/freqtrade/strategy/interface.py b/freqtrade/strategy/interface.py index c521943b1..344c43b15 100644 --- a/freqtrade/strategy/interface.py +++ b/freqtrade/strategy/interface.py @@ -14,6 +14,7 @@ from freqtrade.constants import ListPairsWithTimeframes from freqtrade.data.dataprovider import DataProvider from freqtrade.enums import (CandleType, ExitCheckTuple, ExitType, SignalDirection, SignalTagType, SignalType, TradingMode) +from freqtrade.enums.runmode import RunMode from freqtrade.exceptions import OperationalException, StrategyError from freqtrade.exchange import timeframe_to_minutes, timeframe_to_next_date, timeframe_to_seconds from freqtrade.persistence import Order, PairLocks, Trade @@ -151,6 +152,8 @@ class IStrategy(ABC, HyperStrategyMixin): """ strategy_safe_wrapper(self.bot_start)() + self.ft_load_hyper_params(self.config.get('runmode') == RunMode.HYPEROPT) + @abstractmethod def populate_indicators(self, dataframe: DataFrame, metadata: dict) -> DataFrame: """ From 5bf021be2e8f1479753e66573575fa7cde00a2b6 Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 30 May 2022 07:08:37 +0200 Subject: [PATCH 16/95] Enhance hyperoptable strategy to test instance parameters --- tests/optimize/test_hyperopt.py | 1 - tests/strategy/strats/hyperoptable_strategy.py | 7 ++++++- tests/strategy/test_interface.py | 2 +- 3 files changed, 7 insertions(+), 3 deletions(-) diff --git a/tests/optimize/test_hyperopt.py b/tests/optimize/test_hyperopt.py index 8522894f7..9f3c5845f 100644 --- a/tests/optimize/test_hyperopt.py +++ b/tests/optimize/test_hyperopt.py @@ -509,7 +509,6 @@ def test_generate_optimizer(mocker, hyperopt_conf) -> None: hyperopt.min_date = Arrow(2017, 12, 10) hyperopt.max_date = Arrow(2017, 12, 13) hyperopt.init_spaces() - hyperopt.dimensions = hyperopt.dimensions generate_optimizer_value = hyperopt.generate_optimizer(list(optimizer_param.values())) assert generate_optimizer_value == response_expected diff --git a/tests/strategy/strats/hyperoptable_strategy.py b/tests/strategy/strats/hyperoptable_strategy.py index f4dcf1a05..28ecf617a 100644 --- a/tests/strategy/strats/hyperoptable_strategy.py +++ b/tests/strategy/strats/hyperoptable_strategy.py @@ -27,7 +27,6 @@ class HyperoptableStrategy(StrategyTestV2): 'sell_minusdi': 0.4 } - buy_rsi = IntParameter([0, 50], default=30, space='buy') buy_plusdi = RealParameter(low=0, high=1, default=0.5, space='buy') sell_rsi = IntParameter(low=50, high=100, default=70, space='sell') sell_minusdi = DecimalParameter(low=0, high=1, default=0.5001, decimals=3, space='sell', @@ -45,6 +44,12 @@ class HyperoptableStrategy(StrategyTestV2): }) return prot + def bot_start(self, **kwargs) -> None: + """ + Parameters can also be defined here ... + """ + self.buy_rsi = IntParameter([0, 50], default=30, space='buy') + def informative_pairs(self): """ Define additional, informative pair/interval combinations to be cached from the exchange. diff --git a/tests/strategy/test_interface.py b/tests/strategy/test_interface.py index 12cbf5370..2ce27d36b 100644 --- a/tests/strategy/test_interface.py +++ b/tests/strategy/test_interface.py @@ -893,7 +893,7 @@ def test_auto_hyperopt_interface(default_conf): default_conf.update({'strategy': 'HyperoptableStrategy'}) PairLocks.timeframe = default_conf['timeframe'] strategy = StrategyResolver.load_strategy(default_conf) - + strategy.ft_bot_start() with pytest.raises(OperationalException): next(strategy.enumerate_parameters('deadBeef')) From f323cbc7694986588f8242646c2a6fb5178ad955 Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 30 May 2022 07:23:05 +0200 Subject: [PATCH 17/95] Bump types-requests precommit --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 99bf35f7d..95a1d5002 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -15,7 +15,7 @@ repos: additional_dependencies: - types-cachetools==5.0.1 - types-filelock==3.2.6 - - types-requests==2.27.27 + - types-requests==2.27.29 - types-tabulate==0.8.9 - types-python-dateutil==2.8.17 # stages: [push] From 8e2c7e1298faeb173fd50e57be94cb554232a7a5 Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 30 May 2022 07:22:16 +0200 Subject: [PATCH 18/95] extract detect_parameters to separate function --- freqtrade/strategy/hyper.py | 46 +++++++++++++++++++------------- tests/strategy/test_interface.py | 10 ++++--- 2 files changed, 34 insertions(+), 22 deletions(-) diff --git a/freqtrade/strategy/hyper.py b/freqtrade/strategy/hyper.py index 7f99c9b8a..cdcfc969e 100644 --- a/freqtrade/strategy/hyper.py +++ b/freqtrade/strategy/hyper.py @@ -4,7 +4,7 @@ This module defines a base class for auto-hyperoptable strategies. """ import logging from pathlib import Path -from typing import Any, Dict, Iterator, List, Tuple +from typing import Any, Dict, Iterator, List, Tuple, Type, Union from freqtrade.exceptions import OperationalException from freqtrade.misc import deep_merge_dicts, json_load @@ -53,27 +53,13 @@ class HyperStrategyMixin: for par in params: yield par.name, par - def detect_parameters(self, category: str) -> Iterator[Tuple[str, BaseParameter]]: - """ Detect all parameters for 'category' """ - for attr_name in dir(self): - if not attr_name.startswith('__'): # Ignore internals, not strictly necessary. - attr = getattr(self, attr_name) - if issubclass(attr.__class__, BaseParameter): - if (attr_name.startswith(category + '_') - and attr.category is not None and attr.category != category): - raise OperationalException( - f'Inconclusive parameter name {attr_name}, category: {attr.category}.') - if (category == attr.category or - (attr_name.startswith(category + '_') and attr.category is None)): - yield attr_name, attr - @classmethod def detect_all_parameters(cls) -> Dict: """ Detect all parameters and return them as a list""" params: Dict[str, Any] = { - 'buy': list(cls.detect_parameters('buy')), - 'sell': list(cls.detect_parameters('sell')), - 'protection': list(cls.detect_parameters('protection')), + 'buy': list(detect_parameters(cls, 'buy')), + 'sell': list(detect_parameters(cls, 'sell')), + 'protection': list(detect_parameters(cls, 'protection')), } params.update({ 'count': len(params['buy'] + params['sell'] + params['protection']) @@ -155,7 +141,7 @@ class HyperStrategyMixin: logger.info(f"No params for {space} found, using default values.") param_container: List[BaseParameter] = getattr(self, f"ft_{space}_params") - for attr_name, attr in self.detect_parameters(space): + for attr_name, attr in detect_parameters(self, space): attr.name = attr_name attr.in_space = hyperopt and HyperoptTools.has_space(self.config, space) if not attr.category: @@ -186,3 +172,25 @@ class HyperStrategyMixin: if not p.optimize or not p.in_space: params[p.category][name] = p.value return params + + +def detect_parameters( + obj: Union[HyperStrategyMixin, Type[HyperStrategyMixin]], + category: str + ) -> Iterator[Tuple[str, BaseParameter]]: + """ + Detect all parameters for 'category' for "obj" + :param obj: Strategy object or class + :param category: category - usually `'buy', 'sell', 'protection',... + """ + for attr_name in dir(obj): + if not attr_name.startswith('__'): # Ignore internals, not strictly necessary. + attr = getattr(obj, attr_name) + if issubclass(attr.__class__, BaseParameter): + if (attr_name.startswith(category + '_') + and attr.category is not None and attr.category != category): + raise OperationalException( + f'Inconclusive parameter name {attr_name}, category: {attr.category}.') + if (category == attr.category or + (attr_name.startswith(category + '_') and attr.category is None)): + yield attr_name, attr diff --git a/tests/strategy/test_interface.py b/tests/strategy/test_interface.py index 2ce27d36b..e3c0bcfcb 100644 --- a/tests/strategy/test_interface.py +++ b/tests/strategy/test_interface.py @@ -16,6 +16,7 @@ from freqtrade.exceptions import OperationalException, StrategyError from freqtrade.optimize.space import SKDecimal from freqtrade.persistence import PairLocks, Trade from freqtrade.resolvers import StrategyResolver +from freqtrade.strategy.hyper import detect_parameters from freqtrade.strategy.parameters import (BaseParameter, BooleanParameter, CategoricalParameter, DecimalParameter, IntParameter, RealParameter) from freqtrade.strategy.strategy_wrapper import strategy_safe_wrapper @@ -908,15 +909,18 @@ def test_auto_hyperopt_interface(default_conf): assert strategy.sell_minusdi.value == 0.5 all_params = strategy.detect_all_parameters() assert isinstance(all_params, dict) - assert len(all_params['buy']) == 2 + # Only one buy param at class level + assert len(all_params['buy']) == 1 + # Running detect params at instance level reveals both parameters. + assert len(list(detect_parameters(strategy, 'buy'))) == 2 assert len(all_params['sell']) == 2 # Number of Hyperoptable parameters - assert all_params['count'] == 6 + assert all_params['count'] == 5 strategy.__class__.sell_rsi = IntParameter([0, 10], default=5, space='buy') with pytest.raises(OperationalException, match=r"Inconclusive parameter.*"): - [x for x in strategy.detect_parameters('sell')] + [x for x in detect_parameters(strategy, 'sell')] def test_auto_hyperopt_interface_loadparams(default_conf, mocker, caplog): From d950b0acbe1ab6c9442430654273ddb4215077c0 Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 30 May 2022 18:17:07 +0200 Subject: [PATCH 19/95] Update documentation about dynamic parameters --- docs/advanced-hyperopt.md | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/docs/advanced-hyperopt.md b/docs/advanced-hyperopt.md index 7f1bd0fed..8a1ebaff3 100644 --- a/docs/advanced-hyperopt.md +++ b/docs/advanced-hyperopt.md @@ -98,6 +98,23 @@ class MyAwesomeStrategy(IStrategy): !!! Note All overrides are optional and can be mixed/matched as necessary. +### Dynamic parameters + +Parameters can also be defined dynamically, but must be available to the instance once the * [`bot_start()` callback](strategy-callbacks.md#bot-start) has been called. + +``` python + +class MyAwesomeStrategy(IStrategy): + + def bot_start(self, **kwargs) -> None: + self.buy_adx = IntParameter(20, 30, default=30, optimize=True) + + # ... +``` + +!!! Warning + Parameters created this way will not show up in the `list-strategies` parameter count. + ### Overriding Base estimator You can define your own estimator for Hyperopt by implementing `generate_estimator()` in the Hyperopt subclass. From 88845f6d88c7e02c3ffa98923e992f13dee11a9f Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 31 May 2022 17:49:51 +0200 Subject: [PATCH 20/95] Fix cancel order deleting trade if one order was successfully filled, the trade cannot be deleted. closes #6907 --- freqtrade/freqtradebot.py | 13 +++++++------ tests/test_freqtradebot.py | 8 +++++++- 2 files changed, 14 insertions(+), 7 deletions(-) diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index a2a12a03a..fba63459b 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -1203,15 +1203,15 @@ class FreqtradeBot(LoggingMixin): current_order_rate=order_obj.price, entry_tag=trade.enter_tag, side=trade.entry_side) - full_cancel = False + replacing = True cancel_reason = constants.CANCEL_REASON['REPLACE'] if not adjusted_entry_price: - full_cancel = True if trade.nr_of_successful_entries == 0 else False + replacing = False cancel_reason = constants.CANCEL_REASON['USER_CANCEL'] if order_obj.price != adjusted_entry_price: # cancel existing order if new price is supplied or None self.handle_cancel_enter(trade, order, cancel_reason, - allow_full_cancel=full_cancel) + replacing=replacing) if adjusted_entry_price: # place new order only if new price is supplied self.execute_entry( @@ -1245,10 +1245,11 @@ class FreqtradeBot(LoggingMixin): def handle_cancel_enter( self, trade: Trade, order: Dict, reason: str, - allow_full_cancel: Optional[bool] = True + replacing: Optional[bool] = False ) -> bool: """ Buy cancel - cancel order + :param replacing: Replacing order - prevent trade deletion. :return: True if order was fully cancelled """ was_trade_fully_canceled = False @@ -1286,7 +1287,7 @@ class FreqtradeBot(LoggingMixin): if isclose(filled_amount, 0.0, abs_tol=constants.MATH_CLOSE_PREC): # if trade is not partially completed and it's the only order, just delete the trade open_order_count = len([order for order in trade.orders if order.status == 'open']) - if open_order_count <= 1 and allow_full_cancel: + if open_order_count <= 1 and trade.nr_of_successful_entries == 0 and not replacing: logger.info(f'{side} order fully cancelled. Removing {trade} from database.') trade.delete() was_trade_fully_canceled = True @@ -1295,7 +1296,7 @@ class FreqtradeBot(LoggingMixin): # FIXME TODO: This could possibly reworked to not duplicate the code 15 lines below. self.update_trade_state(trade, trade.open_order_id, corder) trade.open_order_id = None - logger.info(f'Partial {side} order timeout for {trade}.') + logger.info(f'{side} Order timeout for {trade}.') else: # if trade is partially complete, edit the stake details for the trade # and close the order diff --git a/tests/test_freqtradebot.py b/tests/test_freqtradebot.py index 5a5467370..0e4f9db99 100644 --- a/tests/test_freqtradebot.py +++ b/tests/test_freqtradebot.py @@ -2572,6 +2572,7 @@ def test_check_handle_cancelled_buy( get_fee=fee ) freqtrade = FreqtradeBot(default_conf_usdt) + open_trade.orders = [] open_trade.is_short = is_short Trade.query.session.add(open_trade) @@ -2954,6 +2955,7 @@ def test_handle_cancel_enter(mocker, caplog, default_conf_usdt, limit_order, is_ freqtrade = FreqtradeBot(default_conf_usdt) freqtrade._notify_enter_cancel = MagicMock() + # TODO: Convert to real trade trade = MagicMock() trade.pair = 'LTC/USDT' trade.open_rate = 200 @@ -2961,6 +2963,7 @@ def test_handle_cancel_enter(mocker, caplog, default_conf_usdt, limit_order, is_ trade.entry_side = "buy" l_order['filled'] = 0.0 l_order['status'] = 'open' + trade.nr_of_successful_entries = 0 reason = CANCEL_REASON['TIMEOUT'] assert freqtrade.handle_cancel_enter(trade, l_order, reason) assert cancel_order_mock.call_count == 1 @@ -3003,7 +3006,9 @@ def test_handle_cancel_enter_exchanges(mocker, caplog, default_conf_usdt, is_sho freqtrade = FreqtradeBot(default_conf_usdt) reason = CANCEL_REASON['TIMEOUT'] + # TODO: Convert to real trade trade = MagicMock() + trade.nr_of_successful_entries = 0 trade.pair = 'LTC/ETH' trade.entry_side = "sell" if is_short else "buy" assert freqtrade.handle_cancel_enter(trade, limit_buy_order_canceled_empty, reason) @@ -3036,13 +3041,14 @@ def test_handle_cancel_enter_corder_empty(mocker, default_conf_usdt, limit_order freqtrade = FreqtradeBot(default_conf_usdt) freqtrade._notify_enter_cancel = MagicMock() - + # TODO: Convert to real trade trade = MagicMock() trade.pair = 'LTC/USDT' trade.entry_side = "buy" trade.open_rate = 200 trade.entry_side = "buy" trade.open_order_id = "open_order_noop" + trade.nr_of_successful_entries = 0 l_order['filled'] = 0.0 l_order['status'] = 'open' reason = CANCEL_REASON['TIMEOUT'] From 3549176370369242f9fec333d5b6e1644dced186 Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 31 May 2022 17:52:45 +0200 Subject: [PATCH 21/95] Update missleading docstring closes #6913 --- docs/strategy-callbacks.md | 12 +++++++----- freqtrade/strategy/interface.py | 4 ++-- .../subtemplates/strategy_methods_advanced.j2 | 10 +++++----- 3 files changed, 14 insertions(+), 12 deletions(-) diff --git a/docs/strategy-callbacks.md b/docs/strategy-callbacks.md index e1e57a1f3..f0f7d8f69 100644 --- a/docs/strategy-callbacks.md +++ b/docs/strategy-callbacks.md @@ -549,10 +549,11 @@ class AwesomeStrategy(IStrategy): :param pair: Pair that's about to be bought/shorted. :param order_type: Order type (as configured in order_types). usually limit or market. - :param amount: Amount in target (quote) currency that's going to be traded. + :param amount: Amount in target (base) currency that's going to be traded. :param rate: Rate that's going to be used when using limit orders :param time_in_force: Time in force. Defaults to GTC (Good-til-cancelled). :param current_time: datetime object, containing the current datetime + :param entry_tag: Optional entry_tag (buy_tag) if provided with the buy signal. :param side: 'long' or 'short' - indicating the direction of the proposed trade :param **kwargs: Ensure to keep this here so updates to this won't break your strategy. :return bool: When True is returned, then the buy-order is placed on the exchange. @@ -586,7 +587,7 @@ class AwesomeStrategy(IStrategy): rate: float, time_in_force: str, exit_reason: str, current_time: datetime, **kwargs) -> bool: """ - Called right before placing a regular sell order. + Called right before placing a regular exit order. Timing for this function is critical, so avoid doing heavy computations or network requests in this method. @@ -594,9 +595,10 @@ class AwesomeStrategy(IStrategy): When not implemented by a strategy, returns True (always confirming). - :param pair: Pair that's about to be sold. + :param pair: Pair for trade that's about to be exited. + :param trade: trade object. :param order_type: Order type (as configured in order_types). usually limit or market. - :param amount: Amount in quote currency. + :param amount: Amount in base currency. :param rate: Rate that's going to be used when using limit orders :param time_in_force: Time in force. Defaults to GTC (Good-til-cancelled). :param exit_reason: Exit reason. @@ -604,7 +606,7 @@ class AwesomeStrategy(IStrategy): 'exit_signal', 'force_exit', 'emergency_exit'] :param current_time: datetime object, containing the current datetime :param **kwargs: Ensure to keep this here so updates to this won't break your strategy. - :return bool: When True is returned, then the exit-order is placed on the exchange. + :return bool: When True, then the exit-order is placed on the exchange. False aborts the process """ if exit_reason == 'force_exit' and trade.calc_profit_ratio(rate) < 0: diff --git a/freqtrade/strategy/interface.py b/freqtrade/strategy/interface.py index 344c43b15..99dd1bfd7 100644 --- a/freqtrade/strategy/interface.py +++ b/freqtrade/strategy/interface.py @@ -287,7 +287,7 @@ class IStrategy(ABC, HyperStrategyMixin): :param pair: Pair that's about to be bought/shorted. :param order_type: Order type (as configured in order_types). usually limit or market. - :param amount: Amount in target (quote) currency that's going to be traded. + :param amount: Amount in target (base) currency that's going to be traded. :param rate: Rate that's going to be used when using limit orders :param time_in_force: Time in force. Defaults to GTC (Good-til-cancelled). :param current_time: datetime object, containing the current datetime @@ -314,7 +314,7 @@ class IStrategy(ABC, HyperStrategyMixin): :param pair: Pair for trade that's about to be exited. :param trade: trade object. :param order_type: Order type (as configured in order_types). usually limit or market. - :param amount: Amount in quote currency. + :param amount: Amount in base currency. :param rate: Rate that's going to be used when using limit orders :param time_in_force: Time in force. Defaults to GTC (Good-til-cancelled). :param exit_reason: Exit reason. diff --git a/freqtrade/templates/subtemplates/strategy_methods_advanced.j2 b/freqtrade/templates/subtemplates/strategy_methods_advanced.j2 index 3854efd85..103541efe 100644 --- a/freqtrade/templates/subtemplates/strategy_methods_advanced.j2 +++ b/freqtrade/templates/subtemplates/strategy_methods_advanced.j2 @@ -159,7 +159,7 @@ def confirm_trade_entry(self, pair: str, order_type: str, amount: float, rate: f :param pair: Pair that's about to be bought/shorted. :param order_type: Order type (as configured in order_types). usually limit or market. - :param amount: Amount in target (quote) currency that's going to be traded. + :param amount: Amount in target (base) currency that's going to be traded. :param rate: Rate that's going to be used when using limit orders :param time_in_force: Time in force. Defaults to GTC (Good-til-cancelled). :param current_time: datetime object, containing the current datetime @@ -175,7 +175,7 @@ def confirm_trade_exit(self, pair: str, trade: 'Trade', order_type: str, amount: rate: float, time_in_force: str, exit_reason: str, current_time: 'datetime', **kwargs) -> bool: """ - Called right before placing a regular sell order. + Called right before placing a regular exit order. Timing for this function is critical, so avoid doing heavy computations or network requests in this method. @@ -183,10 +183,10 @@ def confirm_trade_exit(self, pair: str, trade: 'Trade', order_type: str, amount: When not implemented by a strategy, returns True (always confirming). - :param pair: Pair that's currently analyzed + :param pair: Pair for trade that's about to be exited. :param trade: trade object. :param order_type: Order type (as configured in order_types). usually limit or market. - :param amount: Amount in quote currency. + :param amount: Amount in base currency. :param rate: Rate that's going to be used when using limit orders :param time_in_force: Time in force. Defaults to GTC (Good-til-cancelled). :param exit_reason: Exit reason. @@ -194,7 +194,7 @@ def confirm_trade_exit(self, pair: str, trade: 'Trade', order_type: str, amount: 'exit_signal', 'force_exit', 'emergency_exit'] :param current_time: datetime object, containing the current datetime :param **kwargs: Ensure to keep this here so updates to this won't break your strategy. - :return bool: When True is returned, then the exit-order is placed on the exchange. + :return bool: When True, then the exit-order is placed on the exchange. False aborts the process """ return True From 66edbcd3d5facf31788319b874ba195ab92b27ab Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 31 May 2022 20:08:34 +0200 Subject: [PATCH 22/95] Fix slight backtesting bug in edge-case scenarios --- freqtrade/optimize/backtesting.py | 19 ++++++++++++------- 1 file changed, 12 insertions(+), 7 deletions(-) diff --git a/freqtrade/optimize/backtesting.py b/freqtrade/optimize/backtesting.py index fa32666cc..bfb4031cc 100755 --- a/freqtrade/optimize/backtesting.py +++ b/freqtrade/optimize/backtesting.py @@ -895,26 +895,30 @@ class Backtesting: self.protections.stop_per_pair(pair, current_time, side) self.protections.global_stop(current_time, side) - def manage_open_orders(self, trade: LocalTrade, current_time, row: Tuple) -> bool: + def manage_open_orders(self, trade: LocalTrade, current_time: datetime, row: Tuple) -> bool: """ Check if any open order needs to be cancelled or replaced. Returns True if the trade should be deleted. """ for order in [o for o in trade.orders if o.ft_is_open]: - if self.check_order_cancel(trade, order, current_time): + oc = self.check_order_cancel(trade, order, current_time) + if oc: # delete trade due to order timeout return True - elif self.check_order_replace(trade, order, current_time, row): + elif oc is None and self.check_order_replace(trade, order, current_time, row): # delete trade due to user request self.canceled_trade_entries += 1 return True # default maintain trade return False - def check_order_cancel(self, trade: LocalTrade, order: Order, current_time) -> bool: + def check_order_cancel( + self, trade: LocalTrade, order: Order, current_time: datetime) -> Optional[bool]: """ Check if current analyzed order has to be canceled. - Returns True if the trade should be Deleted (initial order was canceled). + Returns True if the trade should be Deleted (initial order was canceled), + False if it's Canceled + None if the order is still active. """ timedout = self.strategy.ft_check_timed_out( trade, # type: ignore[arg-type] @@ -928,12 +932,13 @@ class Backtesting: else: # Close additional entry order del trade.orders[trade.orders.index(order)] + return False if order.side == trade.exit_side: self.timedout_exit_orders += 1 # Close exit order and retry exiting on next signal. del trade.orders[trade.orders.index(order)] - - return False + return False + return None def check_order_replace(self, trade: LocalTrade, order: Order, current_time, row: Tuple) -> bool: From 34a44b9dd23212a2f8de07ac13ae5d081658d736 Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 31 May 2022 20:32:29 +0200 Subject: [PATCH 23/95] Fix backtesting bug when canceling orders closes #6911 --- freqtrade/optimize/backtesting.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/freqtrade/optimize/backtesting.py b/freqtrade/optimize/backtesting.py index bfb4031cc..fa5065370 100755 --- a/freqtrade/optimize/backtesting.py +++ b/freqtrade/optimize/backtesting.py @@ -932,11 +932,13 @@ class Backtesting: else: # Close additional entry order del trade.orders[trade.orders.index(order)] + trade.open_order_id = None return False if order.side == trade.exit_side: self.timedout_exit_orders += 1 # Close exit order and retry exiting on next signal. del trade.orders[trade.orders.index(order)] + trade.open_order_id = None return False return None From afd8e85835c27a051b188296fc078a22bb0af5ef Mon Sep 17 00:00:00 2001 From: Anuj Shah Date: Wed, 1 Jun 2022 15:54:32 +0530 Subject: [PATCH 24/95] feat: add support for discord notification --- freqtrade/freqtradebot.py | 131 +++++++++++++++++++++++++++----------- 1 file changed, 94 insertions(+), 37 deletions(-) diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index fba63459b..f7e022987 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -2,6 +2,7 @@ Freqtrade is the main module of this bot. It contains the class Freqtrade() """ import copy +import json import logging import traceback from datetime import datetime, time, timezone @@ -9,6 +10,7 @@ from math import isclose from threading import Lock from typing import Any, Dict, List, Optional, Tuple +import requests from schedule import Scheduler from freqtrade import __version__, constants @@ -34,7 +36,6 @@ from freqtrade.strategy.interface import IStrategy from freqtrade.strategy.strategy_wrapper import strategy_safe_wrapper from freqtrade.wallets import Wallets - logger = logging.getLogger(__name__) @@ -379,9 +380,9 @@ class FreqtradeBot(LoggingMixin): except ExchangeError: logger.warning(f"Error updating {order.order_id}.") -# -# BUY / enter positions / open trades logic and methods -# + # + # BUY / enter positions / open trades logic and methods + # def enter_positions(self) -> int: """ @@ -489,9 +490,9 @@ class FreqtradeBot(LoggingMixin): else: return False -# -# BUY / increase positions / DCA logic and methods -# + # + # BUY / increase positions / DCA logic and methods + # def process_open_trade_positions(self): """ Tries to execute additional buy or sell orders for open trades (positions) @@ -579,16 +580,16 @@ class FreqtradeBot(LoggingMixin): return False def execute_entry( - self, - pair: str, - stake_amount: float, - price: Optional[float] = None, - *, - is_short: bool = False, - ordertype: Optional[str] = None, - enter_tag: Optional[str] = None, - trade: Optional[Trade] = None, - order_adjust: bool = False + self, + pair: str, + stake_amount: float, + price: Optional[float] = None, + *, + is_short: bool = False, + ordertype: Optional[str] = None, + enter_tag: Optional[str] = None, + trade: Optional[Trade] = None, + order_adjust: bool = False ) -> bool: """ Executes a limit buy for the given pair @@ -622,9 +623,9 @@ class FreqtradeBot(LoggingMixin): if not pos_adjust and not strategy_safe_wrapper( self.strategy.confirm_trade_entry, default_retval=True)( - pair=pair, order_type=order_type, amount=amount, rate=enter_limit_requested, - time_in_force=time_in_force, current_time=datetime.now(timezone.utc), - entry_tag=enter_tag, side=trade_side): + pair=pair, order_type=order_type, amount=amount, rate=enter_limit_requested, + time_in_force=time_in_force, current_time=datetime.now(timezone.utc), + entry_tag=enter_tag, side=trade_side): logger.info(f"User requested abortion of buying {pair}") return False order = self.exchange.create_order( @@ -746,11 +747,11 @@ class FreqtradeBot(LoggingMixin): return trade def get_valid_enter_price_and_stake( - self, pair: str, price: Optional[float], stake_amount: float, - trade_side: LongShort, - entry_tag: Optional[str], - trade: Optional[Trade], - order_adjust: bool, + self, pair: str, price: Optional[float], stake_amount: float, + trade_side: LongShort, + entry_tag: Optional[str], + trade: Optional[Trade], + order_adjust: bool, ) -> Tuple[float, float, float]: if price: @@ -885,9 +886,9 @@ class FreqtradeBot(LoggingMixin): # Send the message self.rpc.send_msg(msg) -# -# SELL / exit positions / close trades logic and methods -# + # + # SELL / exit positions / close trades logic and methods + # def exit_positions(self, trades: List[Any]) -> int: """ @@ -1059,10 +1060,10 @@ class FreqtradeBot(LoggingMixin): # Finally we check if stoploss on exchange should be moved up because of trailing. # Triggered Orders are now real orders - so don't replace stoploss anymore if ( - trade.is_open and stoploss_order - and stoploss_order.get('status_stop') != 'triggered' - and (self.config.get('trailing_stop', False) - or self.config.get('use_custom_stoploss', False)) + trade.is_open and stoploss_order + and stoploss_order.get('status_stop') != 'triggered' + and (self.config.get('trailing_stop', False) + or self.config.get('use_custom_stoploss', False)) ): # if trailing stoploss is enabled we check if stoploss value has changed # in which case we cancel stoploss order and put another one with new @@ -1145,7 +1146,7 @@ class FreqtradeBot(LoggingMixin): if not_closed: if fully_cancelled or (order_obj and self.strategy.ft_check_timed_out( - trade, order_obj, datetime.now(timezone.utc))): + trade, order_obj, datetime.now(timezone.utc))): self.handle_timedout_order(order, trade) else: self.replace_order(order, order_obj, trade) @@ -1424,7 +1425,7 @@ class FreqtradeBot(LoggingMixin): # if stoploss is on exchange and we are on dry_run mode, # we consider the sell price stop price if (self.config['dry_run'] and exit_type == 'stoploss' - and self.strategy.order_types['stoploss_on_exchange']): + and self.strategy.order_types['stoploss_on_exchange']): limit = trade.stop_loss # set custom_exit_price if available @@ -1543,6 +1544,43 @@ class FreqtradeBot(LoggingMixin): # Send the message self.rpc.send_msg(msg) + open_date = trade.open_date.strftime('%Y-%m-%d %H:%M:%S') + close_date = trade.close_date.strftime('%Y-%m-%d %H:%M:%S') if trade.close_date else None + + # Send the message to the discord bot + embeds = [{ + 'title': '{} Trade: {}'.format( + 'Profit' if profit_ratio > 0 else 'Loss', + trade.pair), + 'color': (0x00FF00 if profit_ratio > 0 else 0xFF0000), + 'fields': [ + {'name': 'Trade ID', 'value': trade.id, 'inline': True}, + {'name': 'Exchange', 'value': trade.exchange.capitalize(), 'inline': True}, + {'name': 'Pair', 'value': trade.pair, 'inline': True}, + {'name': 'Direction', 'value': 'Short' if trade.is_short else 'Long', 'inline': True}, + {'name': 'Open rate', 'value': trade.open_rate, 'inline': True}, + {'name': 'Close rate', 'value': trade.close_rate, 'inline': True}, + {'name': 'Amount', 'value': trade.amount, 'inline': True}, + {'name': 'Open order', 'value': trade.open_order_id, 'inline': True}, + {'name': 'Open date', 'value': open_date, 'inline': True}, + {'name': 'Close date', 'value': close_date, 'inline': True}, + {'name': 'Profit', 'value': profit_trade, 'inline': True}, + {'name': 'Profitability', 'value': '{:.2f}%'.format(profit_ratio * 100), 'inline': True}, + {'name': 'Stake currency', 'value': self.config['stake_currency'], 'inline': True}, + {'name': 'Fiat currency', 'value': self.config.get('fiat_display_currency', None), 'inline': True}, + {'name': 'Buy Tag', 'value': trade.enter_tag, 'inline': True}, + {'name': 'Sell Reason', 'value': trade.exit_reason, 'inline': True}, + {'name': 'Strategy', 'value': trade.strategy, 'inline': True}, + {'name': 'Timeframe', 'value': trade.timeframe, 'inline': True}, + ], + }] + # convert all value in fields to string + for embed in embeds: + for field in embed['fields']: + field['value'] = str(field['value']) + if fill: + self.discord_send(embeds) + def _notify_exit_cancel(self, trade: Trade, order_type: str, reason: str) -> None: """ Sends rpc notification when a sell cancel occurred. @@ -1593,9 +1631,9 @@ class FreqtradeBot(LoggingMixin): # Send the message self.rpc.send_msg(msg) -# -# Common update trade state methods -# + # + # Common update trade state methods + # def update_trade_state(self, trade: Trade, order_id: str, action_order: Dict[str, Any] = None, stoploss_order: bool = False, send_msg: bool = True) -> bool: @@ -1818,3 +1856,22 @@ class FreqtradeBot(LoggingMixin): return max( min(valid_custom_price, max_custom_price_allowed), min_custom_price_allowed) + + def discord_send(self, embeds): + if not 'discord' in self.config or self.config['discord']['enabled'] == False: + return + if self.config['runmode'].value in ('dry_run', 'live'): + webhook_url = self.config['discord']['webhook_url'] + + payload = { + "embeds": embeds + } + + headers = { + "Content-Type": "application/json" + } + + try: + requests.post(webhook_url, data=json.dumps(payload), headers=headers) + except Exception as e: + logger.error(f"Error sending discord message: {e}") From 45c47bda6000b2b57026fdedffaaa69f8fc1797e Mon Sep 17 00:00:00 2001 From: Anuj Shah Date: Wed, 1 Jun 2022 21:14:48 +0530 Subject: [PATCH 25/95] refactor into discord rpc module --- freqtrade/freqtradebot.py | 131 ++++++++++------------------------- freqtrade/rpc/discord.py | 101 +++++++++++++++++++++++++++ freqtrade/rpc/rpc_manager.py | 6 ++ 3 files changed, 144 insertions(+), 94 deletions(-) create mode 100644 freqtrade/rpc/discord.py diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index f7e022987..fba63459b 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -2,7 +2,6 @@ Freqtrade is the main module of this bot. It contains the class Freqtrade() """ import copy -import json import logging import traceback from datetime import datetime, time, timezone @@ -10,7 +9,6 @@ from math import isclose from threading import Lock from typing import Any, Dict, List, Optional, Tuple -import requests from schedule import Scheduler from freqtrade import __version__, constants @@ -36,6 +34,7 @@ from freqtrade.strategy.interface import IStrategy from freqtrade.strategy.strategy_wrapper import strategy_safe_wrapper from freqtrade.wallets import Wallets + logger = logging.getLogger(__name__) @@ -380,9 +379,9 @@ class FreqtradeBot(LoggingMixin): except ExchangeError: logger.warning(f"Error updating {order.order_id}.") - # - # BUY / enter positions / open trades logic and methods - # +# +# BUY / enter positions / open trades logic and methods +# def enter_positions(self) -> int: """ @@ -490,9 +489,9 @@ class FreqtradeBot(LoggingMixin): else: return False - # - # BUY / increase positions / DCA logic and methods - # +# +# BUY / increase positions / DCA logic and methods +# def process_open_trade_positions(self): """ Tries to execute additional buy or sell orders for open trades (positions) @@ -580,16 +579,16 @@ class FreqtradeBot(LoggingMixin): return False def execute_entry( - self, - pair: str, - stake_amount: float, - price: Optional[float] = None, - *, - is_short: bool = False, - ordertype: Optional[str] = None, - enter_tag: Optional[str] = None, - trade: Optional[Trade] = None, - order_adjust: bool = False + self, + pair: str, + stake_amount: float, + price: Optional[float] = None, + *, + is_short: bool = False, + ordertype: Optional[str] = None, + enter_tag: Optional[str] = None, + trade: Optional[Trade] = None, + order_adjust: bool = False ) -> bool: """ Executes a limit buy for the given pair @@ -623,9 +622,9 @@ class FreqtradeBot(LoggingMixin): if not pos_adjust and not strategy_safe_wrapper( self.strategy.confirm_trade_entry, default_retval=True)( - pair=pair, order_type=order_type, amount=amount, rate=enter_limit_requested, - time_in_force=time_in_force, current_time=datetime.now(timezone.utc), - entry_tag=enter_tag, side=trade_side): + pair=pair, order_type=order_type, amount=amount, rate=enter_limit_requested, + time_in_force=time_in_force, current_time=datetime.now(timezone.utc), + entry_tag=enter_tag, side=trade_side): logger.info(f"User requested abortion of buying {pair}") return False order = self.exchange.create_order( @@ -747,11 +746,11 @@ class FreqtradeBot(LoggingMixin): return trade def get_valid_enter_price_and_stake( - self, pair: str, price: Optional[float], stake_amount: float, - trade_side: LongShort, - entry_tag: Optional[str], - trade: Optional[Trade], - order_adjust: bool, + self, pair: str, price: Optional[float], stake_amount: float, + trade_side: LongShort, + entry_tag: Optional[str], + trade: Optional[Trade], + order_adjust: bool, ) -> Tuple[float, float, float]: if price: @@ -886,9 +885,9 @@ class FreqtradeBot(LoggingMixin): # Send the message self.rpc.send_msg(msg) - # - # SELL / exit positions / close trades logic and methods - # +# +# SELL / exit positions / close trades logic and methods +# def exit_positions(self, trades: List[Any]) -> int: """ @@ -1060,10 +1059,10 @@ class FreqtradeBot(LoggingMixin): # Finally we check if stoploss on exchange should be moved up because of trailing. # Triggered Orders are now real orders - so don't replace stoploss anymore if ( - trade.is_open and stoploss_order - and stoploss_order.get('status_stop') != 'triggered' - and (self.config.get('trailing_stop', False) - or self.config.get('use_custom_stoploss', False)) + trade.is_open and stoploss_order + and stoploss_order.get('status_stop') != 'triggered' + and (self.config.get('trailing_stop', False) + or self.config.get('use_custom_stoploss', False)) ): # if trailing stoploss is enabled we check if stoploss value has changed # in which case we cancel stoploss order and put another one with new @@ -1146,7 +1145,7 @@ class FreqtradeBot(LoggingMixin): if not_closed: if fully_cancelled or (order_obj and self.strategy.ft_check_timed_out( - trade, order_obj, datetime.now(timezone.utc))): + trade, order_obj, datetime.now(timezone.utc))): self.handle_timedout_order(order, trade) else: self.replace_order(order, order_obj, trade) @@ -1425,7 +1424,7 @@ class FreqtradeBot(LoggingMixin): # if stoploss is on exchange and we are on dry_run mode, # we consider the sell price stop price if (self.config['dry_run'] and exit_type == 'stoploss' - and self.strategy.order_types['stoploss_on_exchange']): + and self.strategy.order_types['stoploss_on_exchange']): limit = trade.stop_loss # set custom_exit_price if available @@ -1544,43 +1543,6 @@ class FreqtradeBot(LoggingMixin): # Send the message self.rpc.send_msg(msg) - open_date = trade.open_date.strftime('%Y-%m-%d %H:%M:%S') - close_date = trade.close_date.strftime('%Y-%m-%d %H:%M:%S') if trade.close_date else None - - # Send the message to the discord bot - embeds = [{ - 'title': '{} Trade: {}'.format( - 'Profit' if profit_ratio > 0 else 'Loss', - trade.pair), - 'color': (0x00FF00 if profit_ratio > 0 else 0xFF0000), - 'fields': [ - {'name': 'Trade ID', 'value': trade.id, 'inline': True}, - {'name': 'Exchange', 'value': trade.exchange.capitalize(), 'inline': True}, - {'name': 'Pair', 'value': trade.pair, 'inline': True}, - {'name': 'Direction', 'value': 'Short' if trade.is_short else 'Long', 'inline': True}, - {'name': 'Open rate', 'value': trade.open_rate, 'inline': True}, - {'name': 'Close rate', 'value': trade.close_rate, 'inline': True}, - {'name': 'Amount', 'value': trade.amount, 'inline': True}, - {'name': 'Open order', 'value': trade.open_order_id, 'inline': True}, - {'name': 'Open date', 'value': open_date, 'inline': True}, - {'name': 'Close date', 'value': close_date, 'inline': True}, - {'name': 'Profit', 'value': profit_trade, 'inline': True}, - {'name': 'Profitability', 'value': '{:.2f}%'.format(profit_ratio * 100), 'inline': True}, - {'name': 'Stake currency', 'value': self.config['stake_currency'], 'inline': True}, - {'name': 'Fiat currency', 'value': self.config.get('fiat_display_currency', None), 'inline': True}, - {'name': 'Buy Tag', 'value': trade.enter_tag, 'inline': True}, - {'name': 'Sell Reason', 'value': trade.exit_reason, 'inline': True}, - {'name': 'Strategy', 'value': trade.strategy, 'inline': True}, - {'name': 'Timeframe', 'value': trade.timeframe, 'inline': True}, - ], - }] - # convert all value in fields to string - for embed in embeds: - for field in embed['fields']: - field['value'] = str(field['value']) - if fill: - self.discord_send(embeds) - def _notify_exit_cancel(self, trade: Trade, order_type: str, reason: str) -> None: """ Sends rpc notification when a sell cancel occurred. @@ -1631,9 +1593,9 @@ class FreqtradeBot(LoggingMixin): # Send the message self.rpc.send_msg(msg) - # - # Common update trade state methods - # +# +# Common update trade state methods +# def update_trade_state(self, trade: Trade, order_id: str, action_order: Dict[str, Any] = None, stoploss_order: bool = False, send_msg: bool = True) -> bool: @@ -1856,22 +1818,3 @@ class FreqtradeBot(LoggingMixin): return max( min(valid_custom_price, max_custom_price_allowed), min_custom_price_allowed) - - def discord_send(self, embeds): - if not 'discord' in self.config or self.config['discord']['enabled'] == False: - return - if self.config['runmode'].value in ('dry_run', 'live'): - webhook_url = self.config['discord']['webhook_url'] - - payload = { - "embeds": embeds - } - - headers = { - "Content-Type": "application/json" - } - - try: - requests.post(webhook_url, data=json.dumps(payload), headers=headers) - except Exception as e: - logger.error(f"Error sending discord message: {e}") diff --git a/freqtrade/rpc/discord.py b/freqtrade/rpc/discord.py new file mode 100644 index 000000000..ee9970dc5 --- /dev/null +++ b/freqtrade/rpc/discord.py @@ -0,0 +1,101 @@ +import json +import logging +from typing import Dict, Any + +import requests + +from freqtrade.enums import RPCMessageType +from freqtrade.rpc import RPCHandler, RPC + + +class Discord(RPCHandler): + def __init__(self, rpc: 'RPC', config: Dict[str, Any]): + super().__init__(rpc, config) + self.logger = logging.getLogger(__name__) + self.strategy = config.get('strategy', '') + self.timeframe = config.get('timeframe', '') + self.config = config + + def send_msg(self, msg: Dict[str, str]) -> None: + self._send_msg(msg) + + def _send_msg(self, msg): + """ + msg = { + 'type': (RPCMessageType.EXIT_FILL if fill + else RPCMessageType.EXIT), + 'trade_id': trade.id, + 'exchange': trade.exchange.capitalize(), + 'pair': trade.pair, + 'leverage': trade.leverage, + 'direction': 'Short' if trade.is_short else 'Long', + 'gain': gain, + 'limit': profit_rate, + 'order_type': order_type, + 'amount': trade.amount, + 'open_rate': trade.open_rate, + 'close_rate': trade.close_rate, + 'current_rate': current_rate, + 'profit_amount': profit_trade, + 'profit_ratio': profit_ratio, + 'buy_tag': trade.enter_tag, + 'enter_tag': trade.enter_tag, + 'sell_reason': trade.exit_reason, # Deprecated + 'exit_reason': trade.exit_reason, + 'open_date': trade.open_date, + 'close_date': trade.close_date or datetime.utcnow(), + 'stake_currency': self.config['stake_currency'], + 'fiat_currency': self.config.get('fiat_display_currency', None), + } + """ + self.logger.info(f"Sending discord message: {msg}") + + # TODO: handle other message types + if msg['type'] == RPCMessageType.EXIT_FILL: + profit_ratio = msg.get('profit_ratio') + open_date = msg.get('open_date').strftime('%Y-%m-%d %H:%M:%S') + close_date = msg.get('close_date').strftime('%Y-%m-%d %H:%M:%S') if msg.get('close_date') else '' + + embeds = [{ + 'title': '{} Trade: {}'.format( + 'Profit' if profit_ratio > 0 else 'Loss', + msg.get('pair')), + 'color': (0x00FF00 if profit_ratio > 0 else 0xFF0000), + 'fields': [ + {'name': 'Trade ID', 'value': msg.get('id'), 'inline': True}, + {'name': 'Exchange', 'value': msg.get('exchange').capitalize(), 'inline': True}, + {'name': 'Pair', 'value': msg.get('pair'), 'inline': True}, + {'name': 'Direction', 'value': 'Short' if msg.get('is_short') else 'Long', 'inline': True}, + {'name': 'Open rate', 'value': msg.get('open_rate'), 'inline': True}, + {'name': 'Close rate', 'value': msg.get('close_rate'), 'inline': True}, + {'name': 'Amount', 'value': msg.get('amount'), 'inline': True}, + {'name': 'Open order', 'value': msg.get('open_order_id'), 'inline': True}, + {'name': 'Open date', 'value': open_date, 'inline': True}, + {'name': 'Close date', 'value': close_date, 'inline': True}, + {'name': 'Profit', 'value': msg.get('profit_amount'), 'inline': True}, + {'name': 'Profitability', 'value': '{:.2f}%'.format(profit_ratio * 100), 'inline': True}, + {'name': 'Stake currency', 'value': msg.get('stake_currency'), 'inline': True}, + {'name': 'Fiat currency', 'value': msg.get('fiat_display_currency'), 'inline': True}, + {'name': 'Buy Tag', 'value': msg.get('enter_tag'), 'inline': True}, + {'name': 'Sell Reason', 'value': msg.get('exit_reason'), 'inline': True}, + {'name': 'Strategy', 'value': self.strategy, 'inline': True}, + {'name': 'Timeframe', 'value': self.timeframe, 'inline': True}, + ], + }] + + # convert all value in fields to string for discord + for embed in embeds: + for field in embed['fields']: + field['value'] = str(field['value']) + + # Send the message to discord channel + payload = { + 'embeds': embeds, + } + headers = { + 'Content-Type': 'application/json', + } + try: + requests.post(self.config['discord']['webhook_url'], data=json.dumps(payload), headers=headers) + except Exception as e: + self.logger.error(f"Failed to send discord message: {e}") diff --git a/freqtrade/rpc/rpc_manager.py b/freqtrade/rpc/rpc_manager.py index d97d1df5f..66e84029f 100644 --- a/freqtrade/rpc/rpc_manager.py +++ b/freqtrade/rpc/rpc_manager.py @@ -27,6 +27,12 @@ class RPCManager: from freqtrade.rpc.telegram import Telegram self.registered_modules.append(Telegram(self._rpc, config)) + # Enable discord + if config.get('discord', {}).get('enabled', False): + logger.info('Enabling rpc.discord ...') + from freqtrade.rpc.discord import Discord + self.registered_modules.append(Discord(self._rpc, config)) + # Enable Webhook if config.get('webhook', {}).get('enabled', False): logger.info('Enabling rpc.webhook ...') From eb4adeab4d7511fe084924e72c14065c6c106ebf Mon Sep 17 00:00:00 2001 From: Anuj Shah Date: Thu, 2 Jun 2022 11:19:29 +0530 Subject: [PATCH 26/95] fix flake8 issues --- freqtrade/rpc/discord.py | 17 ++++++++++++----- 1 file changed, 12 insertions(+), 5 deletions(-) diff --git a/freqtrade/rpc/discord.py b/freqtrade/rpc/discord.py index ee9970dc5..43a8e9a05 100644 --- a/freqtrade/rpc/discord.py +++ b/freqtrade/rpc/discord.py @@ -54,7 +54,8 @@ class Discord(RPCHandler): if msg['type'] == RPCMessageType.EXIT_FILL: profit_ratio = msg.get('profit_ratio') open_date = msg.get('open_date').strftime('%Y-%m-%d %H:%M:%S') - close_date = msg.get('close_date').strftime('%Y-%m-%d %H:%M:%S') if msg.get('close_date') else '' + close_date = msg.get('close_date').strftime( + '%Y-%m-%d %H:%M:%S') if msg.get('close_date') else '' embeds = [{ 'title': '{} Trade: {}'.format( @@ -65,7 +66,8 @@ class Discord(RPCHandler): {'name': 'Trade ID', 'value': msg.get('id'), 'inline': True}, {'name': 'Exchange', 'value': msg.get('exchange').capitalize(), 'inline': True}, {'name': 'Pair', 'value': msg.get('pair'), 'inline': True}, - {'name': 'Direction', 'value': 'Short' if msg.get('is_short') else 'Long', 'inline': True}, + {'name': 'Direction', 'value': 'Short' if msg.get( + 'is_short') else 'Long', 'inline': True}, {'name': 'Open rate', 'value': msg.get('open_rate'), 'inline': True}, {'name': 'Close rate', 'value': msg.get('close_rate'), 'inline': True}, {'name': 'Amount', 'value': msg.get('amount'), 'inline': True}, @@ -73,9 +75,11 @@ class Discord(RPCHandler): {'name': 'Open date', 'value': open_date, 'inline': True}, {'name': 'Close date', 'value': close_date, 'inline': True}, {'name': 'Profit', 'value': msg.get('profit_amount'), 'inline': True}, - {'name': 'Profitability', 'value': '{:.2f}%'.format(profit_ratio * 100), 'inline': True}, + {'name': 'Profitability', 'value': '{:.2f}%'.format( + profit_ratio * 100), 'inline': True}, {'name': 'Stake currency', 'value': msg.get('stake_currency'), 'inline': True}, - {'name': 'Fiat currency', 'value': msg.get('fiat_display_currency'), 'inline': True}, + {'name': 'Fiat currency', 'value': msg.get( + 'fiat_display_currency'), 'inline': True}, {'name': 'Buy Tag', 'value': msg.get('enter_tag'), 'inline': True}, {'name': 'Sell Reason', 'value': msg.get('exit_reason'), 'inline': True}, {'name': 'Strategy', 'value': self.strategy, 'inline': True}, @@ -96,6 +100,9 @@ class Discord(RPCHandler): 'Content-Type': 'application/json', } try: - requests.post(self.config['discord']['webhook_url'], data=json.dumps(payload), headers=headers) + requests.post( + self.config['discord']['webhook_url'], + data=json.dumps(payload), + headers=headers) except Exception as e: self.logger.error(f"Failed to send discord message: {e}") From 27bea580d492afa73f2b79e7ad3f63f8995fa4ba Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 5 Jun 2022 09:40:04 +0200 Subject: [PATCH 27/95] Fix rest-client script's force_enter closes #6927 --- scripts/rest_client.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/scripts/rest_client.py b/scripts/rest_client.py index ecbb65253..e5d358c98 100755 --- a/scripts/rest_client.py +++ b/scripts/rest_client.py @@ -261,7 +261,7 @@ class FtRestClient(): } return self._post("forcebuy", data=data) - def force_enter(self, pair, side, price=None): + def forceenter(self, pair, side, price=None): """Force entering a trade :param pair: Pair to buy (ETH/BTC) @@ -273,7 +273,7 @@ class FtRestClient(): "side": side, "price": price, } - return self._post("force_enter", data=data) + return self._post("forceenter", data=data) def forceexit(self, tradeid): """Force-exit a trade. From a790bad1e4dd8248d95c9ed8d2d9d50a76a196b6 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 5 Jun 2022 10:21:06 +0200 Subject: [PATCH 28/95] Add entry_tag to leverage callback closes #6929 --- docs/strategy-callbacks.md | 7 ++++--- freqtrade/freqtradebot.py | 2 +- freqtrade/optimize/backtesting.py | 2 +- freqtrade/strategy/interface.py | 5 +++-- .../templates/subtemplates/strategy_methods_advanced.j2 | 5 +++-- tests/strategy/strats/strategy_test_v3.py | 4 ++-- tests/strategy/test_interface.py | 2 ++ 7 files changed, 16 insertions(+), 11 deletions(-) diff --git a/docs/strategy-callbacks.md b/docs/strategy-callbacks.md index f0f7d8f69..656f206a4 100644 --- a/docs/strategy-callbacks.md +++ b/docs/strategy-callbacks.md @@ -804,17 +804,18 @@ For markets / exchanges that don't support leverage, this method is ignored. ``` python class AwesomeStrategy(IStrategy): - def leverage(self, pair: str, current_time: 'datetime', current_rate: float, - proposed_leverage: float, max_leverage: float, side: str, + def leverage(self, pair: str, current_time: datetime, current_rate: float, + proposed_leverage: float, max_leverage: float, entry_tag: Optional[str], side: str, **kwargs) -> float: """ - Customize leverage for each new trade. + Customize leverage for each new trade. This method is only called in futures mode. :param pair: Pair that's currently analyzed :param current_time: datetime object, containing the current datetime :param current_rate: Rate, calculated based on pricing settings in exit_pricing. :param proposed_leverage: A leverage proposed by the bot. :param max_leverage: Max leverage allowed on this pair + :param entry_tag: Optional entry_tag (buy_tag) if provided with the buy signal. :param side: 'long' or 'short' - indicating the direction of the proposed trade :return: A leverage amount, which is between 1.0 and max_leverage. """ diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index fba63459b..95eb911cf 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -781,7 +781,7 @@ class FreqtradeBot(LoggingMixin): current_rate=enter_limit_requested, proposed_leverage=1.0, max_leverage=max_leverage, - side=trade_side, + side=trade_side, entry_tag=entry_tag, ) if self.trading_mode != TradingMode.SPOT else 1.0 # Cap leverage between 1.0 and max_leverage. leverage = min(max(leverage, 1.0), max_leverage) diff --git a/freqtrade/optimize/backtesting.py b/freqtrade/optimize/backtesting.py index fa5065370..aebaecaca 100755 --- a/freqtrade/optimize/backtesting.py +++ b/freqtrade/optimize/backtesting.py @@ -704,7 +704,7 @@ class Backtesting: current_rate=row[OPEN_IDX], proposed_leverage=1.0, max_leverage=max_leverage, - side=direction, + side=direction, entry_tag=entry_tag, ) if self._can_short else 1.0 # Cap leverage between 1.0 and max_leverage. leverage = min(max(leverage, 1.0), max_leverage) diff --git a/freqtrade/strategy/interface.py b/freqtrade/strategy/interface.py index 99dd1bfd7..3b3d326ff 100644 --- a/freqtrade/strategy/interface.py +++ b/freqtrade/strategy/interface.py @@ -509,8 +509,8 @@ class IStrategy(ABC, HyperStrategyMixin): return current_order_rate def leverage(self, pair: str, current_time: datetime, current_rate: float, - proposed_leverage: float, max_leverage: float, side: str, - **kwargs) -> float: + proposed_leverage: float, max_leverage: float, entry_tag: Optional[str], + side: str, **kwargs) -> float: """ Customize leverage for each new trade. This method is only called in futures mode. @@ -519,6 +519,7 @@ class IStrategy(ABC, HyperStrategyMixin): :param current_rate: Rate, calculated based on pricing settings in exit_pricing. :param proposed_leverage: A leverage proposed by the bot. :param max_leverage: Max leverage allowed on this pair + :param entry_tag: Optional entry_tag (buy_tag) if provided with the buy signal. :param side: 'long' or 'short' - indicating the direction of the proposed trade :return: A leverage amount, which is between 1.0 and max_leverage. """ diff --git a/freqtrade/templates/subtemplates/strategy_methods_advanced.j2 b/freqtrade/templates/subtemplates/strategy_methods_advanced.j2 index 103541efe..acefd0363 100644 --- a/freqtrade/templates/subtemplates/strategy_methods_advanced.j2 +++ b/freqtrade/templates/subtemplates/strategy_methods_advanced.j2 @@ -267,8 +267,8 @@ def adjust_trade_position(self, trade: 'Trade', current_time: 'datetime', return None def leverage(self, pair: str, current_time: datetime, current_rate: float, - proposed_leverage: float, max_leverage: float, side: str, - **kwargs) -> float: + proposed_leverage: float, max_leverage: float, entry_tag: Optional[str], + side: str, **kwargs) -> float: """ Customize leverage for each new trade. This method is only called in futures mode. @@ -277,6 +277,7 @@ def leverage(self, pair: str, current_time: datetime, current_rate: float, :param current_rate: Rate, calculated based on pricing settings in exit_pricing. :param proposed_leverage: A leverage proposed by the bot. :param max_leverage: Max leverage allowed on this pair + :param entry_tag: Optional entry_tag (buy_tag) if provided with the buy signal. :param side: 'long' or 'short' - indicating the direction of the proposed trade :return: A leverage amount, which is between 1.0 and max_leverage. """ diff --git a/tests/strategy/strats/strategy_test_v3.py b/tests/strategy/strats/strategy_test_v3.py index 340001ef2..2c7ccbdf2 100644 --- a/tests/strategy/strats/strategy_test_v3.py +++ b/tests/strategy/strats/strategy_test_v3.py @@ -178,8 +178,8 @@ class StrategyTestV3(IStrategy): return dataframe def leverage(self, pair: str, current_time: datetime, current_rate: float, - proposed_leverage: float, max_leverage: float, side: str, - **kwargs) -> float: + proposed_leverage: float, max_leverage: float, entry_tag: Optional[str], + side: str, **kwargs) -> float: # Return 3.0 in all cases. # Bot-logic must make sure it's an allowed leverage and eventually adjust accordingly. diff --git a/tests/strategy/test_interface.py b/tests/strategy/test_interface.py index e3c0bcfcb..b7b73bdcf 100644 --- a/tests/strategy/test_interface.py +++ b/tests/strategy/test_interface.py @@ -615,6 +615,7 @@ def test_leverage_callback(default_conf, side) -> None: proposed_leverage=1.0, max_leverage=5.0, side=side, + entry_tag=None, ) == 1 default_conf['strategy'] = CURRENT_TEST_STRATEGY @@ -626,6 +627,7 @@ def test_leverage_callback(default_conf, side) -> None: proposed_leverage=1.0, max_leverage=5.0, side=side, + entry_tag='entry_tag_test', ) == 3 From c499bb051f4753f20c9daef9660932c2b610ecd7 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 5 Jun 2022 19:41:17 +0200 Subject: [PATCH 29/95] Allow empty unfilledtimeout for webserver mode --- freqtrade/rpc/api_server/api_schemas.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/freqtrade/rpc/api_server/api_schemas.py b/freqtrade/rpc/api_server/api_schemas.py index f21334bc6..a31c74c2e 100644 --- a/freqtrade/rpc/api_server/api_schemas.py +++ b/freqtrade/rpc/api_server/api_schemas.py @@ -166,7 +166,7 @@ class ShowConfig(BaseModel): trailing_stop_positive: Optional[float] trailing_stop_positive_offset: Optional[float] trailing_only_offset_is_reached: Optional[bool] - unfilledtimeout: UnfilledTimeout + unfilledtimeout: Optional[UnfilledTimeout] # Empty in webserver mode order_types: Optional[OrderTypes] use_custom_stoploss: Optional[bool] timeframe: Optional[str] From f709222943fcc2807561ae4a8fc7bcb9a8d6c66c Mon Sep 17 00:00:00 2001 From: Matthias Date: Fri, 29 Apr 2022 06:53:30 +0200 Subject: [PATCH 30/95] Properly close out orders in backtesting --- freqtrade/optimize/backtesting.py | 1 + freqtrade/persistence/trade_model.py | 1 + 2 files changed, 2 insertions(+) diff --git a/freqtrade/optimize/backtesting.py b/freqtrade/optimize/backtesting.py index aebaecaca..8fe5f509e 100755 --- a/freqtrade/optimize/backtesting.py +++ b/freqtrade/optimize/backtesting.py @@ -1094,6 +1094,7 @@ class Backtesting: # 5. Process exit orders. order = trade.select_order(trade.exit_side, is_open=True) if order and self._get_order_filled(order.price, row): + order.close_bt_order(current_time, trade) trade.open_order_id = None trade.close_date = current_time trade.close(order.price, show_msg=False) diff --git a/freqtrade/persistence/trade_model.py b/freqtrade/persistence/trade_model.py index 45a16bfbd..7b475d618 100644 --- a/freqtrade/persistence/trade_model.py +++ b/freqtrade/persistence/trade_model.py @@ -166,6 +166,7 @@ class Order(_DECL_BASE): def close_bt_order(self, close_date: datetime, trade: 'LocalTrade'): self.order_filled_date = close_date self.filled = self.amount + self.remaining = 0 self.status = 'closed' self.ft_is_open = False if (self.ft_order_side == trade.entry_side From c0ff554d5be871098cd10424fdd579322b5370df Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 24 May 2022 20:12:05 +0200 Subject: [PATCH 31/95] Cleanup old, left open dry-run orders --- freqtrade/persistence/migrations.py | 30 +++++++++++++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/freqtrade/persistence/migrations.py b/freqtrade/persistence/migrations.py index 53e35d9da..b0fdf0412 100644 --- a/freqtrade/persistence/migrations.py +++ b/freqtrade/persistence/migrations.py @@ -247,6 +247,35 @@ def set_sqlite_to_wal(engine): connection.execute(text("PRAGMA journal_mode=wal")) +def fix_old_dry_orders(engine): + with engine.begin() as connection: + connection.execute( + text( + """ + update orders + set ft_is_open = 0 + where ft_is_open = 1 and (ft_trade_id, order_id) not in ( + select id, stoploss_order_id from trades where stoploss_order_id is not null + ) and ft_order_side = 'stoploss' + and order_id like 'dry_%' + """ + ) + ) + connection.execute( + text( + """ + update orders + set ft_is_open = 0 + where ft_is_open = 1 + and (ft_trade_id, order_id) not in ( + select id, open_order_id from trades where open_order_id is not null + ) and ft_order_side != 'stoploss' + and order_id like 'dry_%' + """ + ) + ) + + def check_migrate(engine, decl_base, previous_tables) -> None: """ Checks if migration is necessary and migrates if necessary @@ -288,3 +317,4 @@ def check_migrate(engine, decl_base, previous_tables) -> None: "start with a fresh database.") set_sqlite_to_wal(engine) + fix_old_dry_orders(engine) From 8369d5bedd25f7679b060fd075be2eb061623ebe Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 24 May 2022 20:31:45 +0200 Subject: [PATCH 32/95] Include open orders in json responses --- freqtrade/persistence/trade_model.py | 17 ++++++++++++++++- freqtrade/rpc/telegram.py | 2 +- 2 files changed, 17 insertions(+), 2 deletions(-) diff --git a/freqtrade/persistence/trade_model.py b/freqtrade/persistence/trade_model.py index 7b475d618..ded616f8a 100644 --- a/freqtrade/persistence/trade_model.py +++ b/freqtrade/persistence/trade_model.py @@ -395,7 +395,7 @@ class LocalTrade(): ) def to_json(self) -> Dict[str, Any]: - filled_orders = self.select_filled_orders() + filled_orders = self.select_filled_or_open_orders() orders = [order.to_json(self.entry_side) for order in filled_orders] return { @@ -898,6 +898,21 @@ class LocalTrade(): (o.filled or 0) > 0 and o.status in NON_OPEN_EXCHANGE_STATES] + def select_filled_or_open_orders(self) -> List['Order']: + """ + Finds filled or open orders + :param order_side: Side of the order (either 'buy', 'sell', or None) + :return: array of Order objects + """ + return [o for o in self.orders if + ( + o.ft_is_open is False + and (o.filled or 0) > 0 + and o.status in NON_OPEN_EXCHANGE_STATES + ) + or (o.ft_is_open is True and o.status is not None) + ] + @property def nr_of_successful_entries(self) -> int: """ diff --git a/freqtrade/rpc/telegram.py b/freqtrade/rpc/telegram.py index 4a274002e..e456b1eef 100644 --- a/freqtrade/rpc/telegram.py +++ b/freqtrade/rpc/telegram.py @@ -396,7 +396,7 @@ class Telegram(RPCHandler): first_avg = filled_orders[0]["safe_price"] for x, order in enumerate(filled_orders): - if not order['ft_is_entry']: + if not order['ft_is_entry'] or order['is_open'] is True: continue cur_entry_datetime = arrow.get(order["order_filled_date"]) cur_entry_amount = order["amount"] From 79107fd062e9e60f78c467367b7c34cc68f5b6c6 Mon Sep 17 00:00:00 2001 From: Matthias Date: Thu, 26 May 2022 07:11:43 +0200 Subject: [PATCH 33/95] Add minimal order object serialization --- freqtrade/data/btanalysis.py | 2 +- freqtrade/persistence/trade_model.py | 44 +++++++++++++++------------- 2 files changed, 25 insertions(+), 21 deletions(-) diff --git a/freqtrade/data/btanalysis.py b/freqtrade/data/btanalysis.py index fef432576..0b466241f 100644 --- a/freqtrade/data/btanalysis.py +++ b/freqtrade/data/btanalysis.py @@ -337,7 +337,7 @@ def trade_list_to_dataframe(trades: List[LocalTrade]) -> pd.DataFrame: :param trades: List of trade objects :return: Dataframe with BT_DATA_COLUMNS """ - df = pd.DataFrame.from_records([t.to_json() for t in trades], columns=BT_DATA_COLUMNS) + df = pd.DataFrame.from_records([t.to_json(True) for t in trades], columns=BT_DATA_COLUMNS) if len(df) > 0: df.loc[:, 'close_date'] = pd.to_datetime(df['close_date'], utc=True) df.loc[:, 'open_date'] = pd.to_datetime(df['open_date'], utc=True) diff --git a/freqtrade/persistence/trade_model.py b/freqtrade/persistence/trade_model.py index ded616f8a..0be9d22c1 100644 --- a/freqtrade/persistence/trade_model.py +++ b/freqtrade/persistence/trade_model.py @@ -137,31 +137,35 @@ class Order(_DECL_BASE): 'info': {}, } - def to_json(self, entry_side: str) -> Dict[str, Any]: - return { - 'pair': self.ft_pair, - 'order_id': self.order_id, - 'status': self.status, + def to_json(self, entry_side: str, minified: bool = False) -> Dict[str, Any]: + resp = { 'amount': self.amount, - 'average': round(self.average, 8) if self.average else 0, 'safe_price': self.safe_price, - 'cost': self.cost if self.cost else 0, - 'filled': self.filled, 'ft_order_side': self.ft_order_side, - 'is_open': self.ft_is_open, - 'order_date': self.order_date.strftime(DATETIME_PRINT_FORMAT) - if self.order_date else None, - 'order_timestamp': int(self.order_date.replace( - tzinfo=timezone.utc).timestamp() * 1000) if self.order_date else None, - 'order_filled_date': self.order_filled_date.strftime(DATETIME_PRINT_FORMAT) - if self.order_filled_date else None, 'order_filled_timestamp': int(self.order_filled_date.replace( tzinfo=timezone.utc).timestamp() * 1000) if self.order_filled_date else None, - 'order_type': self.order_type, - 'price': self.price, 'ft_is_entry': self.ft_order_side == entry_side, - 'remaining': self.remaining, } + if not minified: + resp.update({ + 'pair': self.ft_pair, + 'order_id': self.order_id, + 'status': self.status, + 'average': round(self.average, 8) if self.average else 0, + 'cost': self.cost if self.cost else 0, + 'filled': self.filled, + 'is_open': self.ft_is_open, + 'order_date': self.order_date.strftime(DATETIME_PRINT_FORMAT) + if self.order_date else None, + 'order_timestamp': int(self.order_date.replace( + tzinfo=timezone.utc).timestamp() * 1000) if self.order_date else None, + 'order_filled_date': self.order_filled_date.strftime(DATETIME_PRINT_FORMAT) + if self.order_filled_date else None, + 'order_type': self.order_type, + 'price': self.price, + 'remaining': self.remaining, + }) + return resp def close_bt_order(self, close_date: datetime, trade: 'LocalTrade'): self.order_filled_date = close_date @@ -394,9 +398,9 @@ class LocalTrade(): f'open_rate={self.open_rate:.8f}, open_since={open_since})' ) - def to_json(self) -> Dict[str, Any]: + def to_json(self, minified: bool = False) -> Dict[str, Any]: filled_orders = self.select_filled_or_open_orders() - orders = [order.to_json(self.entry_side) for order in filled_orders] + orders = [order.to_json(self.entry_side, minified) for order in filled_orders] return { 'trade_id': self.id, From 786bc3616352a650d4107a539aad84f7c32d1714 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 6 Jun 2022 03:01:44 +0000 Subject: [PATCH 34/95] Bump orjson from 3.6.8 to 3.7.1 Bumps [orjson](https://github.com/ijl/orjson) from 3.6.8 to 3.7.1. - [Release notes](https://github.com/ijl/orjson/releases) - [Changelog](https://github.com/ijl/orjson/blob/master/CHANGELOG.md) - [Commits](https://github.com/ijl/orjson/compare/3.6.8...3.7.1) --- updated-dependencies: - dependency-name: orjson dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index a7dbaf57c..21d80571f 100644 --- a/requirements.txt +++ b/requirements.txt @@ -28,7 +28,7 @@ py_find_1st==1.1.5 # Load ticker files 30% faster python-rapidjson==1.6 # Properly format api responses -orjson==3.6.8 +orjson==3.7.1 # Notify systemd sdnotify==0.3.2 From 04cb49b7e404fb0ab29245e769296f7c5ec17d41 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 6 Jun 2022 03:01:48 +0000 Subject: [PATCH 35/95] Bump filelock from 3.7.0 to 3.7.1 Bumps [filelock](https://github.com/tox-dev/py-filelock) from 3.7.0 to 3.7.1. - [Release notes](https://github.com/tox-dev/py-filelock/releases) - [Changelog](https://github.com/tox-dev/py-filelock/blob/main/docs/changelog.rst) - [Commits](https://github.com/tox-dev/py-filelock/compare/3.7.0...3.7.1) --- updated-dependencies: - dependency-name: filelock dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- requirements-hyperopt.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements-hyperopt.txt b/requirements-hyperopt.txt index b8762214a..94e59ec15 100644 --- a/requirements-hyperopt.txt +++ b/requirements-hyperopt.txt @@ -5,5 +5,5 @@ scipy==1.8.1 scikit-learn==1.1.1 scikit-optimize==0.9.0 -filelock==3.7.0 +filelock==3.7.1 progressbar2==4.0.0 From 6547f3aadb96f60e9d5a42b0008a55de1c47e75c Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 6 Jun 2022 03:01:52 +0000 Subject: [PATCH 36/95] Bump mkdocs-material from 8.2.16 to 8.3.2 Bumps [mkdocs-material](https://github.com/squidfunk/mkdocs-material) from 8.2.16 to 8.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/8.2.16...8.3.2) --- updated-dependencies: - dependency-name: mkdocs-material dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[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 e7ca17c34..f351151ab 100644 --- a/docs/requirements-docs.txt +++ b/docs/requirements-docs.txt @@ -1,5 +1,5 @@ mkdocs==1.3.0 -mkdocs-material==8.2.16 +mkdocs-material==8.3.2 mdx_truly_sane_lists==1.2 pymdown-extensions==9.4 jinja2==3.1.2 From 35316ec06841b9b2638ab6068c6d58aa3c15991a Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 6 Jun 2022 03:01:55 +0000 Subject: [PATCH 37/95] Bump jsonschema from 4.5.1 to 4.6.0 Bumps [jsonschema](https://github.com/python-jsonschema/jsonschema) from 4.5.1 to 4.6.0. - [Release notes](https://github.com/python-jsonschema/jsonschema/releases) - [Changelog](https://github.com/python-jsonschema/jsonschema/blob/main/CHANGELOG.rst) - [Commits](https://github.com/python-jsonschema/jsonschema/compare/v4.5.1...v4.6.0) --- updated-dependencies: - dependency-name: jsonschema dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index a7dbaf57c..d0b662d2c 100644 --- a/requirements.txt +++ b/requirements.txt @@ -12,7 +12,7 @@ arrow==1.2.2 cachetools==4.2.2 requests==2.27.1 urllib3==1.26.9 -jsonschema==4.5.1 +jsonschema==4.6.0 TA-Lib==0.4.24 technical==1.3.0 tabulate==0.8.9 From 963dc0221caa59b8cf4cb2309cab6735f8be6161 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 6 Jun 2022 03:01:59 +0000 Subject: [PATCH 38/95] Bump types-requests from 2.27.29 to 2.27.30 Bumps [types-requests](https://github.com/python/typeshed) from 2.27.29 to 2.27.30. - [Release notes](https://github.com/python/typeshed/releases) - [Commits](https://github.com/python/typeshed/commits) --- updated-dependencies: - dependency-name: types-requests dependency-type: direct:development update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- requirements-dev.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements-dev.txt b/requirements-dev.txt index 6a7e15870..4eb157aae 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -24,6 +24,6 @@ nbconvert==6.5.0 # mypy types types-cachetools==5.0.1 types-filelock==3.2.6 -types-requests==2.27.29 +types-requests==2.27.30 types-tabulate==0.8.9 types-python-dateutil==2.8.17 From 4affa75ff5103e95fdbc6c59fa457423326fdc74 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 6 Jun 2022 03:02:07 +0000 Subject: [PATCH 39/95] Bump sqlalchemy from 1.4.36 to 1.4.37 Bumps [sqlalchemy](https://github.com/sqlalchemy/sqlalchemy) from 1.4.36 to 1.4.37. - [Release notes](https://github.com/sqlalchemy/sqlalchemy/releases) - [Changelog](https://github.com/sqlalchemy/sqlalchemy/blob/main/CHANGES.rst) - [Commits](https://github.com/sqlalchemy/sqlalchemy/commits) --- updated-dependencies: - dependency-name: sqlalchemy dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index a7dbaf57c..717577480 100644 --- a/requirements.txt +++ b/requirements.txt @@ -6,7 +6,7 @@ ccxt==1.84.39 # Pin cryptography for now due to rust build errors with piwheels cryptography==37.0.2 aiohttp==3.8.1 -SQLAlchemy==1.4.36 +SQLAlchemy==1.4.37 python-telegram-bot==13.12 arrow==1.2.2 cachetools==4.2.2 From 05922e9ebc0c7911f0bc75e50b5a57dc6a0cc29d Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 6 Jun 2022 03:02:15 +0000 Subject: [PATCH 40/95] Bump ccxt from 1.84.39 to 1.84.97 Bumps [ccxt](https://github.com/ccxt/ccxt) from 1.84.39 to 1.84.97. - [Release notes](https://github.com/ccxt/ccxt/releases) - [Changelog](https://github.com/ccxt/ccxt/blob/master/exchanges.cfg) - [Commits](https://github.com/ccxt/ccxt/compare/1.84.39...1.84.97) --- updated-dependencies: - dependency-name: ccxt dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index a7dbaf57c..432ff976d 100644 --- a/requirements.txt +++ b/requirements.txt @@ -2,7 +2,7 @@ numpy==1.22.4 pandas==1.4.2 pandas-ta==0.3.14b -ccxt==1.84.39 +ccxt==1.84.97 # Pin cryptography for now due to rust build errors with piwheels cryptography==37.0.2 aiohttp==3.8.1 From 99f6c75c40dc95073ec81a03c82e775d87753667 Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 6 Jun 2022 10:22:19 +0200 Subject: [PATCH 41/95] Bump types-requests precommit --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 95a1d5002..685d789ec 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -15,7 +15,7 @@ repos: additional_dependencies: - types-cachetools==5.0.1 - types-filelock==3.2.6 - - types-requests==2.27.29 + - types-requests==2.27.30 - types-tabulate==0.8.9 - types-python-dateutil==2.8.17 # stages: [push] From ea9b68baddeb76f2581660d13ff11b797f4a6b00 Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 6 Jun 2022 10:50:48 +0200 Subject: [PATCH 42/95] Add updating freqtrade to updating desc --- docs/updating.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/docs/updating.md b/docs/updating.md index 1839edc4c..8dc7279a4 100644 --- a/docs/updating.md +++ b/docs/updating.md @@ -32,4 +32,8 @@ Please ensure that you're also updating dependencies - otherwise things might br ``` bash git pull pip install -U -r requirements.txt +pip install -e . + +# Ensure freqUI is at the latest version +freqtrade install-ui ``` From 82c5a6b29dc1c45e0e542d2caace0fb2d87dad68 Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 6 Jun 2022 10:57:33 +0200 Subject: [PATCH 43/95] Update CI to use concurrency --- .github/workflows/ci.yml | 26 +++++++++++++++----------- 1 file changed, 15 insertions(+), 11 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index d2e420e8e..c3ed6d80d 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -13,6 +13,10 @@ on: schedule: - cron: '0 5 * * 4' +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + jobs: build_linux: @@ -296,17 +300,17 @@ jobs: details: Freqtrade doc test failed! webhookUrl: ${{ secrets.DISCORD_WEBHOOK }} - cleanup-prior-runs: - permissions: - actions: write # for rokroskar/workflow-run-cleanup-action to obtain workflow name & cancel it - contents: read # for rokroskar/workflow-run-cleanup-action to obtain branch - runs-on: ubuntu-20.04 - steps: - - name: Cleanup previous runs on this branch - uses: rokroskar/workflow-run-cleanup-action@v0.3.3 - if: "!startsWith(github.ref, 'refs/tags/') && github.ref != 'refs/heads/stable' && github.repository == 'freqtrade/freqtrade'" - env: - GITHUB_TOKEN: "${{ secrets.GITHUB_TOKEN }}" + # cleanup-prior-runs: + # permissions: + # actions: write # for rokroskar/workflow-run-cleanup-action to obtain workflow name & cancel it + # contents: read # for rokroskar/workflow-run-cleanup-action to obtain branch + # runs-on: ubuntu-20.04 + # steps: + # - name: Cleanup previous runs on this branch + # uses: rokroskar/workflow-run-cleanup-action@v0.3.3 + # if: "!startsWith(github.ref, 'refs/tags/') && github.ref != 'refs/heads/stable' && github.repository == 'freqtrade/freqtrade'" + # env: + # GITHUB_TOKEN: "${{ secrets.GITHUB_TOKEN }}" # Notify only once - when CI completes (and after deploy) in case it's successfull notify-complete: From 0b806af48756bcb5190fadc1cd50cd9b2ff32b3d Mon Sep 17 00:00:00 2001 From: Matthias Date: Thu, 26 May 2022 07:17:22 +0200 Subject: [PATCH 44/95] Add orders column to btresult --- freqtrade/data/btanalysis.py | 4 +++- freqtrade/optimize/optimize_reports.py | 4 ---- tests/data/test_btanalysis.py | 4 ++-- tests/optimize/test_backtesting.py | 17 +++++++++++++++++ .../test_backtesting_adjust_position.py | 7 ++++++- 5 files changed, 28 insertions(+), 8 deletions(-) diff --git a/freqtrade/data/btanalysis.py b/freqtrade/data/btanalysis.py index 0b466241f..9e38f6833 100644 --- a/freqtrade/data/btanalysis.py +++ b/freqtrade/data/btanalysis.py @@ -26,7 +26,7 @@ BT_DATA_COLUMNS = ['pair', 'stake_amount', 'amount', 'open_date', 'close_date', 'profit_ratio', 'profit_abs', 'exit_reason', 'initial_stop_loss_abs', 'initial_stop_loss_ratio', 'stop_loss_abs', 'stop_loss_ratio', 'min_rate', 'max_rate', 'is_open', 'enter_tag', - 'is_short' + 'is_short', 'open_timestamp', 'close_timestamp', 'orders' ] @@ -283,6 +283,8 @@ def load_backtest_data(filename: Union[Path, str], strategy: Optional[str] = Non if 'enter_tag' not in df.columns: df['enter_tag'] = df['buy_tag'] df = df.drop(['buy_tag'], axis=1) + if 'orders' not in df.columns: + df.loc[:, 'orders'] = None else: # old format - only with lists. diff --git a/freqtrade/optimize/optimize_reports.py b/freqtrade/optimize/optimize_reports.py index 93336fa3f..e3dd17411 100644 --- a/freqtrade/optimize/optimize_reports.py +++ b/freqtrade/optimize/optimize_reports.py @@ -4,7 +4,6 @@ from datetime import datetime, timedelta, timezone from pathlib import Path from typing import Any, Dict, List, Union -from numpy import int64 from pandas import DataFrame, to_datetime from tabulate import tabulate @@ -417,9 +416,6 @@ def generate_strategy_stats(pairlist: List[str], key=lambda x: x['profit_sum']) if len(pair_results) > 1 else None worst_pair = min([pair for pair in pair_results if pair['key'] != 'TOTAL'], key=lambda x: x['profit_sum']) if len(pair_results) > 1 else None - if not results.empty: - results['open_timestamp'] = results['open_date'].view(int64) // 1e6 - results['close_timestamp'] = results['close_date'].view(int64) // 1e6 backtest_days = (max_date - min_date).days or 1 strat_stats = { diff --git a/tests/data/test_btanalysis.py b/tests/data/test_btanalysis.py index 4157bd899..977140ebb 100644 --- a/tests/data/test_btanalysis.py +++ b/tests/data/test_btanalysis.py @@ -85,7 +85,7 @@ def test_load_backtest_data_new_format(testdatadir): filename = testdatadir / "backtest_results/backtest-result_new.json" bt_data = load_backtest_data(filename) assert isinstance(bt_data, DataFrame) - assert set(bt_data.columns) == set(BT_DATA_COLUMNS + ['close_timestamp', 'open_timestamp']) + assert set(bt_data.columns) == set(BT_DATA_COLUMNS) assert len(bt_data) == 179 # Test loading from string (must yield same result) @@ -110,7 +110,7 @@ def test_load_backtest_data_multi(testdatadir): bt_data = load_backtest_data(filename, strategy=strategy) assert isinstance(bt_data, DataFrame) assert set(bt_data.columns) == set( - BT_DATA_COLUMNS + ['close_timestamp', 'open_timestamp']) + BT_DATA_COLUMNS) assert len(bt_data) == 179 # Test loading from string (must yield same result) diff --git a/tests/optimize/test_backtesting.py b/tests/optimize/test_backtesting.py index f169e0a35..6912184aa 100644 --- a/tests/optimize/test_backtesting.py +++ b/tests/optimize/test_backtesting.py @@ -795,10 +795,27 @@ def test_backtest_one(default_conf, fee, mocker, testdatadir) -> None: 'is_open': [False, False], 'enter_tag': [None, None], "is_short": [False, False], + 'open_timestamp': [1517251200000, 1517283000000], + 'close_timestamp': [1517265300000, 1517285400000], + 'orders': [ + [ + {'amount': 0.00957442, 'safe_price': 0.104445, 'ft_order_side': 'buy', + 'order_filled_timestamp': 1517251200000, 'ft_is_entry': True}, + {'amount': 0.00957442, 'safe_price': 0.10496853383458644, 'ft_order_side': 'sell', + 'order_filled_timestamp': 1517265300000, 'ft_is_entry': False} + ], [ + {'amount': 0.0097064, 'safe_price': 0.10302485, 'ft_order_side': 'buy', + 'order_filled_timestamp': 1517283000000, 'ft_is_entry': True}, + {'amount': 0.0097064, 'safe_price': 0.10354126528822055, 'ft_order_side': 'sell', + 'order_filled_timestamp': 1517285400000, 'ft_is_entry': False} + ] + ] }) pd.testing.assert_frame_equal(results, expected) + assert 'orders' in results.columns data_pair = processed[pair] for _, t in results.iterrows(): + assert len(t['orders']) == 2 ln = data_pair.loc[data_pair["date"] == t["open_date"]] # Check open trade rate alignes to open rate assert ln is not None diff --git a/tests/optimize/test_backtesting_adjust_position.py b/tests/optimize/test_backtesting_adjust_position.py index 94505e3ce..fca9c01b2 100644 --- a/tests/optimize/test_backtesting_adjust_position.py +++ b/tests/optimize/test_backtesting_adjust_position.py @@ -70,9 +70,14 @@ def test_backtest_position_adjustment(default_conf, fee, mocker, testdatadir) -> 'is_open': [False, False], 'enter_tag': [None, None], 'is_short': [False, False], + 'open_timestamp': [1517251200000, 1517283000000], + 'close_timestamp': [1517265300000, 1517285400000], }) - pd.testing.assert_frame_equal(results, expected) + pd.testing.assert_frame_equal(results.drop(columns=['orders']), expected) data_pair = processed[pair] + assert len(results.iloc[0]['orders']) == 6 + assert len(results.iloc[1]['orders']) == 2 + for _, t in results.iterrows(): ln = data_pair.loc[data_pair["date"] == t["open_date"]] # Check open trade rate alignes to open rate From 057be50941c25fb493b90086dabc7997987b7f05 Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 6 Jun 2022 11:11:47 +0200 Subject: [PATCH 45/95] Remove old concurrency method --- .github/workflows/ci.yml | 12 ------------ 1 file changed, 12 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index c3ed6d80d..bbe0bcf6e 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -300,18 +300,6 @@ jobs: details: Freqtrade doc test failed! webhookUrl: ${{ secrets.DISCORD_WEBHOOK }} - # cleanup-prior-runs: - # permissions: - # actions: write # for rokroskar/workflow-run-cleanup-action to obtain workflow name & cancel it - # contents: read # for rokroskar/workflow-run-cleanup-action to obtain branch - # runs-on: ubuntu-20.04 - # steps: - # - name: Cleanup previous runs on this branch - # uses: rokroskar/workflow-run-cleanup-action@v0.3.3 - # if: "!startsWith(github.ref, 'refs/tags/') && github.ref != 'refs/heads/stable' && github.repository == 'freqtrade/freqtrade'" - # env: - # GITHUB_TOKEN: "${{ secrets.GITHUB_TOKEN }}" - # Notify only once - when CI completes (and after deploy) in case it's successfull notify-complete: needs: [ build_linux, build_macos, build_windows, docs_check, mypy_version_check ] From 9534d6cca177de8aee7edd330fc8103e8d07e4bb Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 7 Jun 2022 07:03:40 +0200 Subject: [PATCH 46/95] Cancel orders which can no longer be found after several days --- freqtrade/freqtradebot.py | 11 ++++++++++- tests/test_freqtradebot.py | 11 ++++++++++- 2 files changed, 20 insertions(+), 2 deletions(-) diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index 95eb911cf..d96c63bcc 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -4,7 +4,7 @@ Freqtrade is the main module of this bot. It contains the class Freqtrade() import copy import logging import traceback -from datetime import datetime, time, timezone +from datetime import datetime, time, timedelta, timezone from math import isclose from threading import Lock from typing import Any, Dict, List, Optional, Tuple @@ -302,6 +302,15 @@ class FreqtradeBot(LoggingMixin): self.update_trade_state(order.trade, order.order_id, fo, stoploss_order=(order.ft_order_side == 'stoploss')) + except InvalidOrderException as e: + logger.warning(f"Error updating Order {order.order_id} due to {e}.") + if order.order_date_utc - timedelta(days=5) < datetime.now(timezone.utc): + logger.warning( + "Order is older than 5 days. Assuming order was fully cancelled.") + fo = order.to_ccxt_object() + fo['status'] = 'canceled' + self.handle_timedout_order(fo, order.trade) + except ExchangeError as e: logger.warning(f"Error updating Order {order.order_id} due to {e}") diff --git a/tests/test_freqtradebot.py b/tests/test_freqtradebot.py index 0e4f9db99..cd7459cbe 100644 --- a/tests/test_freqtradebot.py +++ b/tests/test_freqtradebot.py @@ -4802,10 +4802,19 @@ def test_startup_update_open_orders(mocker, default_conf_usdt, fee, caplog, is_s assert len(Order.get_open_orders()) == 2 caplog.clear() - mocker.patch('freqtrade.exchange.Exchange.fetch_order', side_effect=InvalidOrderException) + mocker.patch('freqtrade.exchange.Exchange.fetch_order', side_effect=ExchangeError) freqtrade.startup_update_open_orders() assert log_has_re(r"Error updating Order .*", caplog) + mocker.patch('freqtrade.exchange.Exchange.fetch_order', side_effect=InvalidOrderException) + hto_mock = mocker.patch('freqtrade.freqtradebot.FreqtradeBot.handle_timedout_order') + # Orders which are no longer found after X days should be assumed as canceled. + freqtrade.startup_update_open_orders() + assert log_has_re(r"Order is older than \d days.*", caplog) + assert hto_mock.call_count == 2 + assert hto_mock.call_args_list[0][0][0]['status'] == 'canceled' + assert hto_mock.call_args_list[1][0][0]['status'] == 'canceled' + @pytest.mark.usefixtures("init_persistence") @pytest.mark.parametrize("is_short", [False, True]) From 381d64833d30ee10684e0633826a473d4f873197 Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 7 Jun 2022 21:05:31 +0200 Subject: [PATCH 47/95] version-bump ccxt --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index acaecd872..05d5a10db 100644 --- a/requirements.txt +++ b/requirements.txt @@ -2,7 +2,7 @@ numpy==1.22.4 pandas==1.4.2 pandas-ta==0.3.14b -ccxt==1.84.97 +ccxt==1.85.57 # Pin cryptography for now due to rust build errors with piwheels cryptography==37.0.2 aiohttp==3.8.1 From ac40ae89b9d50fba5bb088ba902e1ac6bce0f6a1 Mon Sep 17 00:00:00 2001 From: gautier pialat Date: Wed, 8 Jun 2022 00:20:33 +0200 Subject: [PATCH 48/95] give extra info on rate origin for confirm_trade_* Documentation : Take into consideration the market buy/sell rates use case for the confirm_trade_entry and confirm_trade_exit callback function --- docs/strategy-callbacks.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/strategy-callbacks.md b/docs/strategy-callbacks.md index 656f206a4..b897453e7 100644 --- a/docs/strategy-callbacks.md +++ b/docs/strategy-callbacks.md @@ -550,7 +550,7 @@ class AwesomeStrategy(IStrategy): :param pair: Pair that's about to be bought/shorted. :param order_type: Order type (as configured in order_types). usually limit or market. :param amount: Amount in target (base) currency that's going to be traded. - :param rate: Rate that's going to be used when using limit orders + :param rate: Rate that's going to be used when using limit orders or current rate for market orders. :param time_in_force: Time in force. Defaults to GTC (Good-til-cancelled). :param current_time: datetime object, containing the current datetime :param entry_tag: Optional entry_tag (buy_tag) if provided with the buy signal. @@ -599,7 +599,7 @@ class AwesomeStrategy(IStrategy): :param trade: trade object. :param order_type: Order type (as configured in order_types). usually limit or market. :param amount: Amount in base currency. - :param rate: Rate that's going to be used when using limit orders + :param rate: Rate that's going to be used when using limit orders or current rate for market orders. :param time_in_force: Time in force. Defaults to GTC (Good-til-cancelled). :param exit_reason: Exit reason. Can be any of ['roi', 'stop_loss', 'stoploss_on_exchange', 'trailing_stop_loss', From 7eacb847b05c53f7db80016885303be654cfb64b Mon Sep 17 00:00:00 2001 From: Matthias Date: Wed, 8 Jun 2022 20:21:45 +0200 Subject: [PATCH 49/95] Fix backtesting bug when order is not replaced --- freqtrade/optimize/backtesting.py | 1 + 1 file changed, 1 insertion(+) diff --git a/freqtrade/optimize/backtesting.py b/freqtrade/optimize/backtesting.py index 8fe5f509e..1aad8520a 100755 --- a/freqtrade/optimize/backtesting.py +++ b/freqtrade/optimize/backtesting.py @@ -966,6 +966,7 @@ class Backtesting: return False else: del trade.orders[trade.orders.index(order)] + trade.open_order_id = None self.canceled_entry_orders += 1 # place new order if result was not None From d265b8adb621f93cee91d9fdea85a52f9d425171 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 9 Jun 2022 03:01:48 +0000 Subject: [PATCH 50/95] Bump python from 3.10.4-slim-bullseye to 3.10.5-slim-bullseye Bumps python from 3.10.4-slim-bullseye to 3.10.5-slim-bullseye. --- updated-dependencies: - dependency-name: python dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Dockerfile b/Dockerfile index 5f7b52265..5138ecec9 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,4 +1,4 @@ -FROM python:3.10.4-slim-bullseye as base +FROM python:3.10.5-slim-bullseye as base # Setup env ENV LANG C.UTF-8 From c550cd8b0d2b8559f22a91c87e01c7afd1b00dd2 Mon Sep 17 00:00:00 2001 From: Matthias Date: Thu, 9 Jun 2022 07:04:46 +0200 Subject: [PATCH 51/95] Simplify query in freqtradebot --- freqtrade/freqtradebot.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index d96c63bcc..fdccc2f8a 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -227,7 +227,7 @@ class FreqtradeBot(LoggingMixin): Notify the user when the bot is stopped (not reloaded) and there are still open trades active. """ - open_trades = Trade.get_trades([Trade.is_open.is_(True)]).all() + open_trades = Trade.get_open_trades() if len(open_trades) != 0 and self.state != State.RELOAD_CONFIG: msg = { From 3cb15a2a5470e8a915aa5f39123808882b4b93eb Mon Sep 17 00:00:00 2001 From: Matthias Date: Wed, 8 Jun 2022 07:08:01 +0200 Subject: [PATCH 52/95] Combine weekly and daily profit methods --- freqtrade/rpc/rpc.py | 67 ++++++++++----------------------------- freqtrade/rpc/telegram.py | 5 +-- 2 files changed, 20 insertions(+), 52 deletions(-) diff --git a/freqtrade/rpc/rpc.py b/freqtrade/rpc/rpc.py index a98e3f96d..571438059 100644 --- a/freqtrade/rpc/rpc.py +++ b/freqtrade/rpc/rpc.py @@ -285,23 +285,33 @@ class RPC: def _rpc_daily_profit( self, timescale: int, - stake_currency: str, fiat_display_currency: str) -> Dict[str, Any]: - today = datetime.now(timezone.utc).date() - profit_days: Dict[date, Dict] = {} + stake_currency: str, fiat_display_currency: str, + timeunit: str = 'days') -> Dict[str, Any]: + """ + :param timeunit: Valid entries are 'days', 'weeks', 'months' + """ + start_date = datetime.now(timezone.utc).date() + if timeunit == 'weeks': + # weekly + start_date = start_date - timedelta(days=start_date.weekday()) # Monday + if timeunit == 'months': + start_date = start_date.replace(day=1) + + profit_units: Dict[date, Dict] = {} if not (isinstance(timescale, int) and timescale > 0): raise RPCException('timescale must be an integer greater than 0') for day in range(0, timescale): - profitday = today - timedelta(days=day) + profitday = start_date - timedelta(**{timeunit: day}) trades = Trade.get_trades(trade_filter=[ Trade.is_open.is_(False), Trade.close_date >= profitday, - Trade.close_date < (profitday + timedelta(days=1)) + Trade.close_date < (profitday + timedelta(**{timeunit: 1})) ]).order_by(Trade.close_date).all() curdayprofit = sum( trade.close_profit_abs for trade in trades if trade.close_profit_abs is not None) - profit_days[profitday] = { + profit_units[profitday] = { 'amount': curdayprofit, 'trades': len(trades) } @@ -317,50 +327,7 @@ class RPC: ) if self._fiat_converter else 0, 'trade_count': value["trades"], } - for key, value in profit_days.items() - ] - return { - 'stake_currency': stake_currency, - 'fiat_display_currency': fiat_display_currency, - 'data': data - } - - def _rpc_weekly_profit( - self, timescale: int, - stake_currency: str, fiat_display_currency: str) -> Dict[str, Any]: - today = datetime.now(timezone.utc).date() - first_iso_day_of_week = today - timedelta(days=today.weekday()) # Monday - profit_weeks: Dict[date, Dict] = {} - - if not (isinstance(timescale, int) and timescale > 0): - raise RPCException('timescale must be an integer greater than 0') - - for week in range(0, timescale): - profitweek = first_iso_day_of_week - timedelta(weeks=week) - trades = Trade.get_trades(trade_filter=[ - Trade.is_open.is_(False), - Trade.close_date >= profitweek, - Trade.close_date < (profitweek + timedelta(weeks=1)) - ]).order_by(Trade.close_date).all() - curweekprofit = sum( - trade.close_profit_abs for trade in trades if trade.close_profit_abs is not None) - profit_weeks[profitweek] = { - 'amount': curweekprofit, - 'trades': len(trades) - } - - data = [ - { - 'date': key, - 'abs_profit': value["amount"], - 'fiat_value': self._fiat_converter.convert_amount( - value['amount'], - stake_currency, - fiat_display_currency - ) if self._fiat_converter else 0, - 'trade_count': value["trades"], - } - for key, value in profit_weeks.items() + for key, value in profit_units.items() ] return { 'stake_currency': stake_currency, diff --git a/freqtrade/rpc/telegram.py b/freqtrade/rpc/telegram.py index e456b1eef..cfbd3949f 100644 --- a/freqtrade/rpc/telegram.py +++ b/freqtrade/rpc/telegram.py @@ -618,10 +618,11 @@ class Telegram(RPCHandler): except (TypeError, ValueError, IndexError): timescale = 8 try: - stats = self._rpc._rpc_weekly_profit( + stats = self._rpc._rpc_daily_profit( timescale, stake_cur, - fiat_disp_cur + fiat_disp_cur, + 'weeks' ) stats_tab = tabulate( [[week['date'], From d4dd026310b411ee78d7857dde4bec974226bb60 Mon Sep 17 00:00:00 2001 From: Matthias Date: Wed, 8 Jun 2022 19:52:05 +0200 Subject: [PATCH 53/95] Consolidate monthly stats to common method --- freqtrade/rpc/api_server/api_v1.py | 4 +-- freqtrade/rpc/rpc.py | 55 +++++------------------------- freqtrade/rpc/telegram.py | 12 ++++--- 3 files changed, 18 insertions(+), 53 deletions(-) diff --git a/freqtrade/rpc/api_server/api_v1.py b/freqtrade/rpc/api_server/api_v1.py index a8b9873d7..271e3de1b 100644 --- a/freqtrade/rpc/api_server/api_v1.py +++ b/freqtrade/rpc/api_server/api_v1.py @@ -86,8 +86,8 @@ def stats(rpc: RPC = Depends(get_rpc)): @router.get('/daily', response_model=Daily, tags=['info']) def daily(timescale: int = 7, rpc: RPC = Depends(get_rpc), config=Depends(get_config)): - return rpc._rpc_daily_profit(timescale, config['stake_currency'], - config.get('fiat_display_currency', '')) + return rpc._rpc_timeunit_profit(timescale, config['stake_currency'], + config.get('fiat_display_currency', '')) @router.get('/status', response_model=List[OpenTradeSchema], tags=['info']) diff --git a/freqtrade/rpc/rpc.py b/freqtrade/rpc/rpc.py index 571438059..a6290bd5a 100644 --- a/freqtrade/rpc/rpc.py +++ b/freqtrade/rpc/rpc.py @@ -283,7 +283,7 @@ class RPC: columns.append('# Entries') return trades_list, columns, fiat_profit_sum - def _rpc_daily_profit( + def _rpc_timeunit_profit( self, timescale: int, stake_currency: str, fiat_display_currency: str, timeunit: str = 'days') -> Dict[str, Any]: @@ -297,17 +297,22 @@ class RPC: if timeunit == 'months': start_date = start_date.replace(day=1) + def time_offset(step: int): + if timeunit == 'months': + return relativedelta(months=step) + return timedelta(**{timeunit: step}) + profit_units: Dict[date, Dict] = {} if not (isinstance(timescale, int) and timescale > 0): raise RPCException('timescale must be an integer greater than 0') for day in range(0, timescale): - profitday = start_date - timedelta(**{timeunit: day}) + profitday = start_date - time_offset(day) trades = Trade.get_trades(trade_filter=[ Trade.is_open.is_(False), Trade.close_date >= profitday, - Trade.close_date < (profitday + timedelta(**{timeunit: 1})) + Trade.close_date < (profitday + time_offset(1)) ]).order_by(Trade.close_date).all() curdayprofit = sum( trade.close_profit_abs for trade in trades if trade.close_profit_abs is not None) @@ -318,7 +323,7 @@ class RPC: data = [ { - 'date': key, + 'date': f"{key.year}-{key.month:02d}" if timeunit == 'months' else key, 'abs_profit': value["amount"], 'fiat_value': self._fiat_converter.convert_amount( value['amount'], @@ -335,48 +340,6 @@ class RPC: 'data': data } - def _rpc_monthly_profit( - self, timescale: int, - stake_currency: str, fiat_display_currency: str) -> Dict[str, Any]: - first_day_of_month = datetime.now(timezone.utc).date().replace(day=1) - profit_months: Dict[date, Dict] = {} - - if not (isinstance(timescale, int) and timescale > 0): - raise RPCException('timescale must be an integer greater than 0') - - for month in range(0, timescale): - profitmonth = first_day_of_month - relativedelta(months=month) - trades = Trade.get_trades(trade_filter=[ - Trade.is_open.is_(False), - Trade.close_date >= profitmonth, - Trade.close_date < (profitmonth + relativedelta(months=1)) - ]).order_by(Trade.close_date).all() - curmonthprofit = sum( - trade.close_profit_abs for trade in trades if trade.close_profit_abs is not None) - profit_months[profitmonth] = { - 'amount': curmonthprofit, - 'trades': len(trades) - } - - data = [ - { - 'date': f"{key.year}-{key.month:02d}", - 'abs_profit': value["amount"], - 'fiat_value': self._fiat_converter.convert_amount( - value['amount'], - stake_currency, - fiat_display_currency - ) if self._fiat_converter else 0, - 'trade_count': value["trades"], - } - for key, value in profit_months.items() - ] - return { - 'stake_currency': stake_currency, - 'fiat_display_currency': fiat_display_currency, - 'data': data - } - def _rpc_trade_history(self, limit: int, offset: int = 0, order_by_id: bool = False) -> Dict: """ Returns the X last trades """ order_by = Trade.id if order_by_id else Trade.close_date.desc() diff --git a/freqtrade/rpc/telegram.py b/freqtrade/rpc/telegram.py index cfbd3949f..5efdcdbed 100644 --- a/freqtrade/rpc/telegram.py +++ b/freqtrade/rpc/telegram.py @@ -579,10 +579,11 @@ class Telegram(RPCHandler): except (TypeError, ValueError, IndexError): timescale = 7 try: - stats = self._rpc._rpc_daily_profit( + stats = self._rpc._rpc_timeunit_profit( timescale, stake_cur, - fiat_disp_cur + fiat_disp_cur, + 'days' ) stats_tab = tabulate( [[day['date'], @@ -618,7 +619,7 @@ class Telegram(RPCHandler): except (TypeError, ValueError, IndexError): timescale = 8 try: - stats = self._rpc._rpc_daily_profit( + stats = self._rpc._rpc_timeunit_profit( timescale, stake_cur, fiat_disp_cur, @@ -659,10 +660,11 @@ class Telegram(RPCHandler): except (TypeError, ValueError, IndexError): timescale = 6 try: - stats = self._rpc._rpc_monthly_profit( + stats = self._rpc._rpc_timeunit_profit( timescale, stake_cur, - fiat_disp_cur + fiat_disp_cur, + 'months' ) stats_tab = tabulate( [[month['date'], From a547001601f785f5c6d2171edc8a52159241e07d Mon Sep 17 00:00:00 2001 From: Matthias Date: Wed, 8 Jun 2022 20:09:51 +0200 Subject: [PATCH 54/95] Reduce Telegram "unit" stats --- freqtrade/rpc/telegram.py | 158 ++++++++++++++++---------------------- 1 file changed, 65 insertions(+), 93 deletions(-) diff --git a/freqtrade/rpc/telegram.py b/freqtrade/rpc/telegram.py index 5efdcdbed..e64ab7b8a 100644 --- a/freqtrade/rpc/telegram.py +++ b/freqtrade/rpc/telegram.py @@ -6,6 +6,7 @@ This module manage Telegram communication import json import logging import re +from dataclasses import dataclass from datetime import date, datetime, timedelta from functools import partial from html import escape @@ -37,6 +38,15 @@ logger.debug('Included module rpc.telegram ...') MAX_TELEGRAM_MESSAGE_LENGTH = 4096 +@dataclass +class TimeunitMappings: + header: str + message: str + message2: str + callback: str + default: int + + def authorized_only(command_handler: Callable[..., None]) -> Callable[..., Any]: """ Decorator to check if the message comes from the correct chat_id @@ -563,6 +573,58 @@ class Telegram(RPCHandler): except RPCException as e: self._send_msg(str(e)) + @authorized_only + def _timeunit_stats(self, update: Update, context: CallbackContext, unit: str) -> None: + """ + Handler for /daily + Returns a daily profit (in BTC) over the last n days. + :param bot: telegram bot + :param update: message update + :return: None + """ + + vals = { + 'days': TimeunitMappings('Day', 'Daily', 'days', 'update_daily', 7), + 'weeks': TimeunitMappings('Monday', 'Weekly', 'weeks (starting from Monday)', + 'update_weekly', 8), + 'months': TimeunitMappings('Month', 'Monthly', 'months', 'update_monthly', 6), + } + val = vals[unit] + + stake_cur = self._config['stake_currency'] + fiat_disp_cur = self._config.get('fiat_display_currency', '') + try: + timescale = int(context.args[0]) if context.args else val.default + except (TypeError, ValueError, IndexError): + timescale = val.default + try: + stats = self._rpc._rpc_timeunit_profit( + timescale, + stake_cur, + fiat_disp_cur, + unit + ) + stats_tab = tabulate( + [[day['date'], + f"{round_coin_value(day['abs_profit'], stats['stake_currency'])}", + f"{day['fiat_value']:.3f} {stats['fiat_display_currency']}", + f"{day['trade_count']} trades"] for day in stats['data']], + headers=[ + val.header, + f'Profit {stake_cur}', + f'Profit {fiat_disp_cur}', + 'Trades', + ], + tablefmt='simple') + message = ( + f'{val.message} Profit over the last {timescale} {val.message2}:\n' + f'
{stats_tab}
' + ) + self._send_msg(message, parse_mode=ParseMode.HTML, reload_able=True, + callback_path=val.callback, query=update.callback_query) + except RPCException as e: + self._send_msg(str(e)) + @authorized_only def _daily(self, update: Update, context: CallbackContext) -> None: """ @@ -572,36 +634,7 @@ class Telegram(RPCHandler): :param update: message update :return: None """ - stake_cur = self._config['stake_currency'] - fiat_disp_cur = self._config.get('fiat_display_currency', '') - try: - timescale = int(context.args[0]) if context.args else 7 - except (TypeError, ValueError, IndexError): - timescale = 7 - try: - stats = self._rpc._rpc_timeunit_profit( - timescale, - stake_cur, - fiat_disp_cur, - 'days' - ) - stats_tab = tabulate( - [[day['date'], - f"{round_coin_value(day['abs_profit'], stats['stake_currency'])}", - f"{day['fiat_value']:.3f} {stats['fiat_display_currency']}", - f"{day['trade_count']} trades"] for day in stats['data']], - headers=[ - 'Day', - f'Profit {stake_cur}', - f'Profit {fiat_disp_cur}', - 'Trades', - ], - tablefmt='simple') - message = f'Daily Profit over the last {timescale} days:\n
{stats_tab}
' - self._send_msg(message, parse_mode=ParseMode.HTML, reload_able=True, - callback_path="update_daily", query=update.callback_query) - except RPCException as e: - self._send_msg(str(e)) + self._timeunit_stats(update, context, 'days') @authorized_only def _weekly(self, update: Update, context: CallbackContext) -> None: @@ -612,37 +645,7 @@ class Telegram(RPCHandler): :param update: message update :return: None """ - stake_cur = self._config['stake_currency'] - fiat_disp_cur = self._config.get('fiat_display_currency', '') - try: - timescale = int(context.args[0]) if context.args else 8 - except (TypeError, ValueError, IndexError): - timescale = 8 - try: - stats = self._rpc._rpc_timeunit_profit( - timescale, - stake_cur, - fiat_disp_cur, - 'weeks' - ) - stats_tab = tabulate( - [[week['date'], - f"{round_coin_value(week['abs_profit'], stats['stake_currency'])}", - f"{week['fiat_value']:.3f} {stats['fiat_display_currency']}", - f"{week['trade_count']} trades"] for week in stats['data']], - headers=[ - 'Monday', - f'Profit {stake_cur}', - f'Profit {fiat_disp_cur}', - 'Trades', - ], - tablefmt='simple') - message = f'Weekly Profit over the last {timescale} weeks ' \ - f'(starting from Monday):\n
{stats_tab}
' - self._send_msg(message, parse_mode=ParseMode.HTML, reload_able=True, - callback_path="update_weekly", query=update.callback_query) - except RPCException as e: - self._send_msg(str(e)) + self._timeunit_stats(update, context, 'weeks') @authorized_only def _monthly(self, update: Update, context: CallbackContext) -> None: @@ -653,38 +656,7 @@ class Telegram(RPCHandler): :param update: message update :return: None """ - stake_cur = self._config['stake_currency'] - fiat_disp_cur = self._config.get('fiat_display_currency', '') - try: - timescale = int(context.args[0]) if context.args else 6 - except (TypeError, ValueError, IndexError): - timescale = 6 - try: - stats = self._rpc._rpc_timeunit_profit( - timescale, - stake_cur, - fiat_disp_cur, - 'months' - ) - stats_tab = tabulate( - [[month['date'], - f"{round_coin_value(month['abs_profit'], stats['stake_currency'])}", - f"{month['fiat_value']:.3f} {stats['fiat_display_currency']}", - f"{month['trade_count']} trades"] for month in stats['data']], - headers=[ - 'Month', - f'Profit {stake_cur}', - f'Profit {fiat_disp_cur}', - 'Trades', - ], - tablefmt='simple') - message = f'Monthly Profit over the last {timescale} months' \ - f':\n
{stats_tab}
' - self._send_msg(message, parse_mode=ParseMode.HTML, reload_able=True, - callback_path="update_monthly", query=update.callback_query) - except RPCException as e: - self._send_msg(str(e)) - + self._timeunit_stats(update, context, 'months') @authorized_only def _profit(self, update: Update, context: CallbackContext) -> None: """ From b211a5156f5b7e92a652369ed1f6be19d3535b69 Mon Sep 17 00:00:00 2001 From: Matthias Date: Thu, 9 Jun 2022 19:36:15 +0200 Subject: [PATCH 55/95] Add test for strategy_wrapper lazy loading --- tests/strategy/test_interface.py | 25 ++++++++++++++++++++++++- 1 file changed, 24 insertions(+), 1 deletion(-) diff --git a/tests/strategy/test_interface.py b/tests/strategy/test_interface.py index b7b73bdcf..dca87e724 100644 --- a/tests/strategy/test_interface.py +++ b/tests/strategy/test_interface.py @@ -20,7 +20,8 @@ from freqtrade.strategy.hyper import detect_parameters from freqtrade.strategy.parameters import (BaseParameter, BooleanParameter, CategoricalParameter, DecimalParameter, IntParameter, RealParameter) from freqtrade.strategy.strategy_wrapper import strategy_safe_wrapper -from tests.conftest import CURRENT_TEST_STRATEGY, TRADE_SIDES, log_has, log_has_re +from tests.conftest import (CURRENT_TEST_STRATEGY, TRADE_SIDES, create_mock_trades, log_has, + log_has_re) from .strats.strategy_test_v3 import StrategyTestV3 @@ -812,6 +813,28 @@ def test_strategy_safe_wrapper(value): assert ret == value +@pytest.mark.usefixtures("init_persistence") +def test_strategy_safe_wrapper_trade_copy(fee): + create_mock_trades(fee) + + def working_method(trade): + assert len(trade.orders) > 0 + assert trade.orders + trade.orders = [] + assert len(trade.orders) == 0 + return trade + + trade = Trade.get_open_trades()[0] + # Don't assert anything before strategy_wrapper. + # This ensures that relationship loading works correctly. + ret = strategy_safe_wrapper(working_method, message='DeadBeef')(trade=trade) + assert isinstance(ret, Trade) + assert id(trade) != id(ret) + # Did not modify the original order + assert len(trade.orders) > 0 + assert len(ret.orders) == 0 + + def test_hyperopt_parameters(): from skopt.space import Categorical, Integer, Real with pytest.raises(OperationalException, match=r"Name is determined.*"): From 88f8cbe17278f21d459a323d66d85cbe6c03db48 Mon Sep 17 00:00:00 2001 From: Matthias Date: Thu, 9 Jun 2022 06:45:22 +0200 Subject: [PATCH 56/95] Update tests to reflect new naming --- freqtrade/rpc/telegram.py | 1 + tests/rpc/test_rpc.py | 8 ++++---- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/freqtrade/rpc/telegram.py b/freqtrade/rpc/telegram.py index e64ab7b8a..27eb04b89 100644 --- a/freqtrade/rpc/telegram.py +++ b/freqtrade/rpc/telegram.py @@ -657,6 +657,7 @@ class Telegram(RPCHandler): :return: None """ self._timeunit_stats(update, context, 'months') + @authorized_only def _profit(self, update: Update, context: CallbackContext) -> None: """ diff --git a/tests/rpc/test_rpc.py b/tests/rpc/test_rpc.py index 95645c8ba..e1f40bcd2 100644 --- a/tests/rpc/test_rpc.py +++ b/tests/rpc/test_rpc.py @@ -284,8 +284,8 @@ def test_rpc_status_table(default_conf, ticker, fee, mocker) -> None: assert isnan(fiat_profit_sum) -def test_rpc_daily_profit(default_conf, update, ticker, fee, - limit_buy_order, limit_sell_order, markets, mocker) -> None: +def test__rpc_timeunit_profit(default_conf, update, ticker, fee, + limit_buy_order, limit_sell_order, markets, mocker) -> None: mocker.patch('freqtrade.rpc.telegram.Telegram', MagicMock()) mocker.patch.multiple( 'freqtrade.exchange.Exchange', @@ -316,7 +316,7 @@ def test_rpc_daily_profit(default_conf, update, ticker, fee, # Try valid data update.message.text = '/daily 2' - days = rpc._rpc_daily_profit(7, stake_currency, fiat_display_currency) + days = rpc._rpc_timeunit_profit(7, stake_currency, fiat_display_currency) assert len(days['data']) == 7 assert days['stake_currency'] == default_conf['stake_currency'] assert days['fiat_display_currency'] == default_conf['fiat_display_currency'] @@ -332,7 +332,7 @@ def test_rpc_daily_profit(default_conf, update, ticker, fee, # Try invalid data with pytest.raises(RPCException, match=r'.*must be an integer greater than 0*'): - rpc._rpc_daily_profit(0, stake_currency, fiat_display_currency) + rpc._rpc_timeunit_profit(0, stake_currency, fiat_display_currency) @pytest.mark.parametrize('is_short', [True, False]) From 1ddd5f1901d08073dd7d8c9cc3b819c728a20350 Mon Sep 17 00:00:00 2001 From: Matthias Date: Thu, 9 Jun 2022 19:41:08 +0200 Subject: [PATCH 57/95] Update docstring throughout the bot. --- docs/strategy-callbacks.md | 6 ++++-- freqtrade/strategy/interface.py | 2 ++ .../templates/subtemplates/strategy_methods_advanced.j2 | 2 ++ 3 files changed, 8 insertions(+), 2 deletions(-) diff --git a/docs/strategy-callbacks.md b/docs/strategy-callbacks.md index b897453e7..410641f44 100644 --- a/docs/strategy-callbacks.md +++ b/docs/strategy-callbacks.md @@ -550,7 +550,8 @@ class AwesomeStrategy(IStrategy): :param pair: Pair that's about to be bought/shorted. :param order_type: Order type (as configured in order_types). usually limit or market. :param amount: Amount in target (base) currency that's going to be traded. - :param rate: Rate that's going to be used when using limit orders or current rate for market orders. + :param rate: Rate that's going to be used when using limit orders + or current rate for market orders. :param time_in_force: Time in force. Defaults to GTC (Good-til-cancelled). :param current_time: datetime object, containing the current datetime :param entry_tag: Optional entry_tag (buy_tag) if provided with the buy signal. @@ -599,7 +600,8 @@ class AwesomeStrategy(IStrategy): :param trade: trade object. :param order_type: Order type (as configured in order_types). usually limit or market. :param amount: Amount in base currency. - :param rate: Rate that's going to be used when using limit orders or current rate for market orders. + :param rate: Rate that's going to be used when using limit orders + or current rate for market orders. :param time_in_force: Time in force. Defaults to GTC (Good-til-cancelled). :param exit_reason: Exit reason. Can be any of ['roi', 'stop_loss', 'stoploss_on_exchange', 'trailing_stop_loss', diff --git a/freqtrade/strategy/interface.py b/freqtrade/strategy/interface.py index 3b3d326ff..d4ccfc5db 100644 --- a/freqtrade/strategy/interface.py +++ b/freqtrade/strategy/interface.py @@ -289,6 +289,7 @@ class IStrategy(ABC, HyperStrategyMixin): :param order_type: Order type (as configured in order_types). usually limit or market. :param amount: Amount in target (base) currency that's going to be traded. :param rate: Rate that's going to be used when using limit orders + or current rate for market orders. :param time_in_force: Time in force. Defaults to GTC (Good-til-cancelled). :param current_time: datetime object, containing the current datetime :param entry_tag: Optional entry_tag (buy_tag) if provided with the buy signal. @@ -316,6 +317,7 @@ class IStrategy(ABC, HyperStrategyMixin): :param order_type: Order type (as configured in order_types). usually limit or market. :param amount: Amount in base currency. :param rate: Rate that's going to be used when using limit orders + or current rate for market orders. :param time_in_force: Time in force. Defaults to GTC (Good-til-cancelled). :param exit_reason: Exit reason. Can be any of ['roi', 'stop_loss', 'stoploss_on_exchange', 'trailing_stop_loss', diff --git a/freqtrade/templates/subtemplates/strategy_methods_advanced.j2 b/freqtrade/templates/subtemplates/strategy_methods_advanced.j2 index acefd0363..815ca7cd3 100644 --- a/freqtrade/templates/subtemplates/strategy_methods_advanced.j2 +++ b/freqtrade/templates/subtemplates/strategy_methods_advanced.j2 @@ -161,6 +161,7 @@ def confirm_trade_entry(self, pair: str, order_type: str, amount: float, rate: f :param order_type: Order type (as configured in order_types). usually limit or market. :param amount: Amount in target (base) currency that's going to be traded. :param rate: Rate that's going to be used when using limit orders + or current rate for market orders. :param time_in_force: Time in force. Defaults to GTC (Good-til-cancelled). :param current_time: datetime object, containing the current datetime :param entry_tag: Optional entry_tag (buy_tag) if provided with the buy signal. @@ -188,6 +189,7 @@ def confirm_trade_exit(self, pair: str, trade: 'Trade', order_type: str, amount: :param order_type: Order type (as configured in order_types). usually limit or market. :param amount: Amount in base currency. :param rate: Rate that's going to be used when using limit orders + or current rate for market orders. :param time_in_force: Time in force. Defaults to GTC (Good-til-cancelled). :param exit_reason: Exit reason. Can be any of ['roi', 'stop_loss', 'stoploss_on_exchange', 'trailing_stop_loss', From a9c7ad8a0fcbf00063beba6a2b59809b99a97218 Mon Sep 17 00:00:00 2001 From: Matthias Date: Thu, 9 Jun 2022 19:51:21 +0200 Subject: [PATCH 58/95] Add warning about sqlite disabled foreign keys --- docs/sql_cheatsheet.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/docs/sql_cheatsheet.md b/docs/sql_cheatsheet.md index 49372b002..c9fcba557 100644 --- a/docs/sql_cheatsheet.md +++ b/docs/sql_cheatsheet.md @@ -100,6 +100,9 @@ DELETE FROM trades WHERE id = 31; !!! Warning This will remove this trade from the database. Please make sure you got the correct id and **NEVER** run this query without the `where` clause. +!!! Danger + Some systems (Ubuntu) disable foreign keys in their sqlite3 implementation. When using sqlite3 - please ensure that foreign keys are on by running `PRAGMA foreign_keys = ON` before the above query. + ## Use a different database system !!! Warning From 3c2ba99fc480d028f8c6c86db68cfa5813b2b0e5 Mon Sep 17 00:00:00 2001 From: Matthias Date: Thu, 9 Jun 2022 19:57:56 +0200 Subject: [PATCH 59/95] Improve sql cheatsheet docs --- docs/sql_cheatsheet.md | 23 ++++++++++++++--------- 1 file changed, 14 insertions(+), 9 deletions(-) diff --git a/docs/sql_cheatsheet.md b/docs/sql_cheatsheet.md index c9fcba557..c42cb5575 100644 --- a/docs/sql_cheatsheet.md +++ b/docs/sql_cheatsheet.md @@ -89,29 +89,34 @@ WHERE id=31; If you'd still like to remove a trade from the database directly, you can use the below query. -```sql -DELETE FROM trades WHERE id = ; -``` +!!! Danger + Some systems (Ubuntu) disable foreign keys in their sqlite3 packaging. When using sqlite - please ensure that foreign keys are on by running `PRAGMA foreign_keys = ON` before the above query. ```sql +DELETE FROM trades WHERE id = ; + DELETE FROM trades WHERE id = 31; ``` !!! Warning This will remove this trade from the database. Please make sure you got the correct id and **NEVER** run this query without the `where` clause. -!!! Danger - Some systems (Ubuntu) disable foreign keys in their sqlite3 implementation. When using sqlite3 - please ensure that foreign keys are on by running `PRAGMA foreign_keys = ON` before the above query. - ## Use a different database system +Freqtrade is using SQLAlchemy, which supports multiple different database systems. As such, a multitude of database systems should be supported. +Freqtrade does not depend or install any additional database driver. Please refer to the [SQLAlchemy docs](https://docs.sqlalchemy.org/en/14/core/engines.html#database-urls) on installation instructions for the respective database systems. + +The following systems have been tested and are known to work with freqtrade: + +* sqlite (default) +* PostgreSQL) +* MariaDB + !!! Warning - By using one of the below database systems, you acknowledge that you know how to manage such a system. Freqtrade will not provide any support with setup or maintenance (or backups) of the below database systems. + By using one of the below database systems, you acknowledge that you know how to manage such a system. The freqtrade team will not provide any support with setup or maintenance (or backups) of the below database systems. ### PostgreSQL -Freqtrade supports PostgreSQL by using SQLAlchemy, which supports multiple different database systems. - Installation: `pip install psycopg2-binary` From 8fb743b91d33d7187c32765a6c6f3c2c5d7fd2eb Mon Sep 17 00:00:00 2001 From: Matthias Date: Thu, 9 Jun 2022 20:13:26 +0200 Subject: [PATCH 60/95] improve variable wording --- freqtrade/rpc/telegram.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/freqtrade/rpc/telegram.py b/freqtrade/rpc/telegram.py index 27eb04b89..106a5f011 100644 --- a/freqtrade/rpc/telegram.py +++ b/freqtrade/rpc/telegram.py @@ -605,10 +605,10 @@ class Telegram(RPCHandler): unit ) stats_tab = tabulate( - [[day['date'], - f"{round_coin_value(day['abs_profit'], stats['stake_currency'])}", - f"{day['fiat_value']:.3f} {stats['fiat_display_currency']}", - f"{day['trade_count']} trades"] for day in stats['data']], + [[period['date'], + f"{round_coin_value(period['abs_profit'], stats['stake_currency'])}", + f"{period['fiat_value']:.3f} {stats['fiat_display_currency']}", + f"{period['trade_count']} trades"] for period in stats['data']], headers=[ val.header, f'Profit {stake_cur}', From dce9fdd0e4717559862b85df0850d5a1608e62fd Mon Sep 17 00:00:00 2001 From: Italo <45588475+italodamato@users.noreply.github.com> Date: Thu, 9 Jun 2022 20:06:23 +0100 Subject: [PATCH 61/95] don't overwrite is_random this should fix issue #6746 --- freqtrade/optimize/hyperopt.py | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/freqtrade/optimize/hyperopt.py b/freqtrade/optimize/hyperopt.py index d1697709b..ac1b7b8ba 100644 --- a/freqtrade/optimize/hyperopt.py +++ b/freqtrade/optimize/hyperopt.py @@ -429,18 +429,19 @@ class Hyperopt: return new_list i = 0 asked_non_tried: List[List[Any]] = [] - is_random: List[bool] = [] + is_random_non_tried: List[bool] = [] while i < 5 and len(asked_non_tried) < n_points: if i < 3: self.opt.cache_ = {} asked = unique_list(self.opt.ask(n_points=n_points * 5)) is_random = [False for _ in range(len(asked))] else: - asked = unique_list(self.opt.space.rvs(n_samples=n_points * 5)) + asked = unique_list(self.opt.space.rvs( + n_samples=n_points * 5, random_state=self.random_state + i)) is_random = [True for _ in range(len(asked))] - is_random += [rand for x, rand in zip(asked, is_random) - if x not in self.opt.Xi - and x not in asked_non_tried] + is_random_non_tried += [rand for x, rand in zip(asked, is_random) + if x not in self.opt.Xi + and x not in asked_non_tried] asked_non_tried += [x for x in asked if x not in self.opt.Xi and x not in asked_non_tried] @@ -449,7 +450,7 @@ class Hyperopt: if asked_non_tried: return ( asked_non_tried[:min(len(asked_non_tried), n_points)], - is_random[:min(len(asked_non_tried), n_points)] + is_random_non_tried[:min(len(asked_non_tried), n_points)] ) else: return self.opt.ask(n_points=n_points), [False for _ in range(n_points)] From ad3c01736e74f4986cba86f685c2999fd202883f Mon Sep 17 00:00:00 2001 From: Matthias Date: Fri, 10 Jun 2022 07:26:53 +0200 Subject: [PATCH 62/95] time aggregate to only query for data necessary improves the query by not creating a full trade object. --- freqtrade/rpc/rpc.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/freqtrade/rpc/rpc.py b/freqtrade/rpc/rpc.py index a6290bd5a..64584382a 100644 --- a/freqtrade/rpc/rpc.py +++ b/freqtrade/rpc/rpc.py @@ -309,16 +309,18 @@ class RPC: for day in range(0, timescale): profitday = start_date - time_offset(day) - trades = Trade.get_trades(trade_filter=[ + # Only query for necessary columns for performance reasons. + trades = Trade.query.session.query(Trade.close_profit_abs).filter( Trade.is_open.is_(False), Trade.close_date >= profitday, Trade.close_date < (profitday + time_offset(1)) - ]).order_by(Trade.close_date).all() + ).order_by(Trade.close_date).all() + curdayprofit = sum( trade.close_profit_abs for trade in trades if trade.close_profit_abs is not None) profit_units[profitday] = { 'amount': curdayprofit, - 'trades': len(trades) + 'trades': len(trades), } data = [ From 7142394121abc4d511f110d805dd848989eb9126 Mon Sep 17 00:00:00 2001 From: Italo <45588475+italodamato@users.noreply.github.com> Date: Fri, 10 Jun 2022 09:46:45 +0100 Subject: [PATCH 63/95] remove random_state condition otherwise the random sample always draws the same set of points --- freqtrade/optimize/hyperopt.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/freqtrade/optimize/hyperopt.py b/freqtrade/optimize/hyperopt.py index ac1b7b8ba..cb0d788da 100644 --- a/freqtrade/optimize/hyperopt.py +++ b/freqtrade/optimize/hyperopt.py @@ -436,8 +436,7 @@ class Hyperopt: asked = unique_list(self.opt.ask(n_points=n_points * 5)) is_random = [False for _ in range(len(asked))] else: - asked = unique_list(self.opt.space.rvs( - n_samples=n_points * 5, random_state=self.random_state + i)) + asked = unique_list(self.opt.space.rvs(n_samples=n_points * 5)) is_random = [True for _ in range(len(asked))] is_random_non_tried += [rand for x, rand in zip(asked, is_random) if x not in self.opt.Xi From 76f87377ba542a106476828cd04846e29c0cfb88 Mon Sep 17 00:00:00 2001 From: Matthias Date: Fri, 10 Jun 2022 20:18:33 +0200 Subject: [PATCH 64/95] Reduce decimals on FIAT daily column --- freqtrade/rpc/telegram.py | 2 +- tests/rpc/test_rpc_telegram.py | 18 +++++++++--------- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/freqtrade/rpc/telegram.py b/freqtrade/rpc/telegram.py index 106a5f011..61b73553f 100644 --- a/freqtrade/rpc/telegram.py +++ b/freqtrade/rpc/telegram.py @@ -607,7 +607,7 @@ class Telegram(RPCHandler): stats_tab = tabulate( [[period['date'], f"{round_coin_value(period['abs_profit'], stats['stake_currency'])}", - f"{period['fiat_value']:.3f} {stats['fiat_display_currency']}", + f"{period['fiat_value']:.2f} {stats['fiat_display_currency']}", f"{period['trade_count']} trades"] for period in stats['data']], headers=[ val.header, diff --git a/tests/rpc/test_rpc_telegram.py b/tests/rpc/test_rpc_telegram.py index 2bc4fc5c3..5271c5a30 100644 --- a/tests/rpc/test_rpc_telegram.py +++ b/tests/rpc/test_rpc_telegram.py @@ -447,7 +447,7 @@ def test_daily_handle(default_conf, update, ticker, limit_buy_order, fee, assert 'Day ' in msg_mock.call_args_list[0][0][0] assert str(datetime.utcnow().date()) in msg_mock.call_args_list[0][0][0] assert str(' 0.00006217 BTC') in msg_mock.call_args_list[0][0][0] - assert str(' 0.933 USD') in msg_mock.call_args_list[0][0][0] + assert str(' 0.93 USD') in msg_mock.call_args_list[0][0][0] assert str(' 1 trade') in msg_mock.call_args_list[0][0][0] assert str(' 0 trade') in msg_mock.call_args_list[0][0][0] @@ -459,7 +459,7 @@ def test_daily_handle(default_conf, update, ticker, limit_buy_order, fee, assert "Daily Profit over the last 7 days:" in msg_mock.call_args_list[0][0][0] assert str(datetime.utcnow().date()) in msg_mock.call_args_list[0][0][0] assert str(' 0.00006217 BTC') in msg_mock.call_args_list[0][0][0] - assert str(' 0.933 USD') in msg_mock.call_args_list[0][0][0] + assert str(' 0.93 USD') in msg_mock.call_args_list[0][0][0] assert str(' 1 trade') in msg_mock.call_args_list[0][0][0] assert str(' 0 trade') in msg_mock.call_args_list[0][0][0] @@ -482,7 +482,7 @@ def test_daily_handle(default_conf, update, ticker, limit_buy_order, fee, context.args = ["1"] telegram._daily(update=update, context=context) assert str(' 0.00018651 BTC') in msg_mock.call_args_list[0][0][0] - assert str(' 2.798 USD') in msg_mock.call_args_list[0][0][0] + assert str(' 2.80 USD') in msg_mock.call_args_list[0][0][0] assert str(' 3 trades') in msg_mock.call_args_list[0][0][0] @@ -561,7 +561,7 @@ def test_weekly_handle(default_conf, update, ticker, limit_buy_order, fee, first_iso_day_of_current_week = today - timedelta(days=today.weekday()) assert str(first_iso_day_of_current_week) in msg_mock.call_args_list[0][0][0] assert str(' 0.00006217 BTC') in msg_mock.call_args_list[0][0][0] - assert str(' 0.933 USD') in msg_mock.call_args_list[0][0][0] + assert str(' 0.93 USD') in msg_mock.call_args_list[0][0][0] assert str(' 1 trade') in msg_mock.call_args_list[0][0][0] assert str(' 0 trade') in msg_mock.call_args_list[0][0][0] @@ -574,7 +574,7 @@ def test_weekly_handle(default_conf, update, ticker, limit_buy_order, fee, in msg_mock.call_args_list[0][0][0] assert 'Weekly' in msg_mock.call_args_list[0][0][0] assert str(' 0.00006217 BTC') in msg_mock.call_args_list[0][0][0] - assert str(' 0.933 USD') in msg_mock.call_args_list[0][0][0] + assert str(' 0.93 USD') in msg_mock.call_args_list[0][0][0] assert str(' 1 trade') in msg_mock.call_args_list[0][0][0] assert str(' 0 trade') in msg_mock.call_args_list[0][0][0] @@ -599,7 +599,7 @@ def test_weekly_handle(default_conf, update, ticker, limit_buy_order, fee, context.args = ["1"] telegram._weekly(update=update, context=context) assert str(' 0.00018651 BTC') in msg_mock.call_args_list[0][0][0] - assert str(' 2.798 USD') in msg_mock.call_args_list[0][0][0] + assert str(' 2.80 USD') in msg_mock.call_args_list[0][0][0] assert str(' 3 trades') in msg_mock.call_args_list[0][0][0] @@ -678,7 +678,7 @@ def test_monthly_handle(default_conf, update, ticker, limit_buy_order, fee, current_month = f"{today.year}-{today.month:02} " assert current_month in msg_mock.call_args_list[0][0][0] assert str(' 0.00006217 BTC') in msg_mock.call_args_list[0][0][0] - assert str(' 0.933 USD') in msg_mock.call_args_list[0][0][0] + assert str(' 0.93 USD') in msg_mock.call_args_list[0][0][0] assert str(' 1 trade') in msg_mock.call_args_list[0][0][0] assert str(' 0 trade') in msg_mock.call_args_list[0][0][0] @@ -692,7 +692,7 @@ def test_monthly_handle(default_conf, update, ticker, limit_buy_order, fee, assert 'Month ' in msg_mock.call_args_list[0][0][0] assert current_month in msg_mock.call_args_list[0][0][0] assert str(' 0.00006217 BTC') in msg_mock.call_args_list[0][0][0] - assert str(' 0.933 USD') in msg_mock.call_args_list[0][0][0] + assert str(' 0.93 USD') in msg_mock.call_args_list[0][0][0] assert str(' 1 trade') in msg_mock.call_args_list[0][0][0] assert str(' 0 trade') in msg_mock.call_args_list[0][0][0] @@ -717,7 +717,7 @@ def test_monthly_handle(default_conf, update, ticker, limit_buy_order, fee, assert msg_mock.call_count == 1 assert 'Monthly Profit over the last 12 months:' in msg_mock.call_args_list[0][0][0] assert str(' 0.00018651 BTC') in msg_mock.call_args_list[0][0][0] - assert str(' 2.798 USD') in msg_mock.call_args_list[0][0][0] + assert str(' 2.80 USD') in msg_mock.call_args_list[0][0][0] assert str(' 3 trades') in msg_mock.call_args_list[0][0][0] # The one-digit months should contain a zero, Eg: September 2021 = "2021-09" From 2c7c5f9a6e0815760d1bafed9a96e8804c15b7b4 Mon Sep 17 00:00:00 2001 From: Matthias Date: Fri, 10 Jun 2022 20:34:17 +0200 Subject: [PATCH 65/95] Update mock_usdt trade method --- tests/conftest.py | 19 +++-- tests/conftest_trades_usdt.py | 151 +++++++++++++++++++-------------- tests/plugins/test_pairlist.py | 4 +- 3 files changed, 100 insertions(+), 74 deletions(-) diff --git a/tests/conftest.py b/tests/conftest.py index 02738b0e9..b4b98cbeb 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -325,7 +325,7 @@ def create_mock_trades_with_leverage(fee, use_db: bool = True): Trade.query.session.flush() -def create_mock_trades_usdt(fee, use_db: bool = True): +def create_mock_trades_usdt(fee, is_short: Optional[bool] = False, use_db: bool = True): """ Create some fake trades ... """ @@ -335,26 +335,29 @@ def create_mock_trades_usdt(fee, use_db: bool = True): else: LocalTrade.add_bt_trade(trade) + is_short1 = is_short if is_short is not None else True + is_short2 = is_short if is_short is not None else False + # Simulate dry_run entries - trade = mock_trade_usdt_1(fee) + trade = mock_trade_usdt_1(fee, is_short1) add_trade(trade) - trade = mock_trade_usdt_2(fee) + trade = mock_trade_usdt_2(fee, is_short1) add_trade(trade) - trade = mock_trade_usdt_3(fee) + trade = mock_trade_usdt_3(fee, is_short1) add_trade(trade) - trade = mock_trade_usdt_4(fee) + trade = mock_trade_usdt_4(fee, is_short2) add_trade(trade) - trade = mock_trade_usdt_5(fee) + trade = mock_trade_usdt_5(fee, is_short2) add_trade(trade) - trade = mock_trade_usdt_6(fee) + trade = mock_trade_usdt_6(fee, is_short1) add_trade(trade) - trade = mock_trade_usdt_7(fee) + trade = mock_trade_usdt_7(fee, is_short1) add_trade(trade) if use_db: Trade.commit() diff --git a/tests/conftest_trades_usdt.py b/tests/conftest_trades_usdt.py index 59e7f0457..6f83bb8be 100644 --- a/tests/conftest_trades_usdt.py +++ b/tests/conftest_trades_usdt.py @@ -6,12 +6,24 @@ from freqtrade.persistence.models import Order, Trade MOCK_TRADE_COUNT = 6 -def mock_order_usdt_1(): +def entry_side(is_short: bool): + return "sell" if is_short else "buy" + + +def exit_side(is_short: bool): + return "buy" if is_short else "sell" + + +def direc(is_short: bool): + return "short" if is_short else "long" + + +def mock_order_usdt_1(is_short: bool): return { - 'id': '1234', + 'id': f'1234_{direc(is_short)}', 'symbol': 'ADA/USDT', 'status': 'closed', - 'side': 'buy', + 'side': entry_side(is_short), 'type': 'limit', 'price': 2.0, 'amount': 10.0, @@ -20,7 +32,7 @@ def mock_order_usdt_1(): } -def mock_trade_usdt_1(fee): +def mock_trade_usdt_1(fee, is_short: bool): trade = Trade( pair='ADA/USDT', stake_amount=20.0, @@ -32,21 +44,22 @@ def mock_trade_usdt_1(fee): open_date=datetime.now(tz=timezone.utc) - timedelta(minutes=17), open_rate=2.0, exchange='binance', - open_order_id='dry_run_buy_12345', + open_order_id=f'1234_{direc(is_short)}', strategy='StrategyTestV2', timeframe=5, + is_short=is_short, ) - o = Order.parse_from_ccxt_object(mock_order_usdt_1(), 'ADA/USDT', 'buy') + o = Order.parse_from_ccxt_object(mock_order_usdt_1(is_short), 'ADA/USDT', entry_side(is_short)) trade.orders.append(o) return trade -def mock_order_usdt_2(): +def mock_order_usdt_2(is_short: bool): return { - 'id': '1235', + 'id': f'1235_{direc(is_short)}', 'symbol': 'ETC/USDT', 'status': 'closed', - 'side': 'buy', + 'side': entry_side(is_short), 'type': 'limit', 'price': 2.0, 'amount': 100.0, @@ -55,12 +68,12 @@ def mock_order_usdt_2(): } -def mock_order_usdt_2_sell(): +def mock_order_usdt_2_exit(is_short: bool): return { - 'id': '12366', + 'id': f'12366_{direc(is_short)}', 'symbol': 'ETC/USDT', 'status': 'closed', - 'side': 'sell', + 'side': exit_side(is_short), 'type': 'limit', 'price': 2.05, 'amount': 100.0, @@ -69,7 +82,7 @@ def mock_order_usdt_2_sell(): } -def mock_trade_usdt_2(fee): +def mock_trade_usdt_2(fee, is_short: bool): """ Closed trade... """ @@ -86,26 +99,28 @@ def mock_trade_usdt_2(fee): close_profit_abs=3.9875, exchange='binance', is_open=False, - open_order_id='dry_run_sell_12345', + open_order_id=f'12366_{direc(is_short)}', strategy='StrategyTestV2', timeframe=5, - exit_reason='sell_signal', + exit_reason='exit_signal', open_date=datetime.now(tz=timezone.utc) - timedelta(minutes=20), close_date=datetime.now(tz=timezone.utc) - timedelta(minutes=2), + is_short=is_short, ) - o = Order.parse_from_ccxt_object(mock_order_usdt_2(), 'ETC/USDT', 'buy') + o = Order.parse_from_ccxt_object(mock_order_usdt_2(is_short), 'ETC/USDT', entry_side(is_short)) trade.orders.append(o) - o = Order.parse_from_ccxt_object(mock_order_usdt_2_sell(), 'ETC/USDT', 'sell') + o = Order.parse_from_ccxt_object( + mock_order_usdt_2_exit(is_short), 'ETC/USDT', exit_side(is_short)) trade.orders.append(o) return trade -def mock_order_usdt_3(): +def mock_order_usdt_3(is_short: bool): return { - 'id': '41231a12a', + 'id': f'41231a12a_{direc(is_short)}', 'symbol': 'XRP/USDT', 'status': 'closed', - 'side': 'buy', + 'side': entry_side(is_short), 'type': 'limit', 'price': 1.0, 'amount': 30.0, @@ -114,12 +129,12 @@ def mock_order_usdt_3(): } -def mock_order_usdt_3_sell(): +def mock_order_usdt_3_exit(is_short: bool): return { - 'id': '41231a666a', + 'id': f'41231a666a_{direc(is_short)}', 'symbol': 'XRP/USDT', 'status': 'closed', - 'side': 'sell', + 'side': exit_side(is_short), 'type': 'stop_loss_limit', 'price': 1.1, 'average': 1.1, @@ -129,7 +144,7 @@ def mock_order_usdt_3_sell(): } -def mock_trade_usdt_3(fee): +def mock_trade_usdt_3(fee, is_short: bool): """ Closed trade """ @@ -151,20 +166,22 @@ def mock_trade_usdt_3(fee): exit_reason='roi', open_date=datetime.now(tz=timezone.utc) - timedelta(minutes=20), close_date=datetime.now(tz=timezone.utc), + is_short=is_short, ) - o = Order.parse_from_ccxt_object(mock_order_usdt_3(), 'XRP/USDT', 'buy') + o = Order.parse_from_ccxt_object(mock_order_usdt_3(is_short), 'XRP/USDT', entry_side(is_short)) trade.orders.append(o) - o = Order.parse_from_ccxt_object(mock_order_usdt_3_sell(), 'XRP/USDT', 'sell') + o = Order.parse_from_ccxt_object(mock_order_usdt_3_exit(is_short), + 'XRP/USDT', exit_side(is_short)) trade.orders.append(o) return trade -def mock_order_usdt_4(): +def mock_order_usdt_4(is_short: bool): return { - 'id': 'prod_buy_12345', + 'id': f'prod_buy_12345_{direc(is_short)}', 'symbol': 'ETC/USDT', 'status': 'open', - 'side': 'buy', + 'side': entry_side(is_short), 'type': 'limit', 'price': 2.0, 'amount': 10.0, @@ -173,7 +190,7 @@ def mock_order_usdt_4(): } -def mock_trade_usdt_4(fee): +def mock_trade_usdt_4(fee, is_short: bool): """ Simulate prod entry """ @@ -188,21 +205,22 @@ def mock_trade_usdt_4(fee): is_open=True, open_rate=2.0, exchange='binance', - open_order_id='prod_buy_12345', + open_order_id=f'prod_buy_12345_{direc(is_short)}', strategy='StrategyTestV2', timeframe=5, + is_short=is_short, ) - o = Order.parse_from_ccxt_object(mock_order_usdt_4(), 'ETC/USDT', 'buy') + o = Order.parse_from_ccxt_object(mock_order_usdt_4(is_short), 'ETC/USDT', entry_side(is_short)) trade.orders.append(o) return trade -def mock_order_usdt_5(): +def mock_order_usdt_5(is_short: bool): return { - 'id': 'prod_buy_3455', + 'id': f'prod_buy_3455_{direc(is_short)}', 'symbol': 'XRP/USDT', 'status': 'closed', - 'side': 'buy', + 'side': entry_side(is_short), 'type': 'limit', 'price': 2.0, 'amount': 10.0, @@ -211,12 +229,12 @@ def mock_order_usdt_5(): } -def mock_order_usdt_5_stoploss(): +def mock_order_usdt_5_stoploss(is_short: bool): return { - 'id': 'prod_stoploss_3455', + 'id': f'prod_stoploss_3455_{direc(is_short)}', 'symbol': 'XRP/USDT', 'status': 'open', - 'side': 'sell', + 'side': exit_side(is_short), 'type': 'stop_loss_limit', 'price': 2.0, 'amount': 10.0, @@ -225,7 +243,7 @@ def mock_order_usdt_5_stoploss(): } -def mock_trade_usdt_5(fee): +def mock_trade_usdt_5(fee, is_short: bool): """ Simulate prod entry with stoploss """ @@ -241,22 +259,23 @@ def mock_trade_usdt_5(fee): open_rate=2.0, exchange='binance', strategy='SampleStrategy', - stoploss_order_id='prod_stoploss_3455', + stoploss_order_id=f'prod_stoploss_3455_{direc(is_short)}', timeframe=5, + is_short=is_short, ) - o = Order.parse_from_ccxt_object(mock_order_usdt_5(), 'XRP/USDT', 'buy') + o = Order.parse_from_ccxt_object(mock_order_usdt_5(is_short), 'XRP/USDT', entry_side(is_short)) trade.orders.append(o) - o = Order.parse_from_ccxt_object(mock_order_usdt_5_stoploss(), 'XRP/USDT', 'stoploss') + o = Order.parse_from_ccxt_object(mock_order_usdt_5_stoploss(is_short), 'XRP/USDT', 'stoploss') trade.orders.append(o) return trade -def mock_order_usdt_6(): +def mock_order_usdt_6(is_short: bool): return { - 'id': 'prod_buy_6', + 'id': f'prod_entry_6_{direc(is_short)}', 'symbol': 'LTC/USDT', 'status': 'closed', - 'side': 'buy', + 'side': entry_side(is_short), 'type': 'limit', 'price': 10.0, 'amount': 2.0, @@ -265,12 +284,12 @@ def mock_order_usdt_6(): } -def mock_order_usdt_6_sell(): +def mock_order_usdt_6_exit(is_short: bool): return { - 'id': 'prod_sell_6', + 'id': f'prod_exit_6_{direc(is_short)}', 'symbol': 'LTC/USDT', 'status': 'open', - 'side': 'sell', + 'side': exit_side(is_short), 'type': 'limit', 'price': 12.0, 'amount': 2.0, @@ -279,7 +298,7 @@ def mock_order_usdt_6_sell(): } -def mock_trade_usdt_6(fee): +def mock_trade_usdt_6(fee, is_short: bool): """ Simulate prod entry with open sell order """ @@ -295,22 +314,24 @@ def mock_trade_usdt_6(fee): open_rate=10.0, exchange='binance', strategy='SampleStrategy', - open_order_id="prod_sell_6", + open_order_id=f'prod_exit_6_{direc(is_short)}', timeframe=5, + is_short=is_short, ) - o = Order.parse_from_ccxt_object(mock_order_usdt_6(), 'LTC/USDT', 'buy') + o = Order.parse_from_ccxt_object(mock_order_usdt_6(is_short), 'LTC/USDT', entry_side(is_short)) trade.orders.append(o) - o = Order.parse_from_ccxt_object(mock_order_usdt_6_sell(), 'LTC/USDT', 'sell') + o = Order.parse_from_ccxt_object(mock_order_usdt_6_exit(is_short), + 'LTC/USDT', exit_side(is_short)) trade.orders.append(o) return trade -def mock_order_usdt_7(): +def mock_order_usdt_7(is_short: bool): return { - 'id': 'prod_buy_7', + 'id': f'prod_entry_7_{direc(is_short)}', 'symbol': 'LTC/USDT', 'status': 'closed', - 'side': 'buy', + 'side': entry_side(is_short), 'type': 'limit', 'price': 10.0, 'amount': 2.0, @@ -319,12 +340,12 @@ def mock_order_usdt_7(): } -def mock_order_usdt_7_sell(): +def mock_order_usdt_7_exit(is_short: bool): return { - 'id': 'prod_sell_7', + 'id': f'prod_exit_7_{direc(is_short)}', 'symbol': 'LTC/USDT', 'status': 'closed', - 'side': 'sell', + 'side': exit_side(is_short), 'type': 'limit', 'price': 8.0, 'amount': 2.0, @@ -333,7 +354,7 @@ def mock_order_usdt_7_sell(): } -def mock_trade_usdt_7(fee): +def mock_trade_usdt_7(fee, is_short: bool): """ Simulate prod entry with open sell order """ @@ -342,8 +363,8 @@ def mock_trade_usdt_7(fee): stake_amount=20.0, amount=2.0, amount_requested=2.0, - open_date=datetime.now(tz=timezone.utc) - timedelta(minutes=20), - close_date=datetime.now(tz=timezone.utc) - timedelta(minutes=5), + open_date=datetime.now(tz=timezone.utc) - timedelta(days=2, minutes=20), + close_date=datetime.now(tz=timezone.utc) - timedelta(days=2, minutes=5), fee_open=fee.return_value, fee_close=fee.return_value, is_open=False, @@ -353,11 +374,13 @@ def mock_trade_usdt_7(fee): close_profit_abs=-4.0, exchange='binance', strategy='SampleStrategy', - open_order_id="prod_sell_6", + open_order_id=f'prod_exit_7_{direc(is_short)}', timeframe=5, + is_short=is_short, ) - o = Order.parse_from_ccxt_object(mock_order_usdt_7(), 'LTC/USDT', 'buy') + o = Order.parse_from_ccxt_object(mock_order_usdt_7(is_short), 'LTC/USDT', entry_side(is_short)) trade.orders.append(o) - o = Order.parse_from_ccxt_object(mock_order_usdt_7_sell(), 'LTC/USDT', 'sell') + o = Order.parse_from_ccxt_object(mock_order_usdt_7_exit(is_short), + 'LTC/USDT', exit_side(is_short)) trade.orders.append(o) return trade diff --git a/tests/plugins/test_pairlist.py b/tests/plugins/test_pairlist.py index c29e619b1..c56f405e2 100644 --- a/tests/plugins/test_pairlist.py +++ b/tests/plugins/test_pairlist.py @@ -762,8 +762,8 @@ def test_PerformanceFilter_keep_mid_order(mocker, default_conf_usdt, fee, caplog with time_machine.travel("2021-09-01 05:00:00 +00:00") as t: create_mock_trades_usdt(fee) pm.refresh_pairlist() - assert pm.whitelist == ['XRP/USDT', 'ETC/USDT', 'ETH/USDT', - 'NEO/USDT', 'TKN/USDT', 'ADA/USDT', 'LTC/USDT'] + assert pm.whitelist == ['XRP/USDT', 'ETC/USDT', 'ETH/USDT', 'LTC/USDT', + 'NEO/USDT', 'TKN/USDT', 'ADA/USDT', ] # assert log_has_re(r'Removing pair .* since .* is below .*', caplog) # Move to "outside" of lookback window, so original sorting is restored. From ab6a306e074da244c3798670cf00760a3c3c44aa Mon Sep 17 00:00:00 2001 From: Matthias Date: Fri, 10 Jun 2022 20:52:05 +0200 Subject: [PATCH 66/95] Update daily test to USDT --- tests/rpc/test_rpc_telegram.py | 59 ++++++++++------------------------ 1 file changed, 17 insertions(+), 42 deletions(-) diff --git a/tests/rpc/test_rpc_telegram.py b/tests/rpc/test_rpc_telegram.py index 5271c5a30..3cafb2d7d 100644 --- a/tests/rpc/test_rpc_telegram.py +++ b/tests/rpc/test_rpc_telegram.py @@ -27,8 +27,9 @@ from freqtrade.persistence.models import Order from freqtrade.rpc import RPC from freqtrade.rpc.rpc import RPCException from freqtrade.rpc.telegram import Telegram, authorized_only -from tests.conftest import (CURRENT_TEST_STRATEGY, create_mock_trades, get_patched_freqtradebot, - log_has, log_has_re, patch_exchange, patch_get_signal, patch_whitelist) +from tests.conftest import (CURRENT_TEST_STRATEGY, create_mock_trades, create_mock_trades_usdt, + get_patched_freqtradebot, log_has, log_has_re, patch_exchange, + patch_get_signal, patch_whitelist) class DummyCls(Telegram): @@ -404,12 +405,10 @@ def test_status_table_handle(default_conf, update, ticker, fee, mocker) -> None: assert msg_mock.call_count == 1 -def test_daily_handle(default_conf, update, ticker, limit_buy_order, fee, - limit_sell_order, mocker) -> None: - default_conf['max_open_trades'] = 1 +def test_daily_handle(default_conf_usdt, update, ticker, fee, mocker) -> None: mocker.patch( 'freqtrade.rpc.rpc.CryptoToFiatConverter._find_price', - return_value=15000.0 + return_value=1.1 ) mocker.patch.multiple( 'freqtrade.exchange.Exchange', @@ -417,25 +416,10 @@ def test_daily_handle(default_conf, update, ticker, limit_buy_order, fee, get_fee=fee, ) - telegram, freqtradebot, msg_mock = get_telegram_testobject(mocker, default_conf) - - patch_get_signal(freqtradebot) + telegram, freqtradebot, msg_mock = get_telegram_testobject(mocker, default_conf_usdt) # Create some test data - freqtradebot.enter_positions() - trade = Trade.query.first() - assert trade - - # Simulate fulfilled LIMIT_BUY order for trade - oobj = Order.parse_from_ccxt_object(limit_buy_order, limit_buy_order['symbol'], 'buy') - trade.update_trade(oobj) - - # Simulate fulfilled LIMIT_SELL order for trade - oobjs = Order.parse_from_ccxt_object(limit_sell_order, limit_sell_order['symbol'], 'sell') - trade.update_trade(oobjs) - - trade.close_date = datetime.utcnow() - trade.is_open = False + create_mock_trades_usdt(fee) # Try valid data # /daily 2 @@ -446,9 +430,9 @@ def test_daily_handle(default_conf, update, ticker, limit_buy_order, fee, assert "Daily Profit over the last 2 days:" in msg_mock.call_args_list[0][0][0] assert 'Day ' in msg_mock.call_args_list[0][0][0] assert str(datetime.utcnow().date()) in msg_mock.call_args_list[0][0][0] - assert str(' 0.00006217 BTC') in msg_mock.call_args_list[0][0][0] - assert str(' 0.93 USD') in msg_mock.call_args_list[0][0][0] - assert str(' 1 trade') in msg_mock.call_args_list[0][0][0] + assert str(' 13.83 USDT') in msg_mock.call_args_list[0][0][0] + assert str(' 15.21 USD') in msg_mock.call_args_list[0][0][0] + assert str(' 2 trade') in msg_mock.call_args_list[0][0][0] assert str(' 0 trade') in msg_mock.call_args_list[0][0][0] # Reset msg_mock @@ -458,32 +442,23 @@ def test_daily_handle(default_conf, update, ticker, limit_buy_order, fee, assert msg_mock.call_count == 1 assert "Daily Profit over the last 7 days:" in msg_mock.call_args_list[0][0][0] assert str(datetime.utcnow().date()) in msg_mock.call_args_list[0][0][0] - assert str(' 0.00006217 BTC') in msg_mock.call_args_list[0][0][0] - assert str(' 0.93 USD') in msg_mock.call_args_list[0][0][0] + assert str((datetime.utcnow() - timedelta(days=5)).date()) in msg_mock.call_args_list[0][0][0] + assert str(' 13.83 USDT') in msg_mock.call_args_list[0][0][0] + assert str(' 15.21 USD') in msg_mock.call_args_list[0][0][0] + assert str(' 2 trade') in msg_mock.call_args_list[0][0][0] assert str(' 1 trade') in msg_mock.call_args_list[0][0][0] assert str(' 0 trade') in msg_mock.call_args_list[0][0][0] # Reset msg_mock msg_mock.reset_mock() - freqtradebot.config['max_open_trades'] = 2 - # Add two other trades - n = freqtradebot.enter_positions() - assert n == 2 - - trades = Trade.query.all() - for trade in trades: - trade.update_trade(oobj) - trade.update_trade(oobjs) - trade.close_date = datetime.utcnow() - trade.is_open = False # /daily 1 context = MagicMock() context.args = ["1"] telegram._daily(update=update, context=context) - assert str(' 0.00018651 BTC') in msg_mock.call_args_list[0][0][0] - assert str(' 2.80 USD') in msg_mock.call_args_list[0][0][0] - assert str(' 3 trades') in msg_mock.call_args_list[0][0][0] + assert str(' 13.83 USDT') in msg_mock.call_args_list[0][0][0] + assert str(' 15.21 USD') in msg_mock.call_args_list[0][0][0] + assert str(' 2 trade') in msg_mock.call_args_list[0][0][0] def test_daily_wrong_input(default_conf, update, ticker, mocker) -> None: From 1a5c3c587d4936b9e6978197b2e257a7040ba5bc Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 11 Jun 2022 08:38:30 +0200 Subject: [PATCH 67/95] Simplify weekly/monthly tests, convert to usdt --- tests/rpc/test_rpc.py | 3 +- tests/rpc/test_rpc_telegram.py | 178 +++++++++------------------------ 2 files changed, 47 insertions(+), 134 deletions(-) diff --git a/tests/rpc/test_rpc.py b/tests/rpc/test_rpc.py index e1f40bcd2..da477edf4 100644 --- a/tests/rpc/test_rpc.py +++ b/tests/rpc/test_rpc.py @@ -284,7 +284,7 @@ def test_rpc_status_table(default_conf, ticker, fee, mocker) -> None: assert isnan(fiat_profit_sum) -def test__rpc_timeunit_profit(default_conf, update, ticker, fee, +def test__rpc_timeunit_profit(default_conf, ticker, fee, limit_buy_order, limit_sell_order, markets, mocker) -> None: mocker.patch('freqtrade.rpc.telegram.Telegram', MagicMock()) mocker.patch.multiple( @@ -315,7 +315,6 @@ def test__rpc_timeunit_profit(default_conf, update, ticker, fee, trade.is_open = False # Try valid data - update.message.text = '/daily 2' days = rpc._rpc_timeunit_profit(7, stake_currency, fiat_display_currency) assert len(days['data']) == 7 assert days['stake_currency'] == default_conf['stake_currency'] diff --git a/tests/rpc/test_rpc_telegram.py b/tests/rpc/test_rpc_telegram.py index 3cafb2d7d..404fdd2b0 100644 --- a/tests/rpc/test_rpc_telegram.py +++ b/tests/rpc/test_rpc_telegram.py @@ -430,10 +430,11 @@ def test_daily_handle(default_conf_usdt, update, ticker, fee, mocker) -> None: assert "Daily Profit over the last 2 days:" in msg_mock.call_args_list[0][0][0] assert 'Day ' in msg_mock.call_args_list[0][0][0] assert str(datetime.utcnow().date()) in msg_mock.call_args_list[0][0][0] - assert str(' 13.83 USDT') in msg_mock.call_args_list[0][0][0] - assert str(' 15.21 USD') in msg_mock.call_args_list[0][0][0] - assert str(' 2 trade') in msg_mock.call_args_list[0][0][0] - assert str(' 0 trade') in msg_mock.call_args_list[0][0][0] + assert ' 13.83 USDT' in msg_mock.call_args_list[0][0][0] + assert ' 15.21 USD' in msg_mock.call_args_list[0][0][0] + assert ' 2 trade' in msg_mock.call_args_list[0][0][0] + assert '13.83 USDT 15.21 USD 2 trades' in msg_mock.call_args_list[0][0][0] + assert ' 0 trade' in msg_mock.call_args_list[0][0][0] # Reset msg_mock msg_mock.reset_mock() @@ -443,11 +444,11 @@ def test_daily_handle(default_conf_usdt, update, ticker, fee, mocker) -> None: assert "Daily Profit over the last 7 days:" in msg_mock.call_args_list[0][0][0] assert str(datetime.utcnow().date()) in msg_mock.call_args_list[0][0][0] assert str((datetime.utcnow() - timedelta(days=5)).date()) in msg_mock.call_args_list[0][0][0] - assert str(' 13.83 USDT') in msg_mock.call_args_list[0][0][0] - assert str(' 15.21 USD') in msg_mock.call_args_list[0][0][0] - assert str(' 2 trade') in msg_mock.call_args_list[0][0][0] - assert str(' 1 trade') in msg_mock.call_args_list[0][0][0] - assert str(' 0 trade') in msg_mock.call_args_list[0][0][0] + assert ' 13.83 USDT' in msg_mock.call_args_list[0][0][0] + assert ' 15.21 USD' in msg_mock.call_args_list[0][0][0] + assert ' 2 trade' in msg_mock.call_args_list[0][0][0] + assert ' 1 trade' in msg_mock.call_args_list[0][0][0] + assert ' 0 trade' in msg_mock.call_args_list[0][0][0] # Reset msg_mock msg_mock.reset_mock() @@ -456,9 +457,9 @@ def test_daily_handle(default_conf_usdt, update, ticker, fee, mocker) -> None: context = MagicMock() context.args = ["1"] telegram._daily(update=update, context=context) - assert str(' 13.83 USDT') in msg_mock.call_args_list[0][0][0] - assert str(' 15.21 USD') in msg_mock.call_args_list[0][0][0] - assert str(' 2 trade') in msg_mock.call_args_list[0][0][0] + assert ' 13.83 USDT' in msg_mock.call_args_list[0][0][0] + assert ' 15.21 USD' in msg_mock.call_args_list[0][0][0] + assert ' 2 trade' in msg_mock.call_args_list[0][0][0] def test_daily_wrong_input(default_conf, update, ticker, mocker) -> None: @@ -487,15 +488,14 @@ def test_daily_wrong_input(default_conf, update, ticker, mocker) -> None: context = MagicMock() context.args = ["today"] telegram._daily(update=update, context=context) - assert str('Daily Profit over the last 7 days:') in msg_mock.call_args_list[0][0][0] + assert 'Daily Profit over the last 7 days:' in msg_mock.call_args_list[0][0][0] -def test_weekly_handle(default_conf, update, ticker, limit_buy_order, fee, - limit_sell_order, mocker) -> None: - default_conf['max_open_trades'] = 1 +def test_weekly_handle(default_conf_usdt, update, ticker, fee, mocker) -> None: + default_conf_usdt['max_open_trades'] = 1 mocker.patch( 'freqtrade.rpc.rpc.CryptoToFiatConverter._find_price', - return_value=15000.0 + return_value=1.1 ) mocker.patch.multiple( 'freqtrade.exchange.Exchange', @@ -503,25 +503,9 @@ def test_weekly_handle(default_conf, update, ticker, limit_buy_order, fee, get_fee=fee, ) - telegram, freqtradebot, msg_mock = get_telegram_testobject(mocker, default_conf) + telegram, freqtradebot, msg_mock = get_telegram_testobject(mocker, default_conf_usdt) - patch_get_signal(freqtradebot) - - # Create some test data - freqtradebot.enter_positions() - trade = Trade.query.first() - assert trade - - # Simulate fulfilled LIMIT_BUY order for trade - oobj = Order.parse_from_ccxt_object(limit_buy_order, limit_buy_order['symbol'], 'buy') - trade.update_trade(oobj) - - # Simulate fulfilled LIMIT_SELL order for trade - oobjs = Order.parse_from_ccxt_object(limit_sell_order, limit_sell_order['symbol'], 'sell') - trade.update_trade(oobjs) - - trade.close_date = datetime.utcnow() - trade.is_open = False + create_mock_trades_usdt(fee) # Try valid data # /weekly 2 @@ -535,10 +519,10 @@ def test_weekly_handle(default_conf, update, ticker, limit_buy_order, fee, today = datetime.utcnow().date() first_iso_day_of_current_week = today - timedelta(days=today.weekday()) assert str(first_iso_day_of_current_week) in msg_mock.call_args_list[0][0][0] - assert str(' 0.00006217 BTC') in msg_mock.call_args_list[0][0][0] - assert str(' 0.93 USD') in msg_mock.call_args_list[0][0][0] - assert str(' 1 trade') in msg_mock.call_args_list[0][0][0] - assert str(' 0 trade') in msg_mock.call_args_list[0][0][0] + assert ' 9.83 USDT' in msg_mock.call_args_list[0][0][0] + assert ' 10.81 USD' in msg_mock.call_args_list[0][0][0] + assert ' 3 trade' in msg_mock.call_args_list[0][0][0] + assert ' 0 trade' in msg_mock.call_args_list[0][0][0] # Reset msg_mock msg_mock.reset_mock() @@ -548,44 +532,10 @@ def test_weekly_handle(default_conf, update, ticker, limit_buy_order, fee, assert "Weekly Profit over the last 8 weeks (starting from Monday):" \ in msg_mock.call_args_list[0][0][0] assert 'Weekly' in msg_mock.call_args_list[0][0][0] - assert str(' 0.00006217 BTC') in msg_mock.call_args_list[0][0][0] - assert str(' 0.93 USD') in msg_mock.call_args_list[0][0][0] - assert str(' 1 trade') in msg_mock.call_args_list[0][0][0] - assert str(' 0 trade') in msg_mock.call_args_list[0][0][0] - - # Reset msg_mock - msg_mock.reset_mock() - freqtradebot.config['max_open_trades'] = 2 - # Add two other trades - n = freqtradebot.enter_positions() - assert n == 2 - - trades = Trade.query.all() - for trade in trades: - trade.update_trade(oobj) - trade.update_trade(oobjs) - trade.close_date = datetime.utcnow() - trade.is_open = False - - # /weekly 1 - # By default, the 8 previous weeks are shown - # So the previous modified trade should be excluded from the stats - context = MagicMock() - context.args = ["1"] - telegram._weekly(update=update, context=context) - assert str(' 0.00018651 BTC') in msg_mock.call_args_list[0][0][0] - assert str(' 2.80 USD') in msg_mock.call_args_list[0][0][0] - assert str(' 3 trades') in msg_mock.call_args_list[0][0][0] - - -def test_weekly_wrong_input(default_conf, update, ticker, mocker) -> None: - mocker.patch.multiple( - 'freqtrade.exchange.Exchange', - fetch_ticker=ticker - ) - - telegram, freqtradebot, msg_mock = get_telegram_testobject(mocker, default_conf) - patch_get_signal(freqtradebot) + assert ' 9.83 USDT' in msg_mock.call_args_list[0][0][0] + assert ' 10.81 USD' in msg_mock.call_args_list[0][0][0] + assert ' 3 trade' in msg_mock.call_args_list[0][0][0] + assert ' 0 trade' in msg_mock.call_args_list[0][0][0] # Try invalid data msg_mock.reset_mock() @@ -604,16 +554,17 @@ def test_weekly_wrong_input(default_conf, update, ticker, mocker) -> None: context = MagicMock() context.args = ["this week"] telegram._weekly(update=update, context=context) - assert str('Weekly Profit over the last 8 weeks (starting from Monday):') \ + assert ( + 'Weekly Profit over the last 8 weeks (starting from Monday):' in msg_mock.call_args_list[0][0][0] + ) -def test_monthly_handle(default_conf, update, ticker, limit_buy_order, fee, - limit_sell_order, mocker) -> None: - default_conf['max_open_trades'] = 1 +def test_monthly_handle(default_conf_usdt, update, ticker, fee, mocker) -> None: + default_conf_usdt['max_open_trades'] = 1 mocker.patch( 'freqtrade.rpc.rpc.CryptoToFiatConverter._find_price', - return_value=15000.0 + return_value=1.1 ) mocker.patch.multiple( 'freqtrade.exchange.Exchange', @@ -621,25 +572,9 @@ def test_monthly_handle(default_conf, update, ticker, limit_buy_order, fee, get_fee=fee, ) - telegram, freqtradebot, msg_mock = get_telegram_testobject(mocker, default_conf) + telegram, freqtradebot, msg_mock = get_telegram_testobject(mocker, default_conf_usdt) - patch_get_signal(freqtradebot) - - # Create some test data - freqtradebot.enter_positions() - trade = Trade.query.first() - assert trade - - # Simulate fulfilled LIMIT_BUY order for trade - oobj = Order.parse_from_ccxt_object(limit_buy_order, limit_buy_order['symbol'], 'buy') - trade.update_trade(oobj) - - # Simulate fulfilled LIMIT_SELL order for trade - oobjs = Order.parse_from_ccxt_object(limit_sell_order, limit_sell_order['symbol'], 'sell') - trade.update_trade(oobjs) - - trade.close_date = datetime.utcnow() - trade.is_open = False + create_mock_trades_usdt(fee) # Try valid data # /monthly 2 @@ -652,10 +587,10 @@ def test_monthly_handle(default_conf, update, ticker, limit_buy_order, fee, today = datetime.utcnow().date() current_month = f"{today.year}-{today.month:02} " assert current_month in msg_mock.call_args_list[0][0][0] - assert str(' 0.00006217 BTC') in msg_mock.call_args_list[0][0][0] - assert str(' 0.93 USD') in msg_mock.call_args_list[0][0][0] - assert str(' 1 trade') in msg_mock.call_args_list[0][0][0] - assert str(' 0 trade') in msg_mock.call_args_list[0][0][0] + assert ' 9.83 USDT' in msg_mock.call_args_list[0][0][0] + assert ' 10.81 USD' in msg_mock.call_args_list[0][0][0] + assert ' 3 trade' in msg_mock.call_args_list[0][0][0] + assert ' 0 trade' in msg_mock.call_args_list[0][0][0] # Reset msg_mock msg_mock.reset_mock() @@ -666,24 +601,13 @@ def test_monthly_handle(default_conf, update, ticker, limit_buy_order, fee, assert 'Monthly Profit over the last 6 months:' in msg_mock.call_args_list[0][0][0] assert 'Month ' in msg_mock.call_args_list[0][0][0] assert current_month in msg_mock.call_args_list[0][0][0] - assert str(' 0.00006217 BTC') in msg_mock.call_args_list[0][0][0] - assert str(' 0.93 USD') in msg_mock.call_args_list[0][0][0] - assert str(' 1 trade') in msg_mock.call_args_list[0][0][0] - assert str(' 0 trade') in msg_mock.call_args_list[0][0][0] + assert ' 9.83 USDT' in msg_mock.call_args_list[0][0][0] + assert ' 10.81 USD' in msg_mock.call_args_list[0][0][0] + assert ' 3 trade' in msg_mock.call_args_list[0][0][0] + assert ' 0 trade' in msg_mock.call_args_list[0][0][0] # Reset msg_mock msg_mock.reset_mock() - freqtradebot.config['max_open_trades'] = 2 - # Add two other trades - n = freqtradebot.enter_positions() - assert n == 2 - - trades = Trade.query.all() - for trade in trades: - trade.update_trade(oobj) - trade.update_trade(oobjs) - trade.close_date = datetime.utcnow() - trade.is_open = False # /monthly 12 context = MagicMock() @@ -691,24 +615,14 @@ def test_monthly_handle(default_conf, update, ticker, limit_buy_order, fee, telegram._monthly(update=update, context=context) assert msg_mock.call_count == 1 assert 'Monthly Profit over the last 12 months:' in msg_mock.call_args_list[0][0][0] - assert str(' 0.00018651 BTC') in msg_mock.call_args_list[0][0][0] - assert str(' 2.80 USD') in msg_mock.call_args_list[0][0][0] - assert str(' 3 trades') in msg_mock.call_args_list[0][0][0] + assert ' 9.83 USDT' in msg_mock.call_args_list[0][0][0] + assert ' 10.81 USD' in msg_mock.call_args_list[0][0][0] + assert ' 3 trade' in msg_mock.call_args_list[0][0][0] # The one-digit months should contain a zero, Eg: September 2021 = "2021-09" # Since we loaded the last 12 months, any month should appear assert str('-09') in msg_mock.call_args_list[0][0][0] - -def test_monthly_wrong_input(default_conf, update, ticker, mocker) -> None: - mocker.patch.multiple( - 'freqtrade.exchange.Exchange', - fetch_ticker=ticker - ) - - telegram, freqtradebot, msg_mock = get_telegram_testobject(mocker, default_conf) - patch_get_signal(freqtradebot) - # Try invalid data msg_mock.reset_mock() freqtradebot.state = State.RUNNING From 0a801c022316eb5a944f7690cc191d90a3364939 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 11 Jun 2022 08:58:36 +0200 Subject: [PATCH 68/95] Simplify daily RPC test --- tests/rpc/test_rpc.py | 40 +++++++++++++++------------------------- 1 file changed, 15 insertions(+), 25 deletions(-) diff --git a/tests/rpc/test_rpc.py b/tests/rpc/test_rpc.py index da477edf4..982ac65d7 100644 --- a/tests/rpc/test_rpc.py +++ b/tests/rpc/test_rpc.py @@ -15,7 +15,8 @@ from freqtrade.persistence.models import Order from freqtrade.persistence.pairlock_middleware import PairLocks from freqtrade.rpc import RPC, RPCException from freqtrade.rpc.fiat_convert import CryptoToFiatConverter -from tests.conftest import create_mock_trades, get_patched_freqtradebot, patch_get_signal +from tests.conftest import (create_mock_trades, create_mock_trades_usdt, get_patched_freqtradebot, + patch_get_signal) # Functions for recurrent object patching @@ -284,7 +285,7 @@ def test_rpc_status_table(default_conf, ticker, fee, mocker) -> None: assert isnan(fiat_profit_sum) -def test__rpc_timeunit_profit(default_conf, ticker, fee, +def test__rpc_timeunit_profit(default_conf_usdt, ticker, fee, limit_buy_order, limit_sell_order, markets, mocker) -> None: mocker.patch('freqtrade.rpc.telegram.Telegram', MagicMock()) mocker.patch.multiple( @@ -294,38 +295,27 @@ def test__rpc_timeunit_profit(default_conf, ticker, fee, markets=PropertyMock(return_value=markets) ) - freqtradebot = get_patched_freqtradebot(mocker, default_conf) - patch_get_signal(freqtradebot) - stake_currency = default_conf['stake_currency'] - fiat_display_currency = default_conf['fiat_display_currency'] + freqtradebot = get_patched_freqtradebot(mocker, default_conf_usdt) + create_mock_trades_usdt(fee) + + stake_currency = default_conf_usdt['stake_currency'] + fiat_display_currency = default_conf_usdt['fiat_display_currency'] rpc = RPC(freqtradebot) rpc._fiat_converter = CryptoToFiatConverter() - # Create some test data - freqtradebot.enter_positions() - trade = Trade.query.first() - assert trade - - # Simulate buy & sell - oobj = Order.parse_from_ccxt_object(limit_buy_order, limit_buy_order['symbol'], 'buy') - trade.update_trade(oobj) - oobj = Order.parse_from_ccxt_object(limit_sell_order, limit_sell_order['symbol'], 'sell') - trade.update_trade(oobj) - trade.close_date = datetime.utcnow() - trade.is_open = False # Try valid data days = rpc._rpc_timeunit_profit(7, stake_currency, fiat_display_currency) assert len(days['data']) == 7 - assert days['stake_currency'] == default_conf['stake_currency'] - assert days['fiat_display_currency'] == default_conf['fiat_display_currency'] + assert days['stake_currency'] == default_conf_usdt['stake_currency'] + assert days['fiat_display_currency'] == default_conf_usdt['fiat_display_currency'] for day in days['data']: - # [datetime.date(2018, 1, 11), '0.00000000 BTC', '0.000 USD'] - assert (day['abs_profit'] == 0.0 or - day['abs_profit'] == 0.00006217) + # {'date': datetime.date(2022, 6, 11), 'abs_profit': 13.8299999, + # 'fiat_value': 0.0, 'trade_count': 2} + assert day['abs_profit'] in (0.0, pytest.approx(13.8299999), pytest.approx(-4.0)) + assert day['trade_count'] in (0, 1, 2) - assert (day['fiat_value'] == 0.0 or - day['fiat_value'] == 0.76748865) + assert day['fiat_value'] in (0.0, ) # ensure first day is current date assert str(days['data'][0]['date']) == str(datetime.utcnow().date()) From 76827b31a9c59d0d7344ff379f5ef7f0fc1a56f4 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 11 Jun 2022 11:18:21 +0200 Subject: [PATCH 69/95] Add relative profit to daily/weekly commands --- freqtrade/rpc/rpc.py | 11 +++++++++-- freqtrade/rpc/telegram.py | 12 +++++++----- tests/rpc/test_rpc.py | 4 +++- 3 files changed, 19 insertions(+), 8 deletions(-) diff --git a/freqtrade/rpc/rpc.py b/freqtrade/rpc/rpc.py index 64584382a..da5144dab 100644 --- a/freqtrade/rpc/rpc.py +++ b/freqtrade/rpc/rpc.py @@ -302,11 +302,12 @@ class RPC: return relativedelta(months=step) return timedelta(**{timeunit: step}) - profit_units: Dict[date, Dict] = {} - if not (isinstance(timescale, int) and timescale > 0): raise RPCException('timescale must be an integer greater than 0') + profit_units: Dict[date, Dict] = {} + daily_stake = self._freqtrade.wallets.get_total_stake_amount() + for day in range(0, timescale): profitday = start_date - time_offset(day) # Only query for necessary columns for performance reasons. @@ -318,8 +319,12 @@ class RPC: curdayprofit = sum( trade.close_profit_abs for trade in trades if trade.close_profit_abs is not None) + # Calculate this periods starting balance + daily_stake = daily_stake - curdayprofit profit_units[profitday] = { 'amount': curdayprofit, + 'daily_stake': daily_stake, + 'rel_profit': round(curdayprofit / daily_stake, 8) if daily_stake > 0 else 0, 'trades': len(trades), } @@ -327,6 +332,8 @@ class RPC: { 'date': f"{key.year}-{key.month:02d}" if timeunit == 'months' else key, 'abs_profit': value["amount"], + 'starting_balance': value["daily_stake"], + 'rel_profit': value["rel_profit"], 'fiat_value': self._fiat_converter.convert_amount( value['amount'], stake_currency, diff --git a/freqtrade/rpc/telegram.py b/freqtrade/rpc/telegram.py index 61b73553f..c3e4c1152 100644 --- a/freqtrade/rpc/telegram.py +++ b/freqtrade/rpc/telegram.py @@ -605,14 +605,16 @@ class Telegram(RPCHandler): unit ) stats_tab = tabulate( - [[period['date'], + [[f"{period['date']} ({period['trade_count']})", f"{round_coin_value(period['abs_profit'], stats['stake_currency'])}", f"{period['fiat_value']:.2f} {stats['fiat_display_currency']}", - f"{period['trade_count']} trades"] for period in stats['data']], + f"{period['rel_profit']:.2%}", + ] for period in stats['data']], headers=[ - val.header, - f'Profit {stake_cur}', - f'Profit {fiat_disp_cur}', + f"{val.header} (trades)", + f'Prof {stake_cur}', + f'Prof {fiat_disp_cur}', + 'Profit %', 'Trades', ], tablefmt='simple') diff --git a/tests/rpc/test_rpc.py b/tests/rpc/test_rpc.py index 982ac65d7..0273b8237 100644 --- a/tests/rpc/test_rpc.py +++ b/tests/rpc/test_rpc.py @@ -311,10 +311,12 @@ def test__rpc_timeunit_profit(default_conf_usdt, ticker, fee, assert days['fiat_display_currency'] == default_conf_usdt['fiat_display_currency'] for day in days['data']: # {'date': datetime.date(2022, 6, 11), 'abs_profit': 13.8299999, + # 'starting_balance': 1055.37, 'rel_profit': 0.0131044, # 'fiat_value': 0.0, 'trade_count': 2} assert day['abs_profit'] in (0.0, pytest.approx(13.8299999), pytest.approx(-4.0)) + assert day['rel_profit'] in (0.0, pytest.approx(0.01310441), pytest.approx(-0.00377583)) assert day['trade_count'] in (0, 1, 2) - + assert day['starting_balance'] in (pytest.approx(1059.37), pytest.approx(1055.37)) assert day['fiat_value'] in (0.0, ) # ensure first day is current date assert str(days['data'][0]['date']) == str(datetime.utcnow().date()) From 9ba11f7bcc0481bbc6db6c3faf3a9e25b8a0edd3 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 11 Jun 2022 11:26:49 +0200 Subject: [PATCH 70/95] Update docs and tests for new daily command --- docs/telegram-usage.md | 30 +++++++++++++++--------------- freqtrade/rpc/telegram.py | 6 +++--- tests/rpc/test_rpc_telegram.py | 32 ++++++++++++++++---------------- 3 files changed, 34 insertions(+), 34 deletions(-) diff --git a/docs/telegram-usage.md b/docs/telegram-usage.md index 27f5f91b6..6e21d3689 100644 --- a/docs/telegram-usage.md +++ b/docs/telegram-usage.md @@ -328,11 +328,11 @@ Per default `/daily` will return the 7 last days. The example below if for `/dai > **Daily Profit over the last 3 days:** ``` -Day Profit BTC Profit USD ----------- -------------- ------------ -2018-01-03 0.00224175 BTC 29,142 USD -2018-01-02 0.00033131 BTC 4,307 USD -2018-01-01 0.00269130 BTC 34.986 USD +Day (count) USDT USD Profit % +-------------- ------------ ---------- ---------- +2022-06-11 (1) -0.746 USDT -0.75 USD -0.08% +2022-06-10 (0) 0 USDT 0.00 USD 0.00% +2022-06-09 (5) 20 USDT 20.10 USD 5.00% ``` ### /weekly @@ -342,11 +342,11 @@ from Monday. The example below if for `/weekly 3`: > **Weekly Profit over the last 3 weeks (starting from Monday):** ``` -Monday Profit BTC Profit USD ----------- -------------- ------------ -2018-01-03 0.00224175 BTC 29,142 USD -2017-12-27 0.00033131 BTC 4,307 USD -2017-12-20 0.00269130 BTC 34.986 USD +Monday (count) Profit BTC Profit USD Profit % +------------- -------------- ------------ ---------- +2018-01-03 (5) 0.00224175 BTC 29,142 USD 4.98% +2017-12-27 (1) 0.00033131 BTC 4,307 USD 0.00% +2017-12-20 (4) 0.00269130 BTC 34.986 USD 5.12% ``` ### /monthly @@ -356,11 +356,11 @@ if for `/monthly 3`: > **Monthly Profit over the last 3 months:** ``` -Month Profit BTC Profit USD ----------- -------------- ------------ -2018-01 0.00224175 BTC 29,142 USD -2017-12 0.00033131 BTC 4,307 USD -2017-11 0.00269130 BTC 34.986 USD +Month (count) Profit BTC Profit USD Profit % +------------- -------------- ------------ ---------- +2018-01 (20) 0.00224175 BTC 29,142 USD 4.98% +2017-12 (5) 0.00033131 BTC 4,307 USD 0.00% +2017-11 (10) 0.00269130 BTC 34.986 USD 5.10% ``` ### /whitelist diff --git a/freqtrade/rpc/telegram.py b/freqtrade/rpc/telegram.py index c3e4c1152..2e1d23621 100644 --- a/freqtrade/rpc/telegram.py +++ b/freqtrade/rpc/telegram.py @@ -611,9 +611,9 @@ class Telegram(RPCHandler): f"{period['rel_profit']:.2%}", ] for period in stats['data']], headers=[ - f"{val.header} (trades)", - f'Prof {stake_cur}', - f'Prof {fiat_disp_cur}', + f"{val.header} (count)", + f'{stake_cur}', + f'{fiat_disp_cur}', 'Profit %', 'Trades', ], diff --git a/tests/rpc/test_rpc_telegram.py b/tests/rpc/test_rpc_telegram.py index 404fdd2b0..11a783f3a 100644 --- a/tests/rpc/test_rpc_telegram.py +++ b/tests/rpc/test_rpc_telegram.py @@ -432,9 +432,9 @@ def test_daily_handle(default_conf_usdt, update, ticker, fee, mocker) -> None: assert str(datetime.utcnow().date()) in msg_mock.call_args_list[0][0][0] assert ' 13.83 USDT' in msg_mock.call_args_list[0][0][0] assert ' 15.21 USD' in msg_mock.call_args_list[0][0][0] - assert ' 2 trade' in msg_mock.call_args_list[0][0][0] - assert '13.83 USDT 15.21 USD 2 trades' in msg_mock.call_args_list[0][0][0] - assert ' 0 trade' in msg_mock.call_args_list[0][0][0] + assert '(2)' in msg_mock.call_args_list[0][0][0] + assert '(2) 13.83 USDT 15.21 USD 1.31%' in msg_mock.call_args_list[0][0][0] + assert '(0)' in msg_mock.call_args_list[0][0][0] # Reset msg_mock msg_mock.reset_mock() @@ -446,9 +446,9 @@ def test_daily_handle(default_conf_usdt, update, ticker, fee, mocker) -> None: assert str((datetime.utcnow() - timedelta(days=5)).date()) in msg_mock.call_args_list[0][0][0] assert ' 13.83 USDT' in msg_mock.call_args_list[0][0][0] assert ' 15.21 USD' in msg_mock.call_args_list[0][0][0] - assert ' 2 trade' in msg_mock.call_args_list[0][0][0] - assert ' 1 trade' in msg_mock.call_args_list[0][0][0] - assert ' 0 trade' in msg_mock.call_args_list[0][0][0] + assert '(2)' in msg_mock.call_args_list[0][0][0] + assert '(1)' in msg_mock.call_args_list[0][0][0] + assert '(0)' in msg_mock.call_args_list[0][0][0] # Reset msg_mock msg_mock.reset_mock() @@ -459,7 +459,7 @@ def test_daily_handle(default_conf_usdt, update, ticker, fee, mocker) -> None: telegram._daily(update=update, context=context) assert ' 13.83 USDT' in msg_mock.call_args_list[0][0][0] assert ' 15.21 USD' in msg_mock.call_args_list[0][0][0] - assert ' 2 trade' in msg_mock.call_args_list[0][0][0] + assert '(2)' in msg_mock.call_args_list[0][0][0] def test_daily_wrong_input(default_conf, update, ticker, mocker) -> None: @@ -521,8 +521,8 @@ def test_weekly_handle(default_conf_usdt, update, ticker, fee, mocker) -> None: assert str(first_iso_day_of_current_week) in msg_mock.call_args_list[0][0][0] assert ' 9.83 USDT' in msg_mock.call_args_list[0][0][0] assert ' 10.81 USD' in msg_mock.call_args_list[0][0][0] - assert ' 3 trade' in msg_mock.call_args_list[0][0][0] - assert ' 0 trade' in msg_mock.call_args_list[0][0][0] + assert '(3)' in msg_mock.call_args_list[0][0][0] + assert '(0)' in msg_mock.call_args_list[0][0][0] # Reset msg_mock msg_mock.reset_mock() @@ -534,8 +534,8 @@ def test_weekly_handle(default_conf_usdt, update, ticker, fee, mocker) -> None: assert 'Weekly' in msg_mock.call_args_list[0][0][0] assert ' 9.83 USDT' in msg_mock.call_args_list[0][0][0] assert ' 10.81 USD' in msg_mock.call_args_list[0][0][0] - assert ' 3 trade' in msg_mock.call_args_list[0][0][0] - assert ' 0 trade' in msg_mock.call_args_list[0][0][0] + assert '(3)' in msg_mock.call_args_list[0][0][0] + assert '(0)' in msg_mock.call_args_list[0][0][0] # Try invalid data msg_mock.reset_mock() @@ -589,8 +589,8 @@ def test_monthly_handle(default_conf_usdt, update, ticker, fee, mocker) -> None: assert current_month in msg_mock.call_args_list[0][0][0] assert ' 9.83 USDT' in msg_mock.call_args_list[0][0][0] assert ' 10.81 USD' in msg_mock.call_args_list[0][0][0] - assert ' 3 trade' in msg_mock.call_args_list[0][0][0] - assert ' 0 trade' in msg_mock.call_args_list[0][0][0] + assert '(3)' in msg_mock.call_args_list[0][0][0] + assert '(0)' in msg_mock.call_args_list[0][0][0] # Reset msg_mock msg_mock.reset_mock() @@ -603,8 +603,8 @@ def test_monthly_handle(default_conf_usdt, update, ticker, fee, mocker) -> None: assert current_month in msg_mock.call_args_list[0][0][0] assert ' 9.83 USDT' in msg_mock.call_args_list[0][0][0] assert ' 10.81 USD' in msg_mock.call_args_list[0][0][0] - assert ' 3 trade' in msg_mock.call_args_list[0][0][0] - assert ' 0 trade' in msg_mock.call_args_list[0][0][0] + assert '(3)' in msg_mock.call_args_list[0][0][0] + assert '(0)' in msg_mock.call_args_list[0][0][0] # Reset msg_mock msg_mock.reset_mock() @@ -617,7 +617,7 @@ def test_monthly_handle(default_conf_usdt, update, ticker, fee, mocker) -> None: assert 'Monthly Profit over the last 12 months:' in msg_mock.call_args_list[0][0][0] assert ' 9.83 USDT' in msg_mock.call_args_list[0][0][0] assert ' 10.81 USD' in msg_mock.call_args_list[0][0][0] - assert ' 3 trade' in msg_mock.call_args_list[0][0][0] + assert '(3)' in msg_mock.call_args_list[0][0][0] # The one-digit months should contain a zero, Eg: September 2021 = "2021-09" # Since we loaded the last 12 months, any month should appear From 3a06337601b1ff4ca0609010635fb95b7eee7aa7 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 11 Jun 2022 11:28:45 +0200 Subject: [PATCH 71/95] Update API to provide new values. --- freqtrade/rpc/api_server/api_schemas.py | 2 ++ freqtrade/rpc/api_server/api_v1.py | 3 ++- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/freqtrade/rpc/api_server/api_schemas.py b/freqtrade/rpc/api_server/api_schemas.py index a31c74c2e..11fdc0121 100644 --- a/freqtrade/rpc/api_server/api_schemas.py +++ b/freqtrade/rpc/api_server/api_schemas.py @@ -120,6 +120,8 @@ class Stats(BaseModel): class DailyRecord(BaseModel): date: date abs_profit: float + rel_profit: float + starting_balance: float fiat_value: float trade_count: int diff --git a/freqtrade/rpc/api_server/api_v1.py b/freqtrade/rpc/api_server/api_v1.py index 271e3de1b..225fe66b9 100644 --- a/freqtrade/rpc/api_server/api_v1.py +++ b/freqtrade/rpc/api_server/api_v1.py @@ -36,7 +36,8 @@ logger = logging.getLogger(__name__) # versions 2.xx -> futures/short branch # 2.14: Add entry/exit orders to trade response # 2.15: Add backtest history endpoints -API_VERSION = 2.15 +# 2.16: Additional daily metrics +API_VERSION = 2.16 # Public API, requires no auth. router_public = APIRouter() From f816c15e1eb1452b332aa39bbd5d59b105a5324e Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 11 Jun 2022 12:02:41 +0200 Subject: [PATCH 72/95] Update discord message format --- freqtrade/rpc/discord.py | 91 ++++++++++++++-------------------------- 1 file changed, 31 insertions(+), 60 deletions(-) diff --git a/freqtrade/rpc/discord.py b/freqtrade/rpc/discord.py index 43a8e9a05..41185a090 100644 --- a/freqtrade/rpc/discord.py +++ b/freqtrade/rpc/discord.py @@ -1,61 +1,44 @@ -import json import logging -from typing import Dict, Any - -import requests +from typing import Any, Dict +from freqtrade.constants import DATETIME_PRINT_FORMAT from freqtrade.enums import RPCMessageType -from freqtrade.rpc import RPCHandler, RPC +from freqtrade.rpc import RPC +from freqtrade.rpc.webhook import Webhook -class Discord(RPCHandler): +logger = logging.getLogger(__name__) + + +class Discord(Webhook): def __init__(self, rpc: 'RPC', config: Dict[str, Any]): - super().__init__(rpc, config) - self.logger = logging.getLogger(__name__) + # super().__init__(rpc, config) + self.rpc = rpc + self.config = config self.strategy = config.get('strategy', '') self.timeframe = config.get('timeframe', '') - self.config = config - def send_msg(self, msg: Dict[str, str]) -> None: - self._send_msg(msg) + self._url = self.config['discord']['webhook_url'] + self._format = 'json' + self._retries = 1 + self._retry_delay = 0.1 - def _send_msg(self, msg): + def cleanup(self) -> None: """ - msg = { - 'type': (RPCMessageType.EXIT_FILL if fill - else RPCMessageType.EXIT), - 'trade_id': trade.id, - 'exchange': trade.exchange.capitalize(), - 'pair': trade.pair, - 'leverage': trade.leverage, - 'direction': 'Short' if trade.is_short else 'Long', - 'gain': gain, - 'limit': profit_rate, - 'order_type': order_type, - 'amount': trade.amount, - 'open_rate': trade.open_rate, - 'close_rate': trade.close_rate, - 'current_rate': current_rate, - 'profit_amount': profit_trade, - 'profit_ratio': profit_ratio, - 'buy_tag': trade.enter_tag, - 'enter_tag': trade.enter_tag, - 'sell_reason': trade.exit_reason, # Deprecated - 'exit_reason': trade.exit_reason, - 'open_date': trade.open_date, - 'close_date': trade.close_date or datetime.utcnow(), - 'stake_currency': self.config['stake_currency'], - 'fiat_currency': self.config.get('fiat_display_currency', None), - } + Cleanup pending module resources. + This will do nothing for webhooks, they will simply not be called anymore """ - self.logger.info(f"Sending discord message: {msg}") + pass + + def send_msg(self, msg) -> None: + logger.info(f"Sending discord message: {msg}") # TODO: handle other message types if msg['type'] == RPCMessageType.EXIT_FILL: profit_ratio = msg.get('profit_ratio') - open_date = msg.get('open_date').strftime('%Y-%m-%d %H:%M:%S') + open_date = msg.get('open_date').strftime(DATETIME_PRINT_FORMAT) close_date = msg.get('close_date').strftime( - '%Y-%m-%d %H:%M:%S') if msg.get('close_date') else '' + DATETIME_PRINT_FORMAT) if msg.get('close_date') else '' embeds = [{ 'title': '{} Trade: {}'.format( @@ -63,7 +46,7 @@ class Discord(RPCHandler): msg.get('pair')), 'color': (0x00FF00 if profit_ratio > 0 else 0xFF0000), 'fields': [ - {'name': 'Trade ID', 'value': msg.get('id'), 'inline': True}, + {'name': 'Trade ID', 'value': msg.get('trade_id'), 'inline': True}, {'name': 'Exchange', 'value': msg.get('exchange').capitalize(), 'inline': True}, {'name': 'Pair', 'value': msg.get('pair'), 'inline': True}, {'name': 'Direction', 'value': 'Short' if msg.get( @@ -75,11 +58,10 @@ class Discord(RPCHandler): {'name': 'Open date', 'value': open_date, 'inline': True}, {'name': 'Close date', 'value': close_date, 'inline': True}, {'name': 'Profit', 'value': msg.get('profit_amount'), 'inline': True}, - {'name': 'Profitability', 'value': '{:.2f}%'.format( - profit_ratio * 100), 'inline': True}, + {'name': 'Profitability', 'value': f'{profit_ratio:.2%}', 'inline': True}, {'name': 'Stake currency', 'value': msg.get('stake_currency'), 'inline': True}, - {'name': 'Fiat currency', 'value': msg.get( - 'fiat_display_currency'), 'inline': True}, + {'name': 'Fiat currency', 'value': msg.get('fiat_display_currency'), + 'inline': True}, {'name': 'Buy Tag', 'value': msg.get('enter_tag'), 'inline': True}, {'name': 'Sell Reason', 'value': msg.get('exit_reason'), 'inline': True}, {'name': 'Strategy', 'value': self.strategy, 'inline': True}, @@ -89,20 +71,9 @@ class Discord(RPCHandler): # convert all value in fields to string for discord for embed in embeds: - for field in embed['fields']: + for field in embed['fields']: # type: ignore field['value'] = str(field['value']) # Send the message to discord channel - payload = { - 'embeds': embeds, - } - headers = { - 'Content-Type': 'application/json', - } - try: - requests.post( - self.config['discord']['webhook_url'], - data=json.dumps(payload), - headers=headers) - except Exception as e: - self.logger.error(f"Failed to send discord message: {e}") + payload = {'embeds': embeds} + self._send_msg(payload) From fdfa94bcc31b5fc751873ccfa943dc962a24a030 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 11 Jun 2022 17:30:56 +0200 Subject: [PATCH 73/95] make discord notifications fully configurable. --- docs/assets/discord_notification.png | Bin 0 -> 48861 bytes docs/webhook-config.md | 49 +++++++++++++++++++++++ freqtrade/constants.py | 41 +++++++++++++++++++ freqtrade/rpc/discord.py | 57 +++++++++------------------ 4 files changed, 108 insertions(+), 39 deletions(-) create mode 100644 docs/assets/discord_notification.png diff --git a/docs/assets/discord_notification.png b/docs/assets/discord_notification.png new file mode 100644 index 0000000000000000000000000000000000000000..05a7705d7f599de625de31782344f98d93ccb17b GIT binary patch literal 48861 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 V+b%kpPX|ChQeyI=72gei{tsZlJYoO< literal 0 HcmV?d00001 diff --git a/docs/webhook-config.md b/docs/webhook-config.md index 5f5933b47..3677ebe89 100644 --- a/docs/webhook-config.md +++ b/docs/webhook-config.md @@ -239,3 +239,52 @@ Possible parameters are: The fields in `webhook.webhookstatus` are used for regular status messages (Started / Stopped / ...). Parameters are filled using string.format. The only possible value here is `{status}`. + +## Discord + +A special form of webhooks is available for discord. +You can configure this as follows: + +```json +"discord": { + "enabled": true, + "webhook_url": "https://discord.com/api/webhooks/", + "exit_fill": [ + {"Trade ID": "{trade_id}"}, + {"Exchange": "{exchange}"}, + {"Pair": "{pair}"}, + {"Direction": "{direction}"}, + {"Open rate": "{open_rate}"}, + {"Close rate": "{close_rate}"}, + {"Amount": "{amount}"}, + {"Open date": "{open_date:%Y-%m-%d %H:%M:%S}"}, + {"Close date": "{close_date:%Y-%m-%d %H:%M:%S}"}, + {"Profit": "{profit_amount} {stake_currency}"}, + {"Profitability": "{profit_ratio:.2%}"}, + {"Enter tag": "{enter_tag}"}, + {"Exit Reason": "{exit_reason}"}, + {"Strategy": "{strategy}"}, + {"Timeframe": "{timeframe}"}, + ], + "entry_fill": [ + {"Trade ID": "{trade_id}"}, + {"Exchange": "{exchange}"}, + {"Pair": "{pair}"}, + {"Direction": "{direction}"}, + {"Open rate": "{open_rate}"}, + {"Amount": "{amount}"}, + {"Open date": "{open_date:%Y-%m-%d %H:%M:%S}"}, + {"Enter tag": "{enter_tag}"}, + {"Strategy": "{strategy} {timeframe}"}, + ] +} +``` + + +The above represents the default (`exit_fill` and `entry_fill` are optional and will default to the above configuration) - modifications are obviously possible. + +Available fields correspond to the fields for webhooks and are documented in the corresponding webhook sections. + +The notifications will look as follows by default. + +![discord-notification](assets/discord_notification.png) diff --git a/freqtrade/constants.py b/freqtrade/constants.py index 9fbd70e42..18dbea259 100644 --- a/freqtrade/constants.py +++ b/freqtrade/constants.py @@ -336,6 +336,47 @@ CONF_SCHEMA = { 'webhookstatus': {'type': 'object'}, }, }, + 'discord': { + 'type': 'object', + 'properties': { + 'enabled': {'type': 'boolean'}, + 'webhook_url': {'type': 'string'}, + "exit_fill": { + 'type': 'array', 'items': {'type': 'object'}, + 'default': [ + {"Trade ID": "{trade_id}"}, + {"Exchange": "{exchange}"}, + {"Pair": "{pair}"}, + {"Direction": "{direction}"}, + {"Open rate": "{open_rate}"}, + {"Close rate": "{close_rate}"}, + {"Amount": "{amount}"}, + {"Open date": "{open_date:%Y-%m-%d %H:%M:%S}"}, + {"Close date": "{close_date:%Y-%m-%d %H:%M:%S}"}, + {"Profit": "{profit_amount} {stake_currency}"}, + {"Profitability": "{profit_ratio:.2%}"}, + {"Enter tag": "{enter_tag}"}, + {"Exit Reason": "{exit_reason}"}, + {"Strategy": "{strategy}"}, + {"Timeframe": "{timeframe}"}, + ] + }, + "entry_fill": { + 'type': 'array', 'items': {'type': 'object'}, + 'default': [ + {"Trade ID": "{trade_id}"}, + {"Exchange": "{exchange}"}, + {"Pair": "{pair}"}, + {"Direction": "{direction}"}, + {"Open rate": "{open_rate}"}, + {"Amount": "{amount}"}, + {"Open date": "{open_date:%Y-%m-%d %H:%M:%S}"}, + {"Enter tag": "{enter_tag}"}, + {"Strategy": "{strategy} {timeframe}"}, + ] + }, + } + }, 'api_server': { 'type': 'object', 'properties': { diff --git a/freqtrade/rpc/discord.py b/freqtrade/rpc/discord.py index 41185a090..9509b4f23 100644 --- a/freqtrade/rpc/discord.py +++ b/freqtrade/rpc/discord.py @@ -1,8 +1,7 @@ import logging from typing import Any, Dict -from freqtrade.constants import DATETIME_PRINT_FORMAT -from freqtrade.enums import RPCMessageType +from freqtrade.enums.rpcmessagetype import RPCMessageType from freqtrade.rpc import RPC from freqtrade.rpc.webhook import Webhook @@ -33,46 +32,26 @@ class Discord(Webhook): def send_msg(self, msg) -> None: logger.info(f"Sending discord message: {msg}") - # TODO: handle other message types - if msg['type'] == RPCMessageType.EXIT_FILL: - profit_ratio = msg.get('profit_ratio') - open_date = msg.get('open_date').strftime(DATETIME_PRINT_FORMAT) - close_date = msg.get('close_date').strftime( - DATETIME_PRINT_FORMAT) if msg.get('close_date') else '' + if msg['type'].value in self.config['discord']: + + msg['strategy'] = self.strategy + msg['timeframe'] = self.timeframe + fields = self.config['discord'].get(msg['type'].value) + color = 0x0000FF + if msg['type'] in (RPCMessageType.EXIT, RPCMessageType.EXIT_FILL): + profit_ratio = msg.get('profit_ratio') + color = (0x00FF00 if profit_ratio > 0 else 0xFF0000) embeds = [{ - 'title': '{} Trade: {}'.format( - 'Profit' if profit_ratio > 0 else 'Loss', - msg.get('pair')), - 'color': (0x00FF00 if profit_ratio > 0 else 0xFF0000), - 'fields': [ - {'name': 'Trade ID', 'value': msg.get('trade_id'), 'inline': True}, - {'name': 'Exchange', 'value': msg.get('exchange').capitalize(), 'inline': True}, - {'name': 'Pair', 'value': msg.get('pair'), 'inline': True}, - {'name': 'Direction', 'value': 'Short' if msg.get( - 'is_short') else 'Long', 'inline': True}, - {'name': 'Open rate', 'value': msg.get('open_rate'), 'inline': True}, - {'name': 'Close rate', 'value': msg.get('close_rate'), 'inline': True}, - {'name': 'Amount', 'value': msg.get('amount'), 'inline': True}, - {'name': 'Open order', 'value': msg.get('open_order_id'), 'inline': True}, - {'name': 'Open date', 'value': open_date, 'inline': True}, - {'name': 'Close date', 'value': close_date, 'inline': True}, - {'name': 'Profit', 'value': msg.get('profit_amount'), 'inline': True}, - {'name': 'Profitability', 'value': f'{profit_ratio:.2%}', 'inline': True}, - {'name': 'Stake currency', 'value': msg.get('stake_currency'), 'inline': True}, - {'name': 'Fiat currency', 'value': msg.get('fiat_display_currency'), - 'inline': True}, - {'name': 'Buy Tag', 'value': msg.get('enter_tag'), 'inline': True}, - {'name': 'Sell Reason', 'value': msg.get('exit_reason'), 'inline': True}, - {'name': 'Strategy', 'value': self.strategy, 'inline': True}, - {'name': 'Timeframe', 'value': self.timeframe, 'inline': True}, - ], - }] + 'title': f"Trade: {msg['pair']} {msg['type'].value}", + 'color': color, + 'fields': [], - # convert all value in fields to string for discord - for embed in embeds: - for field in embed['fields']: # type: ignore - field['value'] = str(field['value']) + }] + for f in fields: + for k, v in f.items(): + v = v.format(**msg) + embeds[0]['fields'].append({'name': k, 'value': v, 'inline': True}) # Send the message to discord channel payload = {'embeds': embeds} From 4b70e03daadc8cc3213656c6d8eaa32815096dd1 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 11 Jun 2022 17:45:37 +0200 Subject: [PATCH 74/95] Add some rudimentary tsts for discord webhook integration --- freqtrade/rpc/discord.py | 3 ++- tests/rpc/test_rpc_webhook.py | 41 +++++++++++++++++++++++++++++++++++ 2 files changed, 43 insertions(+), 1 deletion(-) diff --git a/freqtrade/rpc/discord.py b/freqtrade/rpc/discord.py index 9509b4f23..5991f7126 100644 --- a/freqtrade/rpc/discord.py +++ b/freqtrade/rpc/discord.py @@ -51,7 +51,8 @@ class Discord(Webhook): for f in fields: for k, v in f.items(): v = v.format(**msg) - embeds[0]['fields'].append({'name': k, 'value': v, 'inline': True}) + embeds[0]['fields'].append( # type: ignore + {'name': k, 'value': v, 'inline': True}) # Send the message to discord channel payload = {'embeds': embeds} diff --git a/tests/rpc/test_rpc_webhook.py b/tests/rpc/test_rpc_webhook.py index db357f80f..4d65b4966 100644 --- a/tests/rpc/test_rpc_webhook.py +++ b/tests/rpc/test_rpc_webhook.py @@ -1,5 +1,6 @@ # pragma pylint: disable=missing-docstring, C0103, protected-access +from datetime import datetime, timedelta from unittest.mock import MagicMock import pytest @@ -7,6 +8,7 @@ from requests import RequestException from freqtrade.enums import ExitType, RPCMessageType from freqtrade.rpc import RPC +from freqtrade.rpc.discord import Discord from freqtrade.rpc.webhook import Webhook from tests.conftest import get_patched_freqtradebot, log_has @@ -406,3 +408,42 @@ def test__send_msg_with_raw_format(default_conf, mocker, caplog): webhook._send_msg(msg) assert post.call_args[1] == {'data': msg['data'], 'headers': {'Content-Type': 'text/plain'}} + + +def test_send_msg_discord(default_conf, mocker): + + default_conf["discord"] = { + 'enabled': True, + 'webhook_url': "https://webhookurl..." + } + msg_mock = MagicMock() + mocker.patch("freqtrade.rpc.webhook.Webhook._send_msg", msg_mock) + discord = Discord(RPC(get_patched_freqtradebot(mocker, default_conf)), default_conf) + + msg = { + 'type': RPCMessageType.EXIT_FILL, + 'trade_id': 1, + 'exchange': 'Binance', + 'pair': 'ETH/BTC', + 'direction': 'Long', + 'gain': "profit", + 'close_rate': 0.005, + 'amount': 0.8, + 'order_type': 'limit', + 'open_date': datetime.now() - timedelta(days=1), + 'close_date': datetime.now(), + 'open_rate': 0.004, + 'current_rate': 0.005, + 'profit_amount': 0.001, + 'profit_ratio': 0.20, + 'stake_currency': 'BTC', + 'enter_tag': 'enter_tagggg', + 'exit_reason': ExitType.STOP_LOSS.value, + } + discord.send_msg(msg=msg) + + assert msg_mock.call_count == 1 + assert 'embeds' in msg_mock.call_args_list[0][0][0] + assert 'title' in msg_mock.call_args_list[0][0][0]['embeds'][0] + assert 'color' in msg_mock.call_args_list[0][0][0]['embeds'][0] + assert 'fields' in msg_mock.call_args_list[0][0][0]['embeds'][0] From c9761f47361203eafbb08f9a5413e88e0e80159b Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 11 Jun 2022 18:02:03 +0200 Subject: [PATCH 75/95] FreqUI should be installed by default when running setup.sh --- setup.sh | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/setup.sh b/setup.sh index bb51c3a2f..202cb70c7 100755 --- a/setup.sh +++ b/setup.sh @@ -87,6 +87,10 @@ function updateenv() { echo "Failed installing Freqtrade" exit 1 fi + + echo "Installing freqUI" + freqtrade install-ui + echo "pip install completed" echo if [[ $dev =~ ^[Yy]$ ]]; then From 56652c2b391fa1714bf706ed156df72910b7dad5 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 12 Jun 2022 17:09:47 +0200 Subject: [PATCH 76/95] Improve test resiliance --- tests/test_freqtradebot.py | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/tests/test_freqtradebot.py b/tests/test_freqtradebot.py index cd7459cbe..7f9bc6a46 100644 --- a/tests/test_freqtradebot.py +++ b/tests/test_freqtradebot.py @@ -210,13 +210,14 @@ def test_edge_overrides_stoploss(limit_order, fee, caplog, mocker, # # mocking the ticker: price is falling ... enter_price = limit_order['buy']['price'] + ticker_val = { + 'bid': enter_price, + 'ask': enter_price, + 'last': enter_price, + } mocker.patch.multiple( 'freqtrade.exchange.Exchange', - fetch_ticker=MagicMock(return_value={ - 'bid': enter_price * buy_price_mult, - 'ask': enter_price * buy_price_mult, - 'last': enter_price * buy_price_mult, - }), + fetch_ticker=MagicMock(return_value=ticker_val), get_fee=fee, ) ############################################# @@ -229,9 +230,12 @@ def test_edge_overrides_stoploss(limit_order, fee, caplog, mocker, freqtrade.enter_positions() trade = Trade.query.first() caplog.clear() - oobj = Order.parse_from_ccxt_object(limit_order['buy'], 'ADA/USDT', 'buy') - trade.update_trade(oobj) ############################################# + ticker_val.update({ + 'bid': enter_price * buy_price_mult, + 'ask': enter_price * buy_price_mult, + 'last': enter_price * buy_price_mult, + }) # stoploss shoud be hit assert freqtrade.handle_trade(trade) is not ignore_strat_sl From dff83ef62045c2b006702d5bc855cc9051a3bc80 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 12 Jun 2022 17:30:01 +0200 Subject: [PATCH 77/95] Update telegram profit test to USDT --- tests/rpc/test_rpc_telegram.py | 30 +++++++++++++----------------- 1 file changed, 13 insertions(+), 17 deletions(-) diff --git a/tests/rpc/test_rpc_telegram.py b/tests/rpc/test_rpc_telegram.py index 11a783f3a..355a8b078 100644 --- a/tests/rpc/test_rpc_telegram.py +++ b/tests/rpc/test_rpc_telegram.py @@ -643,16 +643,16 @@ def test_monthly_handle(default_conf_usdt, update, ticker, fee, mocker) -> None: assert str('Monthly Profit over the last 6 months:') in msg_mock.call_args_list[0][0][0] -def test_profit_handle(default_conf, update, ticker, ticker_sell_up, fee, - limit_buy_order, limit_sell_order, mocker) -> None: - mocker.patch('freqtrade.rpc.rpc.CryptoToFiatConverter._find_price', return_value=15000.0) +def test_profit_handle(default_conf_usdt, update, ticker_usdt, ticker_sell_up, fee, + limit_sell_order_usdt, mocker) -> None: + mocker.patch('freqtrade.rpc.rpc.CryptoToFiatConverter._find_price', return_value=1.1) mocker.patch.multiple( 'freqtrade.exchange.Exchange', - fetch_ticker=ticker, + fetch_ticker=ticker_usdt, get_fee=fee, ) - telegram, freqtradebot, msg_mock = get_telegram_testobject(mocker, default_conf) + telegram, freqtradebot, msg_mock = get_telegram_testobject(mocker, default_conf_usdt) patch_get_signal(freqtradebot) telegram._profit(update=update, context=MagicMock()) @@ -664,10 +664,6 @@ def test_profit_handle(default_conf, update, ticker, ticker_sell_up, fee, freqtradebot.enter_positions() trade = Trade.query.first() - # Simulate fulfilled LIMIT_BUY order for trade - oobj = Order.parse_from_ccxt_object(limit_buy_order, limit_buy_order['symbol'], 'buy') - trade.update_trade(oobj) - context = MagicMock() # Test with invalid 2nd argument (should silently pass) context.args = ["aaa"] @@ -675,15 +671,15 @@ def test_profit_handle(default_conf, update, ticker, ticker_sell_up, fee, assert msg_mock.call_count == 1 assert 'No closed trade' in msg_mock.call_args_list[-1][0][0] assert '*ROI:* All trades' in msg_mock.call_args_list[-1][0][0] - mocker.patch('freqtrade.wallets.Wallets.get_starting_balance', return_value=0.01) - assert ('∙ `-0.000005 BTC (-0.50%) (-0.0 \N{GREEK CAPITAL LETTER SIGMA}%)`' + mocker.patch('freqtrade.wallets.Wallets.get_starting_balance', return_value=1000) + assert ('∙ `0.298 USDT (0.50%) (0.03 \N{GREEK CAPITAL LETTER SIGMA}%)`' in msg_mock.call_args_list[-1][0][0]) msg_mock.reset_mock() # Update the ticker with a market going up mocker.patch('freqtrade.exchange.Exchange.fetch_ticker', ticker_sell_up) # Simulate fulfilled LIMIT_SELL order for trade - oobj = Order.parse_from_ccxt_object(limit_sell_order, limit_sell_order['symbol'], 'sell') + oobj = Order.parse_from_ccxt_object(limit_sell_order_usdt, limit_sell_order_usdt['symbol'], 'sell') trade.update_trade(oobj) trade.close_date = datetime.now(timezone.utc) @@ -694,15 +690,15 @@ def test_profit_handle(default_conf, update, ticker, ticker_sell_up, fee, telegram._profit(update=update, context=context) assert msg_mock.call_count == 1 assert '*ROI:* Closed trades' in msg_mock.call_args_list[-1][0][0] - assert ('∙ `0.00006217 BTC (6.20%) (0.62 \N{GREEK CAPITAL LETTER SIGMA}%)`' + assert ('∙ `5.685 USDT (9.45%) (0.57 \N{GREEK CAPITAL LETTER SIGMA}%)`' in msg_mock.call_args_list[-1][0][0]) - assert '∙ `0.933 USD`' in msg_mock.call_args_list[-1][0][0] + assert '∙ `6.253 USD`' in msg_mock.call_args_list[-1][0][0] assert '*ROI:* All trades' in msg_mock.call_args_list[-1][0][0] - assert ('∙ `0.00006217 BTC (6.20%) (0.62 \N{GREEK CAPITAL LETTER SIGMA}%)`' + assert ('∙ `5.685 USDT (9.45%) (0.57 \N{GREEK CAPITAL LETTER SIGMA}%)`' in msg_mock.call_args_list[-1][0][0]) - assert '∙ `0.933 USD`' in msg_mock.call_args_list[-1][0][0] + assert '∙ `6.253 USD`' in msg_mock.call_args_list[-1][0][0] - assert '*Best Performing:* `ETH/BTC: 6.20%`' in msg_mock.call_args_list[-1][0][0] + assert '*Best Performing:* `ETH/USDT: 9.45%`' in msg_mock.call_args_list[-1][0][0] @pytest.mark.parametrize('is_short', [True, False]) From 7619fd08d65e4cafee6e5a9f227987392dfe8fe2 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 12 Jun 2022 19:31:32 +0200 Subject: [PATCH 78/95] Update telegram tests to use mock_trades --- tests/conftest_trades_usdt.py | 6 +- tests/rpc/test_rpc_telegram.py | 102 ++++++++------------------------- 2 files changed, 27 insertions(+), 81 deletions(-) diff --git a/tests/conftest_trades_usdt.py b/tests/conftest_trades_usdt.py index 6f83bb8be..cc1b1a206 100644 --- a/tests/conftest_trades_usdt.py +++ b/tests/conftest_trades_usdt.py @@ -95,13 +95,14 @@ def mock_trade_usdt_2(fee, is_short: bool): fee_close=fee.return_value, open_rate=2.0, close_rate=2.05, - close_profit=5.0, + close_profit=0.05, close_profit_abs=3.9875, exchange='binance', is_open=False, open_order_id=f'12366_{direc(is_short)}', strategy='StrategyTestV2', timeframe=5, + enter_tag='TEST1', exit_reason='exit_signal', open_date=datetime.now(tz=timezone.utc) - timedelta(minutes=20), close_date=datetime.now(tz=timezone.utc) - timedelta(minutes=2), @@ -157,12 +158,13 @@ def mock_trade_usdt_3(fee, is_short: bool): fee_close=fee.return_value, open_rate=1.0, close_rate=1.1, - close_profit=10.0, + close_profit=0.1, close_profit_abs=9.8425, exchange='binance', is_open=False, strategy='StrategyTestV2', timeframe=5, + enter_tag='TEST3', exit_reason='roi', open_date=datetime.now(tz=timezone.utc) - timedelta(minutes=20), close_date=datetime.now(tz=timezone.utc), diff --git a/tests/rpc/test_rpc_telegram.py b/tests/rpc/test_rpc_telegram.py index 355a8b078..48acda47e 100644 --- a/tests/rpc/test_rpc_telegram.py +++ b/tests/rpc/test_rpc_telegram.py @@ -679,7 +679,8 @@ def test_profit_handle(default_conf_usdt, update, ticker_usdt, ticker_sell_up, f # Update the ticker with a market going up mocker.patch('freqtrade.exchange.Exchange.fetch_ticker', ticker_sell_up) # Simulate fulfilled LIMIT_SELL order for trade - oobj = Order.parse_from_ccxt_object(limit_sell_order_usdt, limit_sell_order_usdt['symbol'], 'sell') + oobj = Order.parse_from_ccxt_object( + limit_sell_order_usdt, limit_sell_order_usdt['symbol'], 'sell') trade.update_trade(oobj) trade.close_date = datetime.now(timezone.utc) @@ -1235,71 +1236,43 @@ def test_force_enter_no_pair(default_conf, update, mocker) -> None: assert fbuy_mock.call_count == 1 -def test_telegram_performance_handle(default_conf, update, ticker, fee, - limit_buy_order, limit_sell_order, mocker) -> None: +def test_telegram_performance_handle(default_conf_usdt, update, ticker, fee, mocker) -> None: mocker.patch.multiple( 'freqtrade.exchange.Exchange', fetch_ticker=ticker, get_fee=fee, ) - telegram, freqtradebot, msg_mock = get_telegram_testobject(mocker, default_conf) - patch_get_signal(freqtradebot) + telegram, freqtradebot, msg_mock = get_telegram_testobject(mocker, default_conf_usdt) # Create some test data - freqtradebot.enter_positions() - trade = Trade.query.first() - assert trade + create_mock_trades_usdt(fee) - # Simulate fulfilled LIMIT_BUY order for trade - oobj = Order.parse_from_ccxt_object(limit_buy_order, limit_buy_order['symbol'], 'buy') - trade.update_trade(oobj) - - # Simulate fulfilled LIMIT_SELL order for trade - oobj = Order.parse_from_ccxt_object(limit_sell_order, limit_sell_order['symbol'], 'sell') - trade.update_trade(oobj) - - trade.close_date = datetime.utcnow() - trade.is_open = False telegram._performance(update=update, context=MagicMock()) assert msg_mock.call_count == 1 assert 'Performance' in msg_mock.call_args_list[0][0][0] - assert 'ETH/BTC\t0.00006217 BTC (6.20%) (1)' in msg_mock.call_args_list[0][0][0] + assert 'XRP/USDT\t9.842 USDT (10.00%) (1)' in msg_mock.call_args_list[0][0][0] def test_telegram_entry_tag_performance_handle( - default_conf, update, ticker, fee, limit_buy_order, limit_sell_order, mocker) -> None: + default_conf_usdt, update, ticker, fee, mocker) -> None: mocker.patch.multiple( 'freqtrade.exchange.Exchange', fetch_ticker=ticker, get_fee=fee, ) - telegram, freqtradebot, msg_mock = get_telegram_testobject(mocker, default_conf) + telegram, freqtradebot, msg_mock = get_telegram_testobject(mocker, default_conf_usdt) patch_get_signal(freqtradebot) - # Create some test data - freqtradebot.enter_positions() - trade = Trade.query.first() - assert trade + create_mock_trades_usdt(fee) - # Simulate fulfilled LIMIT_BUY order for trade - oobj = Order.parse_from_ccxt_object(limit_buy_order, limit_buy_order['symbol'], 'buy') - trade.update_trade(oobj) - - trade.enter_tag = "TESTBUY" - # Simulate fulfilled LIMIT_SELL order for trade - oobj = Order.parse_from_ccxt_object(limit_sell_order, limit_sell_order['symbol'], 'sell') - trade.update_trade(oobj) - - trade.close_date = datetime.utcnow() - trade.is_open = False context = MagicMock() telegram._enter_tag_performance(update=update, context=context) assert msg_mock.call_count == 1 assert 'Entry Tag Performance' in msg_mock.call_args_list[0][0][0] - assert 'TESTBUY\t0.00006217 BTC (6.20%) (1)' in msg_mock.call_args_list[0][0][0] + assert 'TEST1\t3.987 USDT (5.00%) (1)' in msg_mock.call_args_list[0][0][0] - context.args = [trade.pair] + context.args = ['XRP/USDT'] telegram._enter_tag_performance(update=update, context=context) assert msg_mock.call_count == 2 @@ -1312,37 +1285,24 @@ def test_telegram_entry_tag_performance_handle( assert "Error" in msg_mock.call_args_list[0][0][0] -def test_telegram_exit_reason_performance_handle(default_conf, update, ticker, fee, - limit_buy_order, limit_sell_order, mocker) -> None: +def test_telegram_exit_reason_performance_handle(default_conf_usdt, update, ticker, fee, + mocker) -> None: mocker.patch.multiple( 'freqtrade.exchange.Exchange', fetch_ticker=ticker, get_fee=fee, ) - telegram, freqtradebot, msg_mock = get_telegram_testobject(mocker, default_conf) + telegram, freqtradebot, msg_mock = get_telegram_testobject(mocker, default_conf_usdt) patch_get_signal(freqtradebot) - # Create some test data - freqtradebot.enter_positions() - trade = Trade.query.first() - assert trade - trade.exit_reason = 'TESTSELL' - # Simulate fulfilled LIMIT_BUY order for trade - oobj = Order.parse_from_ccxt_object(limit_buy_order, limit_buy_order['symbol'], 'buy') - trade.update_trade(oobj) + create_mock_trades_usdt(fee) - # Simulate fulfilled LIMIT_SELL order for trade - oobj = Order.parse_from_ccxt_object(limit_sell_order, limit_sell_order['symbol'], 'sell') - trade.update_trade(oobj) - - trade.close_date = datetime.utcnow() - trade.is_open = False context = MagicMock() telegram._exit_reason_performance(update=update, context=context) assert msg_mock.call_count == 1 assert 'Exit Reason Performance' in msg_mock.call_args_list[0][0][0] - assert 'TESTSELL\t0.00006217 BTC (6.20%) (1)' in msg_mock.call_args_list[0][0][0] - context.args = [trade.pair] + assert 'roi\t9.842 USDT (10.00%) (1)' in msg_mock.call_args_list[0][0][0] + context.args = ['XRP/USDT'] telegram._exit_reason_performance(update=update, context=context) assert msg_mock.call_count == 2 @@ -1356,43 +1316,27 @@ def test_telegram_exit_reason_performance_handle(default_conf, update, ticker, f assert "Error" in msg_mock.call_args_list[0][0][0] -def test_telegram_mix_tag_performance_handle(default_conf, update, ticker, fee, - limit_buy_order, limit_sell_order, mocker) -> None: +def test_telegram_mix_tag_performance_handle(default_conf_usdt, update, ticker, fee, + mocker) -> None: mocker.patch.multiple( 'freqtrade.exchange.Exchange', fetch_ticker=ticker, get_fee=fee, ) - telegram, freqtradebot, msg_mock = get_telegram_testobject(mocker, default_conf) + telegram, freqtradebot, msg_mock = get_telegram_testobject(mocker, default_conf_usdt) patch_get_signal(freqtradebot) # Create some test data - freqtradebot.enter_positions() - trade = Trade.query.first() - assert trade - - trade.enter_tag = "TESTBUY" - trade.exit_reason = "TESTSELL" - - # Simulate fulfilled LIMIT_BUY order for trade - oobj = Order.parse_from_ccxt_object(limit_buy_order, limit_buy_order['symbol'], 'buy') - trade.update_trade(oobj) - - # Simulate fulfilled LIMIT_SELL order for trade - oobj = Order.parse_from_ccxt_object(limit_sell_order, limit_sell_order['symbol'], 'sell') - trade.update_trade(oobj) - - trade.close_date = datetime.utcnow() - trade.is_open = False + create_mock_trades_usdt(fee) context = MagicMock() telegram._mix_tag_performance(update=update, context=context) assert msg_mock.call_count == 1 assert 'Mix Tag Performance' in msg_mock.call_args_list[0][0][0] - assert ('TESTBUY TESTSELL\t0.00006217 BTC (6.20%) (1)' + assert ('TEST3 roi\t9.842 USDT (10.00%) (1)' in msg_mock.call_args_list[0][0][0]) - context.args = [trade.pair] + context.args = ['XRP/USDT'] telegram._mix_tag_performance(update=update, context=context) assert msg_mock.call_count == 2 From 40c7caac16279c9d1e34ef50fe2fc8178b01d886 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 13 Jun 2022 03:01:53 +0000 Subject: [PATCH 79/95] Bump types-filelock from 3.2.6 to 3.2.7 Bumps [types-filelock](https://github.com/python/typeshed) from 3.2.6 to 3.2.7. - [Release notes](https://github.com/python/typeshed/releases) - [Commits](https://github.com/python/typeshed/commits) --- updated-dependencies: - dependency-name: types-filelock dependency-type: direct:development update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- requirements-dev.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements-dev.txt b/requirements-dev.txt index 4eb157aae..e7d64a2b6 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -23,7 +23,7 @@ nbconvert==6.5.0 # mypy types types-cachetools==5.0.1 -types-filelock==3.2.6 +types-filelock==3.2.7 types-requests==2.27.30 types-tabulate==0.8.9 types-python-dateutil==2.8.17 From 390e600f55cfe868bee74d9d74fc03e323575359 Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 13 Jun 2022 06:46:34 +0200 Subject: [PATCH 80/95] Update statistics output --- tests/conftest_trades_usdt.py | 104 +++++++++++++++++----------------- tests/rpc/test_rpc.py | 87 +++++++++------------------- 2 files changed, 78 insertions(+), 113 deletions(-) diff --git a/tests/conftest_trades_usdt.py b/tests/conftest_trades_usdt.py index cc1b1a206..41d705c01 100644 --- a/tests/conftest_trades_usdt.py +++ b/tests/conftest_trades_usdt.py @@ -20,36 +20,60 @@ def direc(is_short: bool): def mock_order_usdt_1(is_short: bool): return { - 'id': f'1234_{direc(is_short)}', - 'symbol': 'ADA/USDT', + 'id': f'prod_entry_1_{direc(is_short)}', + 'symbol': 'LTC/USDT', 'status': 'closed', 'side': entry_side(is_short), 'type': 'limit', - 'price': 2.0, - 'amount': 10.0, - 'filled': 10.0, + 'price': 10.0, + 'amount': 2.0, + 'filled': 2.0, + 'remaining': 0.0, + } + + +def mock_order_usdt_1_exit(is_short: bool): + return { + 'id': f'prod_exit_1_{direc(is_short)}', + 'symbol': 'LTC/USDT', + 'status': 'closed', + 'side': exit_side(is_short), + 'type': 'limit', + 'price': 8.0, + 'amount': 2.0, + 'filled': 2.0, 'remaining': 0.0, } def mock_trade_usdt_1(fee, is_short: bool): + """ + Simulate prod entry with open sell order + """ trade = Trade( - pair='ADA/USDT', + pair='LTC/USDT', stake_amount=20.0, - amount=10.0, - amount_requested=10.0, + amount=2.0, + amount_requested=2.0, + open_date=datetime.now(tz=timezone.utc) - timedelta(days=2, minutes=20), + close_date=datetime.now(tz=timezone.utc) - timedelta(days=2, minutes=5), fee_open=fee.return_value, fee_close=fee.return_value, - is_open=True, - open_date=datetime.now(tz=timezone.utc) - timedelta(minutes=17), - open_rate=2.0, + is_open=False, + open_rate=10.0, + close_rate=8.0, + close_profit=-0.2, + close_profit_abs=-4.0, exchange='binance', - open_order_id=f'1234_{direc(is_short)}', - strategy='StrategyTestV2', + strategy='SampleStrategy', + open_order_id=f'prod_exit_1_{direc(is_short)}', timeframe=5, is_short=is_short, ) - o = Order.parse_from_ccxt_object(mock_order_usdt_1(is_short), 'ADA/USDT', entry_side(is_short)) + o = Order.parse_from_ccxt_object(mock_order_usdt_1(is_short), 'LTC/USDT', entry_side(is_short)) + trade.orders.append(o) + o = Order.parse_from_ccxt_object(mock_order_usdt_1_exit(is_short), + 'LTC/USDT', exit_side(is_short)) trade.orders.append(o) return trade @@ -330,59 +354,35 @@ def mock_trade_usdt_6(fee, is_short: bool): def mock_order_usdt_7(is_short: bool): return { - 'id': f'prod_entry_7_{direc(is_short)}', - 'symbol': 'LTC/USDT', + 'id': f'1234_{direc(is_short)}', + 'symbol': 'ADA/USDT', 'status': 'closed', 'side': entry_side(is_short), 'type': 'limit', - 'price': 10.0, - 'amount': 2.0, - 'filled': 2.0, - 'remaining': 0.0, - } - - -def mock_order_usdt_7_exit(is_short: bool): - return { - 'id': f'prod_exit_7_{direc(is_short)}', - 'symbol': 'LTC/USDT', - 'status': 'closed', - 'side': exit_side(is_short), - 'type': 'limit', - 'price': 8.0, - 'amount': 2.0, - 'filled': 2.0, + 'price': 2.0, + 'amount': 10.0, + 'filled': 10.0, 'remaining': 0.0, } def mock_trade_usdt_7(fee, is_short: bool): - """ - Simulate prod entry with open sell order - """ trade = Trade( - pair='LTC/USDT', + pair='ADA/USDT', stake_amount=20.0, - amount=2.0, - amount_requested=2.0, - open_date=datetime.now(tz=timezone.utc) - timedelta(days=2, minutes=20), - close_date=datetime.now(tz=timezone.utc) - timedelta(days=2, minutes=5), + amount=10.0, + amount_requested=10.0, fee_open=fee.return_value, fee_close=fee.return_value, - is_open=False, - open_rate=10.0, - close_rate=8.0, - close_profit=-0.2, - close_profit_abs=-4.0, + is_open=True, + open_date=datetime.now(tz=timezone.utc) - timedelta(minutes=17), + open_rate=2.0, exchange='binance', - strategy='SampleStrategy', - open_order_id=f'prod_exit_7_{direc(is_short)}', + open_order_id=f'1234_{direc(is_short)}', + strategy='StrategyTestV2', timeframe=5, is_short=is_short, ) - o = Order.parse_from_ccxt_object(mock_order_usdt_7(is_short), 'LTC/USDT', entry_side(is_short)) - trade.orders.append(o) - o = Order.parse_from_ccxt_object(mock_order_usdt_7_exit(is_short), - 'LTC/USDT', exit_side(is_short)) + o = Order.parse_from_ccxt_object(mock_order_usdt_7(is_short), 'ADA/USDT', entry_side(is_short)) trade.orders.append(o) return trade diff --git a/tests/rpc/test_rpc.py b/tests/rpc/test_rpc.py index 0273b8237..339a6382f 100644 --- a/tests/rpc/test_rpc.py +++ b/tests/rpc/test_rpc.py @@ -407,13 +407,9 @@ def test_rpc_delete_trade(mocker, default_conf, fee, markets, caplog, is_short): assert stoploss_mock.call_count == 0 -def test_rpc_trade_statistics(default_conf, ticker, ticker_sell_up, fee, - limit_buy_order, limit_sell_order, mocker) -> None: - mocker.patch.multiple( - 'freqtrade.rpc.fiat_convert.CoinGeckoAPI', - get_price=MagicMock(return_value={'bitcoin': {'usd': 15000.0}}), - ) - mocker.patch('freqtrade.rpc.rpc.CryptoToFiatConverter._find_price', return_value=15000.0) +def test_rpc_trade_statistics11(default_conf_usdt, ticker, fee, + mocker) -> None: + mocker.patch('freqtrade.rpc.rpc.CryptoToFiatConverter._find_price', return_value=1.1) mocker.patch('freqtrade.rpc.telegram.Telegram', MagicMock()) mocker.patch.multiple( 'freqtrade.exchange.Exchange', @@ -421,10 +417,9 @@ def test_rpc_trade_statistics(default_conf, ticker, ticker_sell_up, fee, get_fee=fee, ) - freqtradebot = get_patched_freqtradebot(mocker, default_conf) - patch_get_signal(freqtradebot) - stake_currency = default_conf['stake_currency'] - fiat_display_currency = default_conf['fiat_display_currency'] + freqtradebot = get_patched_freqtradebot(mocker, default_conf_usdt) + stake_currency = default_conf_usdt['stake_currency'] + fiat_display_currency = default_conf_usdt['fiat_display_currency'] rpc = RPC(freqtradebot) rpc._fiat_converter = CryptoToFiatConverter() @@ -437,62 +432,32 @@ def test_rpc_trade_statistics(default_conf, ticker, ticker_sell_up, fee, assert res['latest_trade_timestamp'] == 0 # Create some test data - freqtradebot.enter_positions() - trade = Trade.query.first() - # Simulate fulfilled LIMIT_BUY order for trade - oobj = Order.parse_from_ccxt_object(limit_buy_order, limit_buy_order['symbol'], 'sell') - trade.update_trade(oobj) - - # Update the ticker with a market going up - mocker.patch.multiple( - 'freqtrade.exchange.Exchange', - fetch_ticker=ticker_sell_up - ) - oobj = Order.parse_from_ccxt_object(limit_sell_order, limit_sell_order['symbol'], 'sell') - trade.update_trade(oobj) - trade.close_date = datetime.utcnow() - trade.is_open = False - - freqtradebot.enter_positions() - trade = Trade.query.first() - # Simulate fulfilled LIMIT_BUY order for trade - oobj = Order.parse_from_ccxt_object(limit_buy_order, limit_buy_order['symbol'], 'buy') - trade.update_trade(oobj) - - # Update the ticker with a market going up - mocker.patch.multiple( - 'freqtrade.exchange.Exchange', - fetch_ticker=ticker_sell_up - ) - oobj = Order.parse_from_ccxt_object(limit_sell_order, limit_sell_order['symbol'], 'sell') - trade.update_trade(oobj) - trade.close_date = datetime.utcnow() - trade.is_open = False + create_mock_trades_usdt(fee) stats = rpc._rpc_trade_statistics(stake_currency, fiat_display_currency) - assert prec_satoshi(stats['profit_closed_coin'], 6.217e-05) - assert prec_satoshi(stats['profit_closed_percent_mean'], 6.2) - assert prec_satoshi(stats['profit_closed_fiat'], 0.93255) - assert prec_satoshi(stats['profit_all_coin'], 5.802e-05) - assert prec_satoshi(stats['profit_all_percent_mean'], 2.89) - assert prec_satoshi(stats['profit_all_fiat'], 0.8703) - assert stats['trade_count'] == 2 - assert stats['first_trade_date'] == 'just now' - assert stats['latest_trade_date'] == 'just now' - assert stats['avg_duration'] in ('0:00:00', '0:00:01', '0:00:02') - assert stats['best_pair'] == 'ETH/BTC' - assert prec_satoshi(stats['best_rate'], 6.2) + assert pytest.approx(stats['profit_closed_coin']) == 9.83 + assert pytest.approx(stats['profit_closed_percent_mean']) == -1.67 + assert pytest.approx(stats['profit_closed_fiat']) == 10.813 + assert pytest.approx(stats['profit_all_coin']) == -77.45964918 + assert pytest.approx(stats['profit_all_percent_mean']) == -57.86 + assert pytest.approx(stats['profit_all_fiat']) == -85.205614098 + assert stats['trade_count'] == 7 + assert stats['first_trade_date'] == '2 days ago' + assert stats['latest_trade_date'] == '17 minutes ago' + assert stats['avg_duration'] in ('0:17:40') + assert stats['best_pair'] == 'XRP/USDT' + assert stats['best_rate'] == 10.0 # Test non-available pair mocker.patch('freqtrade.exchange.Exchange.get_rate', - MagicMock(side_effect=ExchangeError("Pair 'ETH/BTC' not available"))) + MagicMock(side_effect=ExchangeError("Pair 'XRP/USDT' not available"))) stats = rpc._rpc_trade_statistics(stake_currency, fiat_display_currency) - assert stats['trade_count'] == 2 - assert stats['first_trade_date'] == 'just now' - assert stats['latest_trade_date'] == 'just now' - assert stats['avg_duration'] in ('0:00:00', '0:00:01', '0:00:02') - assert stats['best_pair'] == 'ETH/BTC' - assert prec_satoshi(stats['best_rate'], 6.2) + assert stats['trade_count'] == 7 + assert stats['first_trade_date'] == '2 days ago' + assert stats['latest_trade_date'] == '17 minutes ago' + assert stats['avg_duration'] in ('0:17:40') + assert stats['best_pair'] == 'XRP/USDT' + assert stats['best_rate'] == 10.0 assert isnan(stats['profit_all_coin']) From 43c871f2f4e4b7022befea6e4dd8c3b8871231a8 Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 13 Jun 2022 06:49:31 +0200 Subject: [PATCH 81/95] Use time-machine to stabilize time-sensitive tests --- tests/rpc/test_rpc_telegram.py | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/tests/rpc/test_rpc_telegram.py b/tests/rpc/test_rpc_telegram.py index 48acda47e..3bd817ac7 100644 --- a/tests/rpc/test_rpc_telegram.py +++ b/tests/rpc/test_rpc_telegram.py @@ -405,7 +405,7 @@ def test_status_table_handle(default_conf, update, ticker, fee, mocker) -> None: assert msg_mock.call_count == 1 -def test_daily_handle(default_conf_usdt, update, ticker, fee, mocker) -> None: +def test_daily_handle(default_conf_usdt, update, ticker, fee, mocker, time_machine) -> None: mocker.patch( 'freqtrade.rpc.rpc.CryptoToFiatConverter._find_price', return_value=1.1 @@ -418,6 +418,8 @@ def test_daily_handle(default_conf_usdt, update, ticker, fee, mocker) -> None: telegram, freqtradebot, msg_mock = get_telegram_testobject(mocker, default_conf_usdt) + # Move date to within day + time_machine.move_to('2022-06-11 08:00:00+00:00') # Create some test data create_mock_trades_usdt(fee) @@ -491,7 +493,7 @@ def test_daily_wrong_input(default_conf, update, ticker, mocker) -> None: assert 'Daily Profit over the last 7 days:' in msg_mock.call_args_list[0][0][0] -def test_weekly_handle(default_conf_usdt, update, ticker, fee, mocker) -> None: +def test_weekly_handle(default_conf_usdt, update, ticker, fee, mocker, time_machine) -> None: default_conf_usdt['max_open_trades'] = 1 mocker.patch( 'freqtrade.rpc.rpc.CryptoToFiatConverter._find_price', @@ -504,7 +506,8 @@ def test_weekly_handle(default_conf_usdt, update, ticker, fee, mocker) -> None: ) telegram, freqtradebot, msg_mock = get_telegram_testobject(mocker, default_conf_usdt) - + # Move to saturday - so all trades are within that week + time_machine.move_to('2022-06-11') create_mock_trades_usdt(fee) # Try valid data @@ -560,7 +563,7 @@ def test_weekly_handle(default_conf_usdt, update, ticker, fee, mocker) -> None: ) -def test_monthly_handle(default_conf_usdt, update, ticker, fee, mocker) -> None: +def test_monthly_handle(default_conf_usdt, update, ticker, fee, mocker, time_machine) -> None: default_conf_usdt['max_open_trades'] = 1 mocker.patch( 'freqtrade.rpc.rpc.CryptoToFiatConverter._find_price', @@ -573,7 +576,8 @@ def test_monthly_handle(default_conf_usdt, update, ticker, fee, mocker) -> None: ) telegram, freqtradebot, msg_mock = get_telegram_testobject(mocker, default_conf_usdt) - + # Move to day within the month so all mock trades fall into this week. + time_machine.move_to('2022-06-11') create_mock_trades_usdt(fee) # Try valid data From 8fd245c28b6d41be9b45a8c9f5aeb6d5ab7d277c Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 13 Jun 2022 06:58:06 +0200 Subject: [PATCH 82/95] Update pre-commit filelocktypes --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 685d789ec..f5c1a36f5 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -14,7 +14,7 @@ repos: exclude: build_helpers additional_dependencies: - types-cachetools==5.0.1 - - types-filelock==3.2.6 + - types-filelock==3.2.7 - types-requests==2.27.30 - types-tabulate==0.8.9 - types-python-dateutil==2.8.17 From 70966c8a8f0782b1e4d3f94c64b8cecb0e34b71b Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 13 Jun 2022 05:08:12 +0000 Subject: [PATCH 83/95] Bump actions/setup-python from 3 to 4 Bumps [actions/setup-python](https://github.com/actions/setup-python) from 3 to 4. - [Release notes](https://github.com/actions/setup-python/releases) - [Commits](https://github.com/actions/setup-python/compare/v3...v4) --- updated-dependencies: - dependency-name: actions/setup-python dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] --- .github/workflows/ci.yml | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index bbe0bcf6e..551268af7 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -30,7 +30,7 @@ jobs: - uses: actions/checkout@v3 - name: Set up Python - uses: actions/setup-python@v3 + uses: actions/setup-python@v4 with: python-version: ${{ matrix.python-version }} @@ -127,7 +127,7 @@ jobs: - uses: actions/checkout@v3 - name: Set up Python - uses: actions/setup-python@v3 + uses: actions/setup-python@v4 with: python-version: ${{ matrix.python-version }} @@ -211,7 +211,7 @@ jobs: - uses: actions/checkout@v3 - name: Set up Python - uses: actions/setup-python@v3 + uses: actions/setup-python@v4 with: python-version: ${{ matrix.python-version }} @@ -263,7 +263,7 @@ jobs: - uses: actions/checkout@v3 - name: Set up Python - uses: actions/setup-python@v3 + uses: actions/setup-python@v4 with: python-version: "3.10" @@ -282,7 +282,7 @@ jobs: ./tests/test_docs.sh - name: Set up Python - uses: actions/setup-python@v3 + uses: actions/setup-python@v4 with: python-version: "3.10" @@ -336,7 +336,7 @@ jobs: - uses: actions/checkout@v3 - name: Set up Python - uses: actions/setup-python@v3 + uses: actions/setup-python@v4 with: python-version: "3.9" From e67d29cd2f85ac2eb029b1c7904ece2cc7cc35a1 Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 13 Jun 2022 07:10:47 +0200 Subject: [PATCH 84/95] Update more trades to use create_mock_trades --- tests/rpc/test_rpc.py | 179 +++++++++++------------------------------- 1 file changed, 46 insertions(+), 133 deletions(-) diff --git a/tests/rpc/test_rpc.py b/tests/rpc/test_rpc.py index 339a6382f..d20646e60 100644 --- a/tests/rpc/test_rpc.py +++ b/tests/rpc/test_rpc.py @@ -11,7 +11,6 @@ from freqtrade.edge import PairInfo from freqtrade.enums import SignalDirection, State, TradingMode from freqtrade.exceptions import ExchangeError, InvalidOrderException, TemporaryError from freqtrade.persistence import Trade -from freqtrade.persistence.models import Order from freqtrade.persistence.pairlock_middleware import PairLocks from freqtrade.rpc import RPC, RPCException from freqtrade.rpc.fiat_convert import CryptoToFiatConverter @@ -407,8 +406,7 @@ def test_rpc_delete_trade(mocker, default_conf, fee, markets, caplog, is_short): assert stoploss_mock.call_count == 0 -def test_rpc_trade_statistics11(default_conf_usdt, ticker, fee, - mocker) -> None: +def test_rpc_trade_statistics(default_conf_usdt, ticker, fee, mocker) -> None: mocker.patch('freqtrade.rpc.rpc.CryptoToFiatConverter._find_price', return_value=1.1) mocker.patch('freqtrade.rpc.telegram.Telegram', MagicMock()) mocker.patch.multiple( @@ -463,14 +461,9 @@ def test_rpc_trade_statistics11(default_conf_usdt, ticker, fee, # Test that rpc_trade_statistics can handle trades that lacks # trade.open_rate (it is set to None) -def test_rpc_trade_statistics_closed(mocker, default_conf, ticker, fee, - ticker_sell_up, limit_buy_order, limit_sell_order): - mocker.patch.multiple( - 'freqtrade.rpc.fiat_convert.CoinGeckoAPI', - get_price=MagicMock(return_value={'bitcoin': {'usd': 15000.0}}), - ) +def test_rpc_trade_statistics_closed(mocker, default_conf_usdt, ticker, fee): mocker.patch('freqtrade.rpc.fiat_convert.CryptoToFiatConverter._find_price', - return_value=15000.0) + return_value=1.1) mocker.patch('freqtrade.rpc.telegram.Telegram', MagicMock()) mocker.patch.multiple( 'freqtrade.exchange.Exchange', @@ -478,46 +471,32 @@ def test_rpc_trade_statistics_closed(mocker, default_conf, ticker, fee, get_fee=fee, ) - freqtradebot = get_patched_freqtradebot(mocker, default_conf) + freqtradebot = get_patched_freqtradebot(mocker, default_conf_usdt) patch_get_signal(freqtradebot) - stake_currency = default_conf['stake_currency'] - fiat_display_currency = default_conf['fiat_display_currency'] + stake_currency = default_conf_usdt['stake_currency'] + fiat_display_currency = default_conf_usdt['fiat_display_currency'] rpc = RPC(freqtradebot) # Create some test data - freqtradebot.enter_positions() - trade = Trade.query.first() - # Simulate fulfilled LIMIT_BUY order for trade - oobj = Order.parse_from_ccxt_object(limit_buy_order, limit_buy_order['symbol'], 'buy') - trade.update_trade(oobj) - # Update the ticker with a market going up - mocker.patch.multiple( - 'freqtrade.exchange.Exchange', - fetch_ticker=ticker_sell_up, - get_fee=fee - ) - oobj = Order.parse_from_ccxt_object(limit_sell_order, limit_sell_order['symbol'], 'sell') - trade.update_trade(oobj) - trade.close_date = datetime.utcnow() - trade.is_open = False + create_mock_trades_usdt(fee) for trade in Trade.query.order_by(Trade.id).all(): trade.open_rate = None stats = rpc._rpc_trade_statistics(stake_currency, fiat_display_currency) - assert prec_satoshi(stats['profit_closed_coin'], 0) - assert prec_satoshi(stats['profit_closed_percent_mean'], 0) - assert prec_satoshi(stats['profit_closed_fiat'], 0) - assert prec_satoshi(stats['profit_all_coin'], 0) - assert prec_satoshi(stats['profit_all_percent_mean'], 0) - assert prec_satoshi(stats['profit_all_fiat'], 0) - assert stats['trade_count'] == 1 - assert stats['first_trade_date'] == 'just now' - assert stats['latest_trade_date'] == 'just now' + assert stats['profit_closed_coin'] == 0 + assert stats['profit_closed_percent_mean'] == 0 + assert stats['profit_closed_fiat'] == 0 + assert stats['profit_all_coin'] == 0 + assert stats['profit_all_percent_mean'] == 0 + assert stats['profit_all_fiat'] == 0 + assert stats['trade_count'] == 7 + assert stats['first_trade_date'] == '2 days ago' + assert stats['latest_trade_date'] == '17 minutes ago' assert stats['avg_duration'] == '0:00:00' - assert stats['best_pair'] == 'ETH/BTC' - assert prec_satoshi(stats['best_rate'], 6.2) + assert stats['best_pair'] == 'XRP/USDT' + assert stats['best_rate'] == 10.0 def test_rpc_balance_handle_error(default_conf, mocker): @@ -869,8 +848,7 @@ def test_rpc_force_exit(default_conf, ticker, fee, mocker) -> None: assert cancel_order_mock.call_count == 3 -def test_performance_handle(default_conf, ticker, limit_buy_order, fee, - limit_sell_order, mocker) -> None: +def test_performance_handle(default_conf_usdt, ticker, fee, mocker) -> None: mocker.patch('freqtrade.rpc.telegram.Telegram', MagicMock()) mocker.patch.multiple( 'freqtrade.exchange.Exchange', @@ -879,34 +857,21 @@ def test_performance_handle(default_conf, ticker, limit_buy_order, fee, get_fee=fee, ) - freqtradebot = get_patched_freqtradebot(mocker, default_conf) + freqtradebot = get_patched_freqtradebot(mocker, default_conf_usdt) patch_get_signal(freqtradebot) rpc = RPC(freqtradebot) - # Create some test data - freqtradebot.enter_positions() - trade = Trade.query.first() - assert trade + create_mock_trades_usdt(fee) - # Simulate fulfilled LIMIT_BUY order for trade - oobj = Order.parse_from_ccxt_object(limit_buy_order, limit_buy_order['symbol'], 'buy') - trade.update_trade(oobj) - - # Simulate fulfilled LIMIT_SELL order for trade - oobj = Order.parse_from_ccxt_object(limit_sell_order, limit_sell_order['symbol'], 'sell') - trade.update_trade(oobj) - - trade.close_date = datetime.utcnow() - trade.is_open = False res = rpc._rpc_performance() - assert len(res) == 1 - assert res[0]['pair'] == 'ETH/BTC' + assert len(res) == 3 + assert res[0]['pair'] == 'XRP/USDT' assert res[0]['count'] == 1 - assert prec_satoshi(res[0]['profit_pct'], 6.2) + assert res[0]['profit_pct'] == 10.0 -def test_enter_tag_performance_handle(default_conf, ticker, limit_buy_order, fee, - limit_sell_order, mocker) -> None: +def test_enter_tag_performance_handle(default_conf, ticker, fee, mocker) -> None: + mocker.patch('freqtrade.rpc.telegram.Telegram', MagicMock()) mocker.patch.multiple( 'freqtrade.exchange.Exchange', @@ -920,34 +885,22 @@ def test_enter_tag_performance_handle(default_conf, ticker, limit_buy_order, fee rpc = RPC(freqtradebot) # Create some test data + create_mock_trades_usdt(fee) freqtradebot.enter_positions() - trade = Trade.query.first() - assert trade - # Simulate fulfilled LIMIT_BUY order for trade - oobj = Order.parse_from_ccxt_object(limit_buy_order, limit_buy_order['symbol'], 'buy') - trade.update_trade(oobj) - - # Simulate fulfilled LIMIT_SELL order for trade - oobj = Order.parse_from_ccxt_object(limit_sell_order, limit_sell_order['symbol'], 'sell') - trade.update_trade(oobj) - - trade.close_date = datetime.utcnow() - trade.is_open = False res = rpc._rpc_enter_tag_performance(None) - assert len(res) == 1 - assert res[0]['enter_tag'] == 'Other' + assert len(res) == 3 + assert res[0]['enter_tag'] == 'TEST3' assert res[0]['count'] == 1 - assert prec_satoshi(res[0]['profit_pct'], 6.2) + assert res[0]['profit_pct'] == 10.0 - trade.enter_tag = "TEST_TAG" res = rpc._rpc_enter_tag_performance(None) - assert len(res) == 1 - assert res[0]['enter_tag'] == 'TEST_TAG' + assert len(res) == 3 + assert res[0]['enter_tag'] == 'TEST3' assert res[0]['count'] == 1 - assert prec_satoshi(res[0]['profit_pct'], 6.2) + assert res[0]['profit_pct'] == 10.0 def test_enter_tag_performance_handle_2(mocker, default_conf, markets, fee): @@ -979,8 +932,7 @@ def test_enter_tag_performance_handle_2(mocker, default_conf, markets, fee): assert prec_satoshi(res[0]['profit_pct'], 0.5) -def test_exit_reason_performance_handle(default_conf, ticker, limit_buy_order, fee, - limit_sell_order, mocker) -> None: +def test_exit_reason_performance_handle(default_conf_usdt, ticker, fee, mocker) -> None: mocker.patch('freqtrade.rpc.telegram.Telegram', MagicMock()) mocker.patch.multiple( 'freqtrade.exchange.Exchange', @@ -989,39 +941,22 @@ def test_exit_reason_performance_handle(default_conf, ticker, limit_buy_order, f get_fee=fee, ) - freqtradebot = get_patched_freqtradebot(mocker, default_conf) + freqtradebot = get_patched_freqtradebot(mocker, default_conf_usdt) patch_get_signal(freqtradebot) rpc = RPC(freqtradebot) # Create some test data - freqtradebot.enter_positions() - trade = Trade.query.first() - assert trade + create_mock_trades_usdt(fee) - # Simulate fulfilled LIMIT_BUY order for trade - oobj = Order.parse_from_ccxt_object(limit_buy_order, limit_buy_order['symbol'], 'buy') - trade.update_trade(oobj) - - # Simulate fulfilled LIMIT_SELL order for trade - oobj = Order.parse_from_ccxt_object(limit_sell_order, limit_sell_order['symbol'], 'sell') - trade.update_trade(oobj) - - trade.close_date = datetime.utcnow() - trade.is_open = False res = rpc._rpc_exit_reason_performance(None) - assert len(res) == 1 - assert res[0]['exit_reason'] == 'Other' + assert len(res) == 3 + assert res[0]['exit_reason'] == 'roi' assert res[0]['count'] == 1 - assert prec_satoshi(res[0]['profit_pct'], 6.2) + assert res[0]['profit_pct'] == 10.0 - trade.exit_reason = "TEST1" - res = rpc._rpc_exit_reason_performance(None) - - assert len(res) == 1 - assert res[0]['exit_reason'] == 'TEST1' - assert res[0]['count'] == 1 - assert prec_satoshi(res[0]['profit_pct'], 6.2) + assert res[1]['exit_reason'] == 'exit_signal' + assert res[2]['exit_reason'] == 'Other' def test_exit_reason_performance_handle_2(mocker, default_conf, markets, fee): @@ -1053,8 +988,7 @@ def test_exit_reason_performance_handle_2(mocker, default_conf, markets, fee): assert prec_satoshi(res[0]['profit_pct'], 0.5) -def test_mix_tag_performance_handle(default_conf, ticker, limit_buy_order, fee, - limit_sell_order, mocker) -> None: +def test_mix_tag_performance_handle(default_conf, ticker, fee, mocker) -> None: mocker.patch('freqtrade.rpc.telegram.Telegram', MagicMock()) mocker.patch.multiple( 'freqtrade.exchange.Exchange', @@ -1068,35 +1002,14 @@ def test_mix_tag_performance_handle(default_conf, ticker, limit_buy_order, fee, rpc = RPC(freqtradebot) # Create some test data - freqtradebot.enter_positions() - trade = Trade.query.first() - assert trade + create_mock_trades_usdt(fee) - # Simulate fulfilled LIMIT_BUY order for trade - oobj = Order.parse_from_ccxt_object(limit_buy_order, limit_buy_order['symbol'], 'buy') - trade.update_trade(oobj) - - # Simulate fulfilled LIMIT_SELL order for trade - oobj = Order.parse_from_ccxt_object(limit_sell_order, limit_sell_order['symbol'], 'sell') - trade.update_trade(oobj) - - trade.close_date = datetime.utcnow() - trade.is_open = False res = rpc._rpc_mix_tag_performance(None) - assert len(res) == 1 - assert res[0]['mix_tag'] == 'Other Other' + assert len(res) == 3 + assert res[0]['mix_tag'] == 'TEST3 roi' assert res[0]['count'] == 1 - assert prec_satoshi(res[0]['profit_pct'], 6.2) - - trade.enter_tag = "TESTBUY" - trade.exit_reason = "TESTSELL" - res = rpc._rpc_mix_tag_performance(None) - - assert len(res) == 1 - assert res[0]['mix_tag'] == 'TESTBUY TESTSELL' - assert res[0]['count'] == 1 - assert prec_satoshi(res[0]['profit_pct'], 6.2) + assert res[0]['profit_pct'] == 10.0 def test_mix_tag_performance_handle_2(mocker, default_conf, markets, fee): From ee0b9e3a5c3aa907d8901db89e5c375ccb42b406 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 13 Jun 2022 06:25:18 +0000 Subject: [PATCH 85/95] Bump mkdocs-material from 8.3.2 to 8.3.4 Bumps [mkdocs-material](https://github.com/squidfunk/mkdocs-material) from 8.3.2 to 8.3.4. - [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/8.3.2...8.3.4) --- updated-dependencies: - dependency-name: mkdocs-material dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[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 f351151ab..1b4403b97 100644 --- a/docs/requirements-docs.txt +++ b/docs/requirements-docs.txt @@ -1,5 +1,5 @@ mkdocs==1.3.0 -mkdocs-material==8.3.2 +mkdocs-material==8.3.4 mdx_truly_sane_lists==1.2 pymdown-extensions==9.4 jinja2==3.1.2 From 71f314d4c45b39a91380c6b1a02876506b6430af Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 13 Jun 2022 06:25:35 +0000 Subject: [PATCH 86/95] Bump ccxt from 1.85.57 to 1.87.12 Bumps [ccxt](https://github.com/ccxt/ccxt) from 1.85.57 to 1.87.12. - [Release notes](https://github.com/ccxt/ccxt/releases) - [Changelog](https://github.com/ccxt/ccxt/blob/master/exchanges.cfg) - [Commits](https://github.com/ccxt/ccxt/compare/1.85.57...1.87.12) --- updated-dependencies: - dependency-name: ccxt dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 05d5a10db..4ebcdaa8b 100644 --- a/requirements.txt +++ b/requirements.txt @@ -2,7 +2,7 @@ numpy==1.22.4 pandas==1.4.2 pandas-ta==0.3.14b -ccxt==1.85.57 +ccxt==1.87.12 # Pin cryptography for now due to rust build errors with piwheels cryptography==37.0.2 aiohttp==3.8.1 From 43b8b0a083d527318f6033b26a395d8c5dbc7e86 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 13 Jun 2022 06:25:53 +0000 Subject: [PATCH 87/95] Bump mypy from 0.960 to 0.961 Bumps [mypy](https://github.com/python/mypy) from 0.960 to 0.961. - [Release notes](https://github.com/python/mypy/releases) - [Commits](https://github.com/python/mypy/compare/v0.960...v0.961) --- updated-dependencies: - dependency-name: mypy dependency-type: direct:development update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- requirements-dev.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements-dev.txt b/requirements-dev.txt index e7d64a2b6..19912d59c 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -7,7 +7,7 @@ coveralls==3.3.1 flake8==4.0.1 flake8-tidy-imports==4.8.0 -mypy==0.960 +mypy==0.961 pre-commit==2.19.0 pytest==7.1.2 pytest-asyncio==0.18.3 From cb2f89bca63a73aea5b35f5a6b8d2ae48d5455c6 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 13 Jun 2022 06:26:23 +0000 Subject: [PATCH 88/95] Bump requests from 2.27.1 to 2.28.0 Bumps [requests](https://github.com/psf/requests) from 2.27.1 to 2.28.0. - [Release notes](https://github.com/psf/requests/releases) - [Changelog](https://github.com/psf/requests/blob/main/HISTORY.md) - [Commits](https://github.com/psf/requests/compare/v2.27.1...v2.28.0) --- updated-dependencies: - dependency-name: requests dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 05d5a10db..ba9cecafd 100644 --- a/requirements.txt +++ b/requirements.txt @@ -10,7 +10,7 @@ SQLAlchemy==1.4.37 python-telegram-bot==13.12 arrow==1.2.2 cachetools==4.2.2 -requests==2.27.1 +requests==2.28.0 urllib3==1.26.9 jsonschema==4.6.0 TA-Lib==0.4.24 From fdca583c6760a6ba76f04b076e373d09accf8291 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 13 Jun 2022 07:07:39 +0000 Subject: [PATCH 89/95] Bump pymdown-extensions from 9.4 to 9.5 Bumps [pymdown-extensions](https://github.com/facelessuser/pymdown-extensions) from 9.4 to 9.5. - [Release notes](https://github.com/facelessuser/pymdown-extensions/releases) - [Commits](https://github.com/facelessuser/pymdown-extensions/compare/9.4...9.5) --- updated-dependencies: - dependency-name: pymdown-extensions dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[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 1b4403b97..1f342ca02 100644 --- a/docs/requirements-docs.txt +++ b/docs/requirements-docs.txt @@ -1,5 +1,5 @@ mkdocs==1.3.0 mkdocs-material==8.3.4 mdx_truly_sane_lists==1.2 -pymdown-extensions==9.4 +pymdown-extensions==9.5 jinja2==3.1.2 From 850f5d3842008406c9a24611fdb6e40e6c138ae1 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 13 Jun 2022 07:32:39 +0000 Subject: [PATCH 90/95] Bump orjson from 3.7.1 to 3.7.2 Bumps [orjson](https://github.com/ijl/orjson) from 3.7.1 to 3.7.2. - [Release notes](https://github.com/ijl/orjson/releases) - [Changelog](https://github.com/ijl/orjson/blob/master/CHANGELOG.md) - [Commits](https://github.com/ijl/orjson/compare/3.7.1...3.7.2) --- updated-dependencies: - dependency-name: orjson dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 9b7d87e02..b2dbd921e 100644 --- a/requirements.txt +++ b/requirements.txt @@ -28,7 +28,7 @@ py_find_1st==1.1.5 # Load ticker files 30% faster python-rapidjson==1.6 # Properly format api responses -orjson==3.7.1 +orjson==3.7.2 # Notify systemd sdnotify==0.3.2 From 35adeb64122a02d52f2515b53ea3bd125c2a8d31 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 13 Jun 2022 07:33:30 +0000 Subject: [PATCH 91/95] Bump plotly from 5.8.0 to 5.8.2 Bumps [plotly](https://github.com/plotly/plotly.py) from 5.8.0 to 5.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/v5.8.0...v5.8.2) --- updated-dependencies: - dependency-name: plotly dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- requirements-plot.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements-plot.txt b/requirements-plot.txt index e17efbc71..a2a894c57 100644 --- a/requirements-plot.txt +++ b/requirements-plot.txt @@ -1,4 +1,4 @@ # Include all requirements to run the bot. -r requirements.txt -plotly==5.8.0 +plotly==5.8.2 From 848a5d85c63f7655f958c700e1e82caaa69d2b9f Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 13 Jun 2022 08:50:12 +0000 Subject: [PATCH 92/95] Add small stability fix to test --- tests/test_freqtradebot.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/tests/test_freqtradebot.py b/tests/test_freqtradebot.py index 7f9bc6a46..3fd16f925 100644 --- a/tests/test_freqtradebot.py +++ b/tests/test_freqtradebot.py @@ -3775,6 +3775,7 @@ def test_exit_profit_only( trade = Trade.query.first() assert trade.is_short == is_short oobj = Order.parse_from_ccxt_object(limit_order[eside], limit_order[eside]['symbol'], eside) + trade.update_order(limit_order[eside]) trade.update_trade(oobj) freqtrade.wallets.update() if profit_only: @@ -4063,6 +4064,7 @@ def test_trailing_stop_loss_positive( trade = Trade.query.first() assert trade.is_short == is_short oobj = Order.parse_from_ccxt_object(limit_order[eside], limit_order[eside]['symbol'], eside) + trade.update_order(limit_order[eside]) trade.update_trade(oobj) caplog.set_level(logging.DEBUG) # stop-loss not reached From d5fd1f9c3848469b4ce1fa1039ef533acc09c0f2 Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 13 Jun 2022 13:24:27 +0000 Subject: [PATCH 93/95] Improve order filled handling --- freqtrade/persistence/trade_model.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/freqtrade/persistence/trade_model.py b/freqtrade/persistence/trade_model.py index 0be9d22c1..79f58591d 100644 --- a/freqtrade/persistence/trade_model.py +++ b/freqtrade/persistence/trade_model.py @@ -74,7 +74,7 @@ class Order(_DECL_BASE): @property def safe_filled(self) -> float: - return self.filled or self.amount or 0.0 + return self.filled if self.filled is not None else self.amount or 0.0 @property def safe_fee_base(self) -> float: @@ -847,8 +847,6 @@ class LocalTrade(): tmp_amount = o.safe_amount_after_fee tmp_price = o.average or o.price - if o.filled is not None: - tmp_amount = o.filled if tmp_amount > 0.0 and tmp_price is not None: total_amount += tmp_amount total_stake += tmp_price * tmp_amount From 1ffee96bade938c25d025bd32839fb0a8a6430c9 Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 13 Jun 2022 19:59:00 +0200 Subject: [PATCH 94/95] Fix protection parameters not loading from parameter file closes #6978 --- freqtrade/freqtradebot.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index fdccc2f8a..000f74a27 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -73,8 +73,6 @@ class FreqtradeBot(LoggingMixin): PairLocks.timeframe = self.config['timeframe'] - self.protections = ProtectionManager(self.config, self.strategy.protections) - # RPC runs in separate threads, can start handling external commands just after # initialization, even before Freqtradebot has a chance to start its throttling, # so anything in the Freqtradebot instance should be ready (initialized), including @@ -124,6 +122,8 @@ class FreqtradeBot(LoggingMixin): self.last_process = datetime(1970, 1, 1, tzinfo=timezone.utc) self.strategy.ft_bot_start() + # Initialize protections AFTER bot start - otherwise parameters are not loaded. + self.protections = ProtectionManager(self.config, self.strategy.protections) def notify_status(self, msg: str) -> None: """ From 01a68e1060bf0c7a30a8fc9732d035547f7dd11c Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 13 Jun 2022 20:48:49 +0200 Subject: [PATCH 95/95] Remove unnecessary check and condition --- freqtrade/persistence/trade_model.py | 8 -------- 1 file changed, 8 deletions(-) diff --git a/freqtrade/persistence/trade_model.py b/freqtrade/persistence/trade_model.py index 79f58591d..3222a57b8 100644 --- a/freqtrade/persistence/trade_model.py +++ b/freqtrade/persistence/trade_model.py @@ -828,14 +828,6 @@ class LocalTrade(): return float(f"{profit_ratio:.8f}") def recalc_trade_from_orders(self): - # We need at least 2 entry orders for averaging amounts and rates. - # TODO: this condition could probably be removed - if len(self.select_filled_orders(self.entry_side)) < 2: - self.stake_amount = self.amount * self.open_rate / self.leverage - - # Just in case, still recalc open trade value - self.recalc_open_trade_value() - return total_amount = 0.0 total_stake = 0.0