From ac2e8d760e2359ca5d26a8ce7dabf370873b0a6c Mon Sep 17 00:00:00 2001 From: rzrymiak <106121613+rzrymiak@users.noreply.github.com> Date: Tue, 19 Jul 2022 14:24:44 -0700 Subject: [PATCH 01/33] Added description heading to README.md --- README.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 881895c9a..828a3d1f9 100644 --- a/README.md +++ b/README.md @@ -5,6 +5,8 @@ [![Documentation](https://readthedocs.org/projects/freqtrade/badge/)](https://www.freqtrade.io) [![Maintainability](https://api.codeclimate.com/v1/badges/5737e6d668200b7518ff/maintainability)](https://codeclimate.com/github/freqtrade/freqtrade/maintainability) +## Description + Freqtrade is a free and open source crypto trading bot written in Python. It is designed to support all major exchanges and be controlled via Telegram or webUI. It contains backtesting, plotting and money management tools as well as strategy optimization by machine learning. ![freqtrade](https://raw.githubusercontent.com/freqtrade/freqtrade/develop/docs/assets/freqtrade-screenshot.png) @@ -193,7 +195,7 @@ Issues labeled [good first issue](https://github.com/freqtrade/freqtrade/labels/ The clock must be accurate, synchronized to a NTP server very frequently to avoid problems with communication to the exchanges. -### Min hardware required +### Minimum hardware required To run this bot we recommend you a cloud instance with a minimum of: From 1b49e45222547e701b8b90a6cc6d2f905c6c8853 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 25 Jul 2022 03:01:32 +0000 Subject: [PATCH 02/33] Bump orjson from 3.7.7 to 3.7.8 Bumps [orjson](https://github.com/ijl/orjson) from 3.7.7 to 3.7.8. - [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.7...3.7.8) --- 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 b27c8f559..a41c50f84 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.8 # Properly format api responses -orjson==3.7.7 +orjson==3.7.8 # Notify systemd sdnotify==0.3.2 From d5933fb2affac545a7b462cb50f4479fd964d55b Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 25 Jul 2022 03:01:37 +0000 Subject: [PATCH 03/33] Bump mkdocs from 1.3.0 to 1.3.1 Bumps [mkdocs](https://github.com/mkdocs/mkdocs) from 1.3.0 to 1.3.1. - [Release notes](https://github.com/mkdocs/mkdocs/releases) - [Commits](https://github.com/mkdocs/mkdocs/compare/1.3.0...1.3.1) --- updated-dependencies: - dependency-name: mkdocs 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 a07f4f944..ea09b853b 100644 --- a/docs/requirements-docs.txt +++ b/docs/requirements-docs.txt @@ -1,5 +1,5 @@ markdown==3.4.1 -mkdocs==1.3.0 +mkdocs==1.3.1 mkdocs-material==8.3.9 mdx_truly_sane_lists==1.3 pymdown-extensions==9.5 From 98d0ad76bf1ed599ea3e3abd7da15a11c8c3caec Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 25 Jul 2022 03:01:44 +0000 Subject: [PATCH 04/33] Bump types-requests from 2.28.1 to 2.28.3 Bumps [types-requests](https://github.com/python/typeshed) from 2.28.1 to 2.28.3. - [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 3d91f29fd..fa56f1df9 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -24,6 +24,6 @@ nbconvert==6.5.0 # mypy types types-cachetools==5.2.1 types-filelock==3.2.7 -types-requests==2.28.1 +types-requests==2.28.3 types-tabulate==0.8.11 types-python-dateutil==2.8.18 From f93a3a5fca28b3c1677af181cfe7e4fa0443483c Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 25 Jul 2022 03:01:52 +0000 Subject: [PATCH 05/33] Bump ccxt from 1.90.89 to 1.91.22 Bumps [ccxt](https://github.com/ccxt/ccxt) from 1.90.89 to 1.91.22. - [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.90.89...1.91.22) --- 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 b27c8f559..4e1c477cb 100644 --- a/requirements.txt +++ b/requirements.txt @@ -2,7 +2,7 @@ numpy==1.23.1 pandas==1.4.3 pandas-ta==0.3.14b -ccxt==1.90.89 +ccxt==1.91.22 # Pin cryptography for now due to rust build errors with piwheels cryptography==37.0.4 aiohttp==3.8.1 From 3ce46ff09e980570c72ede2b2359fc8c92db4996 Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 25 Jul 2022 07:19:21 +0200 Subject: [PATCH 06/33] Bump types-requests in pre-commit --- .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 a23181c37..cbdeef780 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -15,7 +15,7 @@ repos: additional_dependencies: - types-cachetools==5.2.1 - types-filelock==3.2.7 - - types-requests==2.28.1 + - types-requests==2.28.3 - types-tabulate==0.8.11 - types-python-dateutil==2.8.18 # stages: [push] From 43343d0e55391fde8c0a469e23b387a346a37e38 Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 25 Jul 2022 07:21:12 +0200 Subject: [PATCH 07/33] Revert markdown to 3.3.7 --- 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 ea09b853b..205516d6d 100644 --- a/docs/requirements-docs.txt +++ b/docs/requirements-docs.txt @@ -1,4 +1,4 @@ -markdown==3.4.1 +markdown==3.3.7 mkdocs==1.3.1 mkdocs-material==8.3.9 mdx_truly_sane_lists==1.3 From 93340f546b2395d4e50bb51391299496a7620f4b Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 25 Jul 2022 05:53:10 +0000 Subject: [PATCH 08/33] Bump mypy from 0.961 to 0.971 Bumps [mypy](https://github.com/python/mypy) from 0.961 to 0.971. - [Release notes](https://github.com/python/mypy/releases) - [Commits](https://github.com/python/mypy/compare/v0.961...v0.971) --- 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 fa56f1df9..41e4722aa 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.961 +mypy==0.971 pre-commit==2.20.0 pytest==7.1.2 pytest-asyncio==0.19.0 From 40969f20bfbfe2c84821a4553dc9c6963ca91415 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 25 Jul 2022 05:53:15 +0000 Subject: [PATCH 09/33] Bump types-python-dateutil from 2.8.18 to 2.8.19 Bumps [types-python-dateutil](https://github.com/python/typeshed) from 2.8.18 to 2.8.19. - [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 fa56f1df9..425937d6e 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -26,4 +26,4 @@ types-cachetools==5.2.1 types-filelock==3.2.7 types-requests==2.28.3 types-tabulate==0.8.11 -types-python-dateutil==2.8.18 +types-python-dateutil==2.8.19 From 47b52d4bab6d0d378759eabc72df94f424cfacd6 Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 25 Jul 2022 07:58:16 +0200 Subject: [PATCH 10/33] Bump types-dateutil in pre-commit --- .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 cbdeef780..759ac0a6a 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -17,7 +17,7 @@ repos: - types-filelock==3.2.7 - types-requests==2.28.3 - types-tabulate==0.8.11 - - types-python-dateutil==2.8.18 + - types-python-dateutil==2.8.19 # stages: [push] - repo: https://github.com/pycqa/isort From ea112fb58399a0d00ca0582d462fe3c639d33a7b Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 25 Jul 2022 17:47:28 +0200 Subject: [PATCH 11/33] Add test for empty order (cancelled order) --- tests/exchange/test_exchange.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/tests/exchange/test_exchange.py b/tests/exchange/test_exchange.py index ff8b4b40c..9252040ea 100644 --- a/tests/exchange/test_exchange.py +++ b/tests/exchange/test_exchange.py @@ -2910,6 +2910,9 @@ def test_check_order_canceled_empty(mocker, default_conf, exchange_name, order, ({'amount': 10.0, 'fee': {}}, False), ({'result': 'testest123'}, False), ('hello_world', False), + ({'status': 'canceled', 'amount': None, 'fee': None}, False), + ({'status': 'canceled', 'filled': None, 'amount': None, 'fee': None}, False), + ]) def test_is_cancel_order_result_suitable(mocker, default_conf, exchange_name, order, result): exchange = get_patched_exchange(mocker, default_conf, id=exchange_name) From 4c68bec171c2849afbb5363d9e9bd48d178de854 Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 25 Jul 2022 17:47:52 +0200 Subject: [PATCH 12/33] Fix problem in `is_cancel_order_result_suitable` fixes #7119 --- freqtrade/exchange/exchange.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/freqtrade/exchange/exchange.py b/freqtrade/exchange/exchange.py index 11e37b953..79bc769e6 100644 --- a/freqtrade/exchange/exchange.py +++ b/freqtrade/exchange/exchange.py @@ -1264,7 +1264,7 @@ class Exchange: return False required = ('fee', 'status', 'amount') - return all(k in corder for k in required) + return all(corder.get(k, None) is not None for k in required) def cancel_order_with_result(self, order_id: str, pair: str, amount: float) -> Dict: """ From a0b9388757e00ab04e29e87d265867009ccf6415 Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 26 Jul 2022 17:57:25 +0200 Subject: [PATCH 13/33] Bump ccxt to 1.91.29 closes #7132 --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 411827b62..b9e87749d 100644 --- a/requirements.txt +++ b/requirements.txt @@ -2,7 +2,7 @@ numpy==1.23.1 pandas==1.4.3 pandas-ta==0.3.14b -ccxt==1.91.22 +ccxt==1.91.29 # Pin cryptography for now due to rust build errors with piwheels cryptography==37.0.4 aiohttp==3.8.1 From 229e8864bbeb33bbd0b4c3bc525131939e276ad2 Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 26 Jul 2022 20:15:49 +0200 Subject: [PATCH 14/33] Add send_msg capability to dataprovider --- freqtrade/data/dataprovider.py | 21 +++++++++++++++++++++ freqtrade/enums/rpcmessagetype.py | 2 ++ freqtrade/freqtradebot.py | 1 + freqtrade/rpc/rpc_manager.py | 12 ++++++++++++ freqtrade/rpc/telegram.py | 3 ++- 5 files changed, 38 insertions(+), 1 deletion(-) diff --git a/freqtrade/data/dataprovider.py b/freqtrade/data/dataprovider.py index b9b118c00..800254533 100644 --- a/freqtrade/data/dataprovider.py +++ b/freqtrade/data/dataprovider.py @@ -5,12 +5,14 @@ including ticker and orderbook data, live and historical candle (OHLCV) data Common Interface for bot and strategy to access data. """ import logging +from collections import deque from datetime import datetime, timezone from typing import Any, Dict, List, Optional, Tuple from pandas import DataFrame from freqtrade.configuration import TimeRange +from freqtrade.configuration.PeriodicCache import PeriodicCache from freqtrade.constants import ListPairsWithTimeframes, PairWithTimeframe from freqtrade.data.history import load_pair_history from freqtrade.enums import CandleType, RunMode @@ -33,6 +35,9 @@ class DataProvider: self.__cached_pairs: Dict[PairWithTimeframe, Tuple[DataFrame, datetime]] = {} self.__slice_index: Optional[int] = None self.__cached_pairs_backtesting: Dict[PairWithTimeframe, DataFrame] = {} + self._msg_queue: deque = deque() + self.__msg_cache = PeriodicCache( + maxsize=1000, ttl=timeframe_to_seconds(self._config['timeframe'])) def _set_dataframe_max_index(self, limit_index: int): """ @@ -265,3 +270,19 @@ class DataProvider: if self._exchange is None: raise OperationalException(NO_EXCHANGE_EXCEPTION) return self._exchange.fetch_l2_order_book(pair, maximum) + + def send_msg(self, message: str, always_send: bool = False) -> None: + """ + TODO: Document me + :param message: Message to be sent. Must be below 4096. + :param always_send: If False, will send the message only once per candle, and surpress + identical messages. + Careful as this can end up spaming your chat. + Defaults to False + """ + if self.runmode not in (RunMode.DRY_RUN, RunMode.LIVE): + return + + if always_send or message not in self.__msg_cache: + self._msg_queue.append(message) + self.__msg_cache[message] = True diff --git a/freqtrade/enums/rpcmessagetype.py b/freqtrade/enums/rpcmessagetype.py index 584a011c2..415d8f18c 100644 --- a/freqtrade/enums/rpcmessagetype.py +++ b/freqtrade/enums/rpcmessagetype.py @@ -17,6 +17,8 @@ class RPCMessageType(Enum): PROTECTION_TRIGGER = 'protection_trigger' PROTECTION_TRIGGER_GLOBAL = 'protection_trigger_global' + STRATEGY_MSG = 'strategy_msg' + def __repr__(self): return self.value diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index 43608cae7..9ea195c45 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -214,6 +214,7 @@ class FreqtradeBot(LoggingMixin): if self.trading_mode == TradingMode.FUTURES: self._schedule.run_pending() Trade.commit() + self.rpc.process_msg_queue(self.dataprovider._msg_queue) self.last_process = datetime.now(timezone.utc) def process_stopped(self) -> None: diff --git a/freqtrade/rpc/rpc_manager.py b/freqtrade/rpc/rpc_manager.py index 66e84029f..3ccf23228 100644 --- a/freqtrade/rpc/rpc_manager.py +++ b/freqtrade/rpc/rpc_manager.py @@ -2,6 +2,7 @@ This module contains class to manage RPC communications (Telegram, API, ...) """ import logging +from collections import deque from typing import Any, Dict, List from freqtrade.enums import RPCMessageType @@ -77,6 +78,17 @@ class RPCManager: except NotImplementedError: logger.error(f"Message type '{msg['type']}' not implemented by handler {mod.name}.") + def process_msg_queue(self, queue: deque) -> None: + """ + Process all messages in the queue. + """ + while queue: + msg = queue.popleft() + self.send_msg({ + 'type': RPCMessageType.STRATEGY_MSG, + 'msg': msg, + }) + def startup_messages(self, config: Dict[str, Any], pairlist, protections) -> None: if config['dry_run']: self.send_msg({ diff --git a/freqtrade/rpc/telegram.py b/freqtrade/rpc/telegram.py index 2aff1d210..121324d90 100644 --- a/freqtrade/rpc/telegram.py +++ b/freqtrade/rpc/telegram.py @@ -376,7 +376,8 @@ class Telegram(RPCHandler): elif msg_type == RPCMessageType.STARTUP: message = f"{msg['status']}" - + elif msg_type == RPCMessageType.STRATEGY_MSG: + message = f"{msg['msg']}" else: raise NotImplementedError(f"Unknown message type: {msg_type}") return message From 7bac0546681e770d3156db987620a666e4e1a489 Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 26 Jul 2022 20:24:52 +0200 Subject: [PATCH 15/33] Add documentation and clarity for send_msg --- docs/strategy-customization.md | 17 +++++++++++++++++ docs/telegram-usage.md | 4 +++- freqtrade/constants.py | 4 ++++ freqtrade/data/dataprovider.py | 8 +++++--- 4 files changed, 29 insertions(+), 4 deletions(-) diff --git a/docs/strategy-customization.md b/docs/strategy-customization.md index 6947380dd..38d34d51b 100644 --- a/docs/strategy-customization.md +++ b/docs/strategy-customization.md @@ -731,6 +731,23 @@ if self.dp: !!! Warning "Warning about backtesting" This method will always return up-to-date values - so usage during backtesting / hyperopt will lead to wrong results. +### Send Notification + +The dataprovider `.send_msg()` function allows you to send custom notifications from your strategy. +Identical notifications will only be sent once per candle, unless the 2nd argument (`always_send`) is set to True. + +``` python + self.dp.send_msg(f"{metadata['pair']} just got hot!") + + # Force send this notification, avoid caching (Please read warning below!) + self.dp.send_msg(f"{metadata['pair']} just got hot!", always_send=True) +``` + +Notifications will only be sent in trading modes (Live/Dry-run) - so this method can be called without conditions for backtesting. + +!!! Warning "Spamming" + You can spam yourself pretty good by setting `always_send=True` in this method. Use this with great care and only in conditions you know will not happen throughout a candle to avoid a message every 5 seconds. + ### Complete Data-provider sample ```python diff --git a/docs/telegram-usage.md b/docs/telegram-usage.md index 9853e15c6..a690e18b9 100644 --- a/docs/telegram-usage.md +++ b/docs/telegram-usage.md @@ -98,6 +98,7 @@ Example configuration showing the different settings: "exit_fill": "off", "protection_trigger": "off", "protection_trigger_global": "on", + "strategy_msg": "off", "show_candle": "off" }, "reload": true, @@ -109,7 +110,8 @@ Example configuration showing the different settings: `exit` notifications are sent when the order is placed, while `exit_fill` notifications are sent when the order is filled on the exchange. `*_fill` notifications are off by default and must be explicitly enabled. `protection_trigger` notifications are sent when a protection triggers and `protection_trigger_global` notifications trigger when global protections are triggered. -`show_candle` - show candle values as part of entry/exit messages. Only possible value is "ohlc". +`strategy_msg` - Receive notifications from the strategy, sent via `self.dp.send_msg()` from the strategy [more details](strategy-customization.md#send-notification). +`show_candle` - show candle values as part of entry/exit messages. Only possible values are `"ohlc"` or `"off"`. `balance_dust_level` will define what the `/balance` command takes as "dust" - Currencies with a balance below this will be shown. `reload` allows you to disable reload-buttons on selected messages. diff --git a/freqtrade/constants.py b/freqtrade/constants.py index 6d74ceafd..1d83d21a0 100644 --- a/freqtrade/constants.py +++ b/freqtrade/constants.py @@ -317,6 +317,10 @@ CONF_SCHEMA = { 'type': 'string', 'enum': ['off', 'ohlc'], }, + 'strategy_msg': { + 'type': 'string', + 'enum': TELEGRAM_SETTING_OPTIONS, + }, } }, 'reload': {'type': 'boolean'}, diff --git a/freqtrade/data/dataprovider.py b/freqtrade/data/dataprovider.py index 800254533..e21f10193 100644 --- a/freqtrade/data/dataprovider.py +++ b/freqtrade/data/dataprovider.py @@ -36,8 +36,9 @@ class DataProvider: self.__slice_index: Optional[int] = None self.__cached_pairs_backtesting: Dict[PairWithTimeframe, DataFrame] = {} self._msg_queue: deque = deque() + self.__msg_cache = PeriodicCache( - maxsize=1000, ttl=timeframe_to_seconds(self._config['timeframe'])) + maxsize=1000, ttl=timeframe_to_seconds(self._config.get('timeframe', '1h'))) def _set_dataframe_max_index(self, limit_index: int): """ @@ -271,9 +272,10 @@ class DataProvider: raise OperationalException(NO_EXCHANGE_EXCEPTION) return self._exchange.fetch_l2_order_book(pair, maximum) - def send_msg(self, message: str, always_send: bool = False) -> None: + def send_msg(self, message: str, *, always_send: bool = False) -> None: """ - TODO: Document me + Send custom RPC Notifications from your bot. + Will not send any bot in modes other than Dry-run or Live. :param message: Message to be sent. Must be below 4096. :param always_send: If False, will send the message only once per candle, and surpress identical messages. From 0adfa4d9efe26d9423d769ca8829e723569c0545 Mon Sep 17 00:00:00 2001 From: Matthias Date: Wed, 27 Jul 2022 06:32:23 +0200 Subject: [PATCH 16/33] Add tests for dataprovider send-message methods --- tests/data/test_dataprovider.py | 19 +++++++++++++++++++ tests/rpc/test_rpc_manager.py | 21 +++++++++++++++++++-- tests/rpc/test_rpc_telegram.py | 10 ++++++++++ tests/test_freqtradebot.py | 6 ++++++ 4 files changed, 54 insertions(+), 2 deletions(-) diff --git a/tests/data/test_dataprovider.py b/tests/data/test_dataprovider.py index 93f82de5d..843d60786 100644 --- a/tests/data/test_dataprovider.py +++ b/tests/data/test_dataprovider.py @@ -311,3 +311,22 @@ def test_no_exchange_mode(default_conf): with pytest.raises(OperationalException, match=message): dp.available_pairs() + + +def test_dp_send_msg(default_conf): + + default_conf["runmode"] = RunMode.DRY_RUN + + default_conf["timeframe"] = '1h' + dp = DataProvider(default_conf, None) + msg = 'Test message' + dp.send_msg(msg) + + assert msg in dp._msg_queue + dp._msg_queue.pop() + assert msg not in dp._msg_queue + # Message is not resent due to caching + dp.send_msg(msg) + assert msg not in dp._msg_queue + dp.send_msg(msg, always_send=True) + assert msg in dp._msg_queue diff --git a/tests/rpc/test_rpc_manager.py b/tests/rpc/test_rpc_manager.py index 596b5ae20..b9ae16a20 100644 --- a/tests/rpc/test_rpc_manager.py +++ b/tests/rpc/test_rpc_manager.py @@ -1,6 +1,7 @@ # pragma pylint: disable=missing-docstring, C0103 import logging import time +from collections import deque from unittest.mock import MagicMock from freqtrade.enums import RPCMessageType @@ -81,9 +82,25 @@ def test_send_msg_telegram_disabled(mocker, default_conf, caplog) -> None: assert telegram_mock.call_count == 0 +def test_process_msg_queue(mocker, default_conf, caplog) -> None: + telegram_mock = mocker.patch('freqtrade.rpc.telegram.Telegram.send_msg') + mocker.patch('freqtrade.rpc.telegram.Telegram._init') + + freqtradebot = get_patched_freqtradebot(mocker, default_conf) + rpc_manager = RPCManager(freqtradebot) + queue = deque() + queue.append('Test message') + queue.append('Test message 2') + rpc_manager.process_msg_queue(queue) + + assert log_has("Sending rpc message: {'type': strategy_msg, 'msg': 'Test message'}", caplog) + assert log_has("Sending rpc message: {'type': strategy_msg, 'msg': 'Test message 2'}", caplog) + assert telegram_mock.call_count == 2 + + def test_send_msg_telegram_enabled(mocker, default_conf, caplog) -> None: - telegram_mock = mocker.patch('freqtrade.rpc.telegram.Telegram.send_msg', MagicMock()) - mocker.patch('freqtrade.rpc.telegram.Telegram._init', MagicMock()) + telegram_mock = mocker.patch('freqtrade.rpc.telegram.Telegram.send_msg') + mocker.patch('freqtrade.rpc.telegram.Telegram._init') freqtradebot = get_patched_freqtradebot(mocker, default_conf) rpc_manager = RPCManager(freqtradebot) diff --git a/tests/rpc/test_rpc_telegram.py b/tests/rpc/test_rpc_telegram.py index f69b7e878..8d244f3fd 100644 --- a/tests/rpc/test_rpc_telegram.py +++ b/tests/rpc/test_rpc_telegram.py @@ -1994,6 +1994,16 @@ def test_startup_notification(default_conf, mocker) -> None: assert msg_mock.call_args[0][0] == '*Custom:* `Hello World`' +def test_send_msg_strategy_msg_notification(default_conf, mocker) -> None: + + telegram, _, msg_mock = get_telegram_testobject(mocker, default_conf) + telegram.send_msg({ + 'type': RPCMessageType.STRATEGY_MSG, + 'msg': 'hello world, Test msg' + }) + assert msg_mock.call_args[0][0] == 'hello world, Test msg' + + def test_send_msg_unknown_type(default_conf, mocker) -> None: telegram, _, _ = get_telegram_testobject(mocker, default_conf) with pytest.raises(NotImplementedError, match=r'Unknown message type: None'): diff --git a/tests/test_freqtradebot.py b/tests/test_freqtradebot.py index 66cbd7d9b..438a2704c 100644 --- a/tests/test_freqtradebot.py +++ b/tests/test_freqtradebot.py @@ -68,6 +68,12 @@ def test_process_stopped(mocker, default_conf_usdt) -> None: assert coo_mock.call_count == 1 +def test_process_calls_sendmsg(mocker, default_conf_usdt) -> None: + freqtrade = get_patched_freqtradebot(mocker, default_conf_usdt) + freqtrade.process() + assert freqtrade.rpc.process_msg_queue.call_count == 1 + + def test_bot_cleanup(mocker, default_conf_usdt, caplog) -> None: mock_cleanup = mocker.patch('freqtrade.freqtradebot.cleanup_db') coo_mock = mocker.patch('freqtrade.freqtradebot.FreqtradeBot.cancel_all_open_orders') From 2595e40e47f72970974fff7cc27331bf2387975a Mon Sep 17 00:00:00 2001 From: Matthias Date: Wed, 27 Jul 2022 06:47:16 +0200 Subject: [PATCH 17/33] Remove unused test-strategy --- tests/rpc/test_rpc_apiserver.py | 1 - .../strats/strategy_test_v3_analysis.py | 175 ------------------ tests/strategy/test_strategy_loading.py | 6 +- 3 files changed, 3 insertions(+), 179 deletions(-) delete mode 100644 tests/strategy/strats/strategy_test_v3_analysis.py diff --git a/tests/rpc/test_rpc_apiserver.py b/tests/rpc/test_rpc_apiserver.py index 57c08f48e..57ba8e9f1 100644 --- a/tests/rpc/test_rpc_apiserver.py +++ b/tests/rpc/test_rpc_apiserver.py @@ -1402,7 +1402,6 @@ def test_api_strategies(botclient): 'InformativeDecoratorTest', 'StrategyTestV2', 'StrategyTestV3', - 'StrategyTestV3Analysis', 'StrategyTestV3Futures' ]} diff --git a/tests/strategy/strats/strategy_test_v3_analysis.py b/tests/strategy/strats/strategy_test_v3_analysis.py deleted file mode 100644 index 290fef156..000000000 --- a/tests/strategy/strats/strategy_test_v3_analysis.py +++ /dev/null @@ -1,175 +0,0 @@ -# pragma pylint: disable=missing-docstring, invalid-name, pointless-string-statement - -import talib.abstract as ta -from pandas import DataFrame - -import freqtrade.vendor.qtpylib.indicators as qtpylib -from freqtrade.strategy import (BooleanParameter, DecimalParameter, IntParameter, IStrategy, - RealParameter) - - -class StrategyTestV3Analysis(IStrategy): - """ - Strategy used by tests freqtrade bot. - Please do not modify this strategy, it's intended for internal use only. - Please look at the SampleStrategy in the user_data/strategy directory - or strategy repository https://github.com/freqtrade/freqtrade-strategies - for samples and inspiration. - """ - INTERFACE_VERSION = 3 - - # Minimal ROI designed for the strategy - minimal_roi = { - "40": 0.0, - "30": 0.01, - "20": 0.02, - "0": 0.04 - } - - # Optimal stoploss designed for the strategy - stoploss = -0.10 - - # Optimal timeframe for the strategy - timeframe = '5m' - - # Optional order type mapping - order_types = { - 'entry': 'limit', - 'exit': 'limit', - 'stoploss': 'limit', - 'stoploss_on_exchange': False - } - - # Number of candles the strategy requires before producing valid signals - startup_candle_count: int = 20 - - # Optional time in force for orders - order_time_in_force = { - 'entry': 'gtc', - 'exit': 'gtc', - } - - buy_params = { - 'buy_rsi': 35, - # Intentionally not specified, so "default" is tested - # 'buy_plusdi': 0.4 - } - - sell_params = { - 'sell_rsi': 74, - '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', - load=False) - protection_enabled = BooleanParameter(default=True) - protection_cooldown_lookback = IntParameter([0, 50], default=30) - - # TODO: Can this work with protection tests? (replace HyperoptableStrategy implicitly ... ) - # @property - # def protections(self): - # prot = [] - # if self.protection_enabled.value: - # prot.append({ - # "method": "CooldownPeriod", - # "stop_duration_candles": self.protection_cooldown_lookback.value - # }) - # return prot - - bot_started = False - - def bot_start(self): - self.bot_started = True - - def informative_pairs(self): - - return [] - - def populate_indicators(self, dataframe: DataFrame, metadata: dict) -> DataFrame: - - # Momentum Indicator - # ------------------------------------ - - # ADX - dataframe['adx'] = ta.ADX(dataframe) - - # MACD - macd = ta.MACD(dataframe) - dataframe['macd'] = macd['macd'] - dataframe['macdsignal'] = macd['macdsignal'] - dataframe['macdhist'] = macd['macdhist'] - - # Minus Directional Indicator / Movement - dataframe['minus_di'] = ta.MINUS_DI(dataframe) - - # Plus Directional Indicator / Movement - dataframe['plus_di'] = ta.PLUS_DI(dataframe) - - # RSI - dataframe['rsi'] = ta.RSI(dataframe) - - # Stoch fast - stoch_fast = ta.STOCHF(dataframe) - dataframe['fastd'] = stoch_fast['fastd'] - dataframe['fastk'] = stoch_fast['fastk'] - - # Bollinger bands - bollinger = qtpylib.bollinger_bands(qtpylib.typical_price(dataframe), window=20, stds=2) - dataframe['bb_lowerband'] = bollinger['lower'] - dataframe['bb_middleband'] = bollinger['mid'] - dataframe['bb_upperband'] = bollinger['upper'] - - # EMA - Exponential Moving Average - dataframe['ema10'] = ta.EMA(dataframe, timeperiod=10) - - return dataframe - - def populate_entry_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame: - - dataframe.loc[ - ( - (dataframe['rsi'] < self.buy_rsi.value) & - (dataframe['fastd'] < 35) & - (dataframe['adx'] > 30) & - (dataframe['plus_di'] > self.buy_plusdi.value) - ) | - ( - (dataframe['adx'] > 65) & - (dataframe['plus_di'] > self.buy_plusdi.value) - ), - ['enter_long', 'enter_tag']] = 1, 'enter_tag_long' - - dataframe.loc[ - ( - qtpylib.crossed_below(dataframe['rsi'], self.sell_rsi.value) - ), - ['enter_short', 'enter_tag']] = 1, 'enter_tag_short' - - return dataframe - - def populate_exit_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame: - dataframe.loc[ - ( - ( - (qtpylib.crossed_above(dataframe['rsi'], self.sell_rsi.value)) | - (qtpylib.crossed_above(dataframe['fastd'], 70)) - ) & - (dataframe['adx'] > 10) & - (dataframe['minus_di'] > 0) - ) | - ( - (dataframe['adx'] > 70) & - (dataframe['minus_di'] > self.sell_minusdi.value) - ), - ['exit_long', 'exit_tag']] = 1, 'exit_tag_long' - - dataframe.loc[ - ( - qtpylib.crossed_above(dataframe['rsi'], self.buy_rsi.value) - ), - ['exit_long', 'exit_tag']] = 1, 'exit_tag_short' - - return dataframe diff --git a/tests/strategy/test_strategy_loading.py b/tests/strategy/test_strategy_loading.py index bdfcf3211..666ae2b05 100644 --- a/tests/strategy/test_strategy_loading.py +++ b/tests/strategy/test_strategy_loading.py @@ -34,7 +34,7 @@ def test_search_all_strategies_no_failed(): directory = Path(__file__).parent / "strats" strategies = StrategyResolver.search_all_objects(directory, enum_failed=False) assert isinstance(strategies, list) - assert len(strategies) == 7 + assert len(strategies) == 6 assert isinstance(strategies[0], dict) @@ -42,10 +42,10 @@ def test_search_all_strategies_with_failed(): directory = Path(__file__).parent / "strats" strategies = StrategyResolver.search_all_objects(directory, enum_failed=True) assert isinstance(strategies, list) - assert len(strategies) == 8 + assert len(strategies) == 7 # with enum_failed=True search_all_objects() shall find 2 good strategies # and 1 which fails to load - assert len([x for x in strategies if x['class'] is not None]) == 7 + assert len([x for x in strategies if x['class'] is not None]) == 6 assert len([x for x in strategies if x['class'] is None]) == 1 From 31ddec834816d980d2802810868b62f44df787d1 Mon Sep 17 00:00:00 2001 From: Matthias Date: Wed, 27 Jul 2022 06:51:56 +0200 Subject: [PATCH 18/33] Add missing test to confirm backtesting won't send messages --- tests/data/test_dataprovider.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/tests/data/test_dataprovider.py b/tests/data/test_dataprovider.py index 843d60786..49603feac 100644 --- a/tests/data/test_dataprovider.py +++ b/tests/data/test_dataprovider.py @@ -330,3 +330,8 @@ def test_dp_send_msg(default_conf): assert msg not in dp._msg_queue dp.send_msg(msg, always_send=True) assert msg in dp._msg_queue + + default_conf["runmode"] = RunMode.BACKTEST + dp = DataProvider(default_conf, None) + dp.send_msg(msg, always_send=True) + assert msg not in dp._msg_queue From cc3ead9d7bd289bfceb97a8c165b6b519b4710c4 Mon Sep 17 00:00:00 2001 From: Matthias Date: Wed, 27 Jul 2022 19:52:39 +0200 Subject: [PATCH 19/33] Set required_profit for stoploss guard, allowing to ignore small stoplosses. closes #7076 --- docs/includes/protections.md | 3 +++ freqtrade/plugins/protections/stoploss_guard.py | 5 +++-- tests/plugins/test_protections.py | 6 +++--- 3 files changed, 9 insertions(+), 5 deletions(-) diff --git a/docs/includes/protections.md b/docs/includes/protections.md index d67924cfe..e0ad8189f 100644 --- a/docs/includes/protections.md +++ b/docs/includes/protections.md @@ -50,6 +50,8 @@ This applies across all pairs, unless `only_per_pair` is set to true, which will Similarly, this protection will by default look at all trades (long and short). For futures bots, setting `only_per_side` will make the bot only consider one side, and will then only lock this one side, allowing for example shorts to continue after a series of long stoplosses. +`required_profit` will determine the required relative profit (or loss) for stoplosses to consider. This should normally not be set and defaults to 0.0 - which means all losing stoplosses will be triggering a block. + The below example stops trading for all pairs for 4 candles after the last trade if the bot hit stoploss 4 times within the last 24 candles. ``` python @@ -61,6 +63,7 @@ def protections(self): "lookback_period_candles": 24, "trade_limit": 4, "stop_duration_candles": 4, + "required_profit": 0.0, "only_per_pair": False, "only_per_side": False } diff --git a/freqtrade/plugins/protections/stoploss_guard.py b/freqtrade/plugins/protections/stoploss_guard.py index 713a2da07..abc90a685 100644 --- a/freqtrade/plugins/protections/stoploss_guard.py +++ b/freqtrade/plugins/protections/stoploss_guard.py @@ -23,13 +23,14 @@ class StoplossGuard(IProtection): self._trade_limit = protection_config.get('trade_limit', 10) self._disable_global_stop = protection_config.get('only_per_pair', False) self._only_per_side = protection_config.get('only_per_side', False) + self._profit_limit = protection_config.get('required_profit', 0.0) def short_desc(self) -> str: """ Short method description - used for startup-messages """ return (f"{self.name} - Frequent Stoploss Guard, {self._trade_limit} stoplosses " - f"within {self.lookback_period_str}.") + f"with profit < {self._profit_limit:.2%} within {self.lookback_period_str}.") def _reason(self) -> str: """ @@ -49,7 +50,7 @@ class StoplossGuard(IProtection): trades = [trade for trade in trades1 if (str(trade.exit_reason) in ( ExitType.TRAILING_STOP_LOSS.value, ExitType.STOP_LOSS.value, ExitType.STOPLOSS_ON_EXCHANGE.value) - and trade.close_profit and trade.close_profit < 0)] + and trade.close_profit and trade.close_profit < self._profit_limit)] if self._only_per_side: # Long or short trades only diff --git a/tests/plugins/test_protections.py b/tests/plugins/test_protections.py index 3c333200c..4cebb6492 100644 --- a/tests/plugins/test_protections.py +++ b/tests/plugins/test_protections.py @@ -424,7 +424,7 @@ def test_MaxDrawdown(mocker, default_conf, fee, caplog): @pytest.mark.parametrize("protectionconf,desc_expected,exception_expected", [ ({"method": "StoplossGuard", "lookback_period": 60, "trade_limit": 2, "stop_duration": 60}, "[{'StoplossGuard': 'StoplossGuard - Frequent Stoploss Guard, " - "2 stoplosses within 60 minutes.'}]", + "2 stoplosses with profit < 0.00% within 60 minutes.'}]", None ), ({"method": "CooldownPeriod", "stop_duration": 60}, @@ -442,9 +442,9 @@ def test_MaxDrawdown(mocker, default_conf, fee, caplog): None ), ({"method": "StoplossGuard", "lookback_period_candles": 12, "trade_limit": 2, - "stop_duration": 60}, + "required_profit": -0.05, "stop_duration": 60}, "[{'StoplossGuard': 'StoplossGuard - Frequent Stoploss Guard, " - "2 stoplosses within 12 candles.'}]", + "2 stoplosses with profit < -5.00% within 12 candles.'}]", None ), ({"method": "CooldownPeriod", "stop_duration_candles": 5}, From d70650b074e023c649a04ce15ca84946b3409eb6 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 30 Jul 2022 08:20:22 +0200 Subject: [PATCH 20/33] Add note for plot-dataframe and current-whitelist closes #7142 --- docs/strategy-customization.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/docs/strategy-customization.md b/docs/strategy-customization.md index 6947380dd..78256e0ee 100644 --- a/docs/strategy-customization.md +++ b/docs/strategy-customization.md @@ -646,6 +646,9 @@ This is where calling `self.dp.current_whitelist()` comes in handy. return informative_pairs ``` +??? Note "Plotting with current_whitelist" + Current whitelist is not supported for `plot-dataframe`, as this command is usually used by providing an explicit pairlist - and would therefore make the return values of this method misleading. + ### *get_pair_dataframe(pair, timeframe)* ``` python From 995be90f91a21c351a5c22264135694f3e11d62b Mon Sep 17 00:00:00 2001 From: Matthias Date: Thu, 28 Jul 2022 20:35:23 +0200 Subject: [PATCH 21/33] Liquidation should be a separate exit type --- freqtrade/enums/exittype.py | 1 + freqtrade/optimize/backtesting.py | 14 ++++++++++---- freqtrade/persistence/trade_model.py | 12 ++---------- freqtrade/plugins/protections/stoploss_guard.py | 2 +- freqtrade/strategy/interface.py | 13 ++++++++++++- 5 files changed, 26 insertions(+), 16 deletions(-) diff --git a/freqtrade/enums/exittype.py b/freqtrade/enums/exittype.py index b2c5b62ea..1e15e70cd 100644 --- a/freqtrade/enums/exittype.py +++ b/freqtrade/enums/exittype.py @@ -9,6 +9,7 @@ class ExitType(Enum): STOP_LOSS = "stop_loss" STOPLOSS_ON_EXCHANGE = "stoploss_on_exchange" TRAILING_STOP_LOSS = "trailing_stop_loss" + LIQUIDATION = "liquidation" EXIT_SIGNAL = "exit_signal" FORCE_EXIT = "force_exit" EMERGENCY_EXIT = "emergency_exit" diff --git a/freqtrade/optimize/backtesting.py b/freqtrade/optimize/backtesting.py index 4d16dc0f1..598ce6710 100755 --- a/freqtrade/optimize/backtesting.py +++ b/freqtrade/optimize/backtesting.py @@ -381,7 +381,8 @@ class Backtesting: Get close rate for backtesting result """ # Special handling if high or low hit STOP_LOSS or ROI - if exit.exit_type in (ExitType.STOP_LOSS, ExitType.TRAILING_STOP_LOSS): + if exit.exit_type in ( + ExitType.STOP_LOSS, ExitType.TRAILING_STOP_LOSS, ExitType.LIQUIDATION): return self._get_close_rate_for_stoploss(row, trade, exit, trade_dur) elif exit.exit_type == (ExitType.ROI): return self._get_close_rate_for_roi(row, trade, exit, trade_dur) @@ -396,11 +397,16 @@ class Backtesting: is_short = trade.is_short or False leverage = trade.leverage or 1.0 side_1 = -1 if is_short else 1 + if exit.exit_type == ExitType.LIQUIDATION and trade.liquidation_price: + stoploss_value = trade.liquidation_price + else: + stoploss_value = trade.stop_loss + if is_short: - if trade.stop_loss < row[LOW_IDX]: + if stoploss_value < row[LOW_IDX]: return row[OPEN_IDX] else: - if trade.stop_loss > row[HIGH_IDX]: + if stoploss_value > row[HIGH_IDX]: return row[OPEN_IDX] # Special case: trailing triggers within same candle as trade opened. Assume most @@ -433,7 +439,7 @@ class Backtesting: return max(row[LOW_IDX], stop_rate) # Set close_rate to stoploss - return trade.stop_loss + return stoploss_value def _get_close_rate_for_roi(self, row: Tuple, trade: LocalTrade, exit: ExitCheckTuple, trade_dur: int) -> float: diff --git a/freqtrade/persistence/trade_model.py b/freqtrade/persistence/trade_model.py index 5f302de71..2ff65d9d0 100644 --- a/freqtrade/persistence/trade_model.py +++ b/freqtrade/persistence/trade_model.py @@ -511,17 +511,9 @@ class LocalTrade(): Method you should use to set self.stop_loss. Assures stop_loss is not passed the liquidation price """ - if self.liquidation_price is not None: - if self.is_short: - sl = min(stop_loss, self.liquidation_price) - else: - sl = max(stop_loss, self.liquidation_price) - else: - sl = stop_loss - if not self.stop_loss: - self.initial_stop_loss = sl - self.stop_loss = sl + self.initial_stop_loss = stop_loss + self.stop_loss = stop_loss self.stop_loss_pct = -1 * abs(percent) self.stoploss_last_update = datetime.utcnow() diff --git a/freqtrade/plugins/protections/stoploss_guard.py b/freqtrade/plugins/protections/stoploss_guard.py index abc90a685..e80d13e9d 100644 --- a/freqtrade/plugins/protections/stoploss_guard.py +++ b/freqtrade/plugins/protections/stoploss_guard.py @@ -49,7 +49,7 @@ class StoplossGuard(IProtection): trades1 = Trade.get_trades_proxy(pair=pair, is_open=False, close_date=look_back_until) trades = [trade for trade in trades1 if (str(trade.exit_reason) in ( ExitType.TRAILING_STOP_LOSS.value, ExitType.STOP_LOSS.value, - ExitType.STOPLOSS_ON_EXCHANGE.value) + ExitType.STOPLOSS_ON_EXCHANGE.value, ExitType.LIQUIDATION.value) and trade.close_profit and trade.close_profit < self._profit_limit)] if self._only_per_side: diff --git a/freqtrade/strategy/interface.py b/freqtrade/strategy/interface.py index c60817c99..f50721583 100644 --- a/freqtrade/strategy/interface.py +++ b/freqtrade/strategy/interface.py @@ -963,7 +963,7 @@ class IStrategy(ABC, HyperStrategyMixin): # ROI # Trailing stoploss - if stoplossflag.exit_type == ExitType.STOP_LOSS: + if stoplossflag.exit_type in (ExitType.STOP_LOSS, ExitType.LIQUIDATION): logger.debug(f"{trade.pair} - Stoploss hit. exit_type={stoplossflag.exit_type}") exits.append(stoplossflag) @@ -1035,6 +1035,17 @@ class IStrategy(ABC, HyperStrategyMixin): sl_higher_long = (trade.stop_loss >= (low or current_rate) and not trade.is_short) sl_lower_short = (trade.stop_loss <= (high or current_rate) and trade.is_short) + liq_higher_long = (trade.liquidation_price + and trade.liquidation_price >= (low or current_rate) + and not trade.is_short) + liq_lower_short = (trade.liquidation_price + and trade.liquidation_price <= (high or current_rate) + and trade.is_short) + + if (liq_higher_long or liq_lower_short): + logger.debug(f"{trade.pair} - Liquidation price hit. exit_type=ExitType.LIQUIDATION") + return ExitCheckTuple(exit_type=ExitType.LIQUIDATION) + # evaluate if the stoploss was hit if stoploss is not on exchange # in Dry-Run, this handles stoploss logic as well, as the logic will not be different to # regular stoploss handling. From 8711b7d99f224b3d0ee26553e32774dd247d605a Mon Sep 17 00:00:00 2001 From: Matthias Date: Thu, 28 Jul 2022 20:37:10 +0200 Subject: [PATCH 22/33] Liquidations cannot be rejected. --- freqtrade/freqtradebot.py | 5 +++-- freqtrade/optimize/backtesting.py | 5 +++-- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index 43608cae7..d58c05d7f 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -1463,11 +1463,12 @@ class FreqtradeBot(LoggingMixin): amount = self._safe_exit_amount(trade.pair, trade.amount) time_in_force = self.strategy.order_time_in_force['exit'] - if not strategy_safe_wrapper(self.strategy.confirm_trade_exit, default_retval=True)( + if (exit_check.exit_type != ExitType.LIQUIDATION and not strategy_safe_wrapper( + self.strategy.confirm_trade_exit, default_retval=True)( pair=trade.pair, trade=trade, order_type=order_type, amount=amount, rate=limit, time_in_force=time_in_force, exit_reason=exit_reason, sell_reason=exit_reason, # sellreason -> compatibility - current_time=datetime.now(timezone.utc)): + current_time=datetime.now(timezone.utc))): logger.info(f"User denied exit for {trade.pair}.") return False diff --git a/freqtrade/optimize/backtesting.py b/freqtrade/optimize/backtesting.py index 598ce6710..6bbace185 100755 --- a/freqtrade/optimize/backtesting.py +++ b/freqtrade/optimize/backtesting.py @@ -598,7 +598,8 @@ class Backtesting: # Confirm trade exit: time_in_force = self.strategy.order_time_in_force['exit'] - if not strategy_safe_wrapper(self.strategy.confirm_trade_exit, default_retval=True)( + if (exit_.exit_type != ExitType.LIQUIDATION and not strategy_safe_wrapper( + self.strategy.confirm_trade_exit, default_retval=True)( pair=trade.pair, trade=trade, # type: ignore[arg-type] order_type='limit', @@ -607,7 +608,7 @@ class Backtesting: time_in_force=time_in_force, sell_reason=exit_reason, # deprecated exit_reason=exit_reason, - current_time=exit_candle_time): + current_time=exit_candle_time)): return None trade.exit_reason = exit_reason From f57ecb18615dc2e75fc73d0da78540ea0ebd6804 Mon Sep 17 00:00:00 2001 From: Matthias Date: Fri, 29 Jul 2022 06:55:42 +0200 Subject: [PATCH 23/33] Simplify adjust_stop test --- tests/test_persistence.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/tests/test_persistence.py b/tests/test_persistence.py index 838c4c22a..4703075eb 100644 --- a/tests/test_persistence.py +++ b/tests/test_persistence.py @@ -1537,26 +1537,26 @@ def test_adjust_stop_loss(fee): # Get percent of profit with a custom rate (Higher than open rate) trade.adjust_stop_loss(1.3, -0.1) - assert round(trade.stop_loss, 8) == 1.17 + assert pytest.approx(trade.stop_loss) == 1.17 assert trade.stop_loss_pct == -0.1 assert trade.initial_stop_loss == 0.95 assert trade.initial_stop_loss_pct == -0.05 # current rate lower again ... should not change trade.adjust_stop_loss(1.2, 0.1) - assert round(trade.stop_loss, 8) == 1.17 + assert pytest.approx(trade.stop_loss) == 1.17 assert trade.initial_stop_loss == 0.95 assert trade.initial_stop_loss_pct == -0.05 # current rate higher... should raise stoploss trade.adjust_stop_loss(1.4, 0.1) - assert round(trade.stop_loss, 8) == 1.26 + assert pytest.approx(trade.stop_loss) == 1.26 assert trade.initial_stop_loss == 0.95 assert trade.initial_stop_loss_pct == -0.05 # Initial is true but stop_loss set - so doesn't do anything trade.adjust_stop_loss(1.7, 0.1, True) - assert round(trade.stop_loss, 8) == 1.26 + assert pytest.approx(trade.stop_loss) == 1.26 assert trade.initial_stop_loss == 0.95 assert trade.initial_stop_loss_pct == -0.05 assert trade.stop_loss_pct == -0.1 From 9852733ef7f7284765b8092eb6cd27edd2b7c0e8 Mon Sep 17 00:00:00 2001 From: Matthias Date: Fri, 29 Jul 2022 06:58:59 +0200 Subject: [PATCH 24/33] Improve tests to align with modified logic --- freqtrade/persistence/trade_model.py | 6 ------ tests/test_persistence.py | 17 ++++++++++------- 2 files changed, 10 insertions(+), 13 deletions(-) diff --git a/freqtrade/persistence/trade_model.py b/freqtrade/persistence/trade_model.py index 2ff65d9d0..919750886 100644 --- a/freqtrade/persistence/trade_model.py +++ b/freqtrade/persistence/trade_model.py @@ -535,14 +535,8 @@ class LocalTrade(): leverage = self.leverage or 1.0 if self.is_short: new_loss = float(current_price * (1 + abs(stoploss / leverage))) - # If trading with leverage, don't set the stoploss below the liquidation price - if self.liquidation_price: - new_loss = min(self.liquidation_price, new_loss) else: new_loss = float(current_price * (1 - abs(stoploss / leverage))) - # If trading with leverage, don't set the stoploss below the liquidation price - if self.liquidation_price: - new_loss = max(self.liquidation_price, new_loss) # no stop loss assigned yet if self.initial_stop_loss_pct is None or refresh: diff --git a/tests/test_persistence.py b/tests/test_persistence.py index 4703075eb..5476e1d50 100644 --- a/tests/test_persistence.py +++ b/tests/test_persistence.py @@ -133,13 +133,14 @@ def test_set_stop_loss_isolated_liq(fee): trade.set_isolated_liq(0.11) trade._set_stop_loss(0.1, 0) assert trade.liquidation_price == 0.11 - assert trade.stop_loss == 0.11 + # Stoploss does not change from liquidation price + assert trade.stop_loss == 0.1 assert trade.initial_stop_loss == 0.1 # lower stop doesn't move stoploss trade._set_stop_loss(0.1, 0) assert trade.liquidation_price == 0.11 - assert trade.stop_loss == 0.11 + assert trade.stop_loss == 0.1 assert trade.initial_stop_loss == 0.1 trade.stop_loss = None @@ -174,13 +175,14 @@ def test_set_stop_loss_isolated_liq(fee): trade.set_isolated_liq(0.07) trade._set_stop_loss(0.1, (1.0 / 8.0)) assert trade.liquidation_price == 0.07 - assert trade.stop_loss == 0.07 + # Stoploss does not change from liquidation price + assert trade.stop_loss == 0.1 assert trade.initial_stop_loss == 0.08 # Stop doesn't move stop higher trade._set_stop_loss(0.1, (1.0 / 9.0)) assert trade.liquidation_price == 0.07 - assert trade.stop_loss == 0.07 + assert trade.stop_loss == 0.1 assert trade.initial_stop_loss == 0.08 @@ -1609,9 +1611,10 @@ def test_adjust_stop_loss_short(fee): assert trade.initial_stop_loss == 1.05 assert trade.initial_stop_loss_pct == -0.05 assert trade.stop_loss_pct == -0.1 + # Liquidation price is lower than stoploss - so liquidation would trigger first. trade.set_isolated_liq(0.63) trade.adjust_stop_loss(0.59, -0.1) - assert trade.stop_loss == 0.63 + assert trade.stop_loss == 0.649 assert trade.liquidation_price == 0.63 @@ -2011,8 +2014,8 @@ def test_stoploss_reinitialization_short(default_conf, fee): # Stoploss can't go above liquidation price trade_adj.set_isolated_liq(0.985) trade.adjust_stop_loss(0.9799, -0.05) - assert trade_adj.stop_loss == 0.985 - assert trade_adj.stop_loss == 0.985 + assert trade_adj.stop_loss == 0.989699 + assert trade_adj.liquidation_price == 0.985 def test_update_fee(fee): From ff4cc5d3165b53058c1b80f8f94bb16df2345269 Mon Sep 17 00:00:00 2001 From: Matthias Date: Fri, 29 Jul 2022 07:14:25 +0200 Subject: [PATCH 25/33] Revamp liquidation test to actually make sense --- tests/test_persistence.py | 79 +++++++++++++++++++++++---------------- 1 file changed, 46 insertions(+), 33 deletions(-) diff --git a/tests/test_persistence.py b/tests/test_persistence.py index 5476e1d50..5dbd6b86b 100644 --- a/tests/test_persistence.py +++ b/tests/test_persistence.py @@ -120,70 +120,83 @@ def test_set_stop_loss_isolated_liq(fee): assert trade.stop_loss is None assert trade.initial_stop_loss is None - trade._set_stop_loss(0.1, (1.0 / 9.0)) + trade.adjust_stop_loss(2.0, 0.2, True) assert trade.liquidation_price == 0.09 - assert trade.stop_loss == 0.1 - assert trade.initial_stop_loss == 0.1 + assert trade.stop_loss == 1.8 + assert trade.initial_stop_loss == 1.8 trade.set_isolated_liq(0.08) assert trade.liquidation_price == 0.08 - assert trade.stop_loss == 0.1 - assert trade.initial_stop_loss == 0.1 + assert trade.stop_loss == 1.8 + assert trade.initial_stop_loss == 1.8 trade.set_isolated_liq(0.11) - trade._set_stop_loss(0.1, 0) + trade.adjust_stop_loss(2.0, 0.2) assert trade.liquidation_price == 0.11 # Stoploss does not change from liquidation price - assert trade.stop_loss == 0.1 - assert trade.initial_stop_loss == 0.1 + assert trade.stop_loss == 1.8 + assert trade.initial_stop_loss == 1.8 # lower stop doesn't move stoploss - trade._set_stop_loss(0.1, 0) + trade.adjust_stop_loss(1.8, 0.2) assert trade.liquidation_price == 0.11 - assert trade.stop_loss == 0.1 - assert trade.initial_stop_loss == 0.1 + assert trade.stop_loss == 1.8 + assert trade.initial_stop_loss == 1.8 + + # higher stop does move stoploss + trade.adjust_stop_loss(2.1, 0.1) + assert trade.liquidation_price == 0.11 + assert pytest.approx(trade.stop_loss) == 1.994999 + assert trade.initial_stop_loss == 1.8 trade.stop_loss = None trade.liquidation_price = None trade.initial_stop_loss = None + trade.initial_stop_loss_pct = None - trade._set_stop_loss(0.07, 0) + trade.adjust_stop_loss(2.0, 0.1, True) assert trade.liquidation_price is None - assert trade.stop_loss == 0.07 - assert trade.initial_stop_loss == 0.07 + assert trade.stop_loss == 1.9 + assert trade.initial_stop_loss == 1.9 trade.is_short = True trade.recalc_open_trade_value() trade.stop_loss = None trade.initial_stop_loss = None + trade.initial_stop_loss_pct = None - trade.set_isolated_liq(0.09) - assert trade.liquidation_price == 0.09 + trade.set_isolated_liq(3.09) + assert trade.liquidation_price == 3.09 assert trade.stop_loss is None assert trade.initial_stop_loss is None - trade._set_stop_loss(0.08, (1.0 / 9.0)) - assert trade.liquidation_price == 0.09 - assert trade.stop_loss == 0.08 - assert trade.initial_stop_loss == 0.08 + trade.adjust_stop_loss(2.0, 0.2) + assert trade.liquidation_price == 3.09 + assert trade.stop_loss == 2.2 + assert trade.initial_stop_loss == 2.2 - trade.set_isolated_liq(0.1) - assert trade.liquidation_price == 0.1 - assert trade.stop_loss == 0.08 - assert trade.initial_stop_loss == 0.08 + trade.set_isolated_liq(3.1) + assert trade.liquidation_price == 3.1 + assert trade.stop_loss == 2.2 + assert trade.initial_stop_loss == 2.2 - trade.set_isolated_liq(0.07) - trade._set_stop_loss(0.1, (1.0 / 8.0)) - assert trade.liquidation_price == 0.07 + trade.set_isolated_liq(3.8) + assert trade.liquidation_price == 3.8 # Stoploss does not change from liquidation price - assert trade.stop_loss == 0.1 - assert trade.initial_stop_loss == 0.08 + assert trade.stop_loss == 2.2 + assert trade.initial_stop_loss == 2.2 # Stop doesn't move stop higher - trade._set_stop_loss(0.1, (1.0 / 9.0)) - assert trade.liquidation_price == 0.07 - assert trade.stop_loss == 0.1 - assert trade.initial_stop_loss == 0.08 + trade.adjust_stop_loss(2.0, 0.3) + assert trade.liquidation_price == 3.8 + assert trade.stop_loss == 2.2 + assert trade.initial_stop_loss == 2.2 + + # Stoploss does move lower + trade.adjust_stop_loss(1.8, 0.1) + assert trade.liquidation_price == 3.8 + assert pytest.approx(trade.stop_loss) == 1.89 + assert trade.initial_stop_loss == 2.2 @pytest.mark.parametrize('exchange,is_short,lev,minutes,rate,interest,trading_mode', [ From 15752ce3c2d4b2131e9141c3aa496dda25c5d9d7 Mon Sep 17 00:00:00 2001 From: Matthias Date: Fri, 29 Jul 2022 07:15:01 +0200 Subject: [PATCH 26/33] Rename set_stoploss method to be fully private --- freqtrade/persistence/trade_model.py | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/freqtrade/persistence/trade_model.py b/freqtrade/persistence/trade_model.py index 919750886..1b8bcc42f 100644 --- a/freqtrade/persistence/trade_model.py +++ b/freqtrade/persistence/trade_model.py @@ -506,10 +506,9 @@ class LocalTrade(): return self.liquidation_price = liquidation_price - def _set_stop_loss(self, stop_loss: float, percent: float): + def __set_stop_loss(self, stop_loss: float, percent: float): """ - Method you should use to set self.stop_loss. - Assures stop_loss is not passed the liquidation price + Method used internally to set self.stop_loss. """ if not self.stop_loss: self.initial_stop_loss = stop_loss @@ -540,7 +539,7 @@ class LocalTrade(): # no stop loss assigned yet if self.initial_stop_loss_pct is None or refresh: - self._set_stop_loss(new_loss, stoploss) + self.__set_stop_loss(new_loss, stoploss) self.initial_stop_loss = new_loss self.initial_stop_loss_pct = -1 * abs(stoploss) @@ -555,7 +554,7 @@ class LocalTrade(): # ? decreasing the minimum stoploss if (higher_stop and not self.is_short) or (lower_stop and self.is_short): logger.debug(f"{self.pair} - Adjusting stoploss...") - self._set_stop_loss(new_loss, stoploss) + self.__set_stop_loss(new_loss, stoploss) else: logger.debug(f"{self.pair} - Keeping current stoploss...") From 4da96bc511c1a01ee717f304d60696141797d200 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 30 Jul 2022 07:27:24 +0200 Subject: [PATCH 27/33] Update docs --- docs/strategy-callbacks.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/strategy-callbacks.md b/docs/strategy-callbacks.md index f584bd1bb..59d221bfc 100644 --- a/docs/strategy-callbacks.md +++ b/docs/strategy-callbacks.md @@ -623,6 +623,7 @@ class AwesomeStrategy(IStrategy): !!! Warning `confirm_trade_exit()` can prevent stoploss exits, causing significant losses as this would ignore stoploss exits. + `confirm_trade_exit()` will not be called for Liquidations - as liquidations are forced by the exchange, and therefore cannot be rejected. ## Adjust trade position From 845cecd38fe0acfed72d99eddccc5aea37234cfc Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 30 Jul 2022 08:12:48 +0200 Subject: [PATCH 28/33] Add stoploss or liquidation property --- freqtrade/persistence/trade_model.py | 10 ++++++++++ freqtrade/strategy/interface.py | 7 ------- tests/test_persistence.py | 10 +++++++++- 3 files changed, 19 insertions(+), 8 deletions(-) diff --git a/freqtrade/persistence/trade_model.py b/freqtrade/persistence/trade_model.py index 1b8bcc42f..244ca79cd 100644 --- a/freqtrade/persistence/trade_model.py +++ b/freqtrade/persistence/trade_model.py @@ -302,6 +302,16 @@ class LocalTrade(): # Futures properties funding_fees: Optional[float] = None + @property + def stoploss_or_liquidation(self) -> float: + if self.liquidation_price: + if self.is_short: + return min(self.stop_loss, self.liquidation_price) + else: + return max(self.stop_loss, self.liquidation_price) + + return self.stop_loss + @property def buy_tag(self) -> Optional[str]: """ diff --git a/freqtrade/strategy/interface.py b/freqtrade/strategy/interface.py index f50721583..824f31258 100644 --- a/freqtrade/strategy/interface.py +++ b/freqtrade/strategy/interface.py @@ -1063,13 +1063,6 @@ class IStrategy(ABC, HyperStrategyMixin): f"stoploss is {trade.stop_loss:.6f}, " f"initial stoploss was at {trade.initial_stop_loss:.6f}, " f"trade opened at {trade.open_rate:.6f}") - new_stoploss = ( - trade.stop_loss + trade.initial_stop_loss - if trade.is_short else - trade.stop_loss - trade.initial_stop_loss - ) - logger.debug(f"{trade.pair} - Trailing stop saved " - f"{new_stoploss:.6f}") return ExitCheckTuple(exit_type=exit_type) diff --git a/tests/test_persistence.py b/tests/test_persistence.py index 5dbd6b86b..3eca035c9 100644 --- a/tests/test_persistence.py +++ b/tests/test_persistence.py @@ -148,6 +148,7 @@ def test_set_stop_loss_isolated_liq(fee): assert trade.liquidation_price == 0.11 assert pytest.approx(trade.stop_loss) == 1.994999 assert trade.initial_stop_loss == 1.8 + assert trade.stoploss_or_liquidation == trade.stop_loss trade.stop_loss = None trade.liquidation_price = None @@ -158,6 +159,7 @@ def test_set_stop_loss_isolated_liq(fee): assert trade.liquidation_price is None assert trade.stop_loss == 1.9 assert trade.initial_stop_loss == 1.9 + assert trade.stoploss_or_liquidation == 1.9 trade.is_short = True trade.recalc_open_trade_value() @@ -174,11 +176,13 @@ def test_set_stop_loss_isolated_liq(fee): assert trade.liquidation_price == 3.09 assert trade.stop_loss == 2.2 assert trade.initial_stop_loss == 2.2 + assert trade.stoploss_or_liquidation == 2.2 trade.set_isolated_liq(3.1) assert trade.liquidation_price == 3.1 assert trade.stop_loss == 2.2 assert trade.initial_stop_loss == 2.2 + assert trade.stoploss_or_liquidation == 2.2 trade.set_isolated_liq(3.8) assert trade.liquidation_price == 3.8 @@ -193,10 +197,14 @@ def test_set_stop_loss_isolated_liq(fee): assert trade.initial_stop_loss == 2.2 # Stoploss does move lower + trade.set_isolated_liq(1.5) trade.adjust_stop_loss(1.8, 0.1) - assert trade.liquidation_price == 3.8 + assert trade.liquidation_price == 1.5 assert pytest.approx(trade.stop_loss) == 1.89 assert trade.initial_stop_loss == 2.2 + assert trade.stoploss_or_liquidation == 1.5 + + @pytest.mark.parametrize('exchange,is_short,lev,minutes,rate,interest,trading_mode', [ From dba7a7257dec1371c1bea14c099dbca06474e3ff Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 30 Jul 2022 09:18:15 +0200 Subject: [PATCH 29/33] Use stop_or_liquidation instead of stop_loss --- freqtrade/freqtradebot.py | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index d58c05d7f..7440212c3 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -1015,7 +1015,7 @@ class FreqtradeBot(LoggingMixin): trade.stoploss_order_id = None logger.error(f'Unable to place a stoploss order on exchange. {e}') logger.warning('Exiting the trade forcefully') - self.execute_trade_exit(trade, trade.stop_loss, exit_check=ExitCheckTuple( + self.execute_trade_exit(trade, stop_price, exit_check=ExitCheckTuple( exit_type=ExitType.EMERGENCY_EXIT)) except ExchangeError: @@ -1114,7 +1114,7 @@ class FreqtradeBot(LoggingMixin): :param order: Current on exchange stoploss order :return: None """ - stoploss_norm = self.exchange.price_to_precision(trade.pair, trade.stop_loss) + stoploss_norm = self.exchange.price_to_precision(trade.pair, trade.stoploss_or_liquidation) if self.exchange.stoploss_adjust(stoploss_norm, order, side=trade.exit_side): # we check if the update is necessary @@ -1132,7 +1132,7 @@ class FreqtradeBot(LoggingMixin): f"for pair {trade.pair}") # Create new stoploss order - if not self.create_stoploss_order(trade=trade, stop_price=trade.stop_loss): + if not self.create_stoploss_order(trade=trade, stop_price=stoploss_norm): logger.warning(f"Could not create trailing stoploss order " f"for pair {trade.pair}.") @@ -1431,14 +1431,15 @@ class FreqtradeBot(LoggingMixin): ) exit_type = 'exit' exit_reason = exit_tag or exit_check.exit_reason - if exit_check.exit_type in (ExitType.STOP_LOSS, ExitType.TRAILING_STOP_LOSS): + if exit_check.exit_type in ( + ExitType.STOP_LOSS, ExitType.TRAILING_STOP_LOSS, ExitType.LIQUIDATION): exit_type = 'stoploss' # 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']): - limit = trade.stop_loss + limit = trade.stoploss_or_liquidation # set custom_exit_price if available proposed_limit_rate = limit From d046f0cc5e766ec24f5c88fd967b44ea88698481 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 30 Jul 2022 09:19:48 +0200 Subject: [PATCH 30/33] Improve method wording for liquidation price setter --- freqtrade/freqtradebot.py | 4 ++-- freqtrade/optimize/backtesting.py | 2 +- freqtrade/persistence/trade_model.py | 2 +- tests/test_persistence.py | 22 ++++++++++------------ 4 files changed, 14 insertions(+), 16 deletions(-) diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index 7440212c3..3490b58d6 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -1085,7 +1085,7 @@ class FreqtradeBot(LoggingMixin): if (trade.is_open and stoploss_order and stoploss_order['status'] in ('canceled', 'cancelled')): - if self.create_stoploss_order(trade=trade, stop_price=trade.stop_loss): + if self.create_stoploss_order(trade=trade, stop_price=trade.stoploss_or_liquidation): return False else: trade.stoploss_order_id = None @@ -1662,7 +1662,7 @@ class FreqtradeBot(LoggingMixin): trade = self.cancel_stoploss_on_exchange(trade) # TODO: Margin will need to use interest_rate as well. # interest_rate = self.exchange.get_interest_rate() - trade.set_isolated_liq(self.exchange.get_liquidation_price( + trade.set_liquidation_price(self.exchange.get_liquidation_price( leverage=trade.leverage, pair=trade.pair, amount=trade.amount, diff --git a/freqtrade/optimize/backtesting.py b/freqtrade/optimize/backtesting.py index 6bbace185..2c6cfb0e9 100755 --- a/freqtrade/optimize/backtesting.py +++ b/freqtrade/optimize/backtesting.py @@ -814,7 +814,7 @@ class Backtesting: trade.adjust_stop_loss(trade.open_rate, self.strategy.stoploss, initial=True) - trade.set_isolated_liq(self.exchange.get_liquidation_price( + trade.set_liquidation_price(self.exchange.get_liquidation_price( pair=pair, open_rate=propose_rate, amount=amount, diff --git a/freqtrade/persistence/trade_model.py b/freqtrade/persistence/trade_model.py index 244ca79cd..44e148a0c 100644 --- a/freqtrade/persistence/trade_model.py +++ b/freqtrade/persistence/trade_model.py @@ -507,7 +507,7 @@ class LocalTrade(): self.max_rate = max(current_price, self.max_rate or self.open_rate) self.min_rate = min(current_price_low, self.min_rate or self.open_rate) - def set_isolated_liq(self, liquidation_price: Optional[float]): + def set_liquidation_price(self, liquidation_price: Optional[float]): """ Method you should use to set self.liquidation price. Assures stop_loss is not passed the liquidation price diff --git a/tests/test_persistence.py b/tests/test_persistence.py index 3eca035c9..0c1fc01a5 100644 --- a/tests/test_persistence.py +++ b/tests/test_persistence.py @@ -99,7 +99,7 @@ def test_enter_exit_side(fee, is_short): @pytest.mark.usefixtures("init_persistence") -def test_set_stop_loss_isolated_liq(fee): +def test_set_stop_loss_liquidation(fee): trade = Trade( id=2, pair='ADA/USDT', @@ -115,7 +115,7 @@ def test_set_stop_loss_isolated_liq(fee): leverage=2.0, trading_mode=margin ) - trade.set_isolated_liq(0.09) + trade.set_liquidation_price(0.09) assert trade.liquidation_price == 0.09 assert trade.stop_loss is None assert trade.initial_stop_loss is None @@ -125,12 +125,12 @@ def test_set_stop_loss_isolated_liq(fee): assert trade.stop_loss == 1.8 assert trade.initial_stop_loss == 1.8 - trade.set_isolated_liq(0.08) + trade.set_liquidation_price(0.08) assert trade.liquidation_price == 0.08 assert trade.stop_loss == 1.8 assert trade.initial_stop_loss == 1.8 - trade.set_isolated_liq(0.11) + trade.set_liquidation_price(0.11) trade.adjust_stop_loss(2.0, 0.2) assert trade.liquidation_price == 0.11 # Stoploss does not change from liquidation price @@ -167,7 +167,7 @@ def test_set_stop_loss_isolated_liq(fee): trade.initial_stop_loss = None trade.initial_stop_loss_pct = None - trade.set_isolated_liq(3.09) + trade.set_liquidation_price(3.09) assert trade.liquidation_price == 3.09 assert trade.stop_loss is None assert trade.initial_stop_loss is None @@ -178,13 +178,13 @@ def test_set_stop_loss_isolated_liq(fee): assert trade.initial_stop_loss == 2.2 assert trade.stoploss_or_liquidation == 2.2 - trade.set_isolated_liq(3.1) + trade.set_liquidation_price(3.1) assert trade.liquidation_price == 3.1 assert trade.stop_loss == 2.2 assert trade.initial_stop_loss == 2.2 assert trade.stoploss_or_liquidation == 2.2 - trade.set_isolated_liq(3.8) + trade.set_liquidation_price(3.8) assert trade.liquidation_price == 3.8 # Stoploss does not change from liquidation price assert trade.stop_loss == 2.2 @@ -197,7 +197,7 @@ def test_set_stop_loss_isolated_liq(fee): assert trade.initial_stop_loss == 2.2 # Stoploss does move lower - trade.set_isolated_liq(1.5) + trade.set_liquidation_price(1.5) trade.adjust_stop_loss(1.8, 0.1) assert trade.liquidation_price == 1.5 assert pytest.approx(trade.stop_loss) == 1.89 @@ -205,8 +205,6 @@ def test_set_stop_loss_isolated_liq(fee): assert trade.stoploss_or_liquidation == 1.5 - - @pytest.mark.parametrize('exchange,is_short,lev,minutes,rate,interest,trading_mode', [ ("binance", False, 3, 10, 0.0005, round(0.0008333333333333334, 8), margin), ("binance", True, 3, 10, 0.0005, 0.000625, margin), @@ -1633,7 +1631,7 @@ def test_adjust_stop_loss_short(fee): assert trade.initial_stop_loss_pct == -0.05 assert trade.stop_loss_pct == -0.1 # Liquidation price is lower than stoploss - so liquidation would trigger first. - trade.set_isolated_liq(0.63) + trade.set_liquidation_price(0.63) trade.adjust_stop_loss(0.59, -0.1) assert trade.stop_loss == 0.649 assert trade.liquidation_price == 0.63 @@ -2033,7 +2031,7 @@ def test_stoploss_reinitialization_short(default_conf, fee): assert trade_adj.initial_stop_loss == 1.01 assert trade_adj.initial_stop_loss_pct == -0.05 # Stoploss can't go above liquidation price - trade_adj.set_isolated_liq(0.985) + trade_adj.set_liquidation_price(0.985) trade.adjust_stop_loss(0.9799, -0.05) assert trade_adj.stop_loss == 0.989699 assert trade_adj.liquidation_price == 0.985 From dc82675f00e5ed1b006eeb1490a3f14147cde979 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 30 Jul 2022 17:28:16 +0200 Subject: [PATCH 31/33] Add Test for liquidation in stop-loss-reached --- tests/strategy/test_interface.py | 30 +++++++++++++++++------------- 1 file changed, 17 insertions(+), 13 deletions(-) diff --git a/tests/strategy/test_interface.py b/tests/strategy/test_interface.py index f6996a7a2..4257b2cf9 100644 --- a/tests/strategy/test_interface.py +++ b/tests/strategy/test_interface.py @@ -408,28 +408,31 @@ def test_min_roi_reached3(default_conf, fee) -> None: @pytest.mark.parametrize( - 'profit,adjusted,expected,trailing,custom,profit2,adjusted2,expected2,custom_stop', [ + 'profit,adjusted,expected,liq,trailing,custom,profit2,adjusted2,expected2,custom_stop', [ # Profit, adjusted stoploss(absolute), profit for 2nd call, enable trailing, # enable custom stoploss, expected after 1st call, expected after 2nd call - (0.2, 0.9, ExitType.NONE, False, False, 0.3, 0.9, ExitType.NONE, None), - (0.2, 0.9, ExitType.NONE, False, False, -0.2, 0.9, ExitType.STOP_LOSS, None), - (0.2, 1.14, ExitType.NONE, True, False, 0.05, 1.14, ExitType.TRAILING_STOP_LOSS, None), - (0.01, 0.96, ExitType.NONE, True, False, 0.05, 1, ExitType.NONE, None), - (0.05, 1, ExitType.NONE, True, False, -0.01, 1, ExitType.TRAILING_STOP_LOSS, None), + (0.2, 0.9, ExitType.NONE, None, False, False, 0.3, 0.9, ExitType.NONE, None), + (0.2, 0.9, ExitType.NONE, None, False, False, -0.2, 0.9, ExitType.STOP_LOSS, None), + (0.2, 0.9, ExitType.NONE, 0.8, False, False, -0.2, 0.9, ExitType.LIQUIDATION, None), + (0.2, 1.14, ExitType.NONE, None, True, False, 0.05, 1.14, ExitType.TRAILING_STOP_LOSS, + None), + (0.01, 0.96, ExitType.NONE, None, True, False, 0.05, 1, ExitType.NONE, None), + (0.05, 1, ExitType.NONE, None, True, False, -0.01, 1, ExitType.TRAILING_STOP_LOSS, None), # Default custom case - trails with 10% - (0.05, 0.95, ExitType.NONE, False, True, -0.02, 0.95, ExitType.NONE, None), - (0.05, 0.95, ExitType.NONE, False, True, -0.06, 0.95, ExitType.TRAILING_STOP_LOSS, None), - (0.05, 1, ExitType.NONE, False, True, -0.06, 1, ExitType.TRAILING_STOP_LOSS, + (0.05, 0.95, ExitType.NONE, None, False, True, -0.02, 0.95, ExitType.NONE, None), + (0.05, 0.95, ExitType.NONE, None, False, True, -0.06, 0.95, ExitType.TRAILING_STOP_LOSS, + None), + (0.05, 1, ExitType.NONE, None, False, True, -0.06, 1, ExitType.TRAILING_STOP_LOSS, lambda **kwargs: -0.05), - (0.05, 1, ExitType.NONE, False, True, 0.09, 1.04, ExitType.NONE, + (0.05, 1, ExitType.NONE, None, False, True, 0.09, 1.04, ExitType.NONE, lambda **kwargs: -0.05), - (0.05, 0.95, ExitType.NONE, False, True, 0.09, 0.98, ExitType.NONE, + (0.05, 0.95, ExitType.NONE, None, False, True, 0.09, 0.98, ExitType.NONE, lambda current_profit, **kwargs: -0.1 if current_profit < 0.6 else -(current_profit * 2)), # Error case - static stoploss in place - (0.05, 0.9, ExitType.NONE, False, True, 0.09, 0.9, ExitType.NONE, + (0.05, 0.9, ExitType.NONE, None, False, True, 0.09, 0.9, ExitType.NONE, lambda **kwargs: None), ]) -def test_stop_loss_reached(default_conf, fee, profit, adjusted, expected, trailing, custom, +def test_stop_loss_reached(default_conf, fee, profit, adjusted, expected, liq, trailing, custom, profit2, adjusted2, expected2, custom_stop) -> None: strategy = StrategyResolver.load_strategy(default_conf) @@ -442,6 +445,7 @@ def test_stop_loss_reached(default_conf, fee, profit, adjusted, expected, traili fee_close=fee.return_value, exchange='binance', open_rate=1, + liquidation_price=liq, ) trade.adjust_min_max_rates(trade.open_rate, trade.open_rate) strategy.trailing_stop = trailing From bad15f077c66710314b7dea75d0e5f7abee767a3 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 30 Jul 2022 17:49:06 +0200 Subject: [PATCH 32/33] Simplify fetch_positions by using already existing method --- freqtrade/exchange/exchange.py | 33 ++++++++++++++++----------------- tests/exchange/test_exchange.py | 14 -------------- 2 files changed, 16 insertions(+), 31 deletions(-) diff --git a/freqtrade/exchange/exchange.py b/freqtrade/exchange/exchange.py index 79bc769e6..e180c90b2 100644 --- a/freqtrade/exchange/exchange.py +++ b/freqtrade/exchange/exchange.py @@ -1332,11 +1332,19 @@ class Exchange: raise OperationalException(e) from e @retrier - def fetch_positions(self) -> List[Dict]: + def fetch_positions(self, pair: str = None) -> List[Dict]: + """ + Fetch positions from the exchange. + If no pair is given, all positions are returned. + :param pair: Pair for the query + """ if self._config['dry_run'] or self.trading_mode != TradingMode.FUTURES: return [] try: - positions: List[Dict] = self._api.fetch_positions() + symbols = [] + if pair: + symbols.append(pair) + positions: List[Dict] = self._api.fetch_positions(symbols) self._log_exchange_response('fetch_positions', positions) return positions except ccxt.DDoSProtection as e: @@ -2539,7 +2547,6 @@ class Exchange: else: return 0.0 - @retrier def get_or_calculate_liquidation_price( self, pair: str, @@ -2573,20 +2580,12 @@ class Exchange: upnl_ex_1=upnl_ex_1 ) else: - try: - positions = self._api.fetch_positions([pair]) - if len(positions) > 0: - pos = positions[0] - isolated_liq = pos['liquidationPrice'] - else: - return None - except ccxt.DDoSProtection as e: - raise DDosProtection(e) from e - except (ccxt.NetworkError, ccxt.ExchangeError) as e: - raise TemporaryError( - f'Could not set margin mode due to {e.__class__.__name__}. Message: {e}') from e - except ccxt.BaseError as e: - raise OperationalException(e) from e + positions = self.fetch_positions(pair) + if len(positions) > 0: + pos = positions[0] + isolated_liq = pos['liquidationPrice'] + else: + return None if isolated_liq: buffer_amount = abs(open_rate - isolated_liq) * self.liquidation_buffer diff --git a/tests/exchange/test_exchange.py b/tests/exchange/test_exchange.py index 9252040ea..e968b12c2 100644 --- a/tests/exchange/test_exchange.py +++ b/tests/exchange/test_exchange.py @@ -4099,20 +4099,6 @@ def test_get_or_calculate_liquidation_price(mocker, default_conf): ) assert liq_price == 17.540699999999998 - ccxt_exceptionhandlers( - mocker, - default_conf, - api_mock, - "binance", - "get_or_calculate_liquidation_price", - "fetch_positions", - pair="XRP/USDT", - open_rate=0.0, - is_short=False, - position=0.0, - wallet_balance=0.0, - ) - @pytest.mark.parametrize('exchange,rate_start,rate_end,d1,d2,amount,expected_fees', [ ('binance', 0, 2, "2021-09-01 01:00:00", "2021-09-01 04:00:00", 30.0, 0.0), From 09e5fb2f55cf368b8a3126fb1f00be2f8eb1f613 Mon Sep 17 00:00:00 2001 From: rzrymiak <106121613+rzrymiak@users.noreply.github.com> Date: Sat, 30 Jul 2022 22:37:46 +0000 Subject: [PATCH 33/33] Removed description header --- README.md | 2 -- 1 file changed, 2 deletions(-) diff --git a/README.md b/README.md index 828a3d1f9..aa446ad54 100644 --- a/README.md +++ b/README.md @@ -5,8 +5,6 @@ [![Documentation](https://readthedocs.org/projects/freqtrade/badge/)](https://www.freqtrade.io) [![Maintainability](https://api.codeclimate.com/v1/badges/5737e6d668200b7518ff/maintainability)](https://codeclimate.com/github/freqtrade/freqtrade/maintainability) -## Description - Freqtrade is a free and open source crypto trading bot written in Python. It is designed to support all major exchanges and be controlled via Telegram or webUI. It contains backtesting, plotting and money management tools as well as strategy optimization by machine learning. ![freqtrade](https://raw.githubusercontent.com/freqtrade/freqtrade/develop/docs/assets/freqtrade-screenshot.png)